TOCTOU Time of Check and Time of Use — a Demonstration and Mitigation

Sreeprakash Neelakantan
5 min readDec 4, 2018

--

Using PHP, MySQL and Python

PHP & MySQL Application — Transfer funds from Account-1 to Account-2

Python Script to Cause the Race Condition

Thanks to defuse.ca for the article that details the conditions.

import os
os.fork() #2
os.fork() #4
os.fork() #8
os.fork() #16
os.fork() #32
os.fork() #64
os.fork() #128
print os.popen('php -r ' + \
'"echo file_get_contents(\'http://0.0.0.0:8001/transfer.php?amount=10\');"').read()

CASE-1 transfer.php — PHP Script without Mitigation

<?php$tfr_amount = 0;
if(isset($_GET["amount"])) {
$tfr_amount = $_GET["amount"];
}
$conn = mysqli_connect('db', 'user', 'test', "accounts");
$query = 'SELECT balance FROM account1 WHERE ID = (SELECT MAX(id) FROM account1)';
$result = mysqli_query($conn, $query); # TOC
$row=$result->fetch_assoc();
if(!isset($row)) {
$prevbal = 0;
} else {
$prevbal = (int) $row['balance'];
}
$result->close();
if ($prevbal >= $tfr_amount) {
} else {
echo "Insufficient funds. Cancelling transfer...";
mysqli_close($conn);
}
$tfr_query = "INSERT INTO account2 (record) VALUES ('Received " . $tfr_amount . " USD')";
$tfr_response = $conn->query($tfr_query);
if ($tfr_response) {
$finalbalance = $prevbal-$tfr_amount;
$newquery = "INSERT INTO account1 (tnx_amount, balance) VALUES (";
$newquery .= "'-$tfr_amount',";
$newquery .= " '$finalbalance')";
$response = $conn->query($newquery); #TOU
if ($response) {
echo "Account1 balance updated...";
}
} else {
echo "Deposit failed...";
}
mysqli_close($conn);
?>

Testing Case-1

python2.7 transfer.py 
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
---snip---
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...

Here we can see that after deducting 10 Dollars 128 times, account-1 is reflecting only a debit of $390! while account-2 has a credit of $1280! This is the vulnerability posed by TOCTOU.

CASE-2 transfer.php — PHP Script Using Delay Based Mitigation

<?php
$seconds = rand(1, 10);
$nanoseconds = rand(100, 1000000000);
time_nanosleep($seconds, $nanoseconds);
$tfr_amount = 0;
if(isset($_GET["amount"])) {
$tfr_amount = $_GET["amount"];
}
$conn = mysqli_connect('db', 'user', 'test', "accounts");
$query = 'SELECT balance FROM account1 WHERE ID = (SELECT MAX(id) FROM account1)';
$result = mysqli_query($conn, $query);
$row=$result->fetch_assoc();
if(!isset($row)) {
$prevbal = 0;
} else {
$prevbal = (int) $row['balance'];
}
$result->close();
if ($prevbal >= $tfr_amount) {
} else {
echo "Insufficient funds. Cancelling transfer...";
mysqli_close($conn);
}
$tfr_query = "INSERT INTO account2 (record) VALUES ('Received " . $tfr_amount . " USD')";
$tfr_response = $conn->query($tfr_query);
if ($tfr_response) {
$finalbalance = $prevbal-$tfr_amount;
$newquery = "INSERT INTO account1 (tnx_amount, balance) VALUES (";
$newquery .= "'-$tfr_amount',";
$newquery .= " '$finalbalance')";
$response = $conn->query($newquery);
if ($response) {
echo "Account1 balance updated...";
}
} else {
echo "Deposit failed...";
}
mysqli_close($conn);
?>

Testing Case-2

python2.7 transfer.py 
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
---snip---
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...

Here we can see that after deducting 10 Dollars 128 times, account-1 is reflecting more accurate results. When we use the random delay method to reduce multiple overlapping transactions we can reduce the vulnerability posed by TOCTOU.

CASE-3 transfer.php — PHP Script Using Semaphore based Mitigation

<?php
$sem = sem_get(1234, 1);
if (sem_acquire($sem))
{

$tfr_amount = 0;
if(isset($_GET["amount"])) {
$tfr_amount = $_GET["amount"];
}
$conn = mysqli_connect('db', 'user', 'test', "accounts");
$query = 'SELECT balance FROM account1 WHERE ID = (SELECT MAX(id) FROM account1)';
$result = mysqli_query($conn, $query);
$row=$result->fetch_assoc();
if(!isset($row)) {
$prevbal = 0;
} else {
$prevbal = (int) $row['balance'];
}
$result->close();
if ($prevbal >= $tfr_amount) {
} else {
echo "Insufficient funds. Cancelling transfer...";
mysqli_close($conn);
}
$tfr_query = "INSERT INTO account2 (record) VALUES ('Received " . $tfr_amount . " USD')";
$tfr_response = $conn->query($tfr_query);
if ($tfr_response) {
$finalbalance = $prevbal-$tfr_amount;
$newquery = "INSERT INTO account1 (tnx_amount, balance) VALUES (";
$newquery .= "'-$tfr_amount',";
$newquery .= " '$finalbalance')";
$response = $conn->query($newquery);
if ($response) {
echo "Account1 balance updated...";
}
} else {
echo "Deposit failed...";
}
mysqli_close($conn);
}
?>

Testing Case-3

python2.7 transfer.py 
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
---snip---
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...
Account1 balance updated...

Here we can see that after deducting 10 Dollars 128 times, account-1 is reflecting the actual withdrawals. When we use the Semaphore method to block multiple overlapping transactions we can eliminate the vulnerability posed by TOCTOU.

Thank you for your time, do follow me for more such tiny demos!

--

--