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检查一样,直接搬掉..