The Upload That Passed Every Check
An image-upload feature accepts PNG/JPEG only. It checks the Content-Type header, the file extension, and runs getimagesize() on the temp file. Files are stored at /uploads/{random32}.{ext} and served by nginx with Content-Type derived from extension. The app is PHP-FPM behind nginx; location ~ \.php$ passes to PHP-FPM. You have upload access as a normal user. The client's scope allows uploading test files but forbids leaving a webshell or triggering any process that persists after the engagement. You have a 45-minute window.
1. The three checks are stated. For each, name the specific bypass you would attempt and what a successful result looks like, keeping within scope.
Read the worked answer
- Content-Type header - trivially spoofed; set
image/pngon a PHP file. Success = the file is stored. This check alone is worthless. - Extension check - test double extensions (
shell.php.png), case (shell.PHP), trailing dot/space (shell.php.), andshell.phtml/shell.php5if the check is a denylist of.phponly. Success = stored file whose name ends in something PHP-FPM's\.php$regex matches. Note nginx's regex is\.php$, so.php.pngwill not execute - the extension must actually end in.php. getimagesize()- this reads the file header, so prepend a valid PNG magic (\x89PNG\r\n\x1a\n) and IHDR chunk, then append PHP after the image data.getimagesize()passes on the header; the PHP is still in the file. Success = the file passes validation and is stored.
In-scope proof: I do not need the file to execute to prove the bug. I upload a benign payload (<?php echo 'MARKER'; ?> appended to a real PNG) and, if I can get it stored with an executable extension, I request it and look for MARKER - that proves execution without a webshell. If the extension check holds, the getimagesize() bypass still proves the content check is defeated, which is the finding.
2. The extension check turns out to be strict (only `.png`/`.jpg` stored). What remains exploitable, and how do you demonstrate it without leaving anything behind?
Read the worked answer
With a strict extension, direct PHP execution via the upload path is closed - the stored name ends .png/.jpg and nginx's \.php$ won't route it to FPM. What remains:
- Polyglot content - the stored file is a valid image and contains PHP/HTML. If any feature later includes or renders uploads (a thumbnailer, a PDF generator, an
<img>served with sniffable content type), the payload is live. Test by requesting the stored file and checkingContent-TypeandX-Content-Type-Options; if nginx serves it asimage/pngwithnosniff, browser-side execution is dead too. - Filename/path handling - if the random name is derived from user input anywhere, or if the original filename is preserved in a
Content-Disposition, test for path traversal or header injection in the stored name. - Image-processing RCE - if a thumbnailer (ImageMagick/GD) processes the upload, that's a separate, higher-impact surface; test with a benign malformed image and watch for a crash (in scope: no persistence).
Demonstration without leftovers: I prove the getimagesize() bypass by uploading a PNG-prefixed file containing a unique marker string, then requesting it and showing the marker is present in the served bytes - proving arbitrary content survived validation. I then delete the file via the app's own delete function (or ask the client to) and record the request/response and a hash of the file as evidence. I do not attempt execution if the extension is strict; the finding is 'content validation bypassed, execution blocked only by extension policy,' which is a real risk if that policy ever changes.