GraphQL攻击面总结
GraphQL介绍
GraphQL 是一个开源的数据查询语言(DQL)和数据处理语言(DML).最初,GraphQL由Facebook于2012年左右开发并于2015年公开发布。其主要优点之一是为其他Web服务体系结构(如REST)提供了一种更高效,更强大的替代方案。
结构特性
GraphQL并没有绑定数据库,交互逻辑是客户端→GraphQL→后端代码→数据,不同于REST API的交互逻辑:客户端→后端代码→数据库。
传统REST API实现功能一般是一个api对应一个功能,比如请求http://www.test.com/userinfo,后端返回用户信息;当客户端请求主页菜单,实现这个api就需要后端实现另外一个url对应到指定代码。
而在GraphQL中,用户请求的url路径固定,只需要改变请求的post内容,无需维护多个api
比如查询ID为1的人的生日,post内容如下:
当想查询ID为1的人的身高和发色时请求如下:
两个请求的url路由都为同一个且固定。
基本使用
类型语言
在GraphQL查询过程中,基本上是对对象或者对象的某些字段进行查询。客户端在查询之前可以通过schema了解到对象的基本属性字段,当查询请求到后端时,服务器会根据schema验证并执行查询。
GraphQL 服务可以用任何语言编写,它有一套自己的简单语言,称之为 “GraphQL schema language” —— 它和 GraphQL 的查询语言很相似,让我们能够和 GraphQL schema 之间可以无语言差异地沟通。
基本数据结构
一个 GraphQL schema 中的最基本的组件是对象类型,它就表示你可以从服务上获取到什么类型的对象,以及这个对象有什么字段。使用 GraphQL schema language,我们可以这样表示它:
type Character {
name: String!
appearsIn: [Episode!]!
}
Character
是一个 GraphQL 对象类型,表示其是一个拥有一些字段的类型。你的 schema 中的大多数类型都会是对象类型。name
和appearsIn
是Character
类型上的字段,这也是可查询到的内容。String
是内置的标量类型之一String!
表示这个字段是非空的,GraphQL 服务保证当你查询这个字段后总会给你返回一个值。在类型语言里面,我们用一个感叹号来表示这个特性。[Episode!]!
表示一个Episode
数组。因为它也是非空的,所以当你查询appearsIn
字段的时候,你也总能得到一个数组(零个或者多个元素)。且由于Episode!
也是非空的,你总是可以预期到数组中的每个项目都是一个Episode
对象。
学过面向对象编程的应该会感到熟悉,与定义一个对象的过程很相似
GraphQL端点
记录特征能够有效判断资产使用组件情况,能够帮助我们快速发现攻击方向和脆弱目标。常见的GraphQL端点路径如下:
/graphql
/graphql/console
/graphql.php
/graphiql
/graphiql.php
/api/graphql
/***ql
......
以上路径只是协助发现并确认是否使用GraphQL,如果资产使用GraphQL,也可以通过数据包特征确认:
POST或GET包重复请求同一个url,请求传参不同,返回数据也不同
传参为JSON格式,字段多出现query、variables、operationName,返回包的JSON多为
{"data":{...}}
,如下:
- 报错信息:
"Syntax Error: Expected Name, found
基本操作类型
GraphQL官方提供三种操作类型:query(查询)、mutation(变更)、subscription(订阅),最常用的是query,这里就简单介绍下Query方法的使用。
Query方法
假设定义一个查询类型:
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
在这个查询中可以确定有两个字段hero和droid,那么就可做如下请求去使用定义的查询服务,其中在查询droid的时候传入阐述id,意为查询 id为2000的droid对象的name字段信息:
query {
hero{
name
}
droid(id: "2000") {
name
}
}
传参
在GraphQL中,每一个字段和嵌套对象都能有自己的一组参数,从而使得GraphQL可以完美替代多次API获取请求。基本的使用如上面的例子,通过指定对象中某一个字段的值进行参数传递,完成查询。
其中需要注意的是,一个变更也能包含多个字段,一如查询。查询和变更之间名称之外的一个重要区别是:查询字段时,是并行执行,而变更字段时,是线性执行,一个接着一个。这也保证了变更字段值的时候不会出现资源竞态问题。
内省
在交互中,在使用GraphQL之前我们是不知道shema支持的查询类型和所有对象字段信息的,我们可以通过GraphQL的内省系统获取以上全部的信息。
攻击面
1、内省查询暴露接口信息
GraphQL内省是一个特殊查询,使用该__schema字段为其架构查询GraphQL。内省查询的作用是提供详细接口信息,简单的来说就是可以通过内省查询来获取GraphQL的接口文档,如对象定义、接口参数等信息。
内省本身不是弱点,而是功能。但是如果将其提供,则攻击者可能会使用它并滥用它,以寻求有关GraphQL实现的信息,例如存在哪些查询或变异。
使用以下请求内容可获取全部接口信息:
{"query":"\n query IntrospectionQuery {\n __schema {\n \n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n ...FullType\n }\n directives {\n name\n description\n \n locations\n args {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n description\n \n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n description\n type { ...TypeRef }\n defaultValue\n \n \n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n ","variables":{},"operationName":"IntrospectionQuery"}
常用工具推荐:
- 浏览器插件:Altair GraphQL Client,填入合法GraphQL端点之后就可获取并查看接口文档,点击某条接口可在窗口中直接发起请求
https://github.com/2fd/graphdoc 需要npm环境的工具,可生成本地的html文档
https://ivangoncharov.github.io/graphql-voyager/ 在线的web端的分析平台,可直观展示接口与对象、参数的调用关系。将内省查询的json数据粘贴在内省框中,网页就会展示接口调用全貌
接口信息比较详细,这也导致了后续同样基于内省查询的其他攻击的派生
防御方式:生产环境下关闭introspection即内省查询
2、GraphQL playground外网未授权访问
Playground是GraphQL一款开源的IDE,攻击者可在外网暴露的Playground上进行内省查询或者Query等常规操作,作用上类似插件:Altair GraphQL Client,都是为了方便调试GraphQL查询,本身自带内省查询获取接口文档
常见的路径:graphql_playground
3、敏感信息泄露
这个风险不一定存在,需要通过内省查询或者其他能获取查询接口的方法配合人工挖掘。比如接口文档中的敏感对象或者Query的方法名,例如admin、password、各种key、session、email
等
可通过检索objects.types
查询结果入手敏感字段,也可以利用graphdoc这个工具本地生成对应的接口文档后自行检索
查询objects.types
可使用如下查询:
{
__schema {
types {
name
}
}
}
4、Batch批查询导致的DOS
支持批查询的GraphQL允许客户端同时发送多条独立的查询语句,类型主要有两种:json列表批查询、基于名称的批查询
Json list based batching
当我们进行一次query查询,可能如下
{"query": "query { Query { hacktheplanet } }"}
可以用json列表查询判断当前GraphQL是否支持批处理查询:
[
{
"query":"query { assetnote: Query { hacktheplanet } }"
},
{
"query":"query { assetnote: Query { hacktheplanet } }"
}
]
返回了两次查询结果,这就表明当前GraphQL支持批处理查询,那么就可以挑选返回资源较多,或者系统需要响应时间较久的query,同时进行多次查询,占用服务器资源达到DOS的攻击。
[
{
"errors":[
{
"message":"Cannot query field \"Query\" on type \"Query\".",
"locations":[
{
"line":1,
"column":9
}
]
}
]
},
{
"errors":[
{
"message":"Cannot query field \"Query\" on type \"Query\".",
"locations":[
{
"line":1,
"column":9
}
]
}
]
}
]
Query name based batching
原理类似,只是使用查询的姿势稍有不同,之前使用的是json列表,这里使用一条query进行两次查询
{"query": "query { assetnote: Query { hacktheplanet } assetnote1: Query { hacktheplanet } }"}
同样返回两次查询结果表明当前是支持批处理查询的
利用工具https://github.com/assetnote/batchql可检测此类问题
防御方式:
- 控制查询的资源占用;
在 GraphQL 中,存在一个称为查询成本分析的概念,它将权重值分配给解析成本高于其他字段的字段。使用此功能,我们可以创建一个上限阈值来拒绝昂贵的查询。或者,可以实现缓存功能以避免在短时间内重复相同的请求。
- 非必要无需开启批处理查询
5、深度递归查询攻击DOS
根据前文可知GraphQL的最基本的组件是对象类型,一个对象的字段可能是基本的标量例如string,也可能是另外一个对象。
假使有两个对象相互关联如下,并且定义了一个Query支持这两个对象的查询,容易形成无限嵌套
type Thread {
messages(first: Int, after: String): [Message]
}
type Message {
thread: Thread
}
type Query {
thread(id: ID!): Thread
}
当攻击者做如下查询,嵌套多次之后容易造成DOS
query maliciousQuery {
thread(id: "some-id") {
messages(first: 99999) {
thread {
messages(first: 99999) {
thread {
messages(first: 99999) {
thread {
# ...repeat times 10000...
}
}
}
}
}
}
}
}
关于这种关联对象的发现,可以使用前文提到的工具https://ivangoncharov.github.io/graphql-voyager/ ,就可以直观的看到对象之间的调用关系,如果两个对象存在循环调用的关系,就存在此漏洞
另外可使用工具查询获取某参数的查询路径:https://gitlab.com/dee-see/graphql-path-enum
防御方式:增加深度限制,使用graphql-depth-limit模块查询数量限制;或者使用graphql-input-number创建一个标量,设置最大为100
6、CSRF
修改原先json格式的post内容,假如GraphQL的端点支持将query语句使用传参的方式,将Content-Type修改为application/x-www-form-urlencoded,就有可能存在CSRF
偷一下P牛的ppt
7、SQL注入
GraphQL的注入感觉有点鸡肋,可以参考内省查询的接口,在参数后拼接另外一个query或者mutation。
一般要是直接能拿到GraphQL端点,直接使用可用的query或者mutation查询即可,也就不存在SQL注入这一说了。
假设场景接收用户传参,GraphQL的接口不对外开放,参数在拼接成完整的GraphQL语句之后进行查询,那么如果没有做好参数恶意过滤,那么参数就可拼接一个完整的查询或者更改达成SQL注入了。
继续偷一下P牛的PPT,主要注意下GraphQL的语法问题,拼接后能够友好执行
8、接口权限问题
使用内省查询获取的接口如果缺乏鉴权,就可被攻击者越权利用获取数据或者权限。
参考
https://graphql.cn/learn/schema/
https://blog.assetnote.io/2021/08/29/exploiting-graphql/
https://xzfile.aliyuncs.com/upload/zcon/2018/7_攻击GraphQL_phithon.pdf