Laravel 是一个广泛使用的开源 PHP Web 框架。它可用于相对轻松地创建复杂的 Web 应用程序,并在许多流行的项目中使用。

在最近对该应用程序的渗透测试中,我们获得了对框架环境文件的访问权限。该文件包含许多敏感的 Laravel 配置设置,包括应用程序 APP_KEY 。

APP_KEY 用于多个与安全相关的任务,在过去,APP_KEY的信息是获得远程代码执行的可靠方法,因为它用于对(序列化的)XSRF令牌签名。了解APP_KEY的攻击者能够创建恶意的XSRF令牌,然后使用已知的小工具链,通过不安全的反序列化(CVE-2018-15133)导致RCE。

由于 Lavel 5.6.30 默认禁用 cookie 序列化。因此,APP_KEY不再授予一个有保证的RCE,攻击者需要在攻击路径上更有创意一点。在这篇文章中,我们会重点介绍攻击者可以利用泄露的环境文件利用的一些替代攻击媒介。

APP_KEY 基础知识

Laravel 期望它的环境文件 .env 在应用程序的根文件夹中。该文件包含几个 Laravel 配置设置,包括 APP_KEY、数据库凭据和常规设置等机密信息。文件内容的泄露(例如通过任意文件读取漏洞)会产生严重影响,因为泄露的信息可能会以多种方式被滥用。

最显着的秘密是APP_KEY,它经常可以被滥用来获得RCE。使用 APP_KEY 的最值得注意的函数是:

1.Laravel 的默认“encrypt()”和“decrypt()”函数。如果开发人员想要加密或解密任何值或对象,那么他们很可能会使用这些函数。它们也被用来加密Laravel的会话cookie,从而进行篡改保护。

2.Laravel 还具有创建防篡改签名 URL 的内置功能。此外,大多数签名函数使用应用程序 APP_KEY 作为机密。

3.在复杂的 Web 应用程序中,你可能会看到需要对时间不敏感的任务进行排队(例如发送提醒邮件)。这样的任务可以通过 Laravel 队列来处理。由于队列对象可能存储在外部,它们也使用 APP_KEY 进行签名。

我们将简要讨论过去利用APP_KEY的方式和原因,以及当前最常用的利用方式。然后,我们将详细讨论通过Laravel队列提供程序进行攻击的情况,它允许攻击者利用Laravel实例,甚至不需要APP_KEY。

Laravel队列可以通过各种队列提供程序实现,如Amazon SQS,Redis,local等,在我们的示例中,我们将重点讨论如何利用在Amazon SQS中实现的队列。

过去的攻击媒介

让我们首先快速了解一下过去Laravel是如何通过不安全的反序列化被利用的。在5.6.30版本之前,Laravel默认对Laravel会话cookie进行序列化和反序列化。与许多其他语言一样,这为反序列化攻击打开了大门。

一旦攻击者获得对 APP_KEY 的访问权限,他们还可以将任意对象签名或加密为 cookie。攻击者可以简单地使用已知的 PHP 小工具链(例如 PHPGGC)创建一个恶意序列化对象,对恶意对象进行签名,然后将其发送到会话 cookie 的位置。Laravel 试图反序列化恶意制作的会话 cookie,并触发了小工具链的安全相关副作用(通常是 RCE)。

在 2018 年 8 月发布的 Laravel v5.6.30 之前,这种利用路径是可能的。

负责解密cookie的代码(Laravel 5.4)可以在以下中间软件代码片段中找到。中间软件提取cookie,并将它们发送到加密器类进行解密。

乍一看,这段代码似乎还不错。然而,decrypt()函数有第二个默认参数unserialize=true(请参阅 Laravel API 文档),它定义了在解密过程中必须对传递的(加密的)值进行反序列化。

可以看到,解密后 Laravel 尝试反序列化攻击者控制的 cookie(恶意序列化对象),这会触发 PHPGGC 小工具链的安全相关副作用。现在 cookie 处理行为已经改变,Laravel调用解密函数时无需对内容进行反序列化。这可以防止先前已知的带有泄露 APP_KEY 的简单利用路径:

目前的利用

由于现在的利用并不那么简单,我们需要一些其他的攻击媒介。

滥用其他不安全的“decrypt()”调用

在理想的攻击场景中,易受攻击的 Laravel 应用程序仍然会简单地反序列化一个用户控制的对象,该对象受 APP_KEY 的篡改保护。当我们分析一些流行的 Laravel 应用程序和扩展时,我们注意到一些开发人员犯了与 Laravel 的会话 cookie 相同的错误。因为“decrypt(…)”函数默认反序列化对象,所以开发人员很容易忽略将攻击者控制的加密内容输入其中,即使他们并不期望得到PHP对象。

例如,Laravel Package 的OPcache 就是这种情况,这是一个帮助开发人员处理 PHP OPcache 的插件。该软件包安装了一个中间软件控制器,用于处理对 laravel-opcache 的所有请求。opcache 处理程序通过提取和解密密钥 URL 参数来执行授权检查。这是通过使用默认的“解密”函数而不禁用值的反序列化来完成的。在以下代码片段的第 13 行中,可以看到如何使用默认值 $unserialize = true 解密密钥值。

