网络安全漏洞深度剖析
一、漏洞背景
2021 年 7 月 13 日,美国微软威胁情报中心发布安全公告[1],文中指出黑客利用 Serv-U 0day 对极少数美国军工部门成功进行了攻击,同日 Serv-U 母公司 solarwinds 也发布安全公告[2],并针对最新大版本发布了补丁[3]。
(注意:目前发布的补丁只针对最新的 15.2.3 大版本,且只能是付费用户才能下载安装补丁,非付费用户目前无法从官方渠道获取有效补丁。)
根据补丁比较和 fuzzer,宁静之盾安全团队成功复现了该远程溢出漏洞,并能在实战环境下成功利用。
截至 9 月 10 日,互联网上未发现任何有关该漏洞的技术分析和 POC 发布,也未发现更多新闻细节公布。
该漏洞是 2003 年 Serv-U mdtm 漏洞修补后出现的第一个 RCE 漏洞。
为了感谢广大读者伙伴的支持,准备了以下福利给到大家:
1、200 多本网络安全系列电子书(该有的都有了)
2、全套工具包(最全中文版,想用哪个用哪个)
3、100 份 src 源码技术文档(项目学习不停,实践得真知)
4、网络安全基础入门、Linux、web 安全、攻防方面的视频(2021 最新版)
6、 网络安全学习路线(告别不入流的学习)
7、ctf 夺旗赛解析(题目解析实战操作)[一>获取<一]
二、Serv-U 软件说明
该软件是全球流行的商业闭源 FTP 服务器端软件,官方资料显示该软件全球有超过 10W 用户,软件默认支持 ftp(21 端口)/sftp(22 端口)等,出于安全性考虑现在大部分使用的是 sftp 端口(流量使用非对称算法加密),主要运行在 windows 平台,新版本为 X64 程序,该软件 22 端口默认 banner 信息含有详细版本号,通过 zoomeye 搜索显示近一年 IP 量为 13000 个(全量为 6.7W 多)。
其中 15.2.3 大版本有 1100 台,最新补丁版本 15.2.3.742 有 900 台,该漏洞影响范围是 Serv-U 版本 < 15.2.3 HF2(即 15.2.3.742)。按照近一年存活 13000 台,已安装此漏洞补丁 900 台计算,也就是说现在全网至少有 90%的 Serv-U 处于受此漏洞威胁状态(Serv-U 母公司应该是出于商业考虑未对大量存在的老版本发布补丁,老版本升级到最新版本需要再次付费)。
Banner 信息
三、漏洞分析与利用说明
综述
该漏洞是一个远程内存破坏漏洞(可以稳定控制虚函数指针),针对 WINDOWS 版 Serv-U SFTP(22 端口)进行攻击,该漏洞无需任何账号和密码,输入 IP 和端口即可成功攻击,只要版本低于 15.2.3.742,成功率接近 100%(单次成功率达不到 100%,但可以立即再次攻击),利用成功获取 SYSTEM 权限 SHELL。
3.1 补丁比较
下载最新版本 742 和上一版本 723,补丁包文件如下
可以看到主要修改了 4 个文件,使用 Bindiff 依次对这 4 个文件进行比较,最终可以排除掉 Serv-U.exe、Serv-U-Tray.exe 和 Serv-U-RES.dll,因为这三个文件改动非常小,基本上不可能存在漏洞。
同时结合官方的报告,APT 攻击中利用该漏洞可能会报如下错误:
基本可以判断该漏洞是一个 SSH 相关的内存型漏洞。使用 BINDIFF 和 IDA 对主 DLL 的补丁情况比较如下:
左边为未补丁变化的函数地址,右边为补丁后的函数地址
对 Serv-U.dll 中的十余个有变化的函数逐个分析,排除掉 4 个比较错误的函数,一共有 9 个函数发生变化,发现补丁为某些函数添加了硬编码的判断与赋值,比如,判断某个值 198h 是否为 0、1、2、3、4 或 5 等,或者给 198h 赋值为 0、1、2、3、4 或 5 等。
此时,需要配合 IDA 尝试理解这些值的含义,首先定位到该值存储的位置,可以看到该值被存储到 a1[0x198]。
然后二进制搜索 0x198,找到该值被读写的所有位置。
遗憾的是,即使知道了补丁补的位置与代码,也很难揣测补丁的最终意图。于是另辟蹊径,从打过补丁的函数一直向上回溯,依次记录分析,发现函数 sub_180145070 会引用所有打过补丁的函数。
其它几个变化的函数主要是该函数的 switch case 分支里调用的函数,下面我们具体分析该函数,该函数其实是 RhinoNET!CRhinoSocket::ProcessReceiveBuffer,通过命名可以猜到是用于处理收到的数据 BUF,在该函数入口点下断:
180145070 的调用栈如下:
RhinoNET!CRhinoSocket::OnReceive+0x170
mfc140u!CWnd::OnWndMsg+0xba9
mfc140u!CWnd::WindowProc+0x3f
mfc140u!AfxCallWndProc+0x123
mfc140u!AfxWndProc+0x54
mfc140u!AfxWndProcBase+0x49
USER32!UserCallWinProcCheckWow+0x1ad
USER32!DispatchMessageWorke+0x3b5
Serv_U_180000000!CUPnPNotifyEvent::SetTimeout+0x30d85
Serv_U_180000000!CUPnPNotifyEvent::SetTimeout+0x30dfd
ucrtbase!crt_at_quick_exit+0x7d
kernel32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d
仔细观察该函数的结构,一个外部大循环加内部的 switch..case,明显是某种协议的实现,配合关键字符串 SSH_MSG_IGNORE,不难得出结论:函数 sub_180145070 实现了部分或完整的 SSH 握手协议。
通过调试可知,该函数主要用于处理 SSH 消息,依次处理的 MSG 消息如下:
比如上图中:switch(v21)中 v21 就是 SSH 协议中的消息码,标识了消息类型。
补丁后的程序,主要修补了 20,30,21 三个 MSG,其中在这些 MSG 中处理 CLIENT 支持的 SSH 加密算法处多调用了 SSH 库 578,530 和 538 函数做检查(EVP_aes_128_ctr,EVP_EncryptInit_ex 和 EVP_DecryptInit_ex)
同时通过调试可知,修补后的程序,对 MSG 序列的顺序做了限制!
补丁比较后基本判断该漏洞是一个 SSH 协议握手阶段的逻辑处理出错,进而造成的内存处理出错,那么通过补丁比较就找不到传统内存溢出型漏洞补丁一般会对 COPY 长度做限制的地方,要复现这个漏洞就需要对 Serv-U 的 SSH 握手过程进行 FUZZER。
3.2 FUZZER SSH 握手过程
有了上述的信息,便可以写一个发包 FUZZ 脚本(不断生成消息码去测试),通过模拟 SSH 握手阶段的数据交换来模糊测试 Serv-U 服务器 15.2.3.723。
脚本运行一段时间后,得到了一个崩溃,通过分析该崩溃可以确认我们发送完 20 号消息后,不发送 30 号 MSG,直接发送 21 号消息,是造成 Serv-U 崩溃的原因,打上补丁后不会产生该问题。那么说明很可能找到了漏洞点。
同时该漏洞大概率能够利用,因为 Serv-U.dll 没有开启 ASLR,而我们拥有控制 EIP 的能力,所以配合 ROP 便可以远程执行代码了。
如下图,可以看到被调用的函数指针被覆盖为了 AAAAAA……,也就是我们可以利用可控的数据覆盖一个虚函数指针,从而控制函数执行流程以执行我们的 ShellCode。
崩溃时的调用栈如下:
崩溃的最终位置并不在 Serv-U.dll 里,而是在 libeay32.dll,这个动态链接库是 OpenSSL 用于加解密的一个组件。通过跟踪调试,发现握手数据 AAAAAA…的最后 8 字节数据被 libeay32.dll 错误地当作函数指针来调用,从而触发了崩溃。
定位崩溃时发送的数据包,发现该数据包打乱了 SSH 正常的通信过程(握手包乱序),所以补丁中的硬编码值 0、1、2、3、4、5 等是为限制了数据包的发送顺序,即发送了包 MSG 20 之后只能发包 MSG 30,以此缓解该漏洞。
3.3 漏洞利用
通过分析发包流程和 Serv-U 的 SSH 通信处理过程便得知,该**漏洞的成因为 Serv-U 的 SSH 通信处理流程乱序导致的内存未初始化漏洞。**通过网络数据包列举漏洞触发原理及过程如下:
SSH 通信正常的处理过程为
密钥交换初始化(申请 N 字节内存,每个版本可能不同);
密钥协商算法交换参数,比如 ECDH(填充 N 字节内存:在内存块内填充函数地址等);
加解密数据(调用 N 字节内存中的函数地址)
触发漏洞的过程为
密钥交换初始化(申请 N 字节内存);
加解密数据(调用 N 字节内存中的函数地址);
可以看到漏洞触发过程省略了 ECDH 密钥交换协商这一步。并且在第一步申请 N 字节时并未初始化内存。
漏洞触发利用过程
发包占坑布局 N 字节内存空间,并释放;
密钥交换初始化(申请 N 字节内存,该内存内容已被提前布局)
加解密数据触发漏洞**(由于缺少了密钥协商设置 N 字节内存块内函数指针这一步,所以会调用 N 字节内存中已被提前布局的函数地址);**
漏洞补丁原理
漏洞补丁限制了 SSH 通信过程,所以不会再导致内存中函数指针被攻击者提前布局并利用的情况。
通过分析该崩溃,确定了该漏洞大概率能够利用,因为 Serv-U.dll 并没有开启 ASLR,为我们提供了用于布置 ROP 链的必要条件。除此之外,上述崩溃的位置在 CALL 指令处,这使得我们大概率可以控制 EIP。当同时拥有以上两种能力后,就可以控制程序跳转到布置好的 ROP 链上执行预先指定的代码。
虽然 Serv-U.dll 中没有导入 VirtualProtect 函数,但是它导入了另外一个能够执行代码的函数 ShellExecuteExW,所以目标就是构造 ROP 链传入指定参数执行 ShellExecuteExW。
首先使用 capstone 编写小工具提取 ROP,获取所需指令的地址。
然后拼接这些指令,达到执行 ShellExecuteExW 的目标,具体步骤是:切换栈+布局参数+调用函数。
切换栈是第一步,但非常简单,使用 xchg、pop、mov 以及 lea 等指令修改 rsp 即可。布局参数是整个过程中最难的,因为函数 ShellExecuteExW 只有一个参数,该参数为一个结构体指针,而结构体内部的字符串指针才是执行命令的核心,所以这里涉及到多级指针的布控。由于我们控制了整个栈,所以布置多级指针的也并不难,但步骤比较繁琐,容易出错,一定要耐心谨慎。
参数布置好之后,再次控制 EIP 跳转到导入表内的函数指针执行。一旦函数 ShellExecuteExW 执行完毕,就代表着用户指定的命令也已经执行完毕,但由于栈已经损坏,主程序将随之崩溃。
如果不利用 ShellExecuteExW 也可以利用下图所示位置的代码,自行获得 VirtualProtect 函数地址:
(上图地址 DLL 版本为 15.2.3.723)
四、现有补丁说明
截止 9 月 10 日,发布的补丁情况如下:
| 版本号 | 发布时间 | 有无漏洞 || <15.2.3.717 | ---- | 有 || 15.2.3.717 | 2021/04/20 | 有 || 15.2.3.723 hf1 | 2021/05/14 | 有 || 15.2.3.742 hf2 | 2021/07/12 | 无(最新版) |
五、漏洞利用工具说明
5.1 受影响软件版本
SSH-2.0-Serv-U_15.1.6.25 至 SSH-2.0-Serv-U_15.2.3.723
备注:SSH-2.0-Serv-U_15.1.6.25 版本为 2017 年发布,更早的版本肯定也受影响,只不过当前测试的最早版本为 2017 年。SSH-2.0-Serv-U_15.2.3.723 版本为最新的补丁版本的前一个版本。
5.2 远程利用限制条件
Serv-U 需要以服务方式启动才能利用成功,不过 Serv-U 默认安装本来就是以服务方式启动的。如果以非服务方式启动则无法利用成功。
5.3 远程利用执行演示效果及特殊情况说明
利用成功后,目标机器的 Serv-U.exe 程序会执行我们的 ShellCode 代码,并且启动一个 PowerShell 子进程(父进程为 Serv-U.exe),并通过 PowerShell 脚本回连我们设置的 IP 和端口,获取的 SHELL 权限为 SYSTEM 权限;
ShellCode 代码启动的程序和执行的程序命令可以通过修改攻击脚本自行设定;
上述第 2 点提到的 ShellCode 执行的程序命令有长度限制,不能超过 500 个字节;
如果利用成功,Serv-U.exe 程序会崩溃并自动重启(不会弹框,因为是服务方式启动所以会自动重启),所以可以无限次反复远程利用;
如果利用失败,攻击脚本执行完后稍等十秒再进行下一次攻击,直到成功为止;
每次的攻击成功率大概在 20-50%;
单次攻击发出的网络数据包大小在 2-3M 左右,注意网络情况;
每个 Serv-U 版本需要有对应的 ROP 序列,所以每一个单独的小版本的 Serv-U 都需要定制化开发对应的攻击代码(程序有 DEP,但关键 DLL 无 ASLR)。
本文仅作安全技术分析,旨在为无法获得补丁的用户和安全研究人员提供此漏洞细节分析,所以不提供任何 POC。
评论