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
referencefield fetched server-side → SSRF straight into cloud metadata - A
titlefield 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
● 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.
Phase 1 — Information Gathering
Kick off with a service scan and a header grab.
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.
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:
From there it’s a standard dump — schema, then the users table:
sqlmap reproduces it hands-free:
Phase 4 — SSRF in the reference Field
The reference field on /create is fetched server-side and reflected. Point it inward:
On a real cloud host that path leads to IAM credentials and account takeover.
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:
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.
Phase 6 — Exposed Werkzeug Debugger
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.
Attack Chain
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
- Turn off Flask debug in production (
debug=False, gunicorn/uWSGI) — kills F-003 and the traceback leaks. - Parameterise the /search query and disable verbose SQL errors — F-001.
- Validate/allow-list the
referenceURL, block link-local/RFC1918, use IMDSv2 — F-002. - Encode output on every field, not just
content— F-004. - Add CSRF tokens +
SameSite, auth rate-limiting, a real CORS allow-list, HSTS, andX-Frame-Options— F-005/006/007.
A tidy little box that rewards testing every field and chasing each finding one step past “confirmed.”