What We Found — TL;DR
This is a walkthrough of a web app pentest against pentest-ground.com — a public security-testing practice range running Damn Vulnerable GraphQL Application (DVGA). This post takes you through every phase from the first introspection query to full remote code execution inside the app container.
What to expect reading this:
- A GraphQL API with introspection left on (free schema dump, zero auth)
- A
pastesquery that trusts a client-supplied visibility flag (private data leak) - A debug resolver with a raw
cmdparameter and a weak default credential (instant RCE) - Step-by-step commands with real output at every stage
- A full animated kill chain tying all findings together
● CRITICAL Broken Access Control — pastes(public:false) unauth data leak · CWE-284 · OWASP A01 · WSTG-ATHZ-02
● HIGH SSRF — importPaste(host,port,scheme) attacker-controlled fetch · CWE-918 · OWASP A10
● MEDIUM GraphQL Introspection Enabled — full schema disclosure · CWE-200 · OWASP A05
● MEDIUM Unauthenticated Destructive Mutation — deleteAllPastes() exposed (documented, not executed)
● LOW Missing Security Headers / Insecure Cookie — no HSTS, cookie missing Secure/HttpOnly · CWE-614
Tools used in this walkthrough: nmap · wafw00f · testssl.sh · curl (manual GraphQL introspection/queries)
Skill level: Beginner-friendly — every command is explained.
Phase 1 — Information Gathering
I started by fingerprinting the target at https://pentest-ground.com:5013/. Before touching anything active, I wanted to know what I was dealing with — server headers, TLS posture, and any obvious tells in a plain response.
That Set-Cookie: env=graphiql:disable is a dead giveaway — it’s the exact cookie DVGA (Damn Vulnerable GraphQL Application) uses to toggle its bundled GraphiQL IDE. Before sending a single crafted payload, I already knew the framework.
To confirm, I opened the app in a browser:
The “Damn Vulnerable GraphQL Application” branding on the homepage confirms it outright — no guessing needed.
TLS itself was solid — no legacy protocol support, no classic downgrade bugs. But no HSTS and an insecure cookie went straight onto the findings list as low-severity hardening gaps.
Phase 2 — Reconnaissance
GraphQL doesn’t have “endpoints” the way REST does — everything hides behind one URL. So instead of directory brute-forcing, the move is a standard introspection query against /graphql.
Same query, replayed live in the browser against the app’s own origin — same result, still zero authentication:
Every query and mutation the API exposes — systemDiagnostics, importPaste, deleteAllPastes — handed over before a single auth header was sent.
Unauthenticated, zero effort, and it handed over the entire attack surface. Three fields jumped out immediately: systemDiagnostics(cmd) (a raw command parameter next to a shell-shaped resolver), importPaste(host, port, scheme) (a textbook SSRF shape), and pastes(public) (a visibility flag controlled entirely by the caller).
Phase 3 — Scanning
A straight sequential -p- sweep across all 65535 ports tripped the host’s scan-rate protection and just hung — a good reminder that shared community targets often have anti-scan defenses. Switching to curated, non-sequential port batches got clean results instead. A separate batch confirmed 7001/tcp open as well. Per scope, only :5013 was authorized for vulnerability analysis and exploitation — everything else here is recon-only and was left untouched.
No WAF in front of the app — nothing to bypass, nothing slowing down the requests that followed.
Phase 4 — Vulnerability Analysis
With the schema mapped, I categorised everything by what it could actually do if it worked. Two criticals stacked with a HIGH SSRF — that’s a full compromise chain from a single unauthenticated endpoint.
Phase 5 — Exploitation
F-001 — OS Command Injection via systemDiagnostics (Critical, CVSS 9.8)
systemDiagnostics(username, password, cmd) clearly gates on credentials before doing something — the question was which credentials. First, the obvious guess:
“Password Incorrect” — not “Username Incorrect” — confirmed admin was a real username and the check was live, just guessable. A short, respectful pass through common lab default passwords (not a rockyou brute-force — this is a shared community box) landed a hit:
Replayed live in the browser to capture it in the act:
admin:changeme clears the credential check, then cmd runs straight through to a shell — uid=1000(dvga) back in the response.
That’s a shell result, not an error message. ; chaining worked cleanly, meaning cmd is handed straight to a shell rather than an argv array — full arbitrary command execution as the dvga user inside the container.
F-002 — Broken Access Control: Unauthenticated Private Paste Disclosure (Critical, CVSS 8.6)
The pastes query takes a public boolean. The obvious question: does the server check who’s asking, or does it just trust the flag?
Captured live in the browser, no session established beforehand:
Two pastes flagged "public": false, phone number included, returned to a client that never authenticated.
No Authorization header. No session cookie. No login mutation called first — and it returned private data anyway. Any unauthenticated user can read every private paste in the system.
F-003 — SSRF via importPaste (High, CVSS 7.5)
With RCE already confirmed, importPaste(host, path, port, scheme) was tested for completeness rather than as the primary path:
Replayed live in the browser to capture the exact behavior:
The request hangs until the upstream proxy gives up — consistent with the backend trying to reach an attacker-chosen host with no destination validation.
The 504 is consistent with the backend making a blind outbound request to the attacker-supplied host:port and hanging past the upstream proxy timeout — the destination is fully attacker-controlled with no allow-list visible in behavior. Full out-of-band confirmation was intentionally not pushed further on this shared instance; the RCE above already demonstrates equal or greater internal-network reach.
Documented, Not Executed — deleteAllPastes()
The schema also exposes a zero-argument deleteAllPastes() mutation with the same missing-auth pattern as F-002. This was deliberately not run — the target is a shared public practice instance, and executing it would wipe data for every other person testing against it. It’s recorded as a finding from schema exposure plus the access-control pattern already proven above, not as an executed PoC.
Phase 6 — Post Exploitation
At this point the application is fully compromised from an unauthenticated starting position. Between the two criticals and the SSRF, an attacker would have: full arbitrary command execution inside the container; complete read access to every “private” paste in the system; and a blind internal-network fetch primitive that, combined with the RCE, gives equivalent reach to whatever else sits on that internal network.
Kill Chain
Key Takeaways
- Introspection is a force-multiplier for every other bug on this list — it turned blind API testing into “read the schema, go straight to the interesting fields.”
- Client-supplied flags (
public: false) are not access control. Every access decision has to be re-derived server-side from who is actually asking. - A parameter literally named
cmdnext to a login-gated resolver is still worth testing — weak/default credentials on debug endpoints are common enough to justify a short, respectful guess list. - If your GraphQL API would be safe with introspection on, verbose errors on, and every filter client-trusted — you don’t need any of those safety nets, so turn them off anyway.
Remediation Summary
| Finding | Root Cause | Fix |
|---|---|---|
| F-001 Command Injection | cmd passed to a shell; weak hardcoded credential |
Remove debug resolvers from shared builds; use subprocess with an argument list, never shell=True; rotate credentials |
| F-002 Broken Access Control | Visibility filter trusted from client | Re-check ownership/session server-side for every record returned |
| F-003 SSRF | No destination validation on importPaste |
Allow-list host/port/scheme; block loopback/link-local/RFC1918 |
| GraphQL Introspection | Left enabled outside dev | Disable introspection and GraphiQL in any non-development environment |
| deleteAllPastes exposed | No auth on a destructive mutation | Require authentication + admin authorization + confirmation |
Disclaimer
This assessment was conducted against pentest-ground.com, a public security-testing practice range, with authorization confirmed for this engagement and scope limited to port 5013. The deleteAllPastes() mutation identified in the schema was documented but deliberately not executed, out of consideration for other users of this shared instance. All techniques documented here are for educational purposes only. Do not replicate against systems you don’t own or have explicit permission to test.
Generated with BlackOps — SweshInfoSec