eWPTX study materials
eWPTX study resources

The Upload That Passed Every Check

eWPTX · Put your knowledge to work in context.

All case studies

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
  1. Content-Type header - trivially spoofed; set image/png on a PHP file. Success = the file is stored. This check alone is worthless.
  2. Extension check - test double extensions (shell.php.png), case (shell.PHP), trailing dot/space (shell.php.), and shell.phtml/shell.php5 if the check is a denylist of .php only. Success = stored file whose name ends in something PHP-FPM's \.php$ regex matches. Note nginx's regex is \.php$, so .php.png will not execute - the extension must actually end in .php.
  3. 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 checking Content-Type and X-Content-Type-Options; if nginx serves it as image/png with nosniff, 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.