Asus RT-AX82U vulnerability

Asus RT-AX82U vulnerability

Original text by talosintelligence

Asus RT-AX82U get_IFTTTTtoken.cgi authentication bypass vulnerability

CVE-2022-35401

An authentication bypass vulnerability exists in the get_IFTTTTtoken.cgi functionality of Asus RT-AX82U 3.0.0.4.386_49674-ge182230. A specially-crafted HTTP request can lead to full administrative access to the device. An attacker would need to send a series of HTTP requests to exploit this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Asus RT-AX82U 3.0.0.4.386_49674-ge182230

PRODUCT URLS

RT-AX82U — https://www.asus.com/us/Networking-IoT-Servers/WiFi-Routers/ASUS-Gaming-Routers/RT-AX82U/

DETAILS

The Asus RT-AX82U router is one of the newer Wi-Fi 6 (802.11ax)-enabled routers that also supports mesh networking with other Asus routers. Like basically every other router, it is configurable via a HTTP server running on the local network. However, it can also be configured to support remote administration and monitoring in a more IOT style.

In order to enable remote management and monitoring of our Asus Router, so that it behaves just like any other IoT device, there are a couple of settings changes that need to be made. First we must enable WAN access for the HTTPS server (or else nothing could manage the router), and then we must generate an access code to link our device with either Amazon Alexa or IFTTT. These options can all be found internally at 

http://router.asus.com/Advanced_Smart_Home_Alexa.asp
.

As a high level overview, upon receiving this code, the remote website will connect to your router at the 

get_IFTTTtoken.cgi
 web page and provide a 
shortToken
HTTP query parameter. Assuming this token is received within 2 minutes of the aforementioned access code being generated, and also assuming this token matches what’s in the router’s nvram, the router will respond back with an 
ifttt_token
 that grants full administrative capabilities to the device, just like the normal token used after logging into the device via the HTTP server.

0002863c  int32_t do_get_IFTTTToken_cgi(int32_t arg1, FILE* arg2)
00028660      char* r0 = get_UA_Type(inpstr: &user_agent)                 // [1]
00028668      char* r0_2
00028668      if (r0 != 4)   // asusrouter-Windows-IFTTT-1.0
000286b0          r0_2 = get_UA_Type(inpstr: &user_agent)

// [...]
000286cc      void var_30
000286cc      memset(&var_30, 0, 0x20)
000286d8      char* r0_4 = check_if_queryitem_exists("shortToken")        // [2]
000286e0      if (r0_4 == 0)
000286e4          r0_4 = &nullptr

000286ec      int32_t r0_5 = gen_IFTTTtoken(token: r0_4, outbuf: &var_30) // [3]

00028700      fputs(str: &(*"\tif (disk_num == %d) {\n")[0x15], fp: arg2)
00028708      fflush(fp: arg2)
0002871c      fprintf(stream: arg2, format: ""ifttt_token":"%s",\n", &var_30) // [4]
00028724      fflush(fp: arg2)
00028738      fprintf(stream: arg2, format: ""error_status":"%d"\n", r0_5)
00028740      fflush(fp: arg2)
00028750      fputs(str: &data_81196, fp: arg2)
00028760      return fflush(fp: arg2)

At [1], the function pulls out the “User-Agent” header of our HTTP GET request and checks to see if it starts with “asusrouter”. It also checks if the text after the second dash is either “IFTTT” or “Alexa”. In either of those cases, it returns 4 or 5, and we’re allowed to proceed in the code path. At [2], the function pulls out the 

shortToken
 query parameter from our HTTP GET request and passes that into the 
gen_IFTTTtoken
 function at [3]. Assuming there is a match, 
gen_IFTTTtoken
will output the 
ifttt_token
 authentication buffer to 
var_30
, which is then sent back to the HTTP sender at [4]. Looking at 
gen_IFTTTtoken
:

0007b5c8  int32_t gen_IFTTTtoken(char* token, uint8_t* outbuf)

