CVE-2021-20090 身份验证绕过漏洞分析

0x01 前言

最近Tenable 披露了Arcadyna 网络设备身份验证绕过漏洞,并且很多的厂商都采用产生漏洞的组件,由于Arcadyan 设备固件厂商并没有开源出来,在官网支持里面下载的文件是window和linux 下和设备连接的客户端软件,无法对漏洞点开展分析,这里我们使用同样受影响的华硕产品DSL-AC3100 的固件来进行设备分析。并且复现在网络设备中网络检测ping 功能的远程命令执行漏洞,从而开启设备telentd。

影响范围

ADB ADSL wireless IAD router 1.26S-R-3P
Arcadyan ARV7519 00.96.00.96.617ES
Arcadyan VRV9517 6.00.17 build04
Arcadyan VGV7519 3.01.116
Arcadyan VRV9518 1.01.00 build44
ASMAX BBR-4MG / SMC7908 ADSL 0.08
ASUS DSL-AC88U (Arc VRV9517) 1.10.05 build502
ASUS DSL-AC87VG (Arc VRV9510) 1.05.18 build305
ASUS DSL-AC3100 1.10.05 build503
ASUS DSL-AC68VG 5.00.08 build272
Beeline Smart Box Flash 1.00.13_beta4
British Telecom WE410443-SA 1.02.12 build02
Buffalo WSR-2533DHPL2 1.02
Buffalo WSR-2533DHP3 1.24
Buffalo BBR-4HG  
Buffalo BBR-4MG 2.08 Release 0002
Buffalo WSR-3200AX4S 1.1
Buffalo WSR-1166DHP2 1.15
Buffalo WXR-5700AX7S 1.11
Deutsche Telekom Speedport Smart 3 010137.4.8.001.0
HughesNet HT2000W 0.10.10
KPN ExperiaBox V10A (Arcadyan VRV9517) 5.00.48 build453
KPN VGV7519 3.01.116
O2 HomeBox 6441 1.01.36
Orange LiveBox Fibra (PRV3399) 00.96.00.96.617ES
Skinny Smart Modem (Arcadyan VRV9517) 6.00.16 build01
SparkNZ Smart Modem (Arcadyan VRV9517) 6.00.17 build04
Telecom (Argentina) Arcadyan VRV9518VAC23-A-OS-AM 1.01.00 build44
TelMex PRV33AC 1.31.005.0012
TelMex VRV7006  
Telstra Smart Modem Gen 2 (LH1000) 0.13.01r
Telus WiFi Hub (PRV65B444A-S-TS) v3.00.20
Telus NH20A 1.00.10debug build06
Verizon Fios G3100 1.5.0.10
Vodafone EasyBox 904 4.16
Vodafone EasyBox 903 30.05.714
Vodafone EasyBox 802 20.02.226

0x02 华硕DSL-AC3100 固件

我们从华硕的官网中下载固件。

设备名称: DSL-AC3100

固件版本: DSL-AC3100_v1.10.05_build503

0x03 身份验证绕过漏洞分析

提取固件包

从华硕的官网瞎下载到固件包DSL-AC3100_v1.10.05_build503.w ,这是一个是用.w 为后缀的固件文件,使用binwalk 可以提取出来。根据漏洞信息,可以确定这是一个在http服务中存在的漏洞,可以确定到httpd 文件,本固件的httpd 文件在 /usr/sbin/httpd 中。

image-20210815164522715

httpd 二进制文件分析

在ghidra 导入httpd 文件,自动对文件进行分析,识别文件的各种函数。

由于漏洞是身份认证绕过漏洞,因此首先要确定设备的身份验证相关的函数有哪些,在ghidra对httpd文件中的字符串进行搜寻,根据字符串 “check_auth” ,定位到函数 FUN_0001d0c0(),

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
undefined4 FUN_0001d0c0(int iParm1)

