GraphQL API 漏洞

GraphQL API 漏洞

Tip

如果你还不了解什么是 GraphQL,先看这篇文章。

发现 GraphQL 端点#

Universal queries(通用查询)#

如果你向任何 GraphQL 端点发送:

GraphQL
query { __typename }

其响应的某处会包含下述字符串:

JSON
{ "data": { "__typename": "Query" } }
Note

返回值是根查询类型的名字。大多数服务里是 Query,但有些框架会用别的名字,比如 Hasura 返回的是 query_root。

该查询被称为“通用查询”。之所以“通用”,是因为 __typename 是 GraphQL 规范内置的元字段(meta-field),所有对象类型都自动带有它,根查询类型也不例外。所以无论一个 GraphQL 服务的 schema 定义了哪些业务字段,这条查询都一定合法,也一定能执行成功。

如果是 GET 请求,则尝试构造下述查询:

HTTP
?query=query{__typename}

常见端点探测#

GraphQL 服务经常使用相似的端点后缀:

  • /graphql
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql
Tip

这些端点的后缀也可能包含版本号,例如 /graphql/v1 。

对这些端点发送“通用查询”或者直接访问来判断其是否是 GraphQL 端点。

之所以“直接访问”也能探测,是因为 GraphQL 服务通常会对任何非 GraphQL 请求返回“query not present”或类似的错误。

请求方法切换#

生产环境中的 GraphQL 端点的最佳实践是只接受 content-type 为 application/json 的 POST 请求(用于防范 CSRF 漏洞)。

然而有些端点可能接收其他的 HTTP 方法,例如 GET 请求,或者 content-type 为 x-www-form-urlencoded 的 POST 请求。

因此,如果向常见端点发送 POST 请求未能找到 GraphQL 端点,可以尝试使用其他 HTTP 方法重新发送通用查询或者尝试切换 content-type 的值。

测试查询参数#

如果端点使用参数直接访问对象,则可能存在 IDOR(不安全的直接对象引用)。用户可能只需提供一个与某信息对应的参数,就能访问本不应访问的信息。

例如,下面的查询会请求某网店的商品列表:

GraphQL
#商品查询示例
query {
    products {
        id
        name
        listed
    }
}

返回的商品列表中只包含已上架的商品:

JSON
#商品响应示例
{
    "data": {
        "products": [
            {
                "id": 1,
                "name": "Product 1",
                "listed": true
            },
            {
                "id": 2,
                "name": "Product 2",
                "listed": true
            },
            {
                "id": 4,
                "name": "Product 4",
                "listed": true
            }
        ]
    }
}

从这些信息中,我们可以推断出:

  • 商品按顺序编号分配 ID
  • 列表中缺少 ID 为 3 的商品,可能是因为它已被下架

通过指定 ID 查询缺失的商品:

GraphQL
#获取缺失商品的查询
query {
    product(id: 3) {
        id
        name
        listed
    }
}
JSON
#缺失商品的响应
{
    "data": {
        "product": {
        "id": 3,
        "name": "Product 3",
        "listed": no
        }
    }
}

发现 Schema 信息#

使用 Burp Suite 能很方便地构造内省,详见这篇文章。

Burp Suite 构造的内省查询长这样(Set introspection query):

通过 Schema 信息构造查询/变更/订阅#

Tip

一个完整的、针对自省请求的响应正文见附录。

以 Burp Suite 构造的自省请求对应的响应为例。如果你想要构造查询,则需定位:

JSON
"queryType": {
	"name": "query"
},

如果你想要构造变更,则需要定位:

JSON
"mutationType": {
	"name": "mutation"
},

订阅则定位:

JSON
"subscriptionType": {
	"name": "subscription"
},

以变更为例,你看到的 name 字段的值为 mutation,则继续定位:

JSON
{
	"kind": "OBJECT",
	"name": "mutation",
	……
}

此时你能看到 mutation 对象中支持的字段信息(fields 中定义的),比如:

这里 mutation 仅有一个字段即 deleteOrganizationUser,如果你要使用该变更,则需继续看它:

  • 支持的参数(args)
  • 支持的返回类型(type)

参数就一个即 input, 通过 kind 能知道它是“输入对象类型”,它的类型名是 DeleteOrganizationUserInput,继续追踪该类型名:

JSON
{
	"kind": "INPUT_OBJECT",
	"name": "DeleteOrganizationUserInput",
	"description": null,
	"fields": null,
	"inputFields": [
	{
	  "name": "id",
	  "description": null,
	  "type": {
		"kind": "NON_NULL",
		"name": null,
		"ofType": {
		  "kind": "SCALAR",
		  "name": "Int",
		  "ofType": null
		}
	  },
	  "defaultValue": null
	}
	],
	"interfaces": null,
	"enumValues": null,
	"possibleTypes": null
},

该输入对象类型中有一个名为 id 的字段。

此时,变更请求的大体框架已经可以构建了:

Text
mutation test {
  deleteOrganizationUser(input: { id: 123 }) {
	  # 按需提供返回字段
  }
}

支持的返回类型见:

JSON
"type": {
	"kind": "OBJECT",
	"name": "DeleteOrganizationUserResponse",
	"ofType": null
},

是一个对象类型,名为 DeleteOrganizationUserResponse,追踪得到:

