GraphQL攻击面总结


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 中的大多数类型都会是对象类型。

  • nameappearsInCharacter 类型上的字段,这也是可查询到的内容。

  • 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端点之后就可获取并查看接口文档,点击某条接口可在窗口中直接发起请求

接口信息比较详细,这也导致了后续同样基于内省查询的其他攻击的派生

防御方式:生产环境下关闭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

https://juejin.cn/post/7012133339037450271#heading-11


文章作者: half90
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 half90 !
评论
  目录