| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | File Upload Vulnerabilities |
| Difficulty | Expert |
| Objective | Exploit a TOCTOU race condition in the file upload validation pipeline to execute a PHP web shell before it is deleted, and read /home/carlos/secret |
Web Shell Upload via Race Condition¶
Every technique from previous labs failed — Content-Type bypass, null byte, polyglot, extension obfuscation, .htaccess. The server rejected them all:
Looking at the upload request in Burp:
The response took noticeably longer than a normal file operation — about a second of delay. A fast rejection would be instant. The latency is the tell: a validation that runs before writing to disk produces instant rejection; a validation that runs after produces a delay. The server isn't rejecting at intake — it's writing the file to disk, running validation, and then deleting it if the check fails. That sequence means the file exists on disk between the write and the delete.
That's a TOCTOU (Time-of-Check to Time-of-Use) window. The check (validation) and the use (execution) happen at different points in time with a gap between them — any security decision that isn't enforced atomically with the action it's supposed to prevent is potentially vulnerable to this pattern.
The server's pipeline:
- Receive the file
- Write it to
/files/avatars/ - Validate the file type
- If invalid → delete the file and return error
Steps 2 and 4 are not atomic. Between them, the file exists on disk and is executable. I set up two requests in Burp Repeater grouped to fire simultaneously — the upload and the execution attempt:
POST /my-account/avatar HTTP/2
[multipart body with tetoshell.php]
GET /files/avatars/tetoshell.php?cmd=cat%20/home/carlos/secret HTTP/2
Sending both in parallel is what wins the race — the GET needs to hit the server while the POST is still processing through the validation pipeline. Sending the execution request after the upload completes is too late; the file is already deleted.
The GET hit the file during the window and the PHP interpreter returned the contents of /home/carlos/secret before the validator cleaned up.
Lab solved and section finished