CVE-2021-42887: TOTOLINK EX1200T LOGIN BYPASS

20 minute read

Introduction

A vulnerability discovered in TOTOLINK EX1200T model known as CVE-2021-42887 which lead to authentication bypass, as a results anyone exploit this vulnerability by sending a specific request through formLoginAuth.htm page will be able to access the device without a need to login then control 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

It’s the time for the analysis. We will need Burp Suite to see how is the request of the authentication looks like on the device before it’s go to the device web server.

 

As we can see in the above screenshot from Burp Suite, We can notice that the web app made out of Aasp.net and the formLoginAuth.htm is requested, If we go to it and take a look at the request which is as the following:

GET /formLoginAuth.htm?authCode=1&userName=admin&password=admin&goURL=home.asp&action=login 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/110.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.0.254/login.asp
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1

We can guess that this html page take the parameters as the following: authCode, userName, password, goURL & action. And then pass it to one of the ASP endpoints. So, Let’s go to the firmware we extracted and check the formLoginAuth.htm. Before that we can see also the login.asp, But if we go to it the request and the following:

GET /login.asp 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/110.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Referer: http://192.168.0.254/
Upgrade-Insecure-Requests: 1
If-Modified-Since: Thu, 01 Jan 1970 00:00:03 GMT
If-None-Match: "-1518032379"

And if you take a look at the URL it just for the login page you see. Let’s move forward with formLoginAuth.htm. By going to the \squashfs-root\web_cste directory we can find our web app files:

 

As you can see we can’t find formLoginAuth.htm you may think this page only get generated on the login process at the Runtime. Unforently, I checked it by accessing the device through telnet and it’s not get created. But, we have our login.asp page let’s take a look on this page. While going through the code ypu can notice that the login request is sent to cstecgi.cgi page in the cgi-bin directory:

 

 

And by going back to Burp Suite again, You can find the following requests made to the page:

 

We can see the login request obvious and by checking it out we can confirm it’s where the login request get process through it:

POST /cgi-bin/cstecgi.cgi?action=login 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/110.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
Origin: http://192.168.0.254
DNT: 1
Connection: close
Referer: http://192.168.0.254/login.asp
Upgrade-Insecure-Requests: 1

username=admin&password=admin

Now, By going to the file and check it with the file command, We can see it’s an ELF 32bit MIPS file:

It’s the time to do some reverse using Ghidra, 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, After a long time of searching 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 login 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 "login"; 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 work login and will print the results under the library name. Therefore, we will be able to know which library contains the login process or anything related. Command output:

Lib Name: app.so

Lib Name: cloudupdate.so

Lib Name: global.so
loginAuth
loginflag
loginAuth
loginAuthUrl
mobile/login.asp
login.asp
,"redirectURL":"http://%s/formLoginAuth.htm?authCode=%d&userName=%s&password=%s&goURL=%s&action=login&flag=1"}
,"redirectURL":"http://%s/formLoginAuth.htm?authCode=%d&userName=%s&password=%s&goURL=%s&action=login"}
loginpass
loginFlag
loginIp

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 loginAuth function:

So, the flaw of the login 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

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

Which are param_1, param_2 and param_3. By going through the rest of the code you gonna see that mostly are declaring of variables, Until we reach the line number 73:

We can see here it’s created 3 variables:

  • uVar1: In this variable it’s assigning a value of calling a function called websGetVar() and passing param_2, LoginAuthUrl which it could be the HTML page and an empty string at the end. (We will back to this function used again)

  • uVar2 : Here it’s assigning this variable to store json objects.

  • iVar5: Here it’s assigning 0.
    after that we can see a while loop and inside the loop we can see the following:

  while( true ) {
    iVar8 = iVar5 + 1;
    iVar5 = getNthValueSafe(iVar5,uVar1,0x26,auStack_1718,0x400);
    if (iVar5 == -1) break;
    iVar3 = getNthValueSafe(0,auStack_1718,0x3d,auStack_1318,0x80);
    iVar5 = iVar8;
    if ((iVar3 != -1) &&
       (iVar8 = getNthValueSafe(1,auStack_1718,0x3d,auStack_1298,0x100), iVar8 != -1)) {
      uVar4 = cJSON_CreateString(auStack_1298);
      cJSON_AddItemToObject(uVar2,auStack_1318,uVar4);
    }
  }

