CVE-2021-42885: deviceMac Remote Command Injection

13 minute read

Introduction

A vulnerability discovered in TOTOLINK EX1200T model known as CVE-2021-42885 which is a remote command injection through the deviceMac parameter, As a results a malicious user can control the device and achieve remote command execution RCE. (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.the request made that contains deviceMac parameter where the command get injected is as the following:

POST /cgi-bin/cstecgi.cgi HTTP/1.1
Host: 192.168.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8
If-Modified-Since: Thu, 01 Jan 1970 00:00:03 GMT
Connection: close
Content-Length: 68

{"topicurl":"setting/setDeviceName",
"deviceMac":"Vulnerable",
"deviceName":"1"}

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 deviceMac 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 "deviceMac"; 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 deviceMac and will print the results under the library name. Therefore, we will be able to know which library could has the vulnerable code or anything related. Command output:

Lib Name: app.so

Lib Name: cloudupdate.so

Lib Name: global.so
,{"deviceMac":"%s","deviceName":"%s"}
deviceMac

Lib Name: lan.so

Lib Name: product.so

Lib Name: system.so

Lib Name: upgrade.so

Lib Name: wireless.so

Lib Name: wps.so

And as we can see it’s with-in the global.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 setDeviceName function:

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

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

undefined4 setDeviceName(undefined4 param_1,undefined4 param_2,undefined4 param_3)

After that the following lines declaring variables:

  • char cVar1: This declares a variable cVar1 of type char.

  • bool bVar2: This declares a variable bVar2 of type bool.

  • char *pcVar3: This declares a variable pcVar3 of type char *, which is a pointer to a character.

  • char *pcVar4: This declares a variable pcVar4 of type char *, which is also a pointer to a character.

  • int iVar5: This declares a variable iVar5 of type integer.

  • FILE *__stream: This declares a variable __stream of type FILE *, which is a pointer to a FILE type used for input/output operations.

  • size_t sVar6: This declares a variable sVar6 of type size_t, which is an unsigned integer type. It’s commonly used to represent sizes of objects.

  • char *pcVar7: This declares a variable pcVar7 of type char *, which is another pointer to a character.

  • char *__s: This declares a variable __s of type char *, which is a pointer to a character.

  • char acStack_1138 [127]: This declares an array acStack_1138 of 127 characters. This array is allocated on the stack.

  • char acStack_10b9 [129]: This declares another array acStack_10b9 of 129 characters. This array is also allocated on the stack.

  • char local_1038 [32]: This declares an array local_1038 of 32 characters. This array is also allocated on the stack.

  • char local_1018 [4064]: This declares an array local_1018 of 4064 characters. This array is also allocated on the stack.

  • char *local_38: This declares a variable local_38 of type char *, which is yet another pointer to a character.

  • char *local_34: This declares a variable local_34 of type char *, which is also a pointer to a character.

  • char *local_30: This declares a variable local_30 of type char *, which is also a pointer to a character.

Then, When it comes to line 22 we can see the following calls for functions:

The memset() function is used to initialize arrays to a specific value for the acStack_1138 array with zeros and has a size of 0x80 (128 bytes) & acStack_10b9 array, starting from the second element acStack_10b9 + 1 with zeros and has a size of 0x80 (128 bytes). Then, initializes the local_1038 array with zeros and has a size of 0x1000 (4096 bytes). Finally, retrieves a value of the parameter deviceMac from the web request and stores it in pcVar3 and retrieves a value of the parameter deviceName from the web request and stores it in pcVar4. So, Let’s rename the pcVar3 to deviceMac & pcVar4 to deviceName. At the end executes a shell command using the system() function and the command being executed is /bin/jffs2.sh 1 2> /dev/null, Let’s connect to the router and check the file and what is it do. I connected the device through telnet services:

And as we can see here is the file:

When we cat the file we can get it’s content as the following:

#!/bin/sh
#
# script file to mount userdata.jffs2
#
# Usage: jffs2.sh {mtdname}
#

umount /dev/mtdblock2 2>/dev/null

sleep 1

if [ $1 -eq 1 ]; then
        mount -t jffs2 /dev/mtdblock2 /mnt -o rw 2>/dev/null
else
        mount -t jffs2 /dev/mtdblock2 /mnt -o ro 2>/dev/null
fi

if [ $? -ne 0 ]; then

        if [ $1 -eq 1 ]; then
                mount -t jffs2 /dev/mtdblock2 /mnt -o rw 2>/dev/null
        else
                mount -t jffs2 /dev/mtdblock2 /mnt -o ro 2>/dev/null
        fi

        if [ $? -ne 0 ]; then
                if [ $1 -eq 1 ]; then
                        mount -t jffs2 /dev/mtdblock2 /mnt -o rw 2>/dev/null
                else
                        mount -t jffs2 /dev/mtdblock2 /mnt -o ro 2>/dev/null
                fi

                if [ $? -ne 0 ]; then
                        echo "mount invalid! erase mtd and mount again!"
                        sysconf mtd_erase
                fi
        fi
fi

The script attempts to unmount any existing file system on /dev/mtdblock2. Any error messages produced by the command are discarded and waits for 1 second.After that checks the value of the argument $1. If it is equal to 1, the file system is mounted as read-write (-o rw option). Otherwise, it is mounted as read-only (-o ro option), Then checks the exit status of the previous command. If it is not equal to 0, the mount operation failed. If the mount operation failed, the script attempts to mount the file system again, up to three times. If the mount operation still fails after three attempts, the script outputs an error message and runs the command sysconf mtd_erase which erase the memory technology device (mtd) and mount the file system again. By moving on with the following lines:

the code checks for the existence of a file /mnt/customDeviceName, opens it, reads its contents, and performs some operations on the data. if the file /mnt/customDeviceName exists and is readable. If it does, access() returns 0, and iVar5 is set to 0. and if we go to the device again we can see that the file contains the data we send in the request:

After that checks if access() succeeded in finding the file. If the file exists,It opens the file for reading and if the file was successfully opened, a loop that reads lines from the file using fgets() and stores it in the buffer __s, Then performs some operations on the data until there are no more lines to read.After that strlen(__s) determines the length of the string read by fgets() and acStack_10b9[sVar6] sets the null terminator at the end of the string in the acStack_10b9. Then, checks if the second character in acStack_10b9 is not null and if the string contains a ,. If both conditions are met, the code extracts two strings from the line using strncpy() and strstr() & as we can guess it’s the deviceMac,deviceName and formats them using snprintf(). Finally, stores them in the local_1038 array. If no matching string is found, the code formats the two variables deviceMac and deviceName using snprintf() and stores them in local_1038. Finally, the file is closed using fclose().

If access() doesn’t succeeded in finding the file creates a string containing a command that writes the values of deviceMac and deviceName to the file /mnt/customDeviceName and then execute the command system(acStack_1138). After that checks if the local_1038 array contains any data a loop that writes data to the file /mnt/customDeviceName using by executing command. The loop continues until the end of the local_1018 array is reached and then If iVar3 is 0, the loop writes the first value of local_1038 to the file using the > redirection operator. Otherwise, it appends the value to the file using the >> redirection operator. So, as we can see clearly there is no input validation is done on the deviceMac & deviceName which can be manpulated by the user. Now, It’s time for exploiting it. Opening Burp Suite and take the request to the repeater tab and start to reproduce the bug.

After we send the request we can see that the poc file created under /tmp directory if we open our device through telnet services again we can find the file:

Final Thoughts

The developer shall use an Asp endpoint to operate the save of the deviceMac on the device as a different option instead of executing commands to do it. But, In our case of this code there are many solutions to make sure it will be hard for the user to escape the default command and inject malicious command using regex to check for a valid MAC address as the following:

char *regex_pattern = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$";

    // Compile the regex pattern
    regex_t regex;
    int reti = regcomp(&regex, regex_pattern, REG_EXTENDED);
    if (reti != 0) {
        fprintf(stderr, "Failed to compile regex\n");
        return 1;
    }

    // Execute the regex match
    reti = regexec(&regex, deviceMac, 0, NULL, 0);
    if (reti == 0) {
          system("/bin/jffs2.sh 1 2> /dev/null");
  iVar3 = access("/mnt/customDeviceName",0);
  if (iVar3 == 0) {
    __stream = fopen("/mnt/customDeviceName","r");
    if (__stream != (FILE *)0x0) {
      local_38 = local_1038;
      bVar2 = false;
      iVar3 = 0;
      __s = acStack_10b9 + 1;
      local_34 = "%s,%s";
      while (pcVar5 = fgets(__s,0x80,__stream), pcVar5 != (char *)0x0) {
        sVar4 = strlen(__s);
        acStack_10b9[sVar4] = '\0';
        if ((acStack_10b9[1] != '\0') && (pcVar5 = strchr(__s,0x2c), pcVar5 != (char *)0x0)) {
          local_30 = local_38;
          strncpy(local_38,__s,0x20);
          pcVar5 = strstr(local_38,deviceMac);
          if (pcVar5 != (char *)0x0) {
            snprintf(local_30,0x20,local_34,deviceMac,deviceName);
            bVar2 = true;
          }
          iVar3 = iVar3 + 1;
          local_38 = local_38 + 0x20;
        }
      }
      if (!bVar2) {
        snprintf(local_1038 + iVar3 * 0x20,0x20,"%s,%s",deviceMac,deviceName);
      }
      fclose(__stream);
    }
  }
  else {
    sprintf(acStack_1138,"echo \'%s,%s\' > /mnt/customDeviceName",deviceMac,deviceName);
    system(acStack_1138);
  }
  if (local_1038[0] != '\0') {
    iVar3 = 0;
    deviceMac = local_1038;
    deviceName = local_1018;
    do {
      if (iVar3 == 0) {
        sprintf(acStack_1138,"echo \'%s\' > /mnt/customDeviceName",deviceMac);
      }
      else {
        sprintf(acStack_1138,"echo \'%s\' >> /mnt/customDeviceName",deviceMac);
      }
      system(acStack_1138);
      iVar3 = iVar3 + 1;
      cVar1 = *deviceName;
      deviceMac = deviceName;
      deviceName = deviceName + 0x20;
    } while (cVar1 != '\0');
  }
    } else {
        return 1;
    }

Here we used regex to check the patterns of the deviceMac if it’s valid or no, If it’s not valid it will exit without executing anything. But, If it’s valid then it will execute the code normally.

Conclusion

In this analysis we had a look on CVE-2021-42885 and highlighted the issue made by the developer & Provided a solution that can help in mitigating the issue. Finally, You could use any other decompilers other than Ghidra as it’s not making the codes more clear.

References