{
int iVar1;
undefined4 uVar2;
int iVar3;
undefined4 local_52c;
undefined4 local_528;
undefined4 local_524;
undefined4 uStack1312;
undefined4 local_51c;
char acStack1304 [1024];
char acStack280 [260];

memset(acStack280,0,0x100);
memset(acStack1304,0,0x400);
local_52c = 0;
local_528 = 0;
local_524 = 0;
uStack1312 = 0;
local_51c = 0;
iVar1 = FUN_00017df0();
if (iVar1 == -1) {
uVar2 = 1;
}
else {
iVar3 = mapi_ccfg_match_str(iVar1,"ARC_SYS_LogEnable",&DAT_00046b48);
iVar1 = mapi_ccfg_match_str(iVar1,"ARC_SYS_MPTEST",&DAT_00046b48);
if (iVar1 == 0) {
if (iVar3 != 0) {
iVar3 = 1;
}
if (iVar3 != 0) {
FUN_00017738(iParm1 + 0x76f0,&local_52c);
}
if (*(int *)(iParm1 + 0x774c) == 0) {
uVar2 = FUN_0001b6f4(iParm1 + 0x771e,*(undefined4 *)(iParm1 + 0x76ec));
FUN_0001b8c8(iParm1,uVar2);
}
iVar1 = FUN_0001ce8c(*(undefined4 *)(iParm1 + 0x774c),*(undefined4 *)(iParm1 + 0x76b0),
*(undefined4 *)(iParm1 + 0x76b4),*(undefined4 *)(iParm1 + 0x76b8),
*(undefined4 *)(iParm1 + 0x76bc),*(undefined4 *)(iParm1 + 0x76c0),
*(undefined4 *)(iParm1 + 0x76c4),*(undefined4 *)(iParm1 + 0x76c8),
*(undefined4 *)(iParm1 + 0x7b34));
if (iVar1 == 1) {
printf("[%s] %s login time out, reauth\n","check_auth",iParm1 + 0x76f0);
FUN_00039088(1);
snprintf(acStack1304,0x400,"Location: /relogin.htm\n\n");
}
else {
if (iVar1 == 2) {
printf("[%s] new user %s(%s) comes to login, check user and auth\n","check_auth",
iParm1 + 0x76f0,iParm1 + 0x4c);
snprintf(acStack1304,0x400,"Location: /relogin.htm\n\n");
}
else {
if (iVar1 == 0) {
printf("[%s] %s has already granted, pass\n","check_auth",iParm1 + 0x76f0);
return 0;
}
}
}
if (iVar3 != 0) {
snprintf(acStack280,0x100,"User from %s(%s) authentication fail.",&local_52c,iParm1 +0x76f0
);
append_to_file("/tmp/security_log.txt",acStack280);
}
FUN_00015338(iParm1,acStack1304);
uVar2 = 1;
}
else {
uVar2 = 0;
}
}
return uVar2;
}

根据函数代码的一些细节,可以看出这个函数检查认认证是否符合的功能函数,其中FUN_0001ce8c 函数的返回值iVar1,在函数中 iVar1 的值为2 时,说明是新用户登录,需要检查用户名和验证。iVar1 的值为 0 的时候,则显示验证通过。iVar1 的值为 1 的时候,则表示说明验证超时,并且重新返回到登录界面。

接下来,查看FUN_0001d0c0() 函数在FUN_0001d578() 中被引用。而FUN_0001d0c0() 函数就是漏洞的evaluate_access() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// evaluate_access()
undefined4 FUN_0001d578(undefined4 uParm1,undefined4 uParm2,int iParm3)

{
int iVar1;
undefined4 uVar2;

if (iParm3 == 0) {
return 0;
}
iVar1 = FUN_0001d2e0(iParm3);
if (iVar1 != 0) {
if (*(int *)(iParm3 + 0x76a8) != 0) {
return 0;
}
uVar2 = FUN_0001d0c0(iParm3);
return uVar2;
}
FUN_00014510(iParm3,0x193,"Unauthorized.");
return 1;
}

FUN_0001d578() 函数中的 FUN_0001d2e0() 是使用正则表达式来校验URL中的IP,端口是否符合规范。以及FUN_0001d0c0() 函数也在其中,因此这个函数是httpd 中来做身份验证的函数,也就是漏洞分析中的evaluate_access()。

接下来我们来查看调用evaluate_access() 函数的地方,真正的漏洞点在这个函数,我们来看漏洞点是如何绕过身份验证的。我们来到了FUN_00015058函数,这就是process_request 的函数。

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
void FUN_00015058(int iParm1)