In this loop it’s adding 1 to iVar5 which is 0 above and after that assign the result to iVar8 variable. After that calling a function getNthValueSafe() and passing unknown parameters to it after that assign the result to iVar5. Then, there is an if condition checks for the value of iVar5 if it’s equal to -1 it will break the loop and if this condition didn’t executed it do the same before the condition with the iVar5 and getNthValueSafe() function but with different parameters and assign it to iVar3. Then, It will get back the value of iVar5 by assigning the value of iVar8 to it. in the second if condition it checks if iVar3 & the iVar8(After assign the getNthValueSafe() to it) values are not equal to -1 then it will create unknown JSON string and assign it to uVar4, Finally it will add an item to object to uVar2 which created before and it’s the JSON data then auStack_1318 which is the key and uVar4 which is the value. Let’s move forward with the rest of lines:

__s1 = (char *)websGetVar(uVar2,"username","admin");
pcVar6 = (char *)websGetVar(uVar2,"password","");
__src = (char *)websGetVar(uVar2,"http_host","");
__nptr = (char *)websGetVar(uVar2,&DAT_0001dd80,"");
apmib_get(0xb6,&local_1198);
apmib_get(0xb7,&local_1174);
apmib_get(0xc0,&local_17a0);

Here we can see the following assigned variables which store values retrived by websGetVar() function let’s go step by step:

  • __s1: storing the value retrieved with username from the http request and the admin value will be used if the requested variable is not found.

  • pcVar6: storing the value retrieved with password from the http request and the "" value will be used if the requested variable is not found.

  • __src: storing the value retrieved with http_host from the http request and the "" value will be used if the requested variable is not found.

  • __nptr: storing the value referenced by the memory address &DAT_0001dd80 from the http request and the "" value will be used if the requested variable is not found.

Note: let’s rename each variable that stores a value so we can make the code more clearly.
Finally, each call for the apmib_get() function is for retrieving a value from a data structure and storing it in a local variable local_x for later use. So, we can complete with the rest of the code:

 

After renaming the variables, We can see it’s became more clearly now. IN the lines after the previous lines, There is an IF condition which checks if the first character of Host 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 so the acStack_1150 variable is the device interface. So, let’s rename it and after we rename it. Everything is clear it checks for the Hostname or IP. Finally, if it’s not then it will copy the Host value to the deviceInterface which also could be a host-name.

By checking the next line, It also another condition to check the username and password:

It checks if the userName or passWord is null character or no, If yes, then it will set the loginflag to 1 which maybe indicates that the login is failed. and if this doesn’t happen it comparing the userName with local_1198 and store the results to iVar5 and for the local_1198 then if the results not equal to 0 that means it fails and will set the loginflag to 1, if we go to the previous line we can see that this variable in the following line apmib_get(0xb6,&local_1198); and we remember that the apmlib_get() function is for retrieving a value from a data structure and storing it in a local variable and by doing some search the value is from the configuration. So, we can guess that this variable carry the device username so let’s rename it. And moving to the next lines:

As we can see there is passwordTrans() function which takes 2 parameters the passWord parameter and another parameter which is acStack_50. By clicking on the passwordTrans() function we can see the following code:

void passwordTrans(int param_1,char *param_2)

{
  char cVar1;
  int iVar2;
  int iVar3;
  int iVar4;
  char *pcVar5;
  
  iVar4 = 1;
  iVar3 = 0;
  pcVar5 = param_2;
  while( true ) {
    cVar1 = *(char *)(param_1 + iVar3);
    if (cVar1 == '\0') break;
    if (cVar1 == '%') {
      iVar2 = hextochar(*(char *)(param_1 + iVar3 + 1),*(char *)(param_1 + iVar3 + 2));
      *pcVar5 = (char)iVar2;
      iVar3 = iVar3 + 3;
    }
    else {
      *pcVar5 = cVar1;
      iVar3 = iVar3 + 1;
    }
    iVar4 = iVar4 + 1;
    pcVar5 = pcVar5 + 1;
  }
  param_2[iVar4 + -1] = '\0';
  return;
}