0007b5d4      int32_t r0 = uptime()
0007b5fc      memset(&ifttt_token_copy, 0, 0x20)
0007b614      int32_t r0_8
0007b614      int32_t arg3
0007b614      int32_t arg4
0007b614      if (r0 - nvram_get_int("ifttt_timestamp") s> 120)       // [5]
0007b6ec          if (isFileExist("/tmp/IFTTT_ALEXA") s> 0)
0007b710              Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] short token timeout\n", "gen_IFTTTtoken", 0x3ff, token, outbuf, arg3, arg4)
0007b714          r0_8 = 1
0007b630      else if (nvram_get_and_cmp("ifttt_stoken", token) == 0) // [6]
0007b72c          if (isFileExist("/tmp/IFTTT_ALEXA") s> 0)
0007b760              Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] short token is not the same: endp…", "gen_IFTTTtoken", 0x402, token, p2_nvram_get(item: "ifttt_stoken"), arg3, arg4)
0007b764          r0_8 = 2
0007b64c      else if (get_UA_Type(inpstr: &user_agent) != 4)
0007b77c          if (isFileExist("/tmp/IFTTT_ALEXA") s> 0)
0007b7a0              Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] user_agent not from IFTTT/ALEXA\n", "gen_IFTTTtoken", 0x405, token, outbuf, arg3, 0xf1430)
0007b7a4          r0_8 = 3
0007b668      else
0007b668          int32_t r2
0007b668          uint8_t* r3
0007b668          r2, r3 = nvram_set("skill_act_code", p2_nvram_get(item: "skill_act_code_t"))
0007b674          generate_asus_token(dst: &ifttt_token_copy, len: 0x20, r2, readsrc: r3)     // [7]
0007b684          strlcpy(dst: outbuf, src: &ifttt_token_copy, len: 0x20)
0007b694          nvram_set("ifttt_token", &ifttt_token_copy)
0007b698          nvram_commit()
0007b6ac          if (isFileExist("/tmp/IFTTT_ALEXA") s> 0)
0007b6d0              Debug2File("/tmp/IFTTT_ALEXA.log", "[%s:(%d)][HTTPD] get IFTTT long token success\n", "gen_IFTTTtoken", 0x408, token, outbuf, arg3, 0xf1430)
0007b6d4          r0_8 = 0
0007b7ac      return r0_8

Right at the beginning there is a check [5] to see if the uptime of the device is more than two minutes after the 

ifttt_stoken
 has been generated. Assuming we are within that timeframe, the 
ifttt_stoken
 nvram item is grabbed and compared with our 
shortToken
 at [6]. If there’s a match, we end up hitting the code branch around [7], where the device generates a new 
ifttt_token
 and copies it to the output buffer on the next line of code. As a reminder, this token grants the same admin access as the normal HTTP login token.

While nothing really seems out of place at the moment, let’s take a look over at the code which actually generates the 

ifttt_stoken
:

00074210  uint8_t* do_ifttt_token_generation(uint8_t* output)
// [...]
000742c0      char ifttt_token[0x80]
000742c0      memset(&ifttt_token, 0, 0x80)
000742d0      char timestamp[0x80]
000742d0      memset(&timestamp, 0, 0x80)
000742e0      char rbinstr[0x8]
000742e0      rbinstr[0].d = 0
000742e8      int32_t* randbinstrptr = &rbinstr
000742f4      rbinstr[4].d = 0
00074308      srand(x: time(timer: nullptr))
0007431c      // takes the remainder...
00074324      int_to_binstr(inp: __aeabi_idivmod(rand(), 0xff), cpydst: randbinstrptr, len: 7)              // [8]
// [...]
00074608      snprintf(s: &ifttt_token, maxlen: 0x80, format: &percent_o, binary_str_to_int(randbinstrptr)) // [9]
0007461c      nvram_set("ifttt_stoken", &ifttt_token)
00074638      snprintf(s: &timestamp, maxlen: 0x80, format: &percentld, uptime())                           // [10]
00074648      nvram_set("ifttt_timestamp", &timestamp)
00074658      strlcpy(dst: output, src: &skill_act_code, len: 0x48)
0007465c      nvram_commit()
0007466c      return output

With the unimportant code cut out, we are left with a somewhat clear view of the generation process. At [8] a random number is generated that is then moded against 0xFF. This number is then transformed into a binary string of length 8 (e.g. ‘00101011’). A lot further down at [9], this 

randbinstrptr
 is converted back to an integer and fed into a call to 
snprintf(&ifttt_token, 0x80, "%o", ...)
, which generates the octal version of our original number. With this in mind, we can clearly see that the keyspace for the 
ifttt_stoken
 is only 255 possibilities, which makes brute forcing the 
ifttt_stoken
 a trivial matter. While normally this would not be a problem, since the 
ifttt_stoken
 can only be used for two minutes after generation, we can see a flaw in this scheme if we take a look at the 
ifttt_timestamp
’s creation. At [10] we can clearly see that it is the 
uptime()
 of the device in seconds (which is taken from 
sysinfo()
). If we recall the actual check from before:

0007b5d4      int32_t r0 = uptime()
// [...]
0007b614      if (r0 - nvram_get_int("ifttt_timestamp") s> 120)
// [...]
0007b630      else if (nvram_get_and_cmp("ifttt_stoken", token) == 0)

