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

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

描述

A path traversal vulnerability in the web interfaces of Buffalo WSR-2533DHPL2 firmware version <= 1.02 and WSR-2533DHP3 firmware version <= 1.24 could allow unauthenticated remote attackers to bypass authentication.

漏洞分析

固件下载链接:https://dlcdnets.asus.com/pub/ASUS/wireless/DSL-AC88U/FW_DSL_AC88U_11005502.zip?model=DSL-AC88U

下载之后可以binwalk解开,解开后可以找到httpd文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
.......
while ( 1 )
{
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 0x4650u);
pthread_attr_setdetachstate(&attr, 1);
matched = pthread_create((*(dword_614FC + 4 * v9) + 32), 0, sub_D6E8, (*(dword_614FC + 4 * v9) + 28));
if ( matched )
break;
if ( ++v9 == 10 )
{
if ( !v5 )
......

}

主函数中会利用线程来接收request,线程实现函数为sub_D6E8

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
void __fastcall __noreturn sub_D6E8(_DWORD *id)
{
int req; // r5
int v2; // r2
signed int v3; // r4
int v4; // r3
int v5; // r0
FILE *v6; // r0
FILE **i; // r4
_DWORD *v8; // r0
int *v9; // r0

req = *(dword_614FC + 4 * *id);
printf("[%s] thread #%d start ...\\n", "httpd_child", *id);
sub_D6A0();
while ( 1 )
{
while ( sem_wait((req + 31664)) == -1 )
{
v2 = *_errno_location();
if ( v2 != 4 )
{
printf("[%s] sem_wait err: %d", "httpd_child", v2);
goto LABEL_16;
}
}
printf("[%s] thread %d wake up\\n", "httpd_child", *(req + 28));// req->id
pthread_mutex_lock(req);
pthread_mutex_unlock(req);
v3 = *(req + 56);
if ( v3 >= 0 )
{
sub_C1E8((req + 120), *(req + 56));
v4 = *(req + 32);
*(req + 184) = v4;
*(req + 124) = v4;
*(req + 120) = *(req + 28); // req->id
sub_EC94(req + 120);
sub_ED84(req + 120);
v5 = sub_C008(req + 120, v3, (req + 333), 10000, 1000 * dword_619BC);// 获取http请求的完整头部
*(req + 10336) = v5; // req->flags
if ( v5 > 0 )
{
request_log(*(req + 184), dword_619BC, sub_14CE8);
request_handle(req + 120);
sub_15B88(*(req + 184));
so_flush(req + 120);
}
else if ( v5 == -2 )
{
LOG(req + 120, 500, "Unable to process request headers");
}
}
close(*(req + 132));
v6 = *(req + 136);
if ( v6 )
sub_E360(v6);
for ( i = *(req + 140); i; *(req + 140) = i )
{
sub_E360(*i);
v8 = *(req + 140);
i = v8[1];
free(v8);
}
printf("[%s] thread %d work finished\\n\\n", "httpd_child", *(req + 28));
pthread_mutex_lock(req);
*(req + 24) = 0;
pthread_mutex_unlock(req);
if ( sem_post(&stru_614EC) == -1 )
{
v9 = _errno_location();
printf("[%s] sem_post %d err: %d\\n", "httpd_child", *(req + 28), *v9);
LABEL_16:
pthread_exit(0);
}
}
}

这里没有修复出结构体,看起来很杂乱,打注释分析

如果线程被启动代表已经有数据来了,此时给一个id,这个id里存放着这次发送的数据

接着判断这个id里的数据是否能正常获取完整的头部

如果可以获取则调用request_log这几个函数来处理此次请求

重要的是request_handle函数,这里对请求数据进行解析+权限认证+执行对应的请求

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
void __fastcall request_handle(int a1)
{
int v2; // r5
const char *v3; // r8
_BYTE *v4; // r10
int v5; // r0
int (__fastcall *v6)(int); // r3
const char *v7; // r5

v2 = a1 + 213;
*(a1 + 10224) = sub_16A84(a1 + 213, 13);
*(a1 + 30372) = -1;
*(a1 + 30380) = -1;
v3 = sub_16A84(v2, ' ');
*(a1 + 31512) = v2; // req->method
v4 = sub_16A84(v3, ' ');
*(a1 + 31508) = sub_16A84(v3, '?'); // req->args
strncpy((a1 + 31124), v3, 0xFFu); // req->url
*(a1 + 31379) = 0;
url_decode(a1 + 31124); // req->url
printf("[%s] url=[%s], args=[%s], method=[%s]\\n", "process_request", (a1 + 31124), *(a1 + 31508), *(a1 + 31512));
if ( init(a1) >= 0 )
{
if ( *v4 )
{
*(a1 + 31112) = 0;
if ( body_parm(a1) < 0 )
return;
if ( strncasecmp(*(a1 + 30240), "multipart/form-data", 0x13u) )// req->Content-type
{
v5 = *(a1 + 31524); // req->SOAPAction
if ( v5 )
{
if ( !strcasestr(v5, "FirmwareUpload") && *(a1 + 31104) > 64000 )// Content-length > 64000
{
sub_BEF4(a1, *(a1 + 12));
LOG(a1, 403, "The Content-length is extreme large!");
return;
}
}
}
}
else
{
*(a1 + 31112) = 1;
}
*(a1 + 30376) = sub_DEB0(a1 + 31124); // req->is_url_valid = req->url; 判断url是否在一个预定义表中
v6 = *(off_54FAC[0] + 5);
if ( (!v6 || v6(a1) != 2) && (*(a1 + 30376) || !check_auth(a1 + 31124, 0, a1)) )
{
*(a1 + 31116) = 0;
v7 = *(a1 + 31512);
if ( !strcmp(v7, "HEAD") )
{
*(a1 + 31116) = 1;
if ( *(a1 + 31112) )
{
*(a1 + 31116) = 0;
LOG(a1, 400, "Invalid HTTP/0.9 method.");
return;
}
goto LABEL_19;
}
if ( !strcmp(v7, "GET") )
{
LABEL_19:
process_get(a1);
return;
}
if ( !strcmp(v7, "POST") )
process_post(a1);
else
LOG(a1, 400, "Invalid or unsupported method.");
}
}
}

