Single-Pass Traversal, Preserved PDFs, and a Template Include
A custom HR portal runs PHP 8.2 on Apache. Employees upload documents to POST /api/documents. Images are re-encoded with ImageMagick and stripped of metadata; PDFs are stored byte-for-byte. Filenames are sanitized with preg_replace('/\.\.\//', '', $name) applied once, and the result is stored under /srv/app/uploads/<user_id>/<name>. A separate endpoint, GET /render?tpl=invoice, executes include __DIR__ . '/templates/' . $_GET['tpl'] . '.php'; tpl is checked against an array of known template names with in_array($tpl, $allowed). The Apache user is www-data; /srv/app/templates is owned by deploy:deploy with mode 775, and www-data is in the deploy group. There is no outbound internet. The client wants proof of code execution, not a full shell, within a 4-hour window. A previous tester reported "arbitrary file read" from the download endpoint, but you have not reproduced it.
1. The sanitizer runs once. Construct a working traversal filename and explain the byte-level transformation.
Read the worked answer
The sanitizer is preg_replace('/\.\.\//', '', $name) with a single pass. The regex removes non-overlapping matches, but removal creates a new ../ that is not re-scanned. For ....//, the regex matches the last .. plus / (characters 3–5), leaving ../. So ....//....//templates/invoice.php becomes ../../templates/invoice.php after one pass. The stored path is /srv/app/uploads/<uid>/../../templates/invoice.php, which resolves to /srv/app/templates/invoice.php. Use literal bytes in the multipart filename; ..%2f only works if the server decodes before sanitizing, which is a separate test.
2. Explain how the PDF handling and the include endpoint combine into code execution; what content and path do you use?
Read the worked answer
Execution sink: include __DIR__.'/templates/'.$tpl.'.php' with tpl=invoice. If the upload overwrites templates/invoice.php with a PDF whose bytes contain <?php ... ?>, PHP includes the file and executes the PHP block; the surrounding %PDF bytes are output as text. PDFs are stored byte-for-byte, so the payload survives; images would be re-encoded and stripped.
Content: a minimal PDF with a %PDF-1.7 header and <?php echo 'CWES_'.md5('proof'); ?> in a comment or stream. Save as invoice.php, send Content-Type: application/pdf. The MIME check passes on the magic bytes.
Path: ....//....//templates/invoice.php. After sanitization: ../../templates/invoice.php; resolved from /srv/app/uploads/<uid>/ to /srv/app/templates/invoice.php. www-data is in deploy, and the directory is 775, so the write succeeds. Then GET /render?tpl=invoice executes it.
3. What are the failure modes (permissions, suffix, PHP parsing of PDF bytes), and how would you prove execution with minimal footprint?
Read the worked answer
Failure modes: (1) the upload endpoint may enforce an extension allowlist in addition to MIME; if so, the .php filename is rejected and you need a different sink. (2) The include appends .php, so the stored file must already end in .php; invoice.php.pdf would include invoice.php.pdf.php. (3) PHP may not execute if the file is not parsed (opcache or handler mismatch); verify with a harmless echo. (4) Overwriting a template is destructive; back up the original bytes first. (5) Permissions may differ on the NFS mount.
Minimal proof: <?php echo 'CWES_'.md5('proof'); ?> and a single GET /render?tpl=invoice showing the marker. Do not drop a full web shell. Restore the template afterward and report the exact filename transformation.