Insecure File Upload

We generally want to upload a malicious file to achieve code execution after we upload it. In some cases, the possibility of overwriting a file may also come handy. There are also cases where we might achieve denial of service (DoS).


Client-Side Controls Bypass

Start with testing the functionality. The more you understand a webapp, the easier you will find vulnerabilities. Start with uploading a file based on the webapp stack. If it is a .php server, try a PHP file. If it’s Windows, try an .aspx file.

Second try, do the same thing - but open dev tools Network. See if there is a request to the webserver. If there is no network traffic and the file gets blocked, the check is client-side only. Client-side controls are useless for security because we have full control over the client. We can just disable JavaScript so the check never happens, or intercept the request in Burp.

Intercept the Request

Verify the vulnerability:

  • Get a file which gets accepted - like a normal .png
  • Turn on Burp Proxy
  • Intercept the request where the png gets saved to the server
  • Send the request to the Repeater (CTRL + R)
  • Get rid of all magic bytes and put strings like “Hello There”
  • Change the filename from something like “logo.png” to “logo.txt” to see how the server handles it
  • Maybe play around with the Content-Type header
  • Send the request and check the response
  • Verify the uploaded file on the webserver by navigating to its location. Check the filepath by right-clicking an existing image and inspecting it, or directory brute force to find a hidden uploads subdirectory:
ffuf -u http://TARGET/FUZZ -w /usr/share/wordlists/dirb/common.txt

Exploiting the vulnerability:

  • Replace all text with: <?php system($_GET['cmd']); ?> or anything with PHP cmd on https://www.revshells.com/
  • $_GET is a PHP superglobal - it reads a parameter from the URL query string. Whatever value you pass to cmd= gets executed by system()
  • If system() is blocked on the server, try alternative PHP execution functions: exec(), proc_open(), popen(), passthru()
  • Replace the name from logo.png to cmd.php indicating to the webserver to execute it
  • Locate the file (inspecting file path of images, fuzz the directory using feroxbuster or ffuf - read through the output carefully)
  • Access the webpage: /uploads/cmd.php?cmd=whoami Output: www-data = RCE!

Server-Side Controls Bypass

Same start as above:

  • Get a file which gets accepted - like a normal .png
  • Turn on Burp Proxy
  • Intercept the request where the png gets saved to the server
  • Send the request to the Repeater (CTRL + R)
  • Replace all text with: <?php system($_GET['cmd']); ?> or anything with PHP cmd on https://www.revshells.com/
  • Replace the name from logo.png to cmd.php indicating to the webserver to execute it
  • This time, however, the response is Only .jpg and .png allowed, meaning the webserver has server-side validation

There are three main things a server can check: extension, content-type, and magic bytes.

Extension

The server checks if the file ends with an allowed extension. Bypass methods:

  • Null byte (old servers, PHP < 5.3): cmd.php%00.png - the null byte terminates the string so the server sees .png but saves/executes .php
  • Double extension (misconfigured htaccess): cmd.php.png - if the regex doesn’t anchor to end-of-line, it catches .php in the middle and still executes
  • Upload .htaccess: upload a custom .htaccess file that tells Apache to execute a custom extension (like .asd) as PHP. Then upload your shell with that extension
  • Alternative PHP extensions: if .php is blocked, try: phtml, php3, php4, php5, pht, .inc
  • If one extension does not work - try others!

Important: servers should use an allowlist (only allow .png, .jpg) not a blocklist (block .php, .asp). Blocklists are always bypassable because you can’t anticipate every dangerous extension.

Content-Type

The Content-Type header (e.g. image/png, text/plain) is fully controlled by us. We can set it to whatever we want. It does not affect how the file is stored or executed on the server. So even if the server checks it, we just change it to image/png in Burp and move on. This is a meaningless security check on its own.

Magic Bytes

These are the first few bytes of a file that tell the system what kind of file it is. For example, PNG files start with specific bytes that identify them as PNG.

To check magic bytes of a file: file logo.png “PNG image data” For a list of magic bytes per file type: search Wikipedia for “list of file signatures”.

Bypass technique:

  • Keep the original request with the valid image (including magic bytes at the start)
  • Insert your PHP payload after the magic bytes
  • Clean up trailing binary content that could interfere with PHP parsing (backticks, pipes, and other special characters in the image data can cause PHP syntax errors)
  • Keep enough content at the start for the file to pass the magic bytes check, but make sure the PHP portion is clean
  • Change the filename to .php
  • /uploads/cmd2.php?cmd=whoami Output: www-data = RCE!
  • Access Source Code (CTRL + U) for a better view of the output

Challenge

Same approach as the server-side bypass - changed the extension to .phtml to bypass the blocklist.


Resources

Testing Checklist

  • Test the upload functionality normally first - upload a valid file and understand where it gets stored, how the path is constructed, and what the server returns
  • Check if the validation is client-side only: open DevTools Network tab, upload a blocked file, and see if a request is sent to the server. If no request - client-side only, trivially bypassable
  • Bypass client-side controls: intercept a valid upload request in Burp Repeater, replace the file content with a PHP shell (<?php system($_GET['cmd']); ?>) and change the filename to .php
  • Test server-side extension filtering: if .php is blocked, try alternative extensions - phtml, php3, php4, php5, pht, .inc
  • Test null byte bypass on older servers (PHP < 5.3): cmd.php%00.png - server sees .png but saves/executes .php
  • Test double extension bypass: cmd.php.png - misconfigured regex may catch .php in the middle and still execute
  • Test content-type bypass: change the Content-Type header to image/png in Burp - this is fully attacker-controlled and a meaningless security check on its own
  • Test magic bytes bypass: keep the first few bytes of a valid image (PNG magic bytes), insert your PHP payload after them, clean up trailing binary that could cause PHP syntax errors, and change the extension to .php
  • If system() is blocked on the server, try alternative PHP execution functions: exec(), passthru(), proc_open(), popen()
  • Find the upload directory: inspect existing image paths on the page, or fuzz with ffuf/feroxbuster (/uploads/, /assets/, /files/)
  • Verify execution: navigate to the uploaded file and pass a parameter (/uploads/cmd.php?cmd=whoami) - confirm you get www-data or equivalent
  • Include in report: which validation was bypassed (client-side, extension, content-type, magic bytes), the payload and extension used, the upload path, proof of code execution, and remediation (use allowlists not blocklists, validate server-side, store uploads outside the webroot)