{
undefined4 uVar1;
char *pcVar2;
char *__src;
int iVar3;
char *__dest;

iVar3 = iParm1 + 0xd5;
uVar1 = FUN_00016a84(iVar3,0xd);
*(undefined4 *)(iParm1 + 0x27f0) = uVar1;
*(undefined4 *)(iParm1 + 0x76a4) = 0xffffffff;
*(undefined4 *)(iParm1 + 0x76ac) = 0xffffffff;
__src = (char *)FUN_00016a84(iVar3,0x20);
*(int *)(iParm1 + 0x7b18) = iVar3;
pcVar2 = (char *)FUN_00016a84(__src,0x20);
uVar1 = FUN_00016a84(__src,0x3f);
*(undefined4 *)(iParm1 + 0x7b14) = uVar1;
__dest = (char *)(iParm1 + 0x7994);
strncpy(__dest,__src,0xff);
*(undefined *)(iParm1 + 0x7a93) = 0;
FUN_00016e3c(__dest);
printf("[%s] url=[%s], args=[%s], method=[%s]\n","process_request",__dest,
*(undefined4 *)(iParm1 + 0x7b14),*(undefined4 *)(iParm1 + 0x7b18));
iVar3 = FUN_00018c70(iParm1);
if (iVar3 < 0) {
return;
}
if (*pcVar2 == '\0') {
*(undefined4 *)(iParm1 + 0x7988) = 1;
}
else {
*(undefined4 *)(iParm1 + 0x7988) = 0;
iVar3 = FUN_00018cb8(iParm1);
if (iVar3 < 0) {
return;
}
iVar3 = strncasecmp(*(char **)(iParm1 + 0x7620),"multipart/form-data",0x13);
if ((((iVar3 != 0) && (*(char **)(iParm1 + 0x7b24) != (char *)0x0)) &&
(__src = strcasestr(*(char **)(iParm1 + 0x7b24),"FirmwareUpload"), __src == (char *)0x0))&&
(0 < (int)(*(int *)(iParm1 + 0x7984) + (uint)(64000 < *(uint *)(iParm1 + 0x7980))))) {
FUN_0000bef4(iParm1,*(undefined4 *)(iParm1 + 0xc));
FUN_00014510(iParm1,0x193,"The Content-length is extreme large!");
return;
}
}
uVar1 = FUN_0000deb0(__dest);
*(undefined4 *)(iParm1 + 0x76a8) = uVar1;
// evaluate_access()
if (((*(code **)(PTR_PTR_DAT_00054fac + 0x14) == (code *)0x0) ||
(iVar3 = (**(code **)(PTR_PTR_DAT_00054fac + 0x14))(iParm1), iVar3 != 2)) &&
((*(int *)(iParm1 + 0x76a8) != 0 || (iVar3 = FUN_0001d578(__dest,0,iParm1), iVar3 == 0)))) {
*(undefined4 *)(iParm1 + 0x798c) = 0;
__src = *(char **)(iParm1 + 0x7b18);
iVar3 = strcmp(__src,"HEAD");
if (iVar3 == 0) {
*(undefined4 *)(iParm1 + 0x798c) = 1;
if (*(int *)(iParm1 + 0x7988) == 0) {
FUN_0000eb98(iParm1);
}
else {
*(undefined4 *)(iParm1 + 0x798c) = 0;
FUN_00014510(iParm1,400,"Invalid HTTP/0.9 method.");
}
}
else {
iVar3 = strcmp(__src,"GET");
if (iVar3 == 0) {
FUN_0000eb98(iParm1);
}
else {
iVar3 = strcmp(__src,"POST");
if (iVar3 == 0) {
FUN_00014c30(iParm1);
}
else {
FUN_00014510(iParm1,400,"Invalid or unsupported method.");
}
}
}
}
return;
}

&& : 逻辑与,前后条件同时满足表达式为真;

|| : 逻辑与,前后条件只要有一个满足表达式为真。

如下面的代码,因为逻辑运算符&& 的优先级大于 || ,因此会先计算 && 的值。所以要先判断 iParm1 + 0x76a8 的值。如果值不为0 ,则接着执行 逻辑运算符的|| 的表达式。

(((*(code **)(PTR_PTR_DAT_00054fac + 0x14) == (code *)0x0) ||
(iVar3 = (**(code **)(PTR_PTR_DAT_00054fac + 0x14))(iParm1), iVar3 != 2)) &&
((*(int *)(iParm1 + 0x76a8) != 0 || (iVar3 = FUN_0001d578(__dest,0,iParm1), iVar3 == 0))))

根据 FUN_00015058() 函数的代码,可以看到 iParm1 + 0x76a8 的值是从 FUN_0000deb0(__dest) 获取到的,而 “_dest” 的值在前面可以看出来是用户请求的 URL。

如果 iParm1 + 0x76a8 不为0 ,那么就能跳过身份验证的函数**evaluate_access()**,来直接执行处理POST请求的FUN_00014c30函数。

接下来进入 FUN_0000deb0() 函数,来查看是怎么处理 URL,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
undefined4 FUN_0000deb0(char *pcParm1)

