CNVD-2018-01084 是 Dlink 系列路由器的一个漏洞,影响设备包括 DIR 615/645/815 路由器。
漏洞存在于 /htdocs/cgibin 二进制文件中,漏洞成因是 service.cgi 中未经任何过滤检查就将用户的输入拼接到原始命令中,导致任意代码执行。
环境搭建
首先下载存在漏洞的固件,地址 ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/
我下载的是 1.02 版本,这个漏洞在 1.03 以上版本被修复了,注意不要下错。
固件为 MIPS 架构,所以需要利用 QEMU 来模拟执行,关于 QEMU 和环境的具体搭建流程可以参考我上一篇文章 https://wzt.ac.cn/2019/03/19/CVE-2018-5767/
这个固件属于 CGI(通用网关接口),所以不需要搭建网络环境即可运行。
装好必要的软件之后使用命令
1 | sudo chroot . ./qemu -g 10000 ./htdocs/cgibin |
即可运行程序,如果添加了 -g 选项,那么在外部使用 gdb 链接到 10000 端口就能调试程序了。
运行起来会提示
1 | CGI.BIN, unknown command cgibin |
这是正常现象,因为我们还没有访问任何 CGI 服务。
逆向分析
由于是 MIPS 程序,IDA 的反编译插件不能解析伪代码,在之前我们只能硬着头皮看汇编,或者利用 RETDEC 大概看看函数调用关系,但是今年上半年 NSA 发布了 Ghidra,相信大家都有所耳闻,到现在已经迭代了 5 个版本,在 GITHUB 上可以下载到最新的 9.0.4 版本源代码。
如果不想手动编译的可以直接去官方网站下载 release 版本。
Ghidra 虽然速度和交互性弱于 IDA,但是它能反编译 mips 等 IDA 暂不支持的架构,这次我们就利用它来完成逆向分析。
打开 Ghidra 新建项目,并导入 cgibin 文件,让 ghidra 自动分析:
在左边的函数窗口中有一个搜索栏,搜索 main 即可找到 main 函数,点击汇编窗口,右侧的反编译窗口就自动出现的 main 函数的伪代码:
反编译的效果感觉和 IDA 的不相上下,只是交互性和细节上差一些。
根据披露信息,漏洞位于 service.cgi 中,在 main 函数找一找就能找到这一项:
1 | else { |
观察代码发现 cgibin 从 web 端接受了请求,然后取出 URL 中的访问参数,筛选出对应的 cgi 服务,并调用相关函数。
Service.cgi 对应的是 servicecgi_main 函数,伪代码:
1 |
|
稍微观察一下发现函数内部先处理传递进来的 HTTP 请求,但是这里的请求处理流程很奇怪,利用 getenv 函数从系统环境变量中获取请求字段。
查找文件内容之后发现了调用 cgi 的文件,使用命令在 htdocs 目录查找所有包含 cgi 字符串的文件:
1 | grep -rn "cgi" * |
得到输出:
1 | 匹配到二进制文件 cgibin |
基本都是在 js 中调用的 service.cgi,例如 tools_system.php:
1 | ajaxObj.createRequest(); |
类似的还有很多,web 端通过 js 构造请求,并将请求发送到 cgibin 进行进一步处理。
回到代码上,首先获取了 REQUEST_METHOD 字段,判断是否为 POST,然后进入 cgibin_parse_request 函数进一步解析 HTTP 请求。
cgibin_parse_request 函数中构造了 sobj 结构体,函数大致功能是取出 CONTENT_TYPE、CONTENT_LENGTH、REQUEST_URI 这几个请求字段,进行进一步操作。
关键点在于 REQUEST_URI,它表示了当前请求的访问参数,关键代码如下
1 | __nptr_00 = getenv("REQUEST_URI"); |
从 REQUEST_URI 中定位 ? ,然后取出问号之后的内容,计算长度。
函数 FUN_00402dc0 的主要功能是对取出的参数进行 URLdecode,并写入某块内存区域。
当 cgibin_parse_request 函数完成了 HTTP 请求解析动作,并且正确的返回,servicecgi_main 函数会继续下一步操作。
首先检查当前用户 session 是否为管理员,对应函数是 sess_ispoweruser,由于在模拟环境中,这个函数无法获取到 session,我们可以直接 patch 掉这个函数。身份验证成功之后继续解析 REQUEST_URI 的内容,涉及的指令有三个:EVENT、ACTION、SERVICE。
观察代码发现 EVENT 的限制最少,当解析到 EVENT 的时候跳出 if 判断,进入 else 流程;
1 | else { |
关键函数为 lxmldbc_system:
1 |
|
根据传入的参数,这个函数直接使用 vsnprintf 将 “event %s > /dev/null” 和和用户传入的 EVENT 拼接,并利用 system 执行。
这里显然存在命令注入漏洞,注入的格式大概是;
1 | echo "Hello"&ls&pwd |
运行上面的命令可以同时得到三条指令的结果。
如果我们构造 EVENT=&ls&,那么拼接出来的结果就是 event &ls& > /dev/null,从而完成任意命令执行。
调试
1 | Update(2021-02-24):当 IDA 使用原生 gdb 调试的时候,会弹出警告信息,提示我们 IDA 无法读取某些内存区域的数据,通常来说这是正常情况,想要读取目标内存数据的话可以手动设置地址范围,具体做法大家可自行搜索。 |
根据逆向分析的结果,我们需要手动传入一些参数以防止程序自动退出。按照网上的教程构造如下命令:
1 | sudo chroot . ./qemu -0 "service.cgi" -g 10000 -E REQUEST_METHOD="POST" -E CONTENT_LENGTH=10 -E REQUEST_URI="service.cgi?EVENT=%26ls%26" -E CONTENT_TYPE="application/x-www-form-urlencoded" -E HTTP_COOKIE="uid=aaaaa" ./htdocs/cgibin |
qemu 利用 -0 传入第一个参数,满足 main 函数的需求,进入 servicecgi_main 函数。
利用 -E 选项传入自定义的环境变量,满足判断,REQUEST_URI 中包含待注入的命令(URL 编码)。
在终端运行后打开 gdb-multiarch
1 | file ./htdocs/cgibin |
两条命令连接到 cgibin 进程上面:
按照程序逻辑我们在 0x0040aad8 下断点,然后运行程序
当返回 gdb 界面的时候 v0 可能是 -1,代表 cgibin_parse_request 的逻辑判断失败。不要慌,我们手动 patch 程序逻辑到正确的分支即:0x0040AB20。这里和网上的文档不同,我猜测是 qemu 的环境问题。
1 | set $pc = 0x40ab20 |
接下来执行到 sess_ispoweruser 函数,程序会进入 20 秒左右的假死状态.
此处我们可以打开 -strace 寻找原因,修改启动命令,添加 -strace 选项再执行,到了 sess_ispoweruser 函数窗口会打印一堆信息:
问题在于 qemu 模拟环境中找不到 /var/session 文件,但是函数一直尝试去获取 session,导致程序卡在这里循环。
去除 strace 选项继续调试,patch 掉 session 验证函数,转移到正确分支:0x0040AB58,接着就可以直接在存在漏洞的函数头部下断点了,地址是:00410f14
继续执行 gdb 断在 lxmldbc_system 函数头部,单步调试看到 vsnprintf 函数的几个参数:
显然是把字符串 “&ls&” 拼接到了 “event %s > /dev/null” 中,来到 system 函数内部,参数为:
命令注入成功。
但是不要高兴的太早,如果继续执行程序有很大可能只会打印出如下信息:
1 | HTTP/1.1 200 OK |
我们已经注入了 ls 命令,为什么没有效果呢?
再次打开 strace(注意跳过 sess_ispoweruser 函数)
这里找到了答案,execve 系统调用错误,显示找不到文件。后来在大佬的文章上看到这是 qemu 的锅,我的 qemu 是用 apt 安装的,版本在 2.5.0,这个版本的 qemu user 模式没有实现 execve 函数。需要下载 qemu 2.9 版本并且加上 -execve 参数才行(这么说我上一篇漏洞也是由于 qemu 的问题才起不来 shell QAQ)。
关于 2.9 qemu 加 patch 的问题,没有太多的教程,不过我发现 QEMU 3.0.0 版本存在一个 -sandbox 选项,内部有如下描述
1 | use 'elevateprivileges' to allow or deny QEMU process to elevate its privileges by blacklisting all set*uid|gid system calls. |
不知道是不是官方添加的 execve 系统调用?
最后看一下官方的修复手段:
下载比较新的固件(1.03 以上),用 Ghidra 打开,直接定位到存在漏洞的函数附近:
发现函数的参数传递变成了 文件路径 + 参数形式。进入函数内部:
修复手段简单粗暴,直接 fork 出一个新的进程,然后关闭 stdout,利用 execl 执行命令。
由于 execl 的特性,我们再注入的命令就无效了。例如下面两个程序:
1 |
|
1 |
|
大家可以自行编译运行查看结果。
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2019/09/05/CNVD-2018-01084/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。