CVE-2021-32030 ASUS身份验证绕过

CVE-2021-32030 ASUS身份验证绕过

描述

The administrator application on ASUS GT-AC2900 devices before 3.0.0.4.386.42643 allows authentication bypass when processing remote input from an unauthenticated user, leading to unauthorized access to the administrator interface. This relates to handle_request in router/httpd/httpd.c and auth_check in web_hook.o. An attacker-supplied value of ‘\0’ matches the device’s default value of ‘\0’ in some situations.

漏洞分析

固件下载链接:https://dlcdnets.asus.com/pub/ASUS/wireless/RT-AX56U/FW_RT_AX56U_30043848253.zip

固件下载完成之后直接binwalk解包会出现100000.ubi,用ubireader_extract_files就可以继续解包

1
2
ubireader_extract_files 100000.ubi
ubireader_extract_files`直接用pip安装就行`pip3 install ubi_reader

ASUS之前发布了路由器相关的源码,可以通过源码来辅助分析,笔者在学习这个CVE的时候发现源码和固件的httpd很多地方都是一样的,这里主要分析一下它的handle_request

直接strings搜索Bad Request,然后交叉引用就可以找到了,sub_5FFDC函数就是handle_request

首先会获取第一行的数据,例如:POST /goform/setsys HTTP/1.1\\r\\n

然后切割第一行的数据,切割成line = method = POST, path=/goform/setsys, protocol=HTTP/1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
host_name = 0;
memset(line, 0, sizeof(line));
if ( !fgets(line, 10000, dword_CF700) ) // 获取第一行
return;
path = line;
strsep(&path, " ");
while ( path && *path == ' ' )
++path;
protocol = path;
strsep(&protocol, " ");
while ( protocol && *protocol == ' ' )
++protocol;
stringp = protocol;
strsep(&stringp, " ");
if ( !path || !protocol )
{
v0 = "Can't parse request.";
v1 = "Bad Request";
goto LABEL_60;
}

如果数据不正常会报出Bad Request的错误,之后会取header中的数据并查看是否正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file = path + 1;
v17 = path[1];
v18 = strlen(path + 1);
if ( v17 == '/'
|| !strcmp(file, "..")
|| !strncmp(file, "../", 3u)
|| strstr(file, "/../")
|| !strcmp(&file[v18 - 3], "/..") )
{
v0 = "Illegal filename.";
v1 = "Bad Request";
LABEL_60:
v15 = 400;
LABEL_57:
sub_5ED20(v15, v1, v0);
return;
}

接着会获取对应的cookie等数据,检查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
25
26
27
28
29
30
31
if ( !v17 || file[v18 - 1] == '/' )           // 默认访问的页面,第一次访问是QIS设备,普通访问是index_page
{
if ( sub_5FFAC() )
file = "QIS_default.cgi";
else
file = &indexpage;
}
memset(url, 0, sizeof(url));
v19 = index(file, '?');
if ( v19 )
{
v21 = strlen(file);
v22 = v21 - strlen(v19);
if ( v22 >= 0x81 )
file_len = 128;
else
file_len = v22;
}
else
{
file_len = 127;
}
strncpy(url, file, file_len);
if ( (strstr(url, ".asp") || strstr(url, ".htm"))
&& !strstr(url, "update_networkmapd.asp")
&& !strstr(url, "update_clients.asp")
&& !strstr(url, "update_customList.asp") )
{
memset(current_page_name, 0, sizeof(current_page_name));
snprintf(current_page_name, 0x80u, "%s", url);
}

判断是否存在.asp和.html并把一些特定的url放入current_page_name中

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
if ( cur_login_ip_type )
{
if ( (lock_flag & 2) != 0 )
{
login_timestamp_tmp_wan = uptime(v28);
v29 = login_timestamp_tmp_wan - last_login_timestamp_wan;
login_dt = login_timestamp_tmp_wan - last_login_timestamp_wan;
v32 = last_login_timestamp_wan <= 0;
if ( last_login_timestamp_wan )
v32 = v29 <= 300;
if ( v32 )
{
if ( strncmp(file, "Main_Login.asp", 0xEu) || login_error_status != 7 )
goto LABEL_110;
}
else
{
last_login_timestamp_wan = 0;
login_try_wan = 0;
v31 = lock_flag & 0xFFFFFFFD;
LABEL_107:
lock_flag = v31;
login_error_status = 0;
}
}
}