进行初步解析,解析出method,args,url

然后把url进行解码,并初始化一些东西

请求的主体数据会被body_parm进行解析,并存入相应的地方

检查Content-Length是否过大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __fastcall sub_DEB0(const char *a1)
{
const char *v2; // r4
char **i; // r5
size_t v5; // r0
const char *v6; // t1

v2 = off_54F70[0];
if ( !off_54F70[0] )
return 0;
for ( i = off_54F70; ; ++i )
{
v5 = strlen(v2);
if ( !strncasecmp(a1, v2, v5) )
break;
v6 = i[1];
v2 = v6;
if ( !v6 )
return 0;
}
return 1;
}

调用sub_DEB0函数来确认url是否是预定义表中的,如果是则req->is_url_valid=1

有个特别重要的点是如果req->is_url_valid=1,那么就不会进行check_auth,也就是权限认证

接着通过method进入不同的处理

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
void __fastcall process_post(int a1)
{
const char *path; // r4

path = (a1 + 31124);
printf("[%s] path: %s, args: %s\\n", "process_post", (a1 + 31124), *(a1 + 31508));
switch ( sub_DF50(path) )
{
case -1:
LOG(a1, 302, path);
break;
case 0:
if ( !sub_14F9C(a1) )
LOG(a1, 501, "POST to non-script");
break;
case 2:
sub_158D0(a1);
break;
case 3:
LOG(a1, 400, path);
break;
default:
return;
}
}

看一下process_post函数

会将url放入sub_DF50函数,这个函数会对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
int __fastcall sub_DF50(const char *path)
{
int v2; // r3
bool v3; // zf
int i; // r4
int v5; // r7
size_t v6; // r6

sub_167A8(path);
v2 = *path;
v3 = v2 == '/';
if ( v2 != '/' )
v3 = v2 == 0;
if ( !v3 )
return 3;
for ( i = 0; i != 528; i += 66 )
{
if ( sub_17584(dword_619C0 + i) )
break;
v5 = dword_619C0 + i;
v6 = strlen((dword_619C0 + i));
if ( !strncmp(path, (dword_619C0 + i), v6) && (*(v5 + v6 - 1) == '/' || v6 == strlen(path) || path[v6] == '/') )
{
sub_166D8(v6, path, (v5 + 32));
return *(dword_619C0 + i + 64);
}
}
sub_166D8(0, path, dword_61648);
return 0;
}

url会传入sub_167A8中

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
void __fastcall sub_167A8(char *path)
{
int id; // r2
int idx; // r3
char *v3; // r12
int v4; // r2
bool v5; // zf
int v6; // r5
char *v7; // r1
int v8; // r2
int v9; // t1

id = 0;
idx = 0;
while ( path[idx] )
{
if ( double_dot[id] == path[idx] ) // 判断是否为.
{
if ( double_dot[++id] )
{
++idx;
}
else
{
v3 = &path[idx + 1]; // 指向..的下一个地方
v4 = *v3;
v5 = v4 == '/'; // 判断下一个字节是否是/
if ( v4 != '/' )
v5 = v4 == 0;
if ( v5 ) // 如果下一个字节是/
{
if ( idx <= 3 || path[idx - 2] == '/' )// url长度小于等于3,或者存在/..
{
v6 = idx + 1; // ..的下一个字节
idx -= 3;
if ( idx >= 0 )
{
for ( ; idx && path[idx] != '/'; --idx )// 倒退,直到找到/
;
}
else
{
idx = 0;
}
v7 = &path[idx]; // url开始的地方
v8 = path[v6]; // ..的下一个字节
path[idx] = v8; // 将..下一个字节放到开头
if ( v8 ) // 把后续的东西都拷贝到前面,比如/api/../ppp,变成/ppp
{
do
{
v9 = *++v3;
*++v7 = v9;
}
......
}

漏洞点就存在这个函数中

举个例子,这个函数会把/api/../ppp给转换成/ppp

那么结合上面,将url前放入预定义的数据,后面接上/../xxx,就不需要认证即可访问xxx,造成认证绕过漏洞。例如:/js/../aaaa.cgi

sub_DF50函数结束之后就会跑到sub_158D0中去执行对应的程序

漏洞修复

像上一个ASUS的url检查一样,直接搬掉..