
1. 这个评估到底在考什么先从GraphQL攻击面说起HTB Academy把Attacking GraphQL单独拎出来做技能评估不是因为GraphQL本身有多难懂而是因为在真实渗透测试里现代Web应用用GraphQL做BFF或API层的比例已经高到不能忽视。传统的REST接口还能靠一条条翻Swagger、逐个猜路径来摸资产GraphQL天生就是一个聚合入口传统的那套目录扫描、参数爆破思路在它身上一半是失效的。所以这个评估本质上是在考一件事你会不会用GraphQL自己的语法和特性去攻击GraphQL自己。我最初接触GraphQL的时候也有个错觉觉得它有类型定义、有Schema校验、有强类型约束看上去比REST“安全”。实际打了几个真实目标之后才明白GraphQL的安全边界完全取决于开发者怎么配置它的元信息自描述特性本身就是一把双刃剑一方面方便了前端联调另一方面只要开启Introspection整个后端的数据模型、字段权限、对象关系就是直接摆在攻击者面前的资产清单。这个技能评估里的核心场景几乎都是围绕这个矛盾展开的。再说说GraphQL相对REST的几个关键差异这些差异决定了攻击思路必须换一套。REST是资源导向一个URL对应一个资源权限判断可以落在URL路由这一层很直观。GraphQL是字段导向一个端点对应多个对象和多个字段权限控制如果还是在“接口层”做根本管不到“字段级”。也就是说即使你在POST请求里只查了用户姓名攻击者照样可以通过查询别的字段组合把管理员邮箱、内部ID全拖出来。这个评估里我最深的体会是GraphQL的权限问题核心是字段级授权而不是接口级授权。还有一点GraphQL的请求体是JSON结构一个POST请求里可以同时包含query、variables、operationName等属性。这个特性导致很多传统WAF规则根本没法精确解析GraphQL的查询意图。我在评估过程中尝试过多种注入向量发现稍微调整一下嵌套深度和别名组合就能绕过一些基于关键词匹配的防护这也是为什么GraphQL攻击总是能在边缘地带找到突破口。2. 评估前的知识与工具准备2.1 必须吃透的几个GraphQL核心概念在点开靶场之前我建议先把几条高频基础概念彻底弄明白不然后面连报错都读不懂。一是Query与Mutation的区别前者是查询数据后者是变更数据很多靶场把删改数据的危险操作都藏在Mutation里二是Schema中的对象类型与字段类型尤其要注意那些非空字段和自定义Scalar它们往往暗示着业务的关键数据三是Introspection也就是自省查询这是GraphQL安全测试的万能钥匙。另一个很容易被忽略的点是“变量”机制。GraphQL允许在查询中定义变量并由服务端解析这意味着在测试时你不需要把值硬编码进查询字符串可以单独在variables里传参。这个机制在攻击时非常有用尤其是在构造批量遍历或者IDOR利用时能让你的Payload整洁又高效。很多初学者习惯把所有值都拼在查询里遇到特殊字符被转义就懵其实把值移到variables里就能解决。我在本地也建议搭个最小GraphQL服务做实验。用Node.js的Apollo Server或者Python的Graphene都行不需要搞得多完整只要能起一个带几个对象类型的服务就可以了。我自己当时是用Docker起了一个带PostgreSQL和GraphQL的简单项目然后在里面故意留一个暴露内省的开关和一个缺少授权的Query反复练习各种查询写法。有这样一个本地环境你在打靶场的时候试错了也不会留下什么负担可以放心大胆验证想法。2.2 工具怎么选Burp、GraphQL Playground和命令行工具方面我的建议是一套组合拳不是单靠某一个工具走到底。Burp Suite还是第一主力因为它能同时做代理抓包、改包、重放和批量Intruder操作。GraphQL的请求都是标准POST加JSONBurp对它没有特别的解析限制唯一要注意的是需要打开一个叫做“GraphQL插件”的功能或者用一些社区扩展来格式化查询语句。我自己在评估时习惯把Burp的Repeater当成GraphQL查询调试器用先手工构造完整请求体确认无误后再保存到Intruder做变量遍历。这个流程在遇到需要枚举用户ID和遍历不同字段组合时非常高效。如果你更喜欢可视化交互GraphQL Playground或者Altair是很好的选择。它们最大的好处是可以自动补全字段当你输入一个大括号后它会从Schema里拉取可用的字段名这在初期摸清对象结构时省了不少事。而且它们可以直接显示请求报错报错信息里往往直接吐露了数据类型的名称和校验规则这些都是攻击面的线索。但我必须说一句实际站在红队视角最终还是要回到curl或者Python脚本。因为当你需要批量发送查询、解析大量JSON响应、或者把多阶段攻击串联成自动化脚本时GUI工具就明显拖后腿了。我会在评估中经常开一个终端窗口用Python的requests库写一段小脚本去做内省查询然后把返回的Schema保存成JSON文件再在本地用jq去筛选可疑字段。这个工作流你可以直接借鉴。3. 实战第一步信息收集与内省查询3.1 先摸清到底有没有GraphQL端点HTB的GraphQL技能评估通常不会直接把端点用大写字母写在页面里需要你先做一轮基础信息收集。我习惯的路径是从主页源码开始看有没有关联的JavaScript文件、API前缀或者带GraphQL字样的配置项。很多前端框架会把API地址打包在静态JS里搜索关键词“graphql”“query”“mutation”往往能直接命中。如果源码里没有就试常规路径。常见的有 /graphql、/api/graphql、/v1/graphql、/gql、/query 等。这个评估里我遇到过把GraphQL挂在 /graph 这种简短路径上的情况所以字典最好稍微宽一些。这里有一个很实用的技巧GraphQL端点和服务端其它REST端点不同即使你发一个最简单的非法查询只要它确实是GraphQL服务响应往往是JSON格式的errors数组而不是普通的404页面。我通常在确定候选端点后先发一个最简单的探测请求就是查询 __typename 这个内置元字段。任何合法的GraphQL端点都必须支持这个字段因为它属于GraphQL规范的元字段不需要额外配置。如果响应返回了 {data: {__typename: Query}} 这样的内容那就十拿九稳了。这个探测方式比直接跑大内省Payload温和得多也能避免触发一些防护机制。3.2 内省查询的完整写法与过滤技巧确定端点存在之后下一步就是把Introspection拉满。完整的Introspection查询会拉回所有类型、字段、参数、枚举值和指令方案这里有一段我很常用的核心Payload{ query: query { __schema { types { name kind description fields { name description type { kind name ofType { kind name ofType { kind name } } } args { name description type { kind name ofType { kind name } } } } } } } }这个查询会返回整个Schema的JSON结构但返回信息量太大直接在终端里看会非常痛苦。我的习惯是先把响应保存到文件然后用jq提取出所有对象类型的名称curl -s -X POST http://target.com/graphql -H Content-Type: application/json -d {query:query { __schema { types { name kind } } }} | jq .data.__schema.types[].name这样一过滤你就能快速看到有哪些自定义类型。评估环境里通常会暴露类似 User、Company、Flag、Role 这样的对象类型只要看到Flag或者Secret这种关键词思路基本就清晰了一半。还有一点经验GraphQL的Introspection查询在某些框架里可以被部分禁用或者只允许特定角色触发。如果直接内省被拒可以试着加上Authorization头用未授权用户、普通用户和管理员用户分别跑一遍。很多时候开发只给启用了内省功能却忘了限制谁能调用这个权限盲区非常常见。3.3 从Schema里快速划定攻击目标拿到Schema之后不要逐行读先做这几个维度的筛选。首先是所有Mutation这是最高价值的攻击点因为它意味着你可能拥有写权限其次是所有包含password、token、secret、role、admin、isAdmin等关键词的字段最后是对象之间的关联关系比如User对象里有没有一个指向Admin或Company的字段这种关系往往就是越权的跳板。我在这个评估里第一次拉出Schema时就注意到User类型里有一个字段叫 notes而它的解析器返回的不是数组而是一个单独的Note对象。直觉告诉我这个设计有蹊跷因为正常业务里一个用户应该有多个笔记为什么会用单数后来经过测试发现这个字段的解析器根本没做用户归属校验直接通过请求中携带的某个参数去查询数据库里的笔记导致任何用户都能通过调整参数读取到其他用户甚至管理员的笔记。这就是典型的GraphQL字段级越权它和REST里越权很像但因为字段可以自由组合触发方式更隐蔽。通过内省筛选可疑字段时我还会特别留意有没有直接暴露ID或internalId的字段。GraphQL服务经常把数据库主键直接映射成ID字段这在业务上可能是为了前端缓存方便但对攻击者来说就是现成的越权索引。后续测试时直接遍历这些ID就可以了。4. 攻击思路落地越权、注入与Mutations利用4.1 IDOR与对象关系跳板很多人在做REST接口测试时对IDOR很熟悉但到了GraphQL里往往转不过弯来。其实IDOR在GraphQL中同样高发而且因为GraphQL的强类型结构你可以直接在同一个查询里把多个关联对象一起拉取一次请求就能把用户的全部资料、所属组织、最近操作记录全部拖出来。我在评估中遇到的情况是GraphQL提供了一个名为 user(id: ID!) 的查询按常理它应该只允许查询当前登录用户的信息。但在测试时我发现即使我使用一个低权限账号的Token只要把ID参数替换成其他人的ID返回结果里照样包含目标用户的所有敏感字段。这说明后端只在某个聚合接口上做了鉴权而底层的User解析器完全没有用户维度过滤。面对这种情况最简单的利用方式就是用Burp的Intruder跑一个ID遍历把ID从1跑到1000同时只选择查询一个字段名比如 username。这样响应体很小速度很快。等确认了某个管理员账号的ID之后再回到正常查询里把该ID的邮箱、手机号、密码重置Token等字段全部拉取出来。这个流程的关键在于第一次遍历时不要贪多字段越少响应越快越不容易被服务器限流拦截。4.2 利用Mutation做权限提升仅靠查询越权往往只能拿到数据如果能拿到写权限整个攻击的杀伤力会上一个台阶。GraphQL的Mutation在设计上经常犯的错误是只校验当前用户是否已登录但不校验当前用户是否拥有操作目标资源的权限。这就导致任意登录用户可以修改任意资料。技能评估中有一个很经典的场景提供 resetPassword 或 updateUser 类Mutation其中包含一个直接设置密码的字段。我当时的利用路径是先通过内省查询确认Mutation的参数结构发现它可以传入 id 和 newPassword然后我用自己的Token把id改成管理员ID直接重置了管理员的密码。整个过程不需要管理员原始密码也不需要任何额外的校验Token。这里有一个值得注意的细节Mutation的响应往往会回显更新后的整个对象包括一些你本来不打算查询的字段。比如我修改完密码之后响应里把目标用户的 role 字段也返回了这让我一下子看到了管理员组的具体角色名称。如果你在评估中看不到某个Mutation的返回结构可以用一个内联片段展开所有子字段这样响应里就能拿到完整的更新后对象。4.3 别名、批量查询与绕过技巧GraphQL支持在同一个请求里用别名查询多个对象这个特性在攻击中有两层用法。第一层是绕过WAF限制比如某些防护规则会拒绝包含多个重复字段的查询但用别名后每个字段名都不同规则就不生效了。第二层是批量爆破一个请求里写20个别名就能同时查询20个不同ID的用户速度比Intruder逐个请求快得多而且不容易触发基于请求频率的限流。我常用的一个批量查询Payload大概长这样{ query: query { u1: user(id: 1) { username role } u2: user(id: 2) { username role } u3: user(id: 3) { username role } u4: user(id: 4) { username role } } }这个查询一次就能拿到4个用户的信息。如果你知道目标ID范围把别名扩展到50个、100个也是没问题的。但有一个前提服务器有查询深度和复杂度限制别一次写几百个别名把自己Do了那不是攻击成果是自己给自己制造麻烦。另一个容易忽略的绕过思路是“字段建议”。GraphQL默认的错误提示会返回这样的内容Cannot query field xxx on type User. Did you mean email or username?。这个错误提示本身就是一种信息泄露开发如果关闭了内省却忘了关闭字段建议攻击者依然可以通过试探性查询一点点拼出可用的字段列表。我在评估中把这个方法称为“盲猜字段法”后面会单独讲怎么用。4.4 注入与解析器滥用场景虽然GeaphQL的强类型机制能自动过滤一部分注入攻击但底层的解析器如果不规范还是会引入SQL注入等经典问题。我遇到过的情况是某个字段的参数被直接拼进SQL查询虽然GraphQL层做了参数类型校验但到数据库层时参数值没有经过预处理导致单引号触发数据库报错从报错内容里能看出来后端使用的是PostgreSQL。遇到这种情况时我的操作方式是先在错误提示中观察参数被拼接到SQL的位置然后根据数据库类型构造对应Payload。如果无法直接拿到数据就采用时间盲注思路在GraphQL查询里注入类似AND 1pg_sleep(5)的语句通过响应时间判断条件真伪。这个方法在评估中确实可行但效率不高建议先花时间找找更直接的越权点注入作为备选方案。另外一个值得一提的解析器滥用是嵌套查询导致的资源消耗。GraphQL允许无限嵌套子字段如果开发没有设置查询深度限制攻击者可以构造一个循环嵌套的查询把服务端资源耗尽。比如{ query: query { user(id: 1) { friends { friends { friends { friends { friends { name } } } } } } } }如果后端解析器对friends字段的解析不做层级控制这个查询会让数据库执行大量的关联查询最终拖垮节点。虽然这不是技能评估的核心考点但在真实渗透中往往是压垮目标的最后一根稻草需要掌握。5. 技能评估完整复现思路从枚举到Flag5.1 搭建一个贴合评估场景的本地靶场打技能评估之前强烈建议先在本地把一个相似的漏洞环境跑起来。我常用的方案是用Docker运行一个轻量GraphQL服务模型上模拟了一个“内部用户管理系统”包含User、Company、Note三个对象类型开放了Introspection并且故意在Note查询和用户信息更新里留了越权漏洞。用Apollo Server写这样一个Mock服务大概只需要几百行代码花不了一个下午的时间但对于练习来说价值极大。本地起好服务之后先用Burp确认请求流量正常再用Python写一个自动化的探测脚本。脚本做的事情很简单请求Introspection、解析Schema、筛出所有Mutation和含有敏感关键词的字段、然后对每个可接受ID参数的查询跑一遍ID遍历。这个脚本无论如何都要保留好评估时只需要换一下目标地址和Token就行等于把“侦察到利用”之间的手动劳动减少了一半。5.2 从普通功能入口突破到管理权限实际评估里推荐的路径是先用一个普通用户凭据登录然后通过内省拿到Schema注意观察有没有管理端才应该有的对象类型或查询字段。有相当一部分靶场会故意把Admin类型的某些字段暴露给普通用户的查询只是在前端页面上没有入口而已。通过GraphQL你可以绕过前端UI的限制直接访问那些隐藏字段。我在这个环节里常用的方式是把Schema里所有查询字段拉出来后优先看有没有操作名称里带Admin、Internal或者Debug等字样的方法。一旦发现这样的查询就试着用当前普通用户Token直接调用如果服务端只根据Token的有效性判断权限而没有检查用户的角色属性那这一步就能直接拿到管理端数据。拿到管理端数据结构后通常会有一个包含Flag或者密钥的字段这个字段在普通列表查询里不会展示但通过GraphQL的一次聚合查询就能读出来。5.3 利用mutation扩大战果并读取Flag当越权查询已经能读到大部分数据时如果还没拿到最终Flag那就需要回到Mutation上做文章。很多评估场景在数据读取之外还藏了一个需要通过写操作触发的最终目标比如上传一个恶意配置文件、修改某个业务对象的状态或者重置一个服务账号的密码。我遇到过的最终步骤是这样的通过内省发现一个叫 createTicket 的Mutation它接受一个目标用户ID和一个内容字段。从Schema上看这个Mutation没有任何权限参数于是我尝试使用普通用户Token把目标用户ID指向一个管理员的Ticket队列。结果服务端没有校验该操作是否属于当前用户直接调用了底层创建逻辑导致我向管理员的Ticket区域写入了一条恶意消息。在这个消息里我附带了一个读取服务器文件的路径管理员一旦查看就会触发服务端读取并返回文件内容。虽然这个流程比较绕但完全符合评估的难度设计。这段经历给我的教训是面对GraphQL靶场永远不要只停留在“读数据”这一步。GraphQL的Mutation种类往往比你想的多每一个看似不起眼的写操作都可能成为连接普通用户与管理员权限的桥梁。6. 常见问题与排查技巧实录6.1 高频报错逐一拆解我在整个评估过程中遇到最多的一类报错是 Syntax Error: Expected Name, found String。这个报错多半是因为查询语句里的引号嵌套出了问题。你用Burp构造JSON请求时如果直接在query字段里写查询外层是JSON双引号内层的GraphQL字符串就得用转义双引号稍不留神就会出错。我的建议是先把GraphQL查询语句用单独的工具写好并调试通过再复制到JSON里转义或者干脆用在线JSON转义工具处理一下。第二类高频报错是 Variable $xxx is not defined。这个通常是因为你在GraphQL查询里声明了变量但JSON请求体里没有传variables字段或者在variables里漏掉了对应的变量名。遇到这种情况先检查operationName是否对应再检查variables里的键名是否和查询里的变量名完全一致。GraphQL对变量名大小写敏感一个字母写错都会报这个错非常容易踩坑。第三类报错是 Cannot return null for non-nullable field。这个报错说明你查询的某个非空字段没有被正确解析常见的原因包括当前角色的权限不足解析器内部抛了异常或者你传入的ID不存在。遇到这种报错不要急着放弃它本身就是信息你可以确认这里有一个非空字段而且它的值通常很重要值得重点测试。6.2 内省被禁时的盲打方法如果内省查询返回错误比如 Field __schema is not allowed开发是真的把Introspection关了。这种情况下不要灰心引入一个叫“字段建议”的技巧服务端即使关掉内省通常也不会关闭拼写纠错建议。你只需要尝试查询一个可疑字段名比如查一个不存在的 userr如果服务端建议你改成 user那就等于免费帮你确认了字段名。利用这个机制我可以一点点把对象的字段列表拼出来。假设已经知道有一个User对象我依次尝试 username、email、password、secret、token、role、isAdmin 等常见字段名服务端对所有不存在的字段会返回 Does your type have these? 这样的提示而对存在的字段则正常返回数据。通过一段时间的试探即使没有完整Schema也能拼出一个足够用的攻击面。这个方法在真实目标上比内省还隐蔽因为每次请求看起来都像是开发者调错了字段名不容易触发告警。6.3 我的几个操作习惯与避坑建议第一所有GraphQL测试请求尽量使用单独的Burp Scope把目标限定在靶场域名避免不小心把请求发出去了外部服务。第二保存好每一次内省查询的结果因为Schema可能不是一成不变的有些靶场会随机化字段名保存好原始结果可以在后续测试时反复对比。第三养成用jq或Python脚本处理JSON响应的习惯不要用眼睛硬看大段响应那样既慢又容易漏掉关键字段。另外还有一个容易被忽视的坑就是Token时效。评估环境里的登录Token往往有较短的过期时间或者绑定IP。如果你的自动化脚本跑着跑着突然全部返回认证失败先去检查Token是否过期而不是怀疑Payload写错了。我在一次评估中就浪费了半小时排查一个“时间盲注不生效”的问题最后发现只是Token过期了白白消耗了耐心和精力。7. 个人实操体会与最后一点分享做完整个Attacking GraphQL技能评估我最明显的感觉是GraphQL安全测试并不需要发明新的攻击类型它真正考验的是你对业务数据结构的理解能力。REST攻击很多时候是“猜路径”而GraphQL攻击是“读Schema、筛字段、找关系”。一旦你掌握了内省查询和字段筛选这套方法攻击思路会变得非常清晰。我自己在整个学习过程中最受益的一点是每次拿到一个新的GraphQL服务不管是不是靶场都会先做一次完整的内省查询并保存成文件。即使当时找不到任何利用点这个文件也是后续攻击的基础。很多看似无解的目标回头再去翻Schema总能发现一些当初忽略的字段或者Mutation。最后再分享一个小技巧给所有GraphQL测试请求加一个自定义Header比如 X-Client-Name: pentest这有助于你不小心把测试流量混入正常业务日志时自己能快速识别出来。实际评估中这个习惯已经帮我省了好几次排查时间。GraphQL攻击最大的门槛不是语法或者工具而是能不能耐下心把一个Schema从头到尾吃透。只要你跨过这一步HTB这类技能评估就只是一场结构化思维训练而不是玄学。