Skip to content
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:

Screenshot

Looking at the upload request in Burp:

Screenshot

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:

  1. Receive the file
  2. Write it to /files/avatars/
  3. Validate the file type
  4. 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
Screenshot

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.

Screenshot

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

Resources