在为我们的一位客户进行红队评估期间,我们有机会了解 Fortigate SSL VPN,这是全球最常用的 VPN 解决方案之一。我们在 VPN 面向互联网的接口上发现了一个堆溢出漏洞。此漏洞无需身份验证即可访问,可用于在 Fortigate 实例上远程执行代码。CVE-2023-27997已分配,CVSS 为 9.2(但实际上是 10)。我们相信这个错误已经存在很长时间了(超过 7.x 和 6.x 分支)。有关受影响版本的详细信息,请参阅 FG-IR-23-097 。
我们将在这里描述两个架构上的漏洞和利用过程,以及一些给蓝队的提示。
错误
该错误位于允许用户对 VPN 进行身份验证的 Web 界面上。这个界面在设计上是面向互联网的。如果我们命中路径,我们可以通过 GET 或 POST/remote/hostcheck_validate发送一个名为 的 HTTP 参数。encparameter,现在好像用的不多,好像是Fortigate跨请求转发HTTP参数的老办法了。
该enc参数是一个包含种子、大小(2 字节)和数据的结构。大小和数据都是加密的。
enc 数据包:种子、大小、数据
种子存储为 8 个十六进制字符,用于计算 XOR 密钥流的第一个状态:
salt是由服务器创建的随机值,可以通过向 发出 GET 请求来检索/remote/info。
密钥流的其他状态计算如下:
密钥流示例值
密钥流小号可以与其余的enc有效负载、大小和密文进行异或运算,以对它们进行解密。它作为十六进制字符串发送。
解密方法如下所示(简化代码):
该函数的行为如下:
计算 MD5(16 字节),这是来自盐和种子的密钥的第一个状态(前 8 个字符in
)
分配大小为in_len / 2 + 1
,out
和十六进制解码输入的缓冲区
given_len
通过将有效负载的前两个字节与密钥的前两个字节进行异或运算,计算用户给出的长度
边界检查:验证给定的长度不大于缓冲区的大小
就地解密整个字符串:对前 14 个字节进行 XOR,然后计算一个新状态
在解密数据的末尾放置一个 NULL 字节
将解密值添加到包含 HTTP 输入参数的哈希图中
但是,当程序检查给定长度不大于发送的有效负载的长度时,它搞砸的不是一次,而是两次!首先,我们可以看到实际大小仅与给定大小的低有效字节进行比较。
if (in_len - 5 <= out[4] ^ md5[0]) // <-- This should be: in_len - 5 <= given_len
第二个问题是以十六进制(例如in_len)描述有效负载的长度,而以原始字节(例如)描述其大小。因此,可以是应有的两倍大。所以,即使演员阵容制作得当,并与进行比较,我们仍然会有错误!'41424343'given_len 'ABCD'given_lengiven_lenin_len
这个漏洞让我们不仅可以将解密过程应用于 中的密文out,还可以应用于之后的内存。
这导致了一个有趣的错误:我们不只是覆盖堆中的字节,而是用一些 MD5 对它们进行异或运算!
漏洞利用实践
现在我们有了一个像样的原语,我们需要找到一些东西来覆盖。
选择的目标:SSL
2019 年,Meh Chang 和 Orange Tsai 利用了同一个二进制文件的堆溢出,并选择修改名为SSL的结构。每当客户端连接到 的工作进程时,就会分配这种结构sslvpnd,并在客户端或服务器关闭套接字时销毁。
这对我们来说是一个完美的目标。
首先,因为我们的原语要求我们多次触发错误,而且我们需要攻击在 HTTP 请求中持续存在的堆结构。
其次,因为该结构包含一个回调处理程序,handshake_func. 由于二进制文件不是 PIE,我们可以修改这个函数指针的值,并强制套接字执行 SSL 握手。从那里,一个标准的堆栈枢轴到任何给我们一个 nodejs shell 的地方(没有 sh!)。
要设置堆,我们需要在与我们控制的套接字关联的某些 SSL 结构之前有一个空块。
攻击 64 位
我们的测试环境是在 Intel 64 位上运行的 VM。SSL 结构的大小为 0x1db8 字节,因此它被分配在 0x2000 字节的区域中。
在分配 SSL 结构之前,还会分配一个大小为 0x2000 的缓冲区,以存储客户端发送的原始 HTTP 请求。对于未碎片化的堆,缓冲区位于内存中 SSL 结构的顶部。
这就是我们想要的地方out!幸运的是,如果客户端发送的请求大于 0x2000 字节,程序会重新分配缓冲区,使该区域为空。由于分配器是后进先出法,并且我们控制 的大小out,因此这是一个非常干净的利用:
为 HTTPd 服务创建大量套接字(填补空白):
SSL 结构被创建,与请求缓冲区连续
在最后一个套接字上发送一个巨大的 HTTP 请求,以强制重新分配缓冲区:
重新分配 HTTP 请求缓冲区
使用另一个套接字来利用该漏洞;out在我们需要的地方分配:
out分配在 SSL 结构之上
由于sslvpnd二进制文件很大,因此构建 ROP 链并不难。它留给读者作为练习。!注意:模式很大程度上受此启发。!
攻击 32 位
一段时间后我们发现有针对 32 位架构的 Fortigate 构建,例如 ARM。我们的 redteam 目标运行在这样的处理器上。在 32 位中,SSL 结构的大小基本上是 64 位中的一半(它主要由指针组成),因此它被分配在大小为 0x1000 的区域中。上述堆设置不起作用。
然而,我们非常幸运:除了 0x2000 缓冲区外,程序还分配了一个 0x1000 缓冲区来存储……HTTP 响应。当太大时,它也会被重新分配。不费吹灰之力,我们就找到了一个网页,该网页会回显一些输入,从而导致 32 位漏洞利用:
为 HTTPd 服务创建大量套接字(填补空白)
采用最后一个连接,以及一个带有巨大 POST 参数的请求
参数得到回显,强制重新分配 HTTP 响应缓冲区
使用另一个套接字来利用此漏洞:溢出的缓冲区在我们需要时得到正确分配。
指标
可以通过向以下两个 URL 中的任何一个发出 GET/POST 请求来利用该漏洞:/remote/hostcheck_validate, 和/remote/logincheck。简单的攻击将需要对这些 URL 中的任何一个快速连续地发出多个 HTTP 请求。但是,通过仔细设置堆,可以使漏洞利用速度变慢。
开发后
攻击者有可能只存在于内存中,而不修改磁盘上的任何内容。尽管重新启动可能会删除内存中的有效负载,但有一些方法可以在设备上实现持久性。保护自己的最好方法是PATCH。
示范
该视频展示了我们针对 x64 构建的第一个漏洞:
第二,针对 ARM,效率更高:
结论
在 4 月初报告该漏洞后,Fortinet于2023年 6 月 8日修复了7.2.5、7.0.12、6.4.13和6.2.15版本。他们的回应迅速而亲切。然而,考虑到从 2019 年至今发现的漏洞数量和质量,我们仍然怀疑他们是否对设备进行过适当的安全评估。
原文翻译自:https://blog.lexfo.fr/xortigate-cve-2023-27997.html