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

Using PHP, MySQL and Python

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

Image for post
Image for post

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

Image for post
Image for post
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...
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

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

Image for post
Image for post
<?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

Image for post
Image for post
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...
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

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

Image for post
Image for post
<?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

Image for post
Image for post
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...
Image for post
Image for post
Image for post
Image for post
Image for post
Image for post

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!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store