FortiGate二月份发布版本更新,修复多个中高危漏洞,其中一个严重级别漏洞是SSL VPN的未授权越界写漏洞,漏洞预警称该漏洞可能被在野利用。本文将介绍笔者分析利用该漏洞,实现远程代码执行的过程。
https://www.fortiguard.com/psirt/FG-IR-24-015
本文漏洞分析使用的环境是FGT_VM64-v7.4.2.F-build2571
。
diff patch
对修复版本的二进制进行对比(7.4.2和7.4.3),分析发现修复代码位于函数sub_18F4980
(7.4.2) 。
分析该函数不难发现,该函数逻辑是读取HTTP POST请求的body数据。同时根据Transfer-Encoding
请求头判断是按照chunk格式读取,还是根据Content-Length
读取。根据控制流图对比结果,存在两处代码修改:
1. 解析chunk格式时,调用ap_getline
读取分块长度,检查ap_getline
的返回值是否大于16,大于16认为是非法的chunk length。
2. 读取chunk trailer时,写入\r\n
的偏移line_off
的赋值来源,修复前line_off
的值来源于*(_QWORD *)(a1 + 744)
,修复后line_off
为ap_getline
的返回值。
继续向前回溯,可以找到*(_QWORD *)(a1 + 744)
的值正是第一处校验的chunk length字段的长度。
同时阅读代码可以得知,当chunk length字段经过hex解码后值为0时,就会进入到chunk trailer读取的逻辑。
触发越界写
经过对patch的分析,我们可以得到以下结论:
1、解析chunk时,如果chunk length字段hex解码后值为0,则开始chunk trailer的读取。
2、调用ap_getline读取chunk trailer后,会根据chunk length字段的长度向缓冲区中写入\r\n
。
因此,如果chunk length字段传入很多个0,0的长度大于剩余缓冲区长度的1/2时,就会触发越界写入\r\n
。通过调试可知目标缓冲区位于栈上(函数sub_1A111E0),偏移0x2028的位置保存了返回地址。如果在偏移0x202e的位置写入\r\n
,当函数返回执行ret
指令恢复rip时就会因地址非法产生崩溃。
Crash PoC:
崩溃现场:
通过分析漏洞成因可知,利用该漏洞可以实现栈上越界写\r\n
两个字节,越界范围接近0x2000。由于写入的内容非常有限,无法通过直接劫持rip实现RCE。因此需要把目光放在栈上保存的内存指针上。
失败的尝试
比较容易想到的是劫持rbp,通过覆盖rbp的低字节,使rbp刚好指向可控的内存区域。当上一级函数返回执行leave ret
指令时,就可以完全劫持rip。然而验证时发现即使覆盖了栈上的rbp,也无法劫持rsp和rip,甚至程序不会产生崩溃。继续向上回溯,找到sub_1A111E0
的父函数sub_1A26040
,该函数在返回时并没有调用leave ret
来恢复rsp,而是直接add rsp, 0x18
,因此无法达到预期的效果。
另寻突破点
如上一小节看到的那样,sub_1A26040
函数在栈上保存了rbx、r12-r15五个寄存器的值,并在函数返回时恢复这些寄存器。继续向上回溯找到父函数sub_1A27650
。可以看到r13中保存的正是参数a1
。
a1是一个结构体指针,通过调试也可以看出栈上保存的r13
是一个堆地址。
如果通过越界写覆盖图中红色区域的内存,那么sub_1A26040
函数返回时恢复r13寄存器,就可以篡改a1
指针的值。如果能够对堆内存进行布局,使得a1指向提前布置好的内存区域,那么就可以劫持整个a1结构体。同时通过分析sub_1A27650
和sub_1A26040
的代码逻辑,其中存在大量a1多级结构体成员的动态函数调用,因此劫持a1会有更多的利用机会。
劫持结构体
根据设想,a1指针的低字节被覆盖成\r\n
后,可以恰好指向预先布置好的内存。如图所示:
为实现这一效果,需要达成如下条件:
a1结构体地址比堆喷区域地址更高,并且二者间隔很小。
0x7fxxxxxxx0a0d
一定指向伪造的结构体。
调试可以发现a1结构体的大小是0x730,根据jemalloc的对齐规则,会分配到0x800大小的堆块。0x800的堆块在请求处理的过程中并不常用,因此很容易把tcache中0x800的堆块耗尽,同时申请更多新的0x800的块,使得释放后进入tcache。堆喷也选择不常用的大小的堆块,使得新申请的堆块是连续的,同时与新申请的0x800距离较近;堆喷选择使用较大堆块,以保证其地址为0x800对齐,这样就很容易做到每一个伪造的结构体地址的低12比特为0xa0d;堆喷范围不小于0x10000,以保证0x7fxxxxxxx0a0d
指向堆喷的区域。劫持后的效果如图:
寻找可利用的多级指针
通过上述操作,可实现a1结构体的劫持。梳理函数sub_1A27650
和sub_1A26040
的代码,其中存在多处a1结构体成员二级指针和三级指针的动态调用,例如:
当满足 *(_BYTE *)(a1+0x20*(N+6)+0x10)&6==0
(0<N<5)时,就会动态调用*(__int64 (__fastcall **)(__int64))(*(_QWORD *)(*(_QWORD *)(a1 + 0x298)+0x70)+0xC0)(a1)
因此,需要把a1 + 0x298
成员伪造成一个多级指针,最终指向我们想要调用的函数。由于目标二进制没有开启PIE保护,所以可以在目标二进制寻找符合条件的多级指针。分析二进制,可以发现Rela重定位节中每一个条目的第一个字段都指向对应函数的GOT表地址。
Rela重定位条目
GOT表
因此,以system
函数为例可以找到符合条件的多级指针
在堆喷时,将结构体偏移0x298处的值改成0x4368d0
就可以实现对system函数的调用,效果如下:
如图所示,动态调用的参数正是a1,指向的内存可控。到这里正常就可以利用system函数执行任意命令了。但是在FortiGate中,/bin/sh
文件不具备执行命令的能力,因此使用system函数执行命令无法执行成功。
劫持RIP
由于system函数无法执行命令,只能再想别的办法完成RCE。现有的条件是可以调用任意的GOT表函数,函数的第一个参数指向的内存可控,所以如果GOT表中存在某个函数会回调参数中的某个成员,就有机会实现RIP劫持。很容易想到在以前的FortiGate漏洞利用中经常使用到的函数SSL_do_handshake
。
只需要构造SSL结构体,使得满足条件,最终调用s->handshake_func(s)
,就可以实现rip劫持,把rip劫持到0xdeadbeef如图:
ROP
FortiGate主程序是一个All in One的二进制,大小已超过70MB,有大量的gadget可以利用,利用ROP实现RCE并不困难,不再赘述。
尽管7.4.2版本SSL VPN默认已关闭web mode,浏览器访问返回403,但是该漏洞仍然可以在默认配置下完成利用。
该漏洞与去年的CVE-2023-27997
XOR导致的堆溢出漏洞类似,都是看起来比较鸡肋的溢出漏洞,利用过程比较Trick,更像是一道CTF题目。但是与传统攻击堆管理器的CTF题目相比,真实漏洞更多的需要借助上下文的结构体和代码逻辑完成利用。笔者水平有限,如有纰漏,欢迎指正。
【版权说明】
本作品著作权归zbleet所有
未经作者同意,不得转载
zbleet
天工实验室安全研究员
专注于二进制安全、IoT安全、浏览器安全