1. HTB 这个评估到底考什么:GraphQL 安全测试的全貌
搞安全测试的兄弟应该都清楚,Hack The Box 的 Skills Assessment 系列不是那种随便点点就能混过去的题库,它要求你在限定时间里对一个模拟目标完成从信息收集到漏洞利用的完整链路。这次碰到的Attacking GraphQL技能评估,核心就是把 GraphQL 接口当成攻击面,考察测试者对这种 API 查询语言的安全测试能力。
先把话说清楚:GraphQL 不是一个小众玩意。GitHub、Shopify、Meta、福布斯这些体量的产品都在用,它允许客户端精确请求自己需要的数据,省流量、省请求次数,前后端协作效率高。但麻烦就麻烦在,它的"灵活查询"天生就是双刃剑——REST 接口的端点、参数、返回结构都摆在明面上,你能按图索骥找问题;GraphQL 只有一个统一的端点,所有的查询都靠客户端自己拼,这反而给攻击者留下了远比 REST 更大的发挥空间。Introspection 泄不泄露 schema、嵌套查询能不能打穿资源限制、resolver 里有没有做权限校验,这些在 REST 时代压根不用操心的问题,到了 GraphQL 这里全变成了高频漏洞点。
这套技能评估最值得做的地方就在这:它不是让你背几个 CVE 编号就完事,而是把整个测试流程拆开,逼你一条条走通——找到 GraphQL 端点、枚举出 schema、分析出可利用的 query 和 mutation、构造恶意查询拿数据或绕过限制。整个过程走完一遍,你对 GraphQL 安全的理解会从"知道有这么回事"变成"真能上手干活"。
2. 先搞清楚你面对的是什么:GraphQL 的核心机制和攻击面
2.1 GraphQL 和 REST 的本质差异,决定了攻击思路完全不同
做测试的人如果还抱着 REST 那套思路去搞 GraphQL,十有八九要碰壁。REST 里你有 GET、POST、PUT、DELETE 这些 HTTP 方法对应不同操作,端点是 /users、/orders、/products 这种清晰的路由;GraphQL 不一样,它基本只用一个端点,比如 /graphql 或 /v1/graphql,所有的操作都通过 POST 发一段 query 字符串过去。
这段 query 字符串长什么样?我直接给个例子:
query { user(id: 123) { name email posts { title } } }服务端收到这段查询后,会按图索骥从根节点找起:先解析 user(id: 123) 这个字段,再由 user 下的子字段决定要返回哪些数据。这跟 SQL 的"select 指定列"思路非常像,只不过 GraphQL 的对象图是你自己在 query 里定义的。
理解了这一点,攻击面的轮廓就开始清晰了。REST 的攻击面是"端点+参数+返回结构",你逐个端点去测就完事。GraphQL 的攻击面只有一个入口,但入口背后的对象关系图可能极其复杂。你得先搞到这个图,才能决定打哪、怎么打。而这恰恰就是我在评估里最花时间的阶段。
2.2 GraphQL 的 Schema、Query、Mutation、Resolver 到底怎么回事
很多刚接触 GraphQL 的人会被一堆名词绕晕,其实拆开理解很简单。Schema 就是接口的类型定义文档,它声明了这个 API 里存在哪些对象、每个对象有什么字段、字段是什么类型。比如:
type User { id: ID! username: String! email: String password: String posts: [Post] } type Query { user(id: ID!): User users: [User] post(id: ID!): Post } type Mutation { login(username: String!, password: String!): AuthPayload updateProfile(bio: String!): User }Query 是查询入口,对应 REST 里的 GET,用来读数据;Mutation 是变更入口,对应 POST/PUT/DELETE,用来写数据。至于 Resolver,它是每个字段的实际处理函数,数据从哪拿、权限判断在哪做,全在 Resolver 里。
判断一个 GraphQL 应用安不安全,关键看两层:暴露层和执行层。暴露层就是 Schema 本身,你是否把不该暴露的字段(比如 password、内部 token)也写进了类型定义里;执行层就是 Resolver 实现,权限校验有没有严格做,查询参数有没有被拼接进数据库操作。这两层只要有一处稀烂,这个 GraphQL API 就基本是裸奔状态。
2.3 GraphQL 攻击面全景图:从枚举到利用
我在评估里把攻击步骤拆成了几个阶段,每个阶段对应一类问题,实际操作时也是按这个顺序往下推的:
| 阶段 | 核心动作 | 主要风险点 |
|---|---|---|
| 信息收集 | 找 GraphQL 端点、探测是否有 introspection | 无 introspection 时靠字典爆破字段名 |
| Schema 枚举 | 通过 introspection 或工具拉取完整类型定义 | 敏感字段、内部接口暴露 |
| 查询分析 | 逐个 query/mutation 梳理参数类型和返回结构 | 参数过滤不严、权限缺失 |
| 漏洞利用 | 构造恶意查询,绕权限或打资源耗尽 | 嵌套查询、别名批量、IDOR、注入 |
这套流程跟 REST 测试的最大区别在第 3 和第 4 步:REST 的参数你就照着一个端点慢慢测,GraphQL 你得先理解对象之间的关联关系,再决定如何构造一条高价值查询链。比如评估里可能出现这样的场景:User 对象关联了 Account 对象,Account 对象里又有 role 字段,你需要通过这条链路拼出管理员账号信息。
3. 踩点阶段:从零开始识别 GraphQL 端点并枚举 Schema
3.1 怎么快速找到 GraphQL 端点
拿到靶机或目标站点的基本信息后,第一步永远是找入口。GraphQL 端点不像 REST 端点那样有明显的命名规律,但有迹可循。
最常见的路径有:/graphql、/graphiql、/v1/graphql、/api/graphql、/query、/gql。如果目标是个单页应用,你可以先拉一遍前端 JS 文件,在里面搜 "graphql" 或 "apollo" 或 "urql" 这些关键字,基本能锁定请求发到哪个后端地址。这一步在 HTB 的评估里非常关键,因为很多机器会故意把 GraphQL 接口藏在一个不太显眼的子路径上。
找到疑似端点后,怎么确认它是 GraphQL?很简单:向这个地址发一个 POST 请求,Content-Type 用 application/json,body 里放一段最基础的查询,比如:
{"query": "{ __typename }"}如果返回结果是{"data": {"__typename": "Query"}}之类的内容,恭喜,你找到 GraphQL 端点了。__typename 是 GraphQL 的内置元字段,不需要 schema 里定义就能查,属于非常实用的"验货"方法。
另外一个加分项是看响应头。有些框架会暴露自己的指纹信息,比如响应头里带有X-GraphQL-*之类的自定义头,或者返回 400 错误时错误信息里直接带 "GraphQL" 字样。我在评估里就遇到过目标直接返回了 Apollo Server 的报错 JSON,这等于对方帮你把答案写在了门上。
3.2 Introspection:GraphQL 的"自带说明书"怎么用
确认是 GraphQL 端点后,第一件事就是试 introspection。Introspection 是 GraphQL 规范里内置的自省能力,你可以查询当前 API 支持哪些类型、哪些字段、哪些参数。很多没做安全加固的服务默认开着这个功能,相当于把整个 API 的类型定义文档直接送给你。
最基础的自省查询长这样:
query { __schema { types { name kind } } }这一步能拿到所有类型的名称列表,但还不够细。真正干活时我一般直接跑一段完整的 introspection 查询把 schema 全量拉出来,涵盖 Query、Mutation 和所有自定义类型:
query { __schema { queryType { name } mutationType { name } subscriptionType { name } types { name kind description fields { name description type { kind name ofType { kind name ofType { kind name } } } args { name type { kind name ofType { kind name } } } } inputFields { name type { kind name ofType { kind name } } } } } }这段东西有点长,但在 Burp Suite 里我可以直接复用请求模板。HTTP 方法 POST,Content-Type 是 application/json,body 里把 query 字段写好,就能拿到一份结构化的 schema 数据。拿到之后,我会先扫一眼有没有特别扎眼的字段名,比如 password、token、secret、apiKey、internal、admin 这类关键字。
注意:introspection 返回的数据有时候特别大,一整个 schema 可能有几百 KB。我一般会把响应存成文件,然后用 jq 或者简单的 Python 脚本做过滤,而不是直接肉眼盯着 Burp 的输出窗口看——几十个类型上百个字段,看久了眼睛真的会花。
如果 introspection 被禁了,也别慌,不代表没有其他路。可以用工具对字段名做字典爆破,或者结合前端 JS 里残留的 query 片段来猜测。不过这些都是备选方案,评估里大概率不会把路堵得这么死。
3.3 工具辅助:InQL、GraphQL Map、graphql-cop 怎么用最顺手
用手写 introspection 查询拿 schema 完全可行,但效率一般。我在实际测试里一般配合几款顺手工具一起用。
Burp Suite 的 InQL 插件算是首选。装好之后,对着 GraphQL 请求右键就能选 "InQL Scanner",它会自动帮你跑 introspection,把解析出来的 schema 生成一份可视化的结构树,查到的 query、mutation、字段、参数一目了然。用 InQL 有个好处是它可以生成每个 query/mutation 的模板请求,你直接在 Burp 的 Repeater 里改参数就能逐条测试,不用自己手敲一堆 GraphQL 语法。
另一个我常用的思路是用 graphql-cop 做一轮半自动检查。它是一款基于 Node.js 的开源安全扫描工具,对着 GraphQL 端点跑一下,会自动检查 introspection 是否开启、是否支持 alias 批处理、深度限制、CSRF 等问题。它跑出来的报告不一定覆盖所有风险点,但作为前期踩点挺省时间。
GraphQL Map 也值得一提,它能基于 introspection 结果自动分析出可攻击的 query,并尝试执行一些常见攻击载荷,比如绕过认证、注入、批量查询等。工具输出的信息质量取决于你喂给它的 schema 完整度,所以先确认 introspection 拿到完整数据再跑它,效果会好很多。
不过我还是多说一句:工具能帮你扫,但帮你想。评估里真正值钱的不是把工具跑一遍,而是你能从 schema 里敏锐地看出来"这个 mutation 看似是更新个人资料,其实可以通过传参修改 role 字段"。这个分析能力是任何工具替代不了的。
4. 拿数据:从 Schema 到实战攻击路径
4.1 信息泄露:Schema 里藏着多少不该出现的东西
GraphQL 的 schema 设计如果不够克制,很容易把内部数据结构完全暴露出来。我在评估和真实项目里见过最典型的几个问题:
第一个是敏感字段直接出现在类型定义里。比如 User 类型下直接定义了一个emailVerified字段还算正常,但如果出现passwordHash、resetToken、internalId,这就是明显的设计失误。攻击者只要在查询里加上这些字段名,就能直接拿到本不该暴露的数据。而且很多 GraphQL 服务的 Resolver 是自动生成的,ORM 模型里有什么字段它就暴露什么字段,开发者根本没想到要手动过滤。
第二个问题是过度宽松的返回结构。有些查询返回的是一个完整对象,里面包含了几十上百个字段,其中很多是内部标记位、时间戳、文件路径。你在评估时要养成多拉字段的习惯——有些隐藏字段不会出现在官方文档里,但只要 schema 里有,就能查出来。
实际测试时怎么发现这些信息?去翻 introspection 拿到的字段列表,重点搜索这几个关键字:password、credential、token、secret、key、hash、internal、debug、admin。一个字段都不要放过,尤其是那些看起来很不起眼的字段。有一次我在一个评估环境里发现用户对象的notes字段能返回管理员留下的日志信息,里面直接包含了 flag 文件路径。这就是"字段多拉"的回报。
4.2 嵌套深度攻击:一条查询打垮整个后端
GraphQL 有个特性是对象之间可以无限互相引用,比如 User 有 posts,Post 有 author,author 又是 User,理论上你能写出一层套一层的递归查询。如果服务端没有做查询深度限制,这种嵌套查询能把 CPU 和数据库连接池直接打满,造成拒绝服务。
我构造过的最经典的嵌套攻击长这样:
query { user(id: 1) { posts { author { posts { author { posts { author { posts { author { name } } } } } } } } } }每一次嵌套解析都会触发一次数据库查询或对象解析,呈指数级增长。如果目标服务没有深度限制(比如默认只允许几层),这种查询会非常消耗资源。
在 HTB 的评估环境里,这类攻击更多是验证防护策略是否存在,而不是真的要打垮靶机。测试时我一般会先查一个中等深度的嵌套,看响应时间和错误信息,如果服务端返回了 "maximum depth exceeded" 之类的提示,说明它有深度限制;如果正常返回数据,说明防护缺失。判断完就没有必要继续加深了,毕竟授权测试也要有分寸,为拿 flag 把靶机打崩反而是给自己添麻烦。
4.3 别名批量攻击:绕过速率限制的骚操作
如果说嵌套攻击是"纵向往深处打",那别名批量就是"横向往宽处打"。GraphQL 允许在一次查询里通过 alias 别名多次调用同一个字段:
query { a1: user(id: 1) { name } a2: user(id: 2) { name } a3: user(id: 3) { name } a4: user(id: 4) { name } a5: user(id: 5) { name } }这在正常业务里是性能优化手段,但攻击者可以把它变成暴力破解和枚举工具。比如你想枚举一批用户的手机号,就一个个 id 往别名里塞;想爆破登录接口,就把不同的密码组合用别名塞进同一个 query 里。很多 GraphQL 服务的速率限制是针对 HTTP 请求次数的,但你别名批量塞几百个操作在一个请求里,速率限制直接失效。
在评估里我遇到过要求枚举某个资源的场景:目标有一个 query 可以根据文档 ID 获取内容,但单个请求只允许查一份。我用别名批量构造了上百个 ID 的查询,一次性把所有文档全拉了下来,包括一个标注为"内部使用"的文档,flag 就在里面。
这里有个实操技巧:手动敲这么长的 query 不现实,我习惯生成并复制:
query { a0: getDoc(id: 1) { content } a1: getDoc(id: 2) { content } ... }建议直接用 Burp 的 Intruder 或者脚本工具生成。另外注意一下,别一次性塞太多,有些服务端可能对单次请求的查询复杂度或别名数量做了限制,塞太多会把请求整个拒绝。我知道的是有些框架对 alias 数量超过 1000 的请求直接返回错误,所以测试时可以从 100 起步逐步往上加,看服务端能撑到多少。
4.4 认证绕过与 IDOR:Reslover 层能不能守住权限边界
GraphQL 的认证和授权是两层东西。认证简单,就是确认"你是谁",一般通过 JWT、Session Cookie 或 API Key 完成;授权复杂,是确认"你能不能干这件事"。很多 GraphQL 服务只做了认证没做授权,结果就是一个普通用户可以查所有人的私人数据。
我见过最典型的案例是这种 mutation:
mutation { updateUser( input: { userId: 3, role: "admin", email: "hacker@evil.com" } ) { id role } }如果 Resolver 只校验了"你是否登录",没有校验"你要改的这个用户是不是你自己",那你直接就能把自己的账号提升到管理员权限,或者篡改别人的信息。这在安全测试里叫不安全的直接对象引用,REST 时代有,GraphQL 时代依然存在。
测试这类问题时思路要广一点:先去 schema 里找有没有类似user(id: Int)、updateUser(input)、deleteUser(id: ID!)这种以 ID 为参数的 query 或 mutation,然后尝试用别人的 ID 去调用。不要只盯着当前登录用户的权限,多想想"如果换了别人的 ID,结果会怎样"。
另一个容易忽略的点是 mutation 和 query 的分离。有些服务只对 mutation 做了 CSRF 防护或权限校验,但 query 里同样有关键数据。我在评估里遇到过一种情况:mutation 挂了一层管理员校验,但 query 的users字段没有做任何过滤,直接返回了所有用户的完整信息。而"所有用户"里恰好包含了管理员的 resetToken,这个 token 又能用来重置管理员密码。整条攻击链就这么串起来了。
5. 收尾阶段:常见卡点和实操经验
5.1 我在这套评估里踩过的坑和应对方案
整个过程走下来,有几个地方是新手最容易卡的。
第一个坑是 Introspection 被禁之后不会往下一步。很多测试者一发现 introspection 被禁就觉得思路断掉了,其实完全不用慌。你可以用工具做字段名词典爆破,比如用 SecLists 里的 graphql 字典去测试常见字段名。另一个思路是断网分析它的报错信息,有些服务在查询不存在的字段时会返回错误提示,提示里会包含部分 schema 信息——比如"Unknown field 'passwordd' on type 'User'"这句话直接暴露了有个 type 叫 User。这种错误驱动的信息泄露是个很实用的小技巧。
第二个坑是命中了 mutation 参数类型错误但看不懂服务端的报错。GraphQL 规范的报错里有locations、path等信息,看着像天书,其实都在告诉你哪里出了问题。Unknown argument说明你参数名不对,Expected type Int说明类型不对,Cannot query field说明字段名不存在。熟悉这些报错能帮你少走大量弯路。
第三个坑是忽略 GET 请求。GraphQL 其实支持通过 GET 传 query,只要把 query 拼在 URL 参数里就行。有些服务虽然禁了 POST 的 introspection,但 GET 方式还开着,这等于留了个后门。而且 GET 请求的 query 会出现在访问日志里,如果服务端有日志注入或其他日志相关漏洞,也是一个可拓展的攻击面。
5.2 防御视角:这套评估反推出 GraphQL 安全的真正要点
做过攻击测试之后再看防御,很多问题会变得一目了然。这里把我在评估中体会最深、也是真实生产环境中最容易被忽略的几个防护要点按优先级列出来:
- 生产环境必须禁用 introspection:测试环境、联调环境可以开,线上环境直接关。Introspection 是信息泄露的头号入口,关了它攻击者至少盲人摸象得多花几倍时间。
- 设置查询深度限制和复杂度限制:深度限制建议 5 到 8 层,复杂度限制要结合业务类型来定,像嵌套列表字段权重就要提高。
- 限制单次请求的别名数量:主流框架如 Apollo Server 都有 alias 数量限制配置,默认别嫌太少,可以根据实际业务调高到 100 或 200,但绝对不能无上限。
- 授权校验放 Resolver,不要放全局中间件:中间件最多做认证,"这个用户能不能操作这份数据"的校验必须落到每个 Resolver 的实现里,不然 IDOR 一打一个准。
- 响应用最小化原则:GraphQL 的好处是客户端自选字段,但服务端应该在返回前做一次字段过滤,内部标记位、日志信息不要出现在返回对象里。
还有一个很多开发者容易忽略的点:不要直接把 ORM 模型暴露成 GraphQL 类型。ORM 模型里经常有password_digest、is_admin这种内部字段,暴露出去等于给自己埋雷。正确的做法是定义独立的 DTO 类型,只暴露业务需要的字段。但这个建议在实际项目里推行起来比较难,因为图省事的开发太多了。
5.3 最后一锤定音:这套评估我在实际做的时候的体会
最后再说点跟具体技术无关的东西。我在做 HTB 这套 GraphQL 评估时最强烈的感受是:它考察的不是你会不会背攻击载荷,而是你懂不懂 GraphQL 本身。你如果只背了一堆 payload 去套,遇到不同 schema 直接傻眼;但如果你理解了 Schema 定义了什么、Resolver 做了什么、权限校验藏在哪里,那不管靶机怎么变,你的思路都不会乱。
建议所有准备参加这套评估的兄弟,动手之前先把 GraphQL 官方的学习文档过一遍,至少搞清楚 Schema、Object Type、Query、Mutation、Alias、Fragments、Directives 这几个核心概念。我在评估中已经在靶机上实际体会过多次:前一个漏洞利用成功的关键不是你会不会写某个特定 payload,而是你对 GraphQL 查询特性的理解能给你多少种组合尝试的可能。
举个例子,你如果熟悉 Fragments 的用法,在遇到字段很多的对象时,就能用 fragment 把公共字段抽取出来,让 payload 更短更清晰,排查问题也更快。你如果知道 GraphQL 的 Directives 支持条件查询,就能利用@include和@skip做更精细的探测。这些都不是什么高深攻击技巧,但真的到了实战中就是比别人快半步。
整个评估做完,你最大的收获其实不是"我拿到 flag 了"这个结果,而是你脑子里终于建立起了对 GraphQL 安全的完整认知框架——从端点识别到 Schema 枚举,从查询分析到漏洞利用,最后再回到防御视角去理解"为什么这里会出问题"。这套方法论不止适用于 HTB 的评估环境,放到真实授权测试项目里同样管用。