Chapter 6: Hands-On Lab
Lab Overview: File Upload, Path Traversal & Reflected XSS
In this self-contained lab, you will spin up a lightweight Python Flask web service containing three critical vulnerabilities:
- Unsanitized File Upload (Arbitrary web shell upload)
- Path Traversal (Reading arbitrary system files via relative pathing)
- Reflected Cross-Site Scripting (XSS) (Injecting dynamic HTML payloads)
You will run an automated exploit script (exploit.py) to confirm vulnerability exploitability, then deploy the hardened production application (app_secure.py) to verify complete mitigation.
Lab Architecture & Workflow
Prerequisites & Installation
Ensure Python 3.8+ is installed on your system. Install the required lab dependencies:
pip install Flask requests puremagic nh3 werkzeug
Step 1: The Vulnerable Application (app_vuln.py)
Create app_vuln.py in your lab directory:
# app_vuln.py
from flask import Flask, request, send_file, render_template_string
import os
app = Flask(__name__)
UPLOAD_DIR = os.path.abspath("uploads")
os.makedirs(UPLOAD_DIR, exist_ok=True)
# VULNERABILITY 1: Unsanitized File Upload (Trusts filename & extension completely)
@app.route('/upload', methods=['POST'])
def upload():
file = request.files.get('file')
if not file:
return "No file provided", 400
# Insecure: Using client-supplied filename without magic byte validation or UUIDs
target_path = os.path.join(UPLOAD_DIR, file.filename)
file.save(target_path)
return f"File uploaded successfully to {target_path}", 200
# VULNERABILITY 2: Path Traversal (No canonical containment check)
@app.route('/download', methods=['GET'])
def download():
filename = request.args.get('file', '')
# Insecure: Joining path without checking if target escapes UPLOAD_DIR
target_path = os.path.join(UPLOAD_DIR, filename)
return send_file(target_path)
# VULNERABILITY 3: Reflected XSS (Raw HTML String Formatting)
@app.route('/search', methods=['GET'])
def search():
query = request.args.get('q', '')
# Insecure: Formatting raw string into template bypasses auto-escaping
template = f"<h1>Search Results for: {query}</h1>"
return render_template_string(template)
if __name__ == '__main__':
print("[*] Starting Vulnerable App on http://127.0.0.1:5000")
app.run(port=5000, debug=True)
Step 2: The Exploit Script (exploit.py)
Create exploit.py to automate the attack verification against the running server:
# exploit.py
import requests
BASE_URL = "http://127.0.0.1:5000"
def test_file_upload_exploit():
print("\n--- [1] Testing Arbitrary File Upload Exploit ---")
files = {'file': ('web_shell.py', 'import os; print("EXECUTE ARBITRARY CODE")')}
res = requests.post(f"{BASE_URL}/upload", files=files)
print(f"[+] Status Code: {res.status_code}")
print(f"[+] Server Response: {res.text.strip()}")
def test_path_traversal_exploit():
print("\n--- [2] Testing Path Traversal Exploit ---")
# Payload attempts to escape uploads directory and read app_vuln.py
payload = "../app_vuln.py"
res = requests.get(f"{BASE_URL}/download", params={'file': payload})
print(f"[+] Status Code: {res.status_code}")
if "VULNERABILITY" in res.text:
print("[!] EXPLOIT SUCCESSFUL! Leaked Source Code Content Preview:")
print(res.text[:300])
else:
print("[-] Exploit failed or file not accessible.")
def test_reflected_xss_exploit():
print("\n--- [3] Testing Reflected XSS Exploit ---")
xss_payload = "<script>alert('PWNED-XSS')</script>"
res = requests.get(f"{BASE_URL}/search", params={'q': xss_payload})
print(f"[+] Status Code: {res.status_code}")
if xss_payload in res.text:
print("[!] EXPLOIT SUCCESSFUL! Raw XSS payload reflected in HTTP response body:")
print(res.text.strip())
else:
print("[-] XSS payload was encoded or blocked.")
if __name__ == '__main__':
try:
test_file_upload_exploit()
test_path_traversal_exploit()
test_reflected_xss_exploit()
except requests.exceptions.ConnectionError:
print("[ERROR] Ensure app_vuln.py or app_secure.py is running on port 5000!")
Step 3: Hardened Remediation Application (app_secure.py)
Create app_secure.py incorporating production-grade mitigations:
# app_secure.py
from flask import Flask, request, send_file, abort, render_template
from pathlib import Path
import uuid
import puremagic
import nh3
app = Flask(__name__)
UPLOAD_DIR = Path("safe_uploads").resolve()
UPLOAD_DIR.mkdir(exist_ok=True)
# Explicit MIME Allowlist mapping to safe extensions
ALLOWED_MIME_TYPES = {
"image/png": ".png",
"image/jpeg": ".jpg",
"text/plain": ".txt"
}
# REMEDIATION 1: Magic Byte Inspection + UUID Filename Isolation
@app.route('/upload', methods=['POST'])
def upload():
file = request.files.get('file')
if not file:
return abort(400, "No file provided")
# Read binary header (first 2048 bytes) for signature validation
header_bytes = file.read(2048)
file.seek(0)
try:
matches = puremagic.from_string(header_bytes)
if not matches:
return abort(415, "Unsupported file signature")
detected_mime = matches[0].mime_type
except Exception:
return abort(415, "Failed to analyze binary magic bytes")
if detected_mime not in ALLOWED_MIME_TYPES:
return abort(415, f"Disallowed media type: {detected_mime}")
# Generate random UUID filename
safe_ext = ALLOWED_MIME_TYPES[detected_mime]
random_filename = f"{uuid.uuid4()}{safe_ext}"
target_path = (UPLOAD_DIR / random_filename).resolve()
# Verify target stays strictly within UPLOAD_DIR boundary
if not str(target_path).startswith(str(UPLOAD_DIR)):
return abort(403, "Illegal path destination")
file.save(target_path)
return f"File securely stored as {random_filename}", 200
# REMEDIATION 2: Canonical Containment Traversal Check
@app.route('/download', methods=['GET'])
def download():
filename = request.args.get('file', '')
if not filename:
return abort(400, "Filename parameter required")
# Resolve absolute path
target_path = (UPLOAD_DIR / filename).resolve()
# Boundary check: Ensure path does not escape UPLOAD_DIR
if not str(target_path).startswith(str(UPLOAD_DIR)):
return abort(403, "Access Denied: Path traversal detected")
if not target_path.exists() or not target_path.is_file():
return abort(404, "Requested asset not found")
return send_file(target_path)
# REMEDIATION 3: Context-Aware Escaping + Security Headers
@app.route('/search', methods=['GET'])
def search():
query = request.args.get('q', '')
# Pass variable into template dictionary so Jinja2 auto-escapes HTML characters
template = "<h1>Search Results for: {{ query }}</h1>"
response = app.make_response(render_template_string(template, query=query))
response.headers['Content-Security-Policy'] = "default-src 'self';"
response.headers['X-Content-Type-Options'] = 'nosniff'
return response
if __name__ == '__main__':
print("[*] Starting Hardened Secure App on http://127.0.0.1:5000")
app.run(port=5000)
Step-by-Step Lab Execution Guide
1. Test Vulnerable Application
- Terminal 1: Run
python app_vuln.py - Terminal 2: Run
python exploit.py - Observe Results:
- The exploit successfully uploads
web_shell.pyusing user input. - The path traversal exploit leaks
app_vuln.pysource code. - The reflected XSS payload is returned raw in the response.
- The exploit successfully uploads
2. Test Hardened Secure Application
- Terminal 1: Stop
app_vuln.py(Ctrl+C) and startpython app_secure.py - Terminal 2: Run
python exploit.py - Observe Defensive Results:
- Upload: Rejected with
415 Unsupported Media Type(magic bytes blocked.pyscript). - Download: Blocked with
403 Forbidden(path traversal detected). - Search: Returns
200 OKwith HTML-escaped output (<script>alert(...)</script>).
- Upload: Rejected with
[!TIP] Lab Verification Challenge: Try tweaking
exploit.pyto send a valid PNG file signature (\x89PNG\r\n\x1a\n...) but with a.pyextension. Observe howapp_secure.pyhandles the extension override using its strictALLOWED_MIME_TYPESUUID assignment!