2022 年 2 月 28 日,CVE 官方发布了影响 Watchguard 防火墙等设备的 RCE 漏洞 CVE-2022-26318,3 月 27 日,该漏洞的 EXP 开始在网上流传,本文对此漏洞的成因进行分析。
建立调试环境
获取 shell
FireBox 提供的 busybox 经过大量阉割,并且没有 sh,首先通过挂载磁盘的方式修正设备环境。
导入虚拟机之后将其虚拟硬盘挂载到另外一个系统下,将完整版的 busybox 和 sh 放在 /bin 目录下,并且赋予它们 suid 权限,然后修改 /etc/passwd 文件中的 root 用户密码。
之后将 exp 中执行 /bin/python -i 替换为 /bin/sh -i,执行后即可获取 shell,不过此时是 nobody 权限,执行以下命令提权到 root
1 | busybox su |
调试
通过 wget 将 gdbserver 下载到 /tmp 目录下,尝试执行会提示无权限,检查 mount 信息发现 /tmp 目录被挂载为 noexec,使用以下命令重新挂载
1 | mount -o rw,remount /dev/wgrd.var /tmp |
然后放入的 gdbserver 即可正常运行。
默认情况下只有 web 等向外提供服务的端口可以访问,为了远程调试,我们需要修改防火墙规则。
进入系统后台,在 Firewall 选项下添加新的防火墙规则
这样防火墙其他端口就可以正常访问了。
漏洞分析
根据相关公告,存在漏洞的文件是 wgagent,接口为 /agent/login
逆向分析此文件,搜索 /agent/login 字符串可以找到处理 login 请求的 handler 函数
1 | if ( s1 && (!strcmp(s1, "/login") || !strcmp(s1, "/agent/login")) ) |
login_handler 函数开头就会调用函数 wga_parse_input 处理 POST 数据,部分关键代码如下
1 | void *__fastcall wga_parse_input(__int64 a1, const char *content_type) |
用户提交的数据应该是 XML 格式,程序使用 libxml2 对数据进行解析,在函数开头使用了 xmlSAX2InitDefaultSAXHandler,初始化 SAX handler,SAX 是一种 XML 解析方式,在 libxml2 中使用 SAX 解析 XML 时需要初始化一些 handler,我根据 libxml2 源码对部分 handler 进行了重命名
1 | s[12] = startDocument; |
其中值得注意的是 startElementNs,根据源码注释,这个回调函数会在每次一个新的 element 开始时被调用
1 | SAX2 callback when an element start has been detected by the parser. |
也就是说每当遇到一个新的 xml 标签开始时,程序会执行此函数,而这些 handler 由 Watchguard 开发者自行实现。
我们来看 startElementNs 和 endElementNs 函数代码
1 | int *__fastcall startElementNs(__int64 a1, const char *tag_name) |
1 | int __fastcall endElementNs(__int64 a1, const char *a2) |
关键在于 (a1 + 80) 变量(简称为 swi),此变量控制了分支结构的执行顺序,我们按照 exp 中构造的 xml 节点进行分析。
1 | <methodCall><methodName>agent.login</methodName><params><param><value><struct><member><value><AAAAA...AAMFA><BBBBMFA><BBBBMFA><BBBBMFA>...<BBBBMFA><BBBBMFA>payload |
首先解析到 methodCall 开始标签,调用 startElementNs 函数,解析后将 swi 设置为 1
遇到 methodCall 结束标签,swi == 1 没有匹配情况,值不变
遇到 methodName 开始标签,swi == 1 在 startElementNs 中对应 case 1,设置 swi = 2
遇到 methodName 结束标签,swi == 2 在 endElementNs 中对应 case 2,设置 swi = 3
遇到 params 开始标签,swi == 3 在 startElementNs 对应 case 3,不过没有匹配任何情况,swi 不变
遇到 param 开始标签,swi == 3 在 startElementNs 对应 case 3,有匹配情况但 swi 不变
遇到 value 开始标签,swi == 3 在 startElementNs 对应 case 3,没有匹配情况,swi 不变
遇到 struct 开始标签,swi == 3 在 startElementNs 对应 case 3,没有匹配情况,swi 不变
遇到 member 开始标签,swi == 3 在 startElementNs 对应 case 3,有匹配情况,设置 swi = 4
后续存在大量填充的开始标签,由于此时 swi = 4,在 startElementNs 中对应 case == 4,标签名不等于 name,会直接 break 跳出 switch 结构。
最终会来到关键点 startElementNs 的第 136 行,调用 strcat 函数将 tag_name 即标签名直接拼接到全局变量 s 中。
显然此处缺少对 tag_name 长度的检查,如果构造超长的 tag 填充到全局变量会导致变量溢出。
利用分析
注:调试过程中程序多次重启,所以某些图片中地址可能不同
首先 payload 头部需要构造合适的标签序列,让 startElementNs 函数最终能够达到 “稳定” 状态,即如 exp 中所构造的,让 swi 变量始终等于 4。
所以 payload 第一部分为
1 | payload = "<methodCall><methodName>agent.login</methodName><params><param><value><struct><member><value>".encode() |
这样后续标签只要不等于 name,就可以一直触发 strcat 部分代码
接着查看一下全局变量 s 附近的内存布局
在拷贝第一个标签时看到变量 s 位于 0x427360,由于程序没有开启 PIE,所以这个地址固定。
在 s 下方刚好衔接了程序的 heap 段,所以理论上有两种利用思路,其一是通过溢出覆盖掉 bss 段中其他全局变量,这些变量可能在程序某处被使用,其二则是构造足够长的数据覆盖 heap 中的数据,利用堆的某些操作实现利用。
实际分析程序可以看到 s 下方唯一一个变量 0x427760 默认值为 0,暂时没看到可利用的点,所以只能通过操作堆尝试利用。
调试 EXP
调试原作者的 exp,其 payload 首先构造的很多用于填充的标签,第一部分用一个标签填充 3184 个字节,拷贝后内存布局:
从地址 0x428000 开始就是 heap 区域。
第二部分是 3680 个 <BBBBMFA> 标签,相当于填充了 3680 * (7 + 1) = 29440 字节,但实际调试中这些字节不会被全部覆盖到 heap 中,当覆盖到地址 0x429e60 时,继续执行就会调起新的程序。
观察 0x429e60 附近的内存:
地址 0x429e68 为 startElementNs 函数指针,地址 0x429e70 为 endElementNs 函数指针。
最后一个标签覆盖后 0x429e68 地址会变为 0x41464d,然后在地址 0x7f01f16024a1 会调用此指针
我猜测 payload 覆盖了 libxml2 中的 SAX handler 结构体(xmlSAXHandler 结构?),程序准备解析下一个标签时要调用 startElementNs 函数指针,但是此时指针已经被破坏,而 0x41464d 对应指令为 ret 0x90be
需要注意的是,我们输入的 payload 也存在于栈上:
调用 0x41464d 指针使用的是 call 指令,所以 ret 指令会直接回到 call 下一条指令继续执行,同时 rsp 会增大(下移) 0x90c6 字节,随后进入函数返回流程,最终执行到 ret 指令时内存布局:
我们发现此时已经进入 ROP 流程,栈中的 payload 刚好对应 exp 中第三部分 payload
1 | payload += b'\x00' + b'\x20\x50\x40' + ... |
ROP 所用的 gadget 列举如下
1 | 0x405020 ret |
对 ROP 分析请看代码中的注释,简单来说,此程序的 stack 区域默认具有可执行权限,作者用一种比较巧妙的方法将栈的地址加载到寄存器 rax 中,然后跳转到 rax 执行 shellcode,shellcode 部分利用 syscall 实现了将 python code 写入 /tmp/test.py,然后执行 /usr/bin/python /tmp/test.py 的操作,最终实现任意代码执行。
简单总结
该漏洞成因是使用 libxml2 SAX 解析 xml 时,startElementNs handler 的编码有问题,代码中没有检查 tag_name 长度就直接将其拼接到全局变量中,导致全局变量溢出。
程序内存布局比较特殊,bss 区域后面紧跟着 heap 区域,所以可以将全局变量的溢出转变为堆溢出,另外堆中又保存了 xmlSAXHandler 结构,可以将其中的 handler 指针覆盖,从而劫持控制流。
exp 作者通过巧妙的构造,将控制流首先劫持到 ret 0x90c6,由于传入的 POST 数据也会位于栈上,通过精心控制偏移,可以将程序劫持到 ROP 链上,从而将指针覆盖转换为栈溢出利用手法。
ROP 链中又构造出能够直接将控制流转移到栈上的 gadget,避免了需要爆破地址的问题。由于栈具有可执行权限,通过控制偏移即可跳转到 shellcode 上执行。
shellcode 中构造 syscall 将 python 代码写入本地文件并执行,解决了 /tmp mount 为 noexec 的问题。
很多完美的内存布局降低了开发利用代码的难度,加上作者巧妙的利用手段,使得此 exp 稳定性较高,危害较大。
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2022/03/30/CVE-2022-26318/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。