JSON
{
	"kind": "OBJECT",
	"name": "DeleteOrganizationUserResponse",
	"description": null,
	"fields": [
	{
	  "name": "user",
	  "description": null,
	  "args": [],
	  "type": {
		"kind": "NON_NULL",
		"name": null,
		"ofType": {
		  "kind": "OBJECT",
		  "name": "User",
		  "ofType": null
		}
	  },
	  "isDeprecated": false,
	  "deprecationReason": null
	}
	],
	"inputFields": null,
	"interfaces": [],
	"enumValues": null,
	"possibleTypes": null
},

该对象中有字段 user,并且也是一个对象类型("kind": "OBJECT"),其名为 User,还得追踪 User:

提供了两个字段,并且它们的类型都是标量类型,因此无需继续追踪了。

最终,可能的变更请求:

Text
mutation test{
  deleteOrganizationUser(input: { id: 123 }) {
    user {
      id
      username
    }
  }
}

自省被禁用了#

建议(Suggestions)#

Apollo GraphQL 是围绕 GraphQL 建立的完整的生态系统和平台,它提供了一个名为 Suggestions(建议)的功能,即针对报错信息:

Text
Cannot query field "help" on type "Query"

建议会在后面添加额外信息:

Text
Cannot query field "help" on type "Query". Did you mean "hello"?

这使得即使内省被完全禁用,也可以从建议中获取 Schema 的相关信息。

而且,根据官方文档的描述,Suggestions 是默认被开启的。

专用工具#

clairvoyance 是针对“自省被禁用”场景专门创建的工具。

该工具会将 GraphQL 服务器返回的错误信息当侧信道,通过词表(内置,也可以自指定)进行不断的探测从而最大程度恢复出 Schema。

绕过 GraphQL 的内省防御#

插入特殊字符#

开发者在禁用内省时,可能会使用正则表达式来排除查询中的 __schema 关键字。你应该尝试在 __shcema 关键字后插入空格、换行符和逗号等字符,因为 GraphQL 会忽略它们(而有缺陷的正则不会,从而造成不匹配过滤规则使查询被放行)

例子(假设开发者只排除了 __schema{),下面这个内省查询会被放行。

JSON
#带换行符的内省查询
{
    "query": "query{__schema
    {queryType{name}}}"
}

切换 HTTP 方法#

内省可能仅在 POST 方式下被禁用,可以尝试 GET 请求,或将 content-type 的值换为 x-www-form-urlencoded。

HTTP
# 以 GET 请求发送的内省探测
GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D

使用别名绕过速率限制#

GraphQL 提供的部分功能可能涉及到一些敏感操作:

  • 查询优惠券是否有效
  • 查询用户是否存在
  • 登入是否成功
  • ……

因此,对 GraphQL 的调用次数通常会受限。

但 GraphQL 提供了别名,你可以通过一次调用实现多次查询。

例子(用于检查商店折扣码是否有效):

GraphQL
#带别名查询的请求
query isValidDiscount($code: Int) {
    isValidDiscount(code:$code){
        valid
    }
    isValidDiscount2:isValidDiscount(code:$code){
        valid
    }
    isValidDiscount3:isValidDiscount(code:$code){
        valid
    }
}

GraphQL CSRF#

正常情况下,客户端以 Content-Type 为 application/json 对 GraphQL 端点发起 POST 请求。

在该情况下,CSRF 几乎是无法实现的,因为 Content-Type 为 application/json 的请求属于复杂请求,客户端会先发送 OPTIONS 预检测请求,只有服务器明确返回允许的 CORS(Cross-Origin Resource Sharing,跨域资源请求)头,才会正常发起请求。

但是,如果 GraphQL 端点没有正确校验 Content-Type 或者并没有限制请求方法,则可能出现 CSRF 漏洞。

需要注意的是,当你将 Content-Type 改为 application/x-www-form-urlencoded 之后,你对应的请求体也要改为表单类型。

原先:

HTTP
POST /graphql/v1 HTTP/2
[snip]
Content-Type: application/json
[snip]

{
  "query": "\n    mutation changeEmail($input: ChangeEmailInput!) {\n        changeEmail(input: $input) {\n            email\n        }\n    }\n",
  "operationName": "changeEmail",
  "variables": {
    "input": {
      "email": "foo@test.com"
    }
  }
}

修改后(注意 URL 编码):

HTTP
POST /graphql/v1 HTTP/2
[snip]
Accept: application/x-www-urlencoded
[snip]

query=mutation+changeEmail%28%24input%3A+ChangeEmailInput%21%29+%7B%0A++changeEmail%28input%3A+%24input%29+%7B%0A++++email%0A++%7D%0A%7D&operationName=changeEmail&variables=%7B%22input%22%3A%7B%22email%22%3A%22hackerfoo%40test.com%22%7D%7D

附录#

键盘快捷键

全站

CtrlK
搜索
/
搜索
?
显示快捷键
Esc
关闭弹窗、退出模式

阅读文章

T
目录
F
专注模式
A
批注模式

批注模式

P
笔
H
荧光笔
L
激光笔
E
橡皮擦
1–6
切换颜色
Shift
按住拖动画直线
CtrlZ
撤销
CtrlShiftZ
重做