CVE-2021-42890: Hostime Remote Command Injection

10 minute read

Introduction

A vulnerability discovered in TOTOLINK EX1200T model known as CVE-2021-42889 which is a remote command injection through the HostTime 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, The

refore 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 contains HostTime parameter where the command get injected. In the Time Setting Under Management tab, We can notice Copy PC's Time:

By clicking on it we can see the following request made to cstecgi.cgi:

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 NTPSyncWithHost 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 "NTPSyncWithHost"; 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 copy for us the device time 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
NTPSyncWithHost
NTPSyncWithHost

Lib Name: upgrade.so

Lib Name: wireless.so

Lib Name: wps.so

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

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

As we can see the function is a small function:

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

void NTPSyncWithHost(undefined4 param_1,undefined4 param_2,undefined4 param_3)

Then, We can see Declare variables used within the function:

  undefined4 uVar1;
  FILE *__stream;
  int iVar2;
  char acStack_288 [256];
  undefined4 local_188;
  timeval local_184;
  char acStack_17c [100];
  char acStack_118 [256];
  • uVar1: an undefined4 type variable.

  • __stream: a pointer to a FILE object.

  • iVar2: an integer variable.

  • acStack_288: a character array of size 256.

  • local_188: an undefined4 type variable.

  • local_184: a timeval struct (used to represent time intervals).

  • acStack_17c: a character array of size 100.

  • acStack_118: a character array of size 256.

uVar1 = websGetVar(param_2,0x6110,0x6164);
memset(acStack_17c,0,100);
gettimeofday(&local_184,(__timezone_ptr_t)0x0);

After that Call the websGetVar() function with parameters param_2, 0x6110, and 0x6164, then store the result in uVar1. The websGetVar function is used to get the value of a variable from a web form submitted by a user. The two hexadecimal values 0x6110 and 0x6164 are string pointers representing variable names, which the function will search for in the submitted form. memset() function is to fill the acStack_17c character array with 0's, initializing it with a size of 100. After that gettimeofday() function with the address of the local_184 timeval struct and a null timezone pointer ((__timezone_ptr_t)0x0) and get the current time and stores it in the timeval struct local_184. So, we can see that the uVar1 carry the value of HostTime parameter. Now, Let’s rename it to HostTime.

  __stream = fopen((char *)0x611c,(char *)0x5db4);
  if (__stream != (FILE *)0x0) {
    fscanf(__stream,(char *)0x5e70,acStack_118);
    iVar2 = atoi(acStack_118);
    fclose(__stream);
    sprintf(acStack_17c,(char *)0x6134,local_184.tv_sec - iVar2);
    system(acStack_17c);
  }

In these lines, the fopen() function is called with two parameters, (char *)0x611c and (char *)0x5db4 and these two hexadecimal values are string pointers representing the file name and the file opening mode, respectively. The function opens the specified file and returns a FILE pointer which is stored in the __stream variable. If the file cannot be opened, fopen() will return a null pointer, Then it checks if __stream is not equal to a null pointer ((FILE *)0x0). If it is not null, that means the file has been successfully opened and the code inside the if statement will be executed which is a call to fscanf() function to read data from the opened file __stream, The function reads data according to the format specified by the string pointer (char *)0x5e70 and stores the result in the acStack_118 character array. The format string is a string that specifies how the data should be parsed from the file. Then atoi() function to convert the string in acStack_118 to an integer and store the result in the iVar2 variable. Finally, Close the opened file by calling the fclose() function with the __stream parameter and call the sprintf() function to format a string and store it in acStack_17c variable & Call the system() function to execute the value in acStack_17c variable. By moving to the following lines:

iVar2 = Validity_check(HostTime);
if (iVar2 == 0) {
    sprintf(acStack_288,(char *)0x6158,HostTime);
    CsteSystem(acStack_288,0);
    apmib_set(0x97,&local_188);
    apmib_update_web(4);
    system((char *)0x6168);
    websSetCfgResponse(param_1,param_3,0x5f40,0x5b88);
    system((char *)0x6180);
}

Here we can see Call the Validity_check() function with HostTime as a parameter, This function likely checks the validity of the NTP server address obtained from the web request and store the result in the iVar2 variable. After that the IF condition checks if iVar2 is equal to 0, If it is, this means the NTP server address is valid and the code inside the if statement will be executed. By using the sprintf() function to format a string and store it in the acStack_288 character array. The format string is specified by the string pointer (char *)0x6158 and the NTP server address HostTime is used as an argument to fill the placeholders in the format string, Then calling the CsteSystem() function with the acStack_288 character array and 0 as parameters and this function is a custom function made by the developer to run a system command to update the NTP server configuration with the user-specified server address which include our HostTime parameter value. After that, call apmib_set() function with 0x97 and the address of local_188 as parameters and sets a value in the Application MIB (Management Information Base) database. Call the apmib_update_web() function with 4 as a parameter to update the web-based configuration interface to reflect the changes made to the NTP server configuration. After all of this we can see that the issue occurs at the following lines:

sprintf(acStack_288,(char *)0x6158,HostTime);
    CsteSystem(acStack_288,0);

As there is no filtration to the user input and the parameter value can be manipulated by the user. It’s time to exploit it. I connected the device through telnet services first:

Now, Let’s go to Burp Suite and manipulate the request and show a PoC for the vulnerability:

As we can see in the above screenshot we were able to execute the command successfully.

Final Thoughts

The developer shall use an Asp endpoint to operate the time 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 date as the following:

void NTPSyncWithHost(undefined4 param_1,undefined4 param_2,undefined4 param_3)

{
  undefined4 HostTime;
  FILE *__stream;
  int iVar1;
  char acStack_288 [256];
  undefined4 local_188;
  timeval local_184;
  char acStack_17c [100];
  char acStack_118 [256];
  regex_t regex;
    int regex_status;
  // Compile the regular expression
    regex_status = regcomp(&regex, "^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$", REG_EXTENDED);
    if (regex_status) {
        printf("Could not compile regex.\n");
        return 1;
    }
     
  local_188 = 0;
  HostTime = websGetVar(param_2,0x6110,0x6164);
  memset(acStack_17c,0,100);
  gettimeofday(&local_184,(__timezone_ptr_t)0x0);
  __stream = fopen((char *)0x611c,(char *)0x5db4);
  if (__stream != (FILE *)0x0) {
    fscanf(__stream,(char *)0x5e70,acStack_118);
    iVar1 = atoi(acStack_118);
    fclose(__stream);
    sprintf(acStack_17c,(char *)0x6134,local_184.tv_sec - iVar1);
    system(acStack_17c);
  }
  // Check if the HostTime variable matches the regex pattern
  regex_status = regexec(&regex, HostTime, 0, NULL, 0);
    if (!regex_status) {
      iVar1 = Validity_check(HostTime);
      if (iVar1 == 0) {
        sprintf(acStack_288,(char *)0x6158,HostTime);
        CsteSystem(acStack_288,0);
        apmib_set(0x97,&local_188);
        apmib_update_web(4);
        system((char *)0x6168);
        websSetCfgResponse(param_1,param_3,0x5f40,0x5b88);
        system((char *)0x6180);
  	} else {
            return 1;
        }
  }
  return;
}

Here we used regex to check the patterns of the date 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-42890 and highlighted the issue made by the developer & Provided a solution that can help in mitigating the issue. Finally, You could use any other decompiler other than Ghidra as it’s not making the codes more clear.

 

References