如果攻击者知道 APP_KEY ,他们可以通过以下方式利用易受攻击的 Laravel 实例:

1.创建恶意的序列化PHP对象;

2.使用泄漏的APP_KEY加密对象;

3.将有效负载发送到易受攻击的opcache处理程序;

4.应用程序将尝试不安全地解密攻击者控制的对象。

利用 Laravel 队列

以下漏洞利用路径在 Laravel 8 和 Laravel 9.2.0 上进行了测试。

按照设计,Laravel 队列需要在(外部)队列提供程序中临时存储任务和对象。为了防止在静止时被篡改,这些对象的部分签名都是使用APP_KEY的。

在研究中,我们发现 Laravel 在签名验证检查之前不安全地处理队列对象。这允许任何可以访问配置队列(例如 AWS SQS 访问)的攻击者获得远程代码执行,即使不知道 APP_KEY。

要理解这个问题,我们需要对队列对象结构有一个大致的了解。通常,队列对象包含以下(对我们来说很重要)元素:

job -将作业排队的类;

data -保存我们将执行的实际队列命令的数据对象;

data.commandName -命令的名称;

data.command -作为序列化对象的实际命令;

data.command.hash - data.command 的签名,防止篡改。

命令对象包含一个哈希,它确保序列化的对象没有被篡改。但是,由于哈希是序列化 PHP 对象的一部分,因此只能在对象未序列化后执行此检查。

因此,在执行对命令对象的非序列化调用时没有事先进行任何验证,这导致了不安全的反序列化漏洞。

队列命令的不安全反序列化

攻击者可以通过将恶意作业注入 Laravel 队列来利用命令对象的不安全反序列化。

我们使用 Laravel 框架本身实现了一个 PoC 漏洞利用,这样我们就可以轻松地重用该功能而无需大量复制粘贴。在 Laravel 中,你可以使用 dispatch 函数将对象分派到队列中,如下面的代码片段所示。

队列对象在 Illuminate 类 Illuminate\Queue\Queue 中处理。此类处理队列对象的创建,该队列对象将被序列化并稍后发送到队列中。出于利用目的,我们可以修复createObjectPayload() 函数。在修复的函数中,我们将合法的命令对象替换为恶意制作的序列化对象。

在示例中,我们使用了来自 PHP 反序列化库 PHPGGC 的 PendingBroadcast Gadget 链 (Laravel/RCE9)。

特别是,我们使用了这个链,因为它在所有最近的Laravel版本中都有效,并且在链中使用了魔术方法 __destruct。基于 __toString 魔术方法的小工具链不起作用,因为 Queue 处理程序从不将我们的恶意 Queue 对象作为字符串处理,因此不会触发这些链。

如果我们现在看一下作业对象,我们将在命令部分看到我们的反序列化小工具。因为我们以一种非常快速和不方便的方式替换了命令对象,所以对象没有使用哈希签名。然而,这无关紧要,因为一旦命令被反序列化,目标就会被利用。

此时我们只需要等到目标处理队列中的恶意作业即可。在此过程中,应用程序将尝试通过从作业对象中反序列化命令对象来获取命令对象。以下代码片段显示了如何在作业命令上调用反序列化函数。

请注意,Laravel 还允许用户使用加密的命令对象。如前所述,加密或解密需要有效的 APP_KEY 才能利用。但是,只要我们的命令以字符串 O: 开头(表示序列化 PHP 对象中的“对象”),Laravel 将始终尝试以明文形式反序列化我们的攻击者控制的命令(第 4-6 行)。

攻击者控制的命令成功反序列化后,队列例程继续。稍后,Laravel 注意到命令对象没有预期的格式。应用程序将抛出异常,然后尝试清理无效的恶意对象。在清理过程中,我们的小工具链的魔术方法 __destroy 被调用,攻击者控制的命令 touch /tmp\/pwnedThroughQueue 被执行。

总而言之,Laravel 在调用对象的反序列化之前不会验证队列作业中的命令对象。因此,有权访问队列(SQS、Redis 等)的攻击者可以在不知道 APP_KEY 的示例中获得 RCE。如果攻击者通过不公开 APP_KEY 的漏洞获得对队列的访问权限,则这种攻击场景特别有趣。(例如,队列连接的硬编码或易于猜测的凭据,或泄露的 AWS 访问令牌)。

利用由于队列闭包的任意作用域

此漏洞利用路径也适用于 Laravel 队列闭包。闭包是“需要在当前请求周期之外执行的简单任务”,它们允许攻击者通过队列执行任意 PHP 代码。

然而,在本例中,攻击者需要访问队列和 APP_KEY。如果 Laravel 环境文件被公开,则通常会满足这些条件,因为该文件包含所有必要的信息。与前面的示例不同,这种方法不依赖于反序列化,因此如果没有可用的小工具链可用,它也可以工作。

如下所示,可以使用以下代码片段将闭包分派到队列中:

我们首先假设队列闭包通常期望在用户定义的类上运行,在用户定义的作用域内,例如 Podcast 类或 Newsletter 类。此外,我们认为队列闭包中的函数需要实际存在。然而,这两个假设都是错误的。只要队列关闭作业中的作用域存在,攻击者就可以执行任意 PHP 代码。

作为概念证明,我们修改了现有的供应商类(在我们的攻击者环境中)Illuminate\Cookie\Middleware\EncryptCookies 以包含我们的恶意 sendMaliciousClosure() 函数。EncryptCookies 类应该存在于所有 Laravel 项目中,因此作用域应该始终存在。请注意,我们也可以通过反射手动更改前面提到的作业对象内的作用域。

首先,我们更改 EncryptCookies 类,如以下代码片段(第 11-18 行)所示。这样,我们定义了一个闭包,它只是调用 shell_exec 来执行任意操作系统命令。然后这个闭包作为作业被分派到配置的 Laravel 队列中。

我们的恶意关闭可以通过我们自己的 Laravel Artisan 命令发送,如以下代码片段所示。请注意,我们在 EncryptCookies 作用域内静态调用函数 sendMaliciousClosure。这很重要,因为此作用域也需要存在于目标应用程序中。

在作业调度期间,队列闭包被创建并使用泄露的 APP_KEY 签名。下面我们可以看到存储在队列中的序列化作业。我们的 SerializableClosure 可以在命令对象中找到。

当我们向队列发送一个闭包时,所有需要的函数定义都包含在闭包中:

同样,一旦目标应用程序检索到队列对象,它就会尝试(不安全地)反序列化命令对象。完成此操作后,Laravel 使用 APP_KEY 验证哈希。验证哈希后,Laravel 将尝试解析作用域 App\Http\Middleware\EncryptCookies。如果此作用域存在,则执行攻击者控制的闭包或代码。这可以立即得到验证,因为攻击者控制的 echo 是从目标队列工作人员的上下文中调用的。

特别是考虑到闭包,值得一提的是,Laravel并不期望通过队列接受什么。开发人员唯一需要的设置是Laravel Queue的配置。一旦配置了这个队列,Laravel就不会检查它应该处理哪些队列功能,而是 Laravel 会尝试处理它通过队列提供的所有内容。该代码不区分用于处理排队闭包的队列或应该只处理 Newsletter 类对象的队列。

开发工具包/测试环境

为了验证这些漏洞,我们创建了一个小型 Laravel 测试和利用环境。

你可以按照以下步骤设置环境:

复制测试环境存储库:

手动安装 composer 依赖项(这应该不需要,但我们过去遇到了一些问题):

设置测试环境:

laravel_victim_app 和 laravel_exploit_scope_app 需要有相同的 APP_KEY;

laravel_queue_exploit_client_laravel_exploit_1 会有一个随机的 APP_KEY;

你可以在相应的 .env 文件中编辑 APP_KEY 变量;

所有三个环境文件都需要设置相同的 AWS SQS。可以在此处找到有关如何设置的教程。

为了利用目标,应用程序需要监听/工作配置的队列。这可以使用以下命令完成:

然后可以使用漏洞利用客户端发送有效负载。以下命令会将作业发送到队列中,这将利用命令的不安全反序列化:

下一个命令将通过队列闭包执行任意 PHP 代码:

你将看到目标定期处理传入队列。成功的利用应该在目标文件系统的 /tmp/ 目录中创建文件:

如果你想检查哪些代码片段已更改,可以使用 grep 查找字符串“恶意更改”(Malicious Changes),该字符串表示在客户端中更改的代码段。

创建恶意签名 URL

另一个不太严重的利用场景可能是创建签名 URL。使用签名 URL,开发人员可以验证请求的 URL 是否未被篡改。这可以用于各种示例。Laravel 在官方文档中描述的一个示例是将签名 URL 用于“取消订阅”链接。在本示例中,签名链接通过验证防篡改URL的签名来防止用户取消订阅其他链接。

大多数示例中,与其他 APP_KEY 攻击向量相比,创建签名 URL 的影响相对较小。但是,让开发人员依赖签名 URL 来弥补输入验证的不足也是可行的,例如防止 SQL 注入或 IDOR 漏洞。

从以下代码示例中可以看出,签名 URL 创建过程非常简单,可以在 Laravel Artisan 命令中轻松创建:

总结

虽然获得对泄露的 APP_KEY 的访问权限不再是代码执行的保证,但仍有许多攻击场景可以将此目标存档。

大多数示例中,泄露的 APP_KEY 本身不足以利用应用程序。攻击者总是需要额外的错误,例如对“decrypt()”函数的不安全调用或对应用程序使用的队列提供程序的访问。后一种情况甚至完全不需要 APP_KEY。复杂的 Laravel 应用程序使用队列的可能性非常高,因为它们可以减少应用程序对耗时任务的延迟。

再加上云原生应用的份额不断增长,SaaS模型的使用(例如通过 AWS 发送邮件)和 Laravel 构建了对多个外部队列提供程序的支持,这给攻击者提供了一个很好的机会来获得可写队列,总之,这为攻击者提供了另一个攻击媒介,可以在 Laravel 实例上实现远程代码执行。