Basically, the passwordTrans() function appears to take a encoded password string and returns the decoded password string by replacing any percent encoded characters with their ASCII equivalent characters by iterating through each character in the password string, checking if the character is a percent sign and converting the two hexadecimal characters following the percent sign to their ASCII equivalent, Then it saves it in the second passed parameter which in this case is acStack_50. So, We can guess that this is the final password, Let’s rename it to finalPassword. By going back to our loginAuth() function. After the passwordTrans() function call we can see the following:

Here it compares the finalPassword with a variable loaded using memset() function and we can guess it’s the configured password (so we will rename it to devicePassword), and it’s assign the result into the iVar5 & After that it checks if the iVar5 is not equal to 0 which indicates that the login is not successful and set the loginflag to 1. After that coming the next lines

It comparing the userName provided by the user with the one in the device which is deviceUserName and storing the results, then it checks if iVar5 is equal to 0 and do another compare between finalPassword and devicePassword and save the results also in iVar5. If the condition is true then it will set the loginflag to 0, As a results the login is successful. After that it uses the apmib_get() function to get some configuration. And now by looking into the next lines:

In these lines it’s all about conditions that will be executed, If the previous condition is true. By taking a look we can see it checks if the local_17a0 is equal to 0 which we don’t know yet. Then it create the iVar5 stores the value of __nptr which converted by atoi() function which converts a string of characters representing an integer value into an actual integer value. Then it checks if iVar5 to 1 and then assign some values to the local_x variables in this point if we take a look at the disassembled code we can notice the following:

And now it’s obvious that it loads a word and it’s is mobie/home.asp which is a mobile user endpoint, Now we can say that the __nptr is used to check if the user is on Desktop or Mobile, So let’s rename it as mobileUser and rename the local_x variables and it’s values. So we can say the code will be as the following:

iVar5 = atoi(mobileUser);
if (iVar5 == 1) {
    endPoint = "mobile/home.asp";
}

If this condition didn’t achieved then it will check for the Desktop User as we can see also in the disassembled code has the home.asp without mobile:

And after that if the user desktop or mobile is not identified it will be wizard.asp endpoint as we can see:

So, the full clear code as the following:

