CVE-2021-42887: TOTOLINK EX1200T LOGIN BYPASS
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 calledwebsGetVar()
and passingparam_2
,LoginAuthUrl
which it could be theHTML
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 assigning0
.
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 theadmin
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
-
https://www.totolink.net/home/news/me_name/id/39/menu_listtpl/DownloadC.html
-
https://ghidra-sre.org/
-
#CVE-2021-42887