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

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

Python Script to Cause the Race Condition

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...

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...

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...

Sreeprakash Neelakantan

Written by

AWS Certified DevOps Engineer & Solutions Architect Professional — Docker | Kubernetes | DevOps — Trainer | Running | Swimming | Cycling

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