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 ] == '/' ) { 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, 0x80 u, "%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" , 0xE u) || 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" , 0xE u) || 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" , 0xE u); 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是否为空即可