判断WAN口是否登陆频繁,如果频繁则进行锁定处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
else if ( (lock_flag & 1) != 0 )
{
login_timestamp_tmp = uptime(v28);
v29 = login_timestamp_tmp - last_login_timestamp;
login_dt = login_timestamp_tmp - last_login_timestamp;
v30 = last_login_timestamp <= 0;
if ( last_login_timestamp )
v30 = v29 <= 300;
if ( !v30 )
{
last_login_timestamp = 0;
login_try = 0;
v31 = lock_flag & 0xFFFFFFFE;
goto LABEL_107;
}
if ( strncmp(file, "Main_Login.asp", 0xEu) || login_error_status != 7 )
{
LABEL_110:
if ( !strstr(url, ".png") )
{
send_login_page(0, 7, url, 0, v29, 0);
return;
}
}

判断LAN口是否登陆频繁,如果频繁则进行锁定处理

1
2
3
4
5
6
7
8
9
10
11
12
13
while ( 1 )
{
handler = (mime_handlers + dest);
pattern = *(mime_handlers + dest);
if ( !pattern )
goto LABEL_241;
if ( !do_ssl || (v81 = handler->pattern, v41 = strcmp(url, "offline.htm"), pattern = v81, v41) )
{
if ( match(pattern, url) )
break;
}
dest += 24;
}

查看url是否在mime_handlers中

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
login_state_fromapp = delim == 3;
if ( delim == 3 )
login_state_fromapp = fromapp == 0;
if ( login_state_fromapp && (mime_exception & 8) == 0 )
{
v45 = strncmp(file, "Main_Login.asp", 0xEu);
if ( v45 || login_error_status != 9 )
{
v46 = v45 == 0;
if ( handler->auth )
v46 |= 1u;
if ( v46 )
{
if ( !strcasecmp(line, "post") && handler->input )
{
for ( i = v76; i; --i )
fgetc(dword_CF700);
}
v47 = 0;
v48 = 9;
v49 = 0;
v50 = 0;
v71 = 0;
a5 = 0;
goto LABEL_140;
}
}
}

如果是APP发起但未登录成功的请求,则对需要登录的页面进行保护,返回登录页面

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
if ( !handler->auth )                         // 不需要认证
{
if ( (do_referer & 1) == 0 ) // referer是否正确
goto LABEL_190;
v53 = sub_59CE0(v74, fromapp);
if ( !v53 )
goto LABEL_190;
if ( !strcasecmp(line, "post") && handler->input )
{
for ( j = v76; j; --j )
fgetc(dword_CF700);
}
LABEL_184:
v47 = 0;
v71 = 0;
v49 = 0;
a5 = 0;
LABEL_185:
v48 = v53;
LABEL_192:
v50 = fromapp;
LABEL_140:
send_login_page(v50, v48, v49, v47, a5, v71);
return;
}

如果不需要认证,则判断referer是否正确,如果referer不正确则需要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
if ( (mime_exception & 2) == 0 || x_Setting )
{
if ( !fromapp && sub_5EB38("re_mode", "1") && !nvram_get_int(&unk_6B710) && !sub_5CBC4(file) )
{
v52 = strcpy(v90, "<meta http-equiv=\\"refresh\\" content=\\"0; url=message.htm\\">\\r\\n");
send_page(200, "OK", 0, v52, 0);
return;
}
if ( (mime_exception & 1) == 0 )
{
if ( (do_referer & 1) != 0 )
{
v53 = sub_599A8(v74, fromapp);
if ( v53 )
{
if ( !strcasecmp(line, "post") && handler->input )
{
for ( k = v76; k; --k )
fgetc(dword_CF700);
}
goto LABEL_184;
}
}
handler->auth(auth_userid, &auth_passwd, auth_realm);// auth函数进行用户名密码验证
v53 = auth_check(auth_realm, authorization, url, file, v72, fromapp);
if ( v53 )
{
if ( !strcasecmp(line, "post") && handler->input )
{
for ( m = v76; m; --m )
fgetc(dword_CF700);
}
v49 = url;
v71 = add_try;
a5 = auth_check_dt;
v47 = file;
goto LABEL_185;
}
}
}

进行认证检查,会认证用户名密码、referer等信息,如果失败则返回login页面,这里跟进一下auth_check

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
int __fastcall auth_check(
int auth_realm,
int authorization,
const char *url,
int file,
const char *cookies,
int fromapp)
{
void *v7; // r0
bool v8; // cc
char *v9; // r5
int *v10; // r0
int v11; // r5
int *v12; // r4
bool v13; // cc
char *v14; // r5
int *v15; // r0
int result; // r0
char *asus_token_string; // r0
char *asus_token_value; // r9
size_t v19; // r0
unsigned int v20; // r2
int *v22; // r0
int v23; // r5
int *v24; // r4
char token[64]; // [sp+8h] [bp-40h] BYREF

v7 = memset(token, 0, 0x20u);
if ( cur_login_ip_type ) // 检查登录失败锁定状态
{
login_timestamp_tmp_wan = uptime(v7);
auth_check_dt = login_timestamp_tmp_wan - last_login_timestamp_wan;
v13 = last_login_timestamp_wan <= 0;
if ( last_login_timestamp_wan )
v13 = login_timestamp_tmp_wan - last_login_timestamp_wan <= 300;
if ( !v13 )
{
last_login_timestamp_wan = 0;
login_try_wan = 0;
lock_flag &= ~2u;
}
if ( login_try_wan > 4 )
{
lock_flag |= 2u;
v14 = inet_ntoa(login_ip_tmp);
if ( !(login_try_wan % 5u) )
{
sub_5B2F4(login_try_wan / 5u);
logmessage_normal(
"HTTP login",
"Detect abnormal logins at %d times. The newest one was from %s in auth check.",
login_try_wan,
v14);
}
add_try = 1;
v15 = _errno_location();
v11 = *v15;
v12 = v15;
if ( f_exists("/tmp/HTTPD_DEBUG") > 0 || nvram_get_int("HTTPD_DBG") > 0 )
Debug2File("/jffs/HTTPD_DEBUG.log", "[%s:(%d)]: LOGINLOCK\\n", "auth_check", 985);
goto LABEL_23;
}
}
else
{
login_timestamp_tmp = uptime(v7);
auth_check_dt = login_timestamp_tmp - last_login_timestamp;
v8 = last_login_timestamp <= 0;
if ( last_login_timestamp )
v8 = login_timestamp_tmp - last_login_timestamp <= 300;
if ( !v8 )
{
last_login_timestamp = 0;
login_try = 0;
lock_flag &= ~1u;
}
if ( login_try > 4 )
{
lock_flag |= 1u;
v9 = inet_ntoa(login_ip_tmp);
if ( !(login_try % 5u) )
{
sub_5B2F4(login_try / 5u);
logmessage_normal(
"HTTP login",
"Detect abnormal logins at %d times. The newest one was from %s in auth check.",
login_try,
v9);
}
add_try = 1;
v10 = _errno_location();
v11 = *v10;
v12 = v10;
if ( f_exists("/tmp/HTTPD_DEBUG") > 0 || nvram_get_int("HTTPD_DBG") > 0 )
Debug2File("/jffs/HTTPD_DEBUG.log", "[%s:(%d)]: LOGINLOCK\\n", "auth_check", 959);
LABEL_23:
*v12 = v11;
return 7;
}
}
result = auth_passwd;
if ( auth_passwd )
{
if ( !cookies || (asus_token_string = strstr(cookies, "asus_token")) == 0 )
{
if ( !sub_5FFAC() )
{
add_try = 0;
return 1;
}
goto LABEL_27;
}
asus_token_value = asus_token_string + 11;
v19 = strspn(asus_token_string + 11, " \\t");
snprintf(token, 0x20u, "%s", &asus_token_value[v19]);
if ( !sub_5B478(token, 0) && !sub_59588(token) )// 验证是否通过
{
if ( !sub_5FFAC() )
{
if ( !strcmp(last_fail_token, token) )
{
add_try = 0;
}
else
{
strlcpy(last_fail_token, token, 32);
add_try = 1;
}
v22 = _errno_location();
v23 = *v22;
v24 = v22;
if ( f_exists("/tmp/HTTPD_DEBUG") > 0 || nvram_get_int("HTTPD_DBG") > 0 )
Debug2File("/jffs/HTTPD_DEBUG.log", "[%s:(%d)]: AUTHFAIL\\n", "auth_check", 1055);
result = 2;
*v24 = v23;
return result;
}
LABEL_27:
sub_5F004(fromapp, url);
return 0;
}
if ( !strncmp(url, "get_IFTTTPincode.cgi", 0x14u) || !strncmp(url, "get_IFTTTtoken.cgi", 0x12u) )
sub_5F8B8();
result = cur_login_ip_type;
if ( cur_login_ip_type )
{
result = 0;
login_try_wan = 0;
last_login_timestamp_wan = 0;
v20 = lock_flag & 0xFFFFFFFD;
}
else
{
login_try = 0;
last_login_timestamp = 0;
v20 = lock_flag & 0xFFFFFFFE;
}
lock_flag = v20;
}
return result;
}

检查登录失败的锁定状态,如果连续失败超过5次则进行锁定

从cookies中取出asus_token然后调用两个函数进行token检查,下面关注sub_59588函数

通过登录则失败次数清0,重新开始

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
int __fastcall sub_59588(const char *token)
{
char *v2; // r0
char *v3; // r0

v2 = my_nvram_get("ifttt_token");
if ( !strcmp(token, v2) )
{
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token success.\\n", "check_ifttt_token", 761);
return 1;
}
else
{
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token fail.\\n", "check_ifttt_token", 767);
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File(
"/tmp/IFTTT_ALEXA.log",
"[%s:(%d)][HTTPD] IFTTT/ALEXA long token is %s.\\n",
"check_ifttt_token",
768,
token);
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
{
v3 = my_nvram_get("ifttt_token");
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] httpd long token is %s.\\n", "check_ifttt_token", 769, v3);
}
return 0;
}
}

从配置中读取token,然后将取出的token和配置中的token进行比较,如果相等则token检查通过

这里就出现了一个问题,因为是用strcmp进行比较的,如果取出的token为空,那么就可以绕过strcmp使得检查通过,导致认证绕过漏洞的发生

漏洞修复

漏洞修复也很简单,判断一下从header中取出的token是否为空即可