{
size_t __n;
int iVar1;
char *__s;
undefined **ppuVar2;

ppuVar2 = &PTR_s_/images/_00054f70;
__s = PTR_s_/images/_00054f70;
if (PTR_s_/images/_00054f70 == (undefined *)0x0) {
return 0;
}
do {
__n = strlen(__s);
iVar1 = strncasecmp(pcParm1,__s,__n);
if (iVar1 == 0) {
return 1;
}
ppuVar2 = ppuVar2 + 1;
__s = *ppuVar2;
} while (*ppuVar2 != (char *)0x0);
return 0;
}

函数会将 url 和 &PTR_s_/images/00054f70 字符串进行比较,直到符合为止,而 &PTR_s/images/_00054f70 的值是 “/images/” ,所以只需要请求的URL中带有 “/images/” 字符串,就可以绕过身份认证函数访问其他页面。

image-20210815170700457

image-20210815170721467

前面已经分析出来了身份验证绕过的漏洞点,但是并不能绕过验证访问任意界面,因为在访问的时候,需要正确的httoken值。接下来我们来分析设备的httoken 是怎么获取和生成的,在这个设备里,httoken 是设备的token值,并且访问设备的页面需要带有给定的httoken 值。根据漏洞披露来看,httoken 是在服务端进行生成,然后前端js 中进行解密,最终向服务器请求的时候,将httoken加入到请求数据中,但是漏洞披露并没有说明httoken 是那一段字符串生成的。

我在httpd 的逆向工程中找到了生成httoken 的函数,

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
undefined4 FUN_00022520(int iParm1)

{
int iVar1;
undefined4 uVar2;
undefined *puVar3;
size_t sVar4;
char cStack120;
undefined auStack119 [107];

memset(&cStack120,0,0x65);
iVar1 = FUN_00017df0();
if (iVar1 == -1) {
uVar2 = 0;
}
else {
puVar3 = (undefined *)(iParm1 + 0x7994);
if (puVar3 == (undefined *)0x0) {
puVar3 = &DAT_0003d274;
}
uVar2 = FUN_000393e0(puVar3);
sprintf(&cStack120,"%lu",uVar2);
sVar4 = strlen(&cStack120);
FUN_00017e78(&cStack120,sVar4,auStack119 + sVar4,100 - sVar4);
uVar2 = so_printf(iParm1,
"<img title=spacersrc=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7%s\" border=0>"
,auStack119 + sVar4);
}
return uVar2;

接下来我们来分析FUN_00022520函数。在函数中我们可以看到最终生成了一个img 标签,并且src 的值是一段“data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 + auStack119 + sVar4 ”字符串,而其中 auStack119 + sVar4 的值是从FUN_00017e78 函数中进行base64 以及其他的方式进行处理后的字符串,并且这段字符串就是httoken的值。

生成的img 标签会在设备的login.html 中html 代码中出现,如下图所示,

image-20210816152015429

根据生成httoken函数拼接这段字符串的方式,使用脚本对把token 解密出来,可以确定如下图的“372646849” 为设备的 token。

ArcBase.decode(“image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7MTU2OTQzNDE0OA==”)

根据解密出来的信息,“;” 后面的字符串 “ 372646849” 就是设备解密后的httoken 值。

0x04 ping 命令注入+配置选项

这一部分,漏洞披露的相对来说比较详细,很多的网络设备中在ping网络诊断 这个功能中,出现过大量的历史漏洞,比如NetGear,D-Link等都出现过此类漏洞,因此关于ping 这一步部分命令拼接就不展开来讲述,但是本漏洞的不同点在于使用设备内部的配置选项ARC_TELNETD_ENABLE 来开启设备的telentd,这一点可以在以后的漏洞挖掘中遇到无法执行命令的时候,提供了不同的执行命令的方式。
我们来重点的关注一下ARC_TELNETD_ENABLE 这个配置。在文件 /sbin/arc_telnetd 文件中可以看到文件内容。文件可以获取ARC_TELNETD_ENABLE的值,当ARC_TELNETD_ENABLE的值为1的时候,设备会开启telnetd。

image-20210815163855557

漏洞复现

image-20210817151131987

image-20210817151237633


CVE-2021-20090 身份验证绕过漏洞分析
https://tig3rhu.github.io/2023/12/16/CVE-2021-20090(华硕DSL-AC3100)身份验证绕过漏洞分析/
Author
Tig3rHu
Posted on
December 16, 2023
Licensed under