if (local_17a0 == 0) {
  iVar5 = atoi(mobileUser);
  if (iVar5 == 1) {
        endPoint = "mobile/home.asp";
  }
  else if (desktopUser == 1) {
        endPoint = "home.asp";
  }
  else {
        endPoint = "wizard.asp";
  }

Now, if the first condition which is if (local_17a0 == 0) { not true then it will jump to the following:

Which also check if the User is on mobile or desktop, Then back to the condition that check the credentials which is if ((iVar5 == 0) && (iVar5 = strcmp(finalPassword,(char *)&devicePassword), iVar5 == 0) if it’s not true it will check if the user is on mobile or no and assign the mobile/login.asp endpoint which the user will redirect to it:

So, the clear code for this is as the following:

if (local_17a0 == 0) {
  iVar5 = atoi(mobileUser);
  if (iVar5 == 1) {
        endPoint = "mobile/home.asp";
  }
  else if (desktopUser == 1) {
        endPoint = "home.asp";
  }
  else {
        endPoint = "wizard.asp";
  }
else {
iVar5 = atoi(mobileUser);
    if (iVar5 == 1) {
  endPoint = "mobile/login.asp";
  iVar5 = 0;
}
    else {
  endPoint = "login.asp";
  iVar5 = 0;
}
}

From the clear code we can guess that local_17a0 is checks for the authentication or the login. So, we will rename it to isAuth. Now, we getting closer to the place of the bug as we see:

snprintf(acStack_1050,0x1000,"{\"httpStatus\":\"%s\",\"host\":\"%s\"","302",deviceInterface);

Here, It assigns the status of request to the acSTack_1050 we can rename it to reqResponse, In this case it will redirect the user to the login page as authentication failed, Then it will assign the length of the reqResonse to sVar6 = strlen(reqResponse); to prevent overflow attacks. Now, coming here we gonna see it does a check again on the user to see if it’s on Desktop or mobile and here what the developer missed it out:

Out of the first checks this code assign the following to the response if the user is on mobile:

snprintf(reqResponse + sVar6,0x1000 - sVar6,
         ",\"redirectURL\":\"http://%s/formLoginAuth.htm?authCode=%d&userName=%s&password=%s&goU RL=%s&action=login&flag=1\"}"
         ,deviceInterface,iVar5,userName,finalPassword,(char *)&local_1798);

The values of the parameters as the following the first %s is for the deviceInterface then the second one is %d which is for iVar5 and if we back to the code where the login check failed it assigned 0 to the iVar5, So we can say that the iVar5 here in this case is for authCode and it’s equal 0 and here it comes the issue as the authCode value can be passed by the user through a request it can be manipulate all of these parameters. By the way, If it’s not mobile user it will do the same response, But without the flag=1 which indicates that user is mobile. Now, let’s take this response URL and manipulate it:

mydevice/formLoginAuth.htm?authCode=1&userName=admin&password=Idontneedit&goURL=home.asp&action=login

If we try it we will be able to see that we bypassed all these checks as the mistakes done by the developer to assign the redirect and response out of the checks.

 

Final Thoughts

The developer created a big mistake in 2 things first sending the response out and after of the checks & relay on authCode parameter results and it can be manipulated by the user. The developer shall assign the authCod hardcoded and not editable by the user. But, the best solution is to relay on the username and password as any authentication process which happens in the code and not send them back to the response. The secure code can be as the following:

// If authentication success
if (isAuth == 0) {
          // create sessin for user
      snprintf(Session, Session_Length, "Set-Cookie: session_id=%s; Path=/\n", session_id);
  iVar5 = atoi(mobileUser);
    // If user on mobile
  if (iVar5 == 1) {
      // assign mobile endpoint
        endPoint = "mobile/home.asp";
      
      // set user authentication to successful
        snprintf(reqResponse + sVar6,0x1000 - sVar6,
         ",\"redirectURL\":\"http://%s/formLoginAuth.htm?goURL=%s&action=login&flag=1\"}"
         ,deviceInterface,(char *)&endPoint);
      // send the response to user and exit
        websGetCfgResponse(param_1,param_3,reqResponse, Session);
        exit(0);
  }
    // if it's a desktop user
  else if (desktopUser == 1) {
      // assign desktop endpoint
        endPoint = "home.asp";
      // set user authentication to successful
        snprintf(reqResponse + sVar6,0x1000 - sVar6,
         ",\"redirectURL\":\"http://%s/formLoginAuth.htm?goURL=%s&action=login\"}"
         ,deviceInterface,(char *)&endPoint);
      // send the response to user and exit
        websGetCfgResponse(param_1,param_3,reqResponse, Session);
        exit(0);
  }
    // if it's not known mobile or desktop
  else {
      // assign wizard.asp endpoint
        endPoint = "wizard.asp";
      // set user authentication to successful
        snprintf(reqResponse + sVar6,0x1000 - sVar6,
         ",\"redirectURL\":\"http://%s/formLoginAuth.htm?goURL=%s&action=login\"}"
         ,deviceInterface,userName,finalPassword,(char *)&endPoint);
       // send the response to user and exit
        websGetCfgResponse(param_1,param_3,reqResponse, Session);
        exit(0);
  }

// If authentication not success
else {
iVar5 = atoi(mobileUser);
    // If user on mobile
  if (iVar5 == 1) {
      // assign mobile endpoint
  endPoint = "mobile/login.asp";
  // send the response to user and exit
      snprintf(reqResponse,0x1000,"{\"httpStatus\":\"%s\",\"host\":\"%s\"","302",deviceInterface, endPoint);
        websGetCfgResponse(param_1,param_3,reqResponse, Session);
        exit(0);
}
    // if it's a desktop user or else
  else {
      // assign desktop endpoint
  endPoint = "login.asp";
        // send the response to user and exit
      snprintf(reqResponse,0x1000,"{\"httpStatus\":\"%s\",\"host\":\"%s\"","302",deviceInterface, endPoint);
      websGetCfgResponse(param_1,param_3,reqResponse, Session);
      exit(0);
}
}

In this code the developer will not relay on the authCode and after any condition came true it will send the response to the user and exit immediately, Which leaves no chance for the user to do anything.

 

Conclusion

In this analysis we saw the root cause of the issue and how it can be solved in an example code, Also how we can analysis the unclear decompiled code to understand as much as we can of the code and make it clear.Finally, You have to keep each code under it’s condition.

 

References

#CVE-2021-42887