We can see that the current uptime is used against the uptime of the generated token. Unfortunately for the device, 

uptime
 starts from when the device was booted, so if the device ever restarts or reboots for any reason, the 
ifttt_stoken
 suddenly becomes valid again since the current uptime will most likely be less than the 
uptime()
 call at the point of 
ifttt_stoken
 generation. Neither the 
ifttt_timestamp
 or the 
ifttt_stoken
 are ever cleared from nvram, even if the Amazon Alexa and IFTTT setting are disabled, and so the device will remain vulnerable from the moment of first generation of the configuration.

Asus RT-AX82U cfg_server cm_processREQ_NC information disclosure vulnerability

CVE-2022-38105

SUMMARY

An information disclosure vulnerability exists in the cm_processREQ_NC opcode of Asus RT-AX82U 3.0.0.4.386_49674-ge182230 router’s configuration service. A specially-crafted network packets can lead to a disclosure of sensitive information. An attacker can send a network request to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Asus RT-AX82U 3.0.0.4.386_49674-ge182230

PRODUCT URLS

RT-AX82U — https://www.asus.com/us/Networking-IoT-Servers/WiFi-Routers/ASUS-Gaming-Routers/RT-AX82U/

DETAILS

The Asus RT-AX82U router is one of the newer Wi-Fi 6 (802.11ax)-enabled routers that also supports mesh networking with other Asus routers. Like basically every other router, it is configurable via a HTTP server running on the local network. However, it can also be configured to support remote administration and monitoring in a more IOT style.

The 

cfg_server
 and 
cfg_client
 binaries living on the Asus RT-AX82U are both used for easy configuration of a mesh network setup, which can be done with multiple Asus routers via their GUI. Interestingly though, the 
cfg_server
 binary is bound to TCP and UDP port 7788 by default, exposing some basic functionality. The TCP port and UDP ports have different opcodes, but for our sake, we’re only dealing with the TCP opcodes which look like such:

type_dict = {
   0x1    :   "cm_processREQ_KU",   // [1]
   0x3    :   "cm_processREQ_NC",   // [2]
   0x4    :   "cm_processRSP_NC",
   0x5    :   "cm_processREP_OK",
   0x8    :   "cm_processREQ_CHK",
   0xa    :   "cm_processACK_CHK",
   0xf    :   "cm_processREQ_JOIN",
   0x12   :   "cm_processREQ_RPT",
   0x14   :   "cm_processREQ_GKEY",
   0x17   :   "cm_processREQ_GREKEY",
   0x19   :   "cm_processREQ_WEVENT",
   0x1b   :   "cm_processREQ_STALIST",
   0x1d   :   "cm_processREQ_FWSTAT",
   0x22   :   "cm_processREQ_COST",
   0x24   :   "cm_processREQ_CLIENTLIST",
   0x26   :   "cm_processREQ_ONBOARDING",
   0x28   :   "cm_processREQ_GROUPID",
   0x2a   :   "cm_processACK_GROUPID",
   0x2b   :   "cm_processREQ_SREKEY",
   0x2d   :   "cm_processREQ_TOPOLOGY",
   0x2f   :   "cm_processREQ_RADARDET",
   0x31   :   "cm_processREQ_RELIST",
   0x33   :   "cm_processREQ_APLIST",
   0x37   :   "cm_processREQ_CHANGED_CONFIG",
   0x3b   :   "cm_processREQ_LEVEL",
}  

Out of the 24 different opcodes, only 3 or so can be used without authentication, and so let’s start from the top with 

cm_processREQ_KU
 [1]. The simplest request, it demonstrates the basic TLV structure of the 
cfg_server
:

struct REQ_TLV = {
    uint32_t tlv_type;
    uint32_t size;
    uint32_t crc;
    char buffer[];
}

For the 

cm_processREQ_KU
 request, 
type
 is 1 and the 
crc
 doesn’t actually matter, but the 
size
 field will always be the size of the 
buffer
 field, not the rest of the headers. Regardless, this particular request gets responded to with the server’s public RSA key. This RSA key is needed in order to send a valid 
cm_processREQ_NC
[2] packet, which is where our bug is. The 
cm_processREQ_NC
 request is a bit complex, but the structure is given below:

struct REQ_NC = {
    uint32_t tlv_type = "\x00\x00\00\x03",
    uint32_t size,
    uint32_t crc,
    uint32_t tlv_subpkt1 = "\x00\x00\x00\x01", //[3]
    uint32_t sizeof_subpkt1,
    uint32_t crcof_subpkt1,
    char master_key[ ],                        //[4]
    uint32_t tlv_subpkt2 = "\x00\x00\x00\x03",
    uint32_t sizeof_subpkt2,   
    uint32_t crcof_subpkt2,
    char client_nonce[ ],                      //[5]
}

The 

cm_processREQ_KU
 request provides the server with two different items that are used to generate the session key needed for all subsequent requests, the 
master_key
[3] and the 
client_nonce
[4]. A quick note before we get to that: Everything in the packet starting from the 
tlv_subpkt1
 field at [3] gets encrypted by the RSA public key that we get from the 
cm_processREQ_KU
 request, so there’s an implicit length limitation due to RSA encryption. Continuing on, the 
master_key
[4] buffer is used as the  
aes_ebc_256
 key that the server will use to encrypt the response to this packet, and the 
client_nonce
 buffer is used to generate a session key later on. Let us now examine what the server sends in return:

[~.~]> x/60bx $r1
0xb62014e0:     0x00    0x00    0x00    0x02    0x00    0x00    0x00    0x20 // headers
0xb62014e8:     0x06    0x42    0x18    0x4f    
   
0xb62014ec:     0x13    0x9f    0x09    0x97 // server nonce [6]
0xb62014f0:     0x90    0x92    0x9b    0x85    0xe5    0x40    0xa1    0x38
0xb62014f8:     0xd7    0x81    0x62    0x72    0xf6    0x88    0x5c    0xef
0xb6201500:     0x61    0x86    0x5c    0xc0    0xef    0xc0    0x06    0x23
0xb6201508:     0xa2    0x6d    0x6a    0x85    
                                 
0xb620150c:     0x00    0x00    0x00    0x03                     // headers
0xb6201510:     0x00    0x00    0x00    0x04    0x51    0xb3    0x28    0x43
0xb6201518:     0xcc    0xcc    0xcc    0xcc // [...]            // client nonce [7]

Both the 

server_nonce
 at [6] and the 
client_nonce
[7] are AES encrypted and sent back to us. Subsequent authentication consists of generating a session key from 
sha256(groupid + server_nonce + client_nonce)
. In order to hit our bug, we don’t even need to go that far. Let us take a quick look at how the AES encryption happens:

0001d8b0    void *aes_encrypt(char *enckey, char *inpbuf, int32_t inpsize, uint32_t outsize){
0001d8c8         int32_t ctx = EVP_CIPHER_CTX_new();
0001d8d4         if (ctx == 0){ ... }
0001d904         else {
0001d914              int32_t r0_2 = EVP_EncryptInit_ex(ctx: ctx, type: EVP_aes_256_ecb(), imple: null, key: enckey, iv: nullptr);

We don’t need to delve too much into what occurs; it suffices to know that the key is passed directly from the 

enckey
 parameter straight into the 
EVP_EncryptInit_ex
. Backing up to find what exactly is passed:

00057534 int32_t cm_processREQ_NC(int32_t clifd, struct ctrlblk *ctrl, void *tlvtype, int32_t pktlen, void *tlv_checksum, struct tlv_ret_struct* sess_block, char *pktbuf, uint32_t client_ip, char *cli_mac){
{...}
00058b58    void *aes_resp = aes_encrypt(enckey: sess_block->master_key, inpbuf: nonce_buff, inpsize: clinonce_len + sess_block->server_nonce_len + 0x18, outsize: &act_resp_b_size);

We can see it involves the masterkey that we provided. Let’s back up further in 

cm_processREQ_NC
 to see exactly how it’s populated:

00057534 int32_t cm_processREQ_NC(int32_t clifd, struct ctrlblk *ctrl, void *tlvtype, int32_t pktlen, void *tlv_checksum, struct tlv_ret_struct* sess_block, char *pktbuf, uint32_t client_ip, char *cli_mac){
// [...]
00057af4              int32_t req_type = decbuf_0x1002.request_type_le
00057af4              int32_t req_len = decbuf_0x1002.total_len_le
00057af4              int32_t req_crc = decbuf_0x1002.crc_mb
00057b00              int32_t reqlen = req_len u>> 0x18 | (req_len u>> 0x10 & 0xff) << 8 | (req_len u>> 8 & 0xff) << 0x10 | (req_len & 0xff) << 0x18
00057b08              int32_t reqcrcle_
00057b08              if (reqlen != 0)
00057b10                  reqcrcle_ = req_crc u>> 0x18 | (req_crc u>> 0x10 & 0xff) << 8 | (req_crc u>> 8 & 0xff) << 0x10 | (req_crc & 0xff) << 0x18
00057b18                  if (reqcrcle_ != 0)
00057bb4                      if (req_type != 0x1000000)  // master key [8]
                                    // [...]
00057c48                      int32_t decsize_m0xc = size_of_decrypted - 0xc
00057c50                      if (decsize_m0xc u< reqlen)  // [9]
                                    // [...]
00057cf0                      char (* var_1048_1)[0x1000] = &dec_buf_contents
00057d00                      if (do_crc32(IV: 0, buf: &dec_buf_contents, bufsize: reqlen) != reqcrcle_) [10]
                                    // [...]
00057d94                      sess_block->masterkey_size = reqlen
00057d9c                      char* aeskey_malloc = malloc(bytes: reqlen) // [11]
00057da8                      sess_block->master_key = aeskey_malloc
                                    // [...]
00057db8                      memset(aeskey_malloc, 0x0, reqlen);  
00057dd8                      memcpy(aeskey_malloc, &dec_buf_contents, reqlen); 

Trimming out all the error cases, we start from where the server starts reading the bytes decrypted with its RSA private key. All the fields have their endianess reversed, and the sub-request type is checked at [8]. A size check at [9] prevents us from doing anything silly with the length field in our master_key message, and a CRC check occurs at [10]. Finally the 

sess_block-&gt;master_key
 allocation occurs at [11] with a size that is provided by our packet.

Now, an important fact about AES encryption is that the key is always a fixed size, and for AES_256, our key needs to be 0x20 bytes. As noted above however, there’s not actually any explicit length check to make sure the provided 

master_key
 is 0x20 bytes. Thus, if we provide a 
master_key
 that’s say, 0x4 bytes, a 
malloc
memset
 and 
memcpy
 of size 0x4 will occur.  
aes_encrypt
 will read 0x20 bytes from the start of our 
master_key
’s heap allocation, resulting in an out-of-bound read and assorted heap data being included into the AES key that encrypts the response. While not exactly a straight-forward leak, we can figure out these bytes if we slowly oracle them out. Since we know what the last bytes of the response should be (the 
client_nonce
 that we provide), we can simply give a 
master_key
that’s 0x1F bytes, and then brute force the last byte locally, trying to decrypt the response with each of the 0xFF possibilities until we get one that correctly decrypts. Since we know the last byte, we can then move onto the second-to-last byte, and so-on and so forth, until we get useful data.

While the malloc that occurs can go into a different bucket based on the size of our provided 

master_key
, heuristically it seems that the same heap chunk is returned with a 
master_key
 of less than 0x1E bytes. A different chunk is returned if the key is 0x1F or 0x1E bytes long. If we thus give a key of 0x1D bytes, we have to brute-force 3 bytes at once, which takes a little longer but is still doable. After that we can go byte-by-byte again and leak important information such as thread stack addresses.

Crash Information

$python infoleak.py

Type: 1 (cm_processREQ_KU)
Len:  0x4
CRC:  0x56b642cd
===MSG===
\x11\x22\x33\x44
=========

b'\x00\x00\x00\x01\x00\x00\x00\x04V\xb6B\xcd\x11"3D'
[^_^] Importing:
-----BEGIN PUBLIC KEY-----
[...]
-----END PUBLIC KEY-----

Type: 3 (cm_processREQ_NC)
Len:  0x100
CRC:  0x92657321
===MSG===
\x1a\x54\xd7\x4a\xf6\x7a\xe1\x4c\x16\x76\x69\x74\x2b\x96\x41\xc6\xa0\xbc\x57\x58\x45\x61\xa9\xa9\x04\x09\xae\xb4\xb2\x9c\x54\xdd\xb8\xd1\x8f\x0d\x25\xf6\x79\x07\xd6\x65\x12\x75\xbb\x7d\x2d\x4e\x41\xf0\xa9\x47\x75\xa5\x73\x2d\x4c\x02\x10\x9e\xb1\x3a\x2c\xa5\x1c\x11\xfe\x35\x8e\xd3\x95\x53\xe5\x90\x3a\x9a\x8b\xad\x9b\x10\x81\xde\xd3\x67\x19\x9d\x34\x44\x52\x75\x1d\x90\xc7\xbf\x19\xf1\x04\x15\x19\xd4\x11\x2d\x70\xbd\xa9\x87\xdf\x22\x59\xc2\xb0\xb1\xd5\x7b\x5a\xcb\xe7\xc7\x34\x0f\xcb\xa6\x9f\x81\x5c\xb3\x6d\xf7\x1c\x49\xd7\xed\x72\x54\x85\xe0\xca\x32\x96\xa9\xa2\x44\xda\x56\xfb\xf7\x96\x21\x53\xb7\xbe\x9c\xc9\x5f\x4a\x00\xdb\x2f\xd2\x6e\x1b\xf5\xdc\xa9\xa5\x8f\xde\xf5\x80\x83\xd7\xd8\x65\xe8\x6f\xd6\x0a\x3e\x10\x92\xca\xd2\xbf\x14\x1c\x06\xf0\x53\xb5\x41\xea\x2a\xe2\x5c\x2a\xa8\xb9\xa2\x92\xe7\xd5\x44\x55\x1c\x8e\x9b\xff\x13\x37\x60\x5b\x82\xfa\xa0\xe7\x44\x8f\x0b\xe9\x8f\x64\xcd\xa4\x50\xe9\xcd\xbc\x14\x34\xed\x57\xc5\x0a\xaf\xc3\x8d\x71\xee\x48\x35\x90\xa6\xb7\x08\x6c\xfb\xb1\xbf\xee\x0c\x72\x21\xdf\x4e\x29\xf9
=========

[^_^] Leaked Bytes: 0x0000b620
b'\x00\x00\x00\x02\x00\x00\x00 \x1a=\xac\x11\xebVxU\xe7\\\xdb8\x02\\k\n<\x91_>\x17\xc6r\x08\xfc\xbc\xde\xf6\x1a\x1ev\xfa\x03_\xf0y\x00\x00\x00\x03\x00\x00\x00\x07\x10\xc1\x06\xa9\xcc\xcc\xcc\xcc\xcc\xcc\xcc\x01'

Asus RT-AX82U cfg_server cm_processConnDiagPktList denial of service vulnerability

CVE-2022-38393

SUMMARY

A denial of service vulnerability exists in the cfg_server cm_processConnDiagPktList opcode of Asus RT-AX82U 3.0.0.4.386_49674-ge182230 router’s configuration service. A specially-crafted network packet can lead to denial of service. An attacker can send a malicious packet to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Asus RT-AX82U 3.0.0.4.386_49674-ge182230

PRODUCT URLS

RT-AX82U — https://www.asus.com/us/Networking-IoT-Servers/WiFi-Routers/ASUS-Gaming-Routers/RT-AX82U/

DETAILS

The Asus RT-AX82U router is one of the newer Wi-Fi 6 (802.11ax)-enabled routers that also supports mesh networking with other Asus routers. Like basically every other router, it is configurable via a HTTP server running on the local network. However, it can also be configured to support remote administration and monitoring in a more IOT style.

The 

cfg_server
 and 
cfg_client
 binaries living on the Asus RT-AX82U are both used for easy configuration of a mesh network setup, which can be done with multiple Asus routers via their GUI. Interestingly though, the 
cfg_server
 binary is bound to TCP and UDP port 7788 by default, exposing some basic functionality. The TCP port and UDP ports have different opcodes, but for our sake, we’re only dealing with a particular set of ConnDiag opcodes which look like such:

struct tlv_holder connDiagPacketHandlers = 
{
    uint32_t type = 0x5
    tlv_func *tfunc = cm_processREQ_CHKSTA
}
struct tlv_holder connDiagPacketHandlers[1] = 
{
    uint32_t type = 0x6
   tlv_func *tfunc = cm_processRSP_CHKSTA
}

The above TLVs are accessible from the 

cm_recvUDPHandler
 thread in a particular codeflow:

0001ed90      cm_recvUdpHandler()
              // [...]
0001edf8      int32_t bytes_read = recvfrom(sfd: cm_ctrlBlock.udp_sock, buf: &readbuf, len: 0x7ff, flags: 0, srcaddr: &sockadd, addrlen: &sockaddsize) // [1]
                // [...]
0001ee00      if (bytes_read == 0xffffffff)
                // [...]
0001ee98      else if (sockadd.sa_data[2].d != cm_ctrlBlock.self_address)
                // [...]
0001f0e0          char* malloc_824 = malloc(bytes: 0x824) // [2]
0001f0e4          struct udp_resp* inp = malloc_824
0001f0e8          if (malloc_824 != 0)
0001f184              memset(malloc_824, 0, 0x824)        // [3]
0001f194              memcpy(inp, &readbuf, bytes_read)
0001f198              int32_t ipaddr = sockadd.sa_data[2].d
0001f19c              inp->bytes_read = bytes_read
0001f1a4              int32_t ip = ipaddr u>> 0x18 | (ipaddr u>> 0x10 & 0xff) << 8 | (ipaddr u>> 8 & 0xff) << 0x10 | (ipaddr & 0xff) << 0x18
0001f1d4              snprintf(s: &inp->ip_addr_str, maxlen: 0x20, format: "%d.%d.%d.%d", ip u>> 0x18, ip u>> 0x10 & 0xff, ip u>> 8 & 0xff, ror.d(ip, 0) & 0xff, var_864, var_860, var_85c, var_858, var_854)
0001f1dc              int32_t var_838_1 = readbuf[4].d
0001f1dc              int32_t var_834_1 = readbuf[8].d
0001f1e8              if (readbuf[0].d == 0x6000000)      // [4]
0001f1f0                  r0_6 = cm_addConnDiagPktToList(inp: inp)

At [1], the server reads in 0x7ff bytes from its UDP 7788 port, and at [2] and [3], the data is then copied from the stack over to a cleared-out heap allocation of size 0x824. Assuming the first four bytes of the input packet are “\x00\x00\x00\x06”, then the packet gets added to a particular linked list structure, the 

connDiagUdpList
. Before we continue on though, it’s appropriate to list out the structure of the input packet:

struct tlv_pkt {
    uint32_t type;
    uint32_t datalen;
    uint32_t crc;
    uint8_t data[];
}

Continuing on, another thread is constantly polling the 

connDiagUdpList
, and if a packet is seen, then we jump over to 
cm_processConnDiagPktList()
:

00053ca8  int32_t cm_processConnDiagPktList()    
00053cc8      pthread_mutex_lock(mutex: &connDiagLock)
00053cd8      struct list* connDiagUdp = connDiagUdpList
00053ce8      if (connDiagUdp->entry_count s> 0)
00053d2c          for (struct listitem* item = connDiagUdp->tail; item != 0; item = item->next)
00053d30              struct udp_resp* input_pkt = item->inp
00053d38              if (input_pkt != 0)
00053d44                  uint32_t null = terminateConnDiagPktList
00053d4c                  if (null != 0)
00053d4c                      break
00053d50                  uint32_t hex_6000000 = input_pkt->req_type_le
00053d58                  uint32_t dlen = input_pkt->datalen_le
00053d68                  int32_t dlenle = input_pkt->bytes_read - 0xc  // [5]
00053d6c                  uint32_t crcle = input_pkt->crcle
                            // [...]
00053d80                  if (dlenle == (dlen u>> 0x18 | (dlen u>> 0x10 & 0xff) << 8 | (dlen u>> 8 & 0xff) << 0x10 | (dlen & 0xff) << 0x18)) //[6]
00053e0c                      char* buf = &input_pkt->readbuf
00053e18                      crc = do_crc32(IV: null, buf: buf, bufsize: dlenle) // [7]

At [5], the actual length of the input packet minus twelve is compared against the length field inside the packet itself [6]. Assuming they match, the CRC is then checked, another field provided in the packet itself. A flaw is present in this function, however, in that there is a check missing in this code path that can be seen in both the TCP and UDP handlers: the code needs to verify that the size of the received packet is >= 0xC bytes. Thus, if a packet is received that is less than 0xC bytes, the 

dlenle
 field at [5] underflows to somewhere between 
0xFFFFFFFC
 and 
0xFFFFFFFF
. The check against the length field [6] can be easily bypassed by just correctly putting the underflowed length inside the packet. The CRC check at [7] isn’t an issue, since if the 
bufsize
 parameter is less than zero, it automatically skips CRC calculation. Since a CRC skip results in a return value of 0x0, we need to make sure that the 
crc
 field is “\x00\x00\x00\x00”. Conveniently, this is handled already for us if our packet is only 8 bytes long, since the buffer that the packet lives in was 
memset
 to 0x0 beforehand.

While we can pass all the above checks with an 8-byte packet, it does prevent us from having any control over what occurs after. We end up hitting 

cm_processConnDiagPkt(uint32_t tlv_type, uint32_t datalen, uint32_t crc, char *databuf, char *ipaddr)
 which just passes us off to the appropriate TLV handler. Since our opcode has to be “\x00\x00\x00\x06”, we always hit 
cm_processRSP_CHKSTA(char *pktbuf, uint32_t pktlen, uint32_t ipaddr)
:

00052f20  int32_t cm_processRSP_CHKSTA(char* pktbuf, uint32_t pktlen, int32_t ipaddr)
00052f50      char jsonbuf[0x800]
00052f50      memset(&jsonbuf, 0, 0x800)
                             // [...]
00052f64      if (cm_ctrlBlock.group_key_ready != 0)
00053004          char* groupkey = cm_selectGroupKey(which_key: 1)
0005300c          if (groupkey == 0)
                                              // [...]
00053098              goto label_530a0
000530c0          char* r0_11 = do_decrypt(sesskey1: groupkey, sesskey2: cm_selectGroupKey(which_key: 0), pktbuf: pktbuf, pktlen: pktlen) //[8]

Assuming there is a group key (which there should always be, even if the AImesh setting is not configured), then we end up hitting the 

do_decrypt
 function at [8], which decrypts the data of our input packet with one of the groupkeys. The 
do_decrypt
 function ends up hitting 
aes_decrypt
 as shown below:

0001db18  void* aes_decrypt(char* sesskey1, char* pktbuf, char* pktlen, int32_t* outlen)
0001db30      int32_t ctx = EVP_CIPHER_CTX_new()
0001db38      int32_t outl = 0
0001db3c      void* ctx = ctx
0001db40      void* ret
0001db40      if (ctx == 0)
                                    // [...]
0001db6c      else
0001db6c          char* bytesleft = nullptr
0001db7c          int32_t r0_2 = EVP_DecryptInit_ex(ctx, EVP_aes_256_ecb(), 0, sesskey1, 0)
                                     // [...]
0001db84          if (r0_2 != 0)
0001dba0              *outlen = 0
0001dbac              void* alloc_size = EVP_CIPHER_CTX_block_size(ctx) + pktlen
0001dbb4              maloced = malloc(bytes: alloc_size)  // 0xc...
0001dbbc              if (maloced == 0)
                                                     //[...]
0001dbe4              else
0001dbe4                  memset(maloced, 0, alloc_size)
0001dbec                  void* mbuf = maloced
0001dbf0                  char* pktiter = pktlen
0001dc00                  void* inpbuf
0001dc00                  void* r3_2
0001dc00                  while (true)
0001dc00                      inpbuf = &pktbuf[pktlen - pktiter]
0001dc04                      if (pktiter u<= 0x10)
0001dc04                          break
0001dc10                      bytesleft = 0x10
0001dc1c                      int32_t r0_8 = EVP_DecryptUpdate(ctx, mbuf, &outl, inpbuf, 0x10) //[9]
0001dc20                      r3_2 = r0_8
0001dc24                      if (r0_8 == 0)
0001dc24                          break
0001dc60                      int32_t outl_len = outl
0001dc64                      pktiter = pktiter - 0x10
0001dc6c                      mbuf = mbuf + outl_len
0001dc74                      *outlen = *outlen + outl_len

For brevity’s sake, we can skip all the way to [9], where 

EVP_DecryptUpdate
 is called repeatedly in a loop over the input buffer. Since the 
pktlen
 argument has been underflowed to atleast 0xFFFFFFFC, it suffices to say that we have a wild read, resulting in a crash when reading unmapped memory.

Crash Information

potentially unexpected fatal signal 11.
CPU: 1 PID: 12452 Comm: cfg_server Tainted: P           O    4.1.52 #2
Hardware name: Generic DT based system
task: d04cd800 ti: d0632000 task.ti: d0632000
PC is at 0xb6c7f460
LR is at 0xb6d3ca04
pc : [<b6c7f460>]    lr : [<b6d3ca04>]    psr: 60070010
sp : b677c46c  ip : 00ff4ff4  fp : b6600670
r10: b6c7ef40  r9 : 00000000  r8 : beec0b82
r7 : b6600670  r6 : 00000010  r5 : b6620c38  r4 : 00ff5004
r3 : b6c7f440  r2 : 00000000  r1 : 00000000  r0 : 00000000
Flags: nZCv  IRQs on  FIQs on  Mode USER_32  ISA ARM  Segment user
Control: 10c5387d  Table: 1048c04a  DAC: 00000015
CPU: 1 PID: 12452 Comm: cfg_server Tainted: P           O    4.1.52 #2
Hardware name: Generic DT based system
[<c0026fe0>] (unwind_backtrace) from [<c0022c38>] (show_stack+0x10/0x14)
[<c0022c38>] (show_stack) from [<c047f89c>] (dump_stack+0x8c/0xa0)
[<c047f89c>] (dump_stack) from [<c003ac30>] (get_signal+0x490/0x558)
[<c003ac30>] (get_signal) from [<c00221d0>] (do_signal+0xc8/0x3ac)
[<c00221d0>] (do_signal) from [<c0022658>] (do_work_pending+0x94/0xa4)
[<c0022658>] (do_work_pending) from [<c001f4cc>] (work_pending+0xc/0x20)