This blog is about a Root RCE on an old SMC router product that has passed its EOL and is cataloged at CVE-2020-13776. You can find the official disclosure here.
In addition to explaining the technical details of the bug, I will try to show the process I went through to discover the vulnerability. Finding a bug is often a stab in the dark and one does not always know right away what they have when they find a bug. So the titles are the result of what I know from the process now and not what was initially intended to find. In fact, I found the Session Injection bug way after the Command injection bug. But following the processing of the web request chronologically it comes later. Here she goes.
I found the session injection bug by following the code execution of the HTTP server step by step and perusing the code for any vulnerabilities I might find. So let’s follow the code as de-compiled by Ghidra. Ghidra is a software reverse engineering (SRE) suite of tools developed by NSA’s Research Directorate in support of the Cybersecurity mission
So if you are looking at a web server, an obvious way to start is the httpd [http-daemon a binary that responds to web requests on a server] . There are two ways to find the binary if you don’t know where it is.
Check the auto-start scripts to see how the server is started when the device booted. In Linux devices, there is a script that usually runs at boot.
/etc/init.d/rcS
-> /etc/scripts/sys_startup.sh
-> /usr/sbin/pcd -f /etc/scripts/vgwsdk.pcd
-> /etc/scripts/vendor.pcd
-> /usr/cgr/bin/start_cgr.sh
1
2
3
4
CGR_HOME=/usr/cgr/
CGR_HOME_BIN=$CGR_HOME"bin/"
echo "Start web server ..."
$CGR_HOME_BIN/cgr_httpd&
So our binary is /usr/cgr/bin/cgr_httpd.
Netstat Netstat is a command that presents network port statuses in the operating system. But the busybox compiled version in the SMC devices does not have an option to display the binaries using or listening on open ports. So we can’t use this method here.
This is the main HTTP daemon that listens on port 80. So this is the first door of vulnerability. There is not much code in this binary. It does not even start listening here. It just loads the /usr/cgr/lib/libwebs.so
file and calls its entry-point.
Here is where most of the setup happens.
1
2
3
4
5
6
7
8
9
10
11
12
projectWebCgiDefine();#
*(int *)((int)aiStack1480 + iVar5) = 0;
websUrlHandlerDefine(&DAT_00028826,0,0,FUN_000271a8);
websSSLOpen();
do {
iVar5 = socketReady(0xffffffff);
if ((iVar5 != 0) || (iVar5 = socketSelect(0xffffffff,1000), iVar5 != 0)) {
socketProcess(0xffffffff);
}
websCgiCleanup();
emfSchedProcess();
} while( true );
It calls projectWebCgiDefine()
which we will delve into. Then it registers some callback handlers and goes into the main loop that listens for incoming connections and responds to them according to previously registered callbacks.
This function is defining how to respond to different kinds of requests and URIs.
1
2
3
4
void FUN_00022384(undefined4 uParm1,undefined4 uParm2){
...
websFormDefine("formParamRedirectUrl",formParamRedirectUrl);
... So in going through these callback functions, looking for anything that can be bypassed or overlooked, we reach the formParamRedirectUrl callback.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void formParamRedirectUrl(undefined4 request){
char *G_param_int;
char *G_param_str;
undefined4 G_subUrl;
int iVar1;
...
int pCgrGuiObject;
G_param_int = (char *)websGetVar(request,"param_int",&DAT_000f6341);
G_param_str = (char *)websGetVar(request,"param_str",0xf643d);
G_subUrl = websGetVar(request,"subUrl","/error.asp");
...
pCgrGuiObject = _pCgrGuiObject;
iVar1 = atoi(G_param_int);
*(int *)(pCgrGuiObject + 0x18) = iVar1;
strcpy((char *)(pCgrGuiObject + 0x1c),G_param_str); // ***** GOOD OLD strcpy
websRedirect(request,G_subUrl);
return;
}
So what this function does is it uses websGetVar function, which just fetches POST parameters from a request object, and fetches 3 POST parameters. I know it says websGetVar but what are you gonna do? :confused:
Let’s request this page with these parameters and without and see if we are on track so far.
From these two requests, I concluded that the call to the callback method is not authenticated. Because the response did not change with an authenticated session cookie. Also, notice that the subUrl parameter we gave it is reflected in the response page. If we remove our parameter the default is substituted which is error.asp. This means that the callback method has reached that last websRedirect call, which is as we see above, what is causing the 302 response.
We have discovered that there is an unauthenticated and unbounded write to somewhere in memory using the param_str POST parameter of the URI /goform/formParamRedirectUrl
.
1
strcpy((char *)(pCgrGuiObject + 0x1c),G_param_str); // ***** GOOD OLD strcpy
So let’s focus on this unbound and unauthenticated copy. Where does it copy to? What is this pCgrGuiObject
object? Since we do not see a definition of the object in the function it must be a global variable.
The first two entries there are from the function in focus here.
strcpy((char *)(pCgrGuiObject + 0x1c),G_param_str);
*(int *)(pCgrGuiObject + 0x18) = iVar1;
As for the rest of the other 2700 locations of reference, it is highly indicative that the strcpy is very dangerous and can be used to change the execution flow of the program highly since we freely write to a place in memory so frequently used in the code base. What is more, is that the object is on the export table so it is also used by other loaded modules so it wreaks more potential havoc.
So our next step is to go through all these references to the object in this binary and other binaries that import it and see if we can craft a payload for param_str parameter that will greatly impact the system. A common subsystem to focus on is the authentication subsystem. Let’s see if the login functions or the session validator functions use the infamous pCgrGuiObject.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
undefined4 guiUtilGetSessionIdByCookie(int pCgrGuiObject,char *session_cookie_str){
int iVar1;
uint session_index;
if (session_cookie_str == (char *)0x0) {
return 0xffffffff;
}
session_index = 0;
while( true ) {
if (*(uint *)(_pCgrGuiObject + 0x11c) <= session_index) {
return 0xffffffff;
}
iVar1 = strcmp((char *)(pCgrGuiObject + session_index * 0x26c + 400),session_cookie_str);
if (iVar1 == 0) break;
session_index += 1;
}
return *(undefined4 *)(pCgrGuiObject + session_index * 0x26c + 0x124);
}
This is the function that looks up the session table and matches it with the session cookie from the request. And we see that it deals with the pCgrGuiObject object so let’s take a closer look at what is going on.
1
2
3
if (session_cookie_str == (char *)0x0) {
return 0xffffffff;
}
If we give it an empty session cookie it returns 0xffffffff. To get a valid session match we don’t want this to happen.
1
2
3
4
5
6
7
while( true ) {
if (*(uint *)(_pCgrGuiObject + 0x11c) <= session_index) {
return 0xffffffff;
}
...
session_index += 1;
}
Loops until session_index is greater than \*(uint\*)(_pCgrGuiObject + 0x11c)
. This means we have looked at as many sessions as there are sessions on the session table and have reached the end so no session match. We don’t want this to happen either.
1
2
iVar1 = strcmp((char *)(pCgrGuiObject + session_index * 0x26c + 400),session_cookie_str);
if (iVar1 == 0) break;
This is where valid sessions are matched. So session data is written as a table entry at 0x26c intervals from the pCgrGuiObject. If we want to add our own session we need to write session data there. Seems too easy to be true but let’s try it.
To check if our session write worked we request the home page with the new random session. If we are logged in it has worked. Instead, the binary cgr_httpd crashed with this stacktrace.
Program received signal SIGSEGV, Segmentation fault. 0x04321810 in strcmp () from /lib/libc.so.0 (gdb) backtrace #0 0x0431e810 in strcmp () from /lib/libc.so.0 #1 0x041316d0 in guiUtilDelSessionByCookie () from /usr/cgr/lib//libgui.so #2 0x04130b80 in ?? () from /usr/cgr/lib/libgui.so
The line Where the error occurs is
1
=> 0x4321810 <strcmp>: ldrb r2, [r0], #1
The error is caused by an out-of-memory reference due to the dereferencing of the value in register r0. To verify that the value of r0 at the time of the error is 0xf0001a4
(gdb) info register r0 r0 0xf0001a4 251658660
It looks like we have a memory location being accessed that is way out of bounds of the memory layout. By looking at guiUtilDelSessionByCookie’s decompiled source code we can see that it is the while loop that ran too long and calculated an address that is out of bounds.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
undefined8 guiUtilDelSessionByCookie(int iParm1,char *pcParm2){
int iVar1;
int iVar2;
uint uVar3;
int iVar4;
char *__s1;
uVar3 = 0;
iVar2 = iParm1;
while (uVar3 < *(uint *)(_pCgrGuiObject + 0x11c)) {
iVar4 = uVar3 * 0x26c;
__s1 = (char *)(iParm1 + iVar4 + 400);//<-2 that parameter is being calculated here.
iVar1 = strcmp(__s1,pcParm2);//<-1 error is in strcmp's first parameter
if (iVar1 == 0) {
...
}
uVar3 += 1;
iVar2 += 0x26c;
}
return 0xf633b;
}
We can guess that uVar3 which keeps growing by one each loop iteration gets too large and an address that is out of bounds is calculated and given to strcmp which tries to access it and that causes the Segmentation fault. This variable is checked at the start of the loop against \*(uint \*)(_pCgrGuiObject + 0x11c)
to check if it has gotten too big. This check is failing. Let’s check that area to see why the bounds check is failing.
1 2 (gdb) x/s _pCgrGuiObject + 0x11c 0xeffeb64: 'A' \<repeats 116 times>, "session=randomrandomrandomrandomrandomrandomrandom"
Ah, there is our problem. We appear to be overwriting an import number that limits the while loop from running into an error with our buffer padding to reach the session data write location.
By debugging a regularly functioning binary we find that the value should be 0x4. That is the number of sessions the session table can hold before it runs out of space. So let’s write that.
We encounter another problem writing that value to a remote memory over a web request. The problem is that it is not enough to write that single 0x4 to _pCgrGuiObject + 0x11c because other junk data can change the number. We need to write 4 bytes to overwrite any previous bytes and get an unsigned integer. It has to be 4 bytes because the while loop compares that number as unit(unsigned integer) which is 4 bytes wide on 32-bit arm systems.
1
while (uVar3 < *(uint *)(_pCgrGuiObject + 0x11c)) {
The problem is that to write 0x4 as an unsigned integer we need to write 0x00000004. That is 3 null bytes (0x00) and then 0x04. In web requests having null bytes is not allowed. If we were to include the null bytes altogether the webserver evaluating the string will stop at the first null byte and will not copy or interpret the rest of the payload. Our null bytes need to be written at 0x11c offset of pCgrGuiObject and our session cookie is at 0x190 offset. The null bytes would be in the middle of the payload and would break it.
The key to writing null bytes in web requests is making subsequent requests and aligning the null byte at the end of the string to our advantage. So if we need to write 0xf4f1f4f500dd first we would write the whole thing except replacing the null byte with something else like 0xf4f1f4f5aadd then cut the string right before the null byte like 0xf4f1f4f5. When the server writes any string it adds a null byte to the end of the string and that will be our null byte.
1
2
3
4
#1 | f4 f1 f4 f5 aa dd ----- server ----> f4 f1 f4 f5 aa dd 00
#2 | f4 f1 f4 f5 ----- server ----> f4 f1 f4 f5 00
----------------------------------------------||-||-||-||-||-||-||-
f4 f1 f4 f5 00 dd 00
So by using this method we can write null bytes in our payloads. That is if our partial payload does not crash the system.
Now that we can write null bytes let’s write 0x00000004 to _pCgrGuiObject + 0x11c
and try our luck.
Another crash. Before we could write our subsequent writes for our null bytes it crashes on the second request.
#0 0x043b0810 in strcmp () from /lib/libc.so.0 #1 0x0426ce38 in guiUtilDelSessionByCookie () from /usr/cgr/lib//libgui.so #2 0x04266470 in ?? () from /usr/cgr/lib//libgui.so
We didn’t get far enough to write the whole integer. We will need to find another way around this. Let’s find out how guiUtilDelSessionByCookie gets called and prevent that from happening.
1
2
3
4
5
6
7
8
9
10
11
12
uParm1 = guiUtilGetSessionIdByCookie((int)_pCgrGuiObject,*(char **)(iParm1 + 0xd8));
if (uParm1 < 0) {
iVar1 = 0;
}
else {
iVar1 = CgrSessionUICheck(uParm1);
if (iVar1 == 0) {
CgrSessionUIReset(uParm1,4,0);
}
else {
guiUtilDelSessionByCookie(_pCgrGuiObject,*(undefined4 *)(iParm1 + 0xd8));
}
This is the function that handles all callbacks for URIs /goform/* which is what includes our unauthenticated write endpoint /goform/formParamRedirectUrl. Here guiUtilDelSessionByCookie is called because CgrSessionUICheck returns a value other than 0. CgrSessionUICheck takes as a parameter the return value of guiUtilGetSessionIdByCookie. So in addition to the session cookie, we need to write the correct return value to make sure CgrSessionUICheck returns 0.
1
return *(undefined4 *)(pCgrGuiObject + session_index * 0x26c + 0x124);
So we need to write at 0x124 offset from pCgrGuiObject. As for the value to write by debugging a valid login and breaking at this function, we can find the value it returns. For an admin login guiUtilGetSessionIdByCookie returns 0x34. So we will write 0x00000034(‘4’ in ASCII) at pCgrGuiObject + 0x124
(we are assuming first session entry so session_index is 0)
#0 0x043d5810 in strcmp () from /lib/libc.so.0 #1 0x0423be38 in guiUtilDelSessionByCookie () from /usr/cgr/lib//libgui.so #2 0x04235470 in ?? () from /usr/cgr/lib//libgui.so
Well, that is progress. It didn’t crash after the first request. After some debugging, I found that the first write was written correctly. The 0x34 that is. But this value which is passed on to CgrSessionUICheck is not making it return 0. So guiUtilDelSessionByCookie gets called and since we didn’t write the second session table size value the same crash happens.
Here is where things got weird. Since I am working on decompiled code I don’t have structs and other debugging information to follow exactly why an actual value retrieved from memory debugging using the actual login mechanism didn’t make CgrSessionUICheck return 0 as it should. So I fuzzed that value as a last-ditch attempt. Trying value 0x03 as return value yielded this.
No crash so far, good. Let’s use the session id injected to make a request to an authenticated page and if we don’t get redirected we have successfully injected the session.
Okay. We got the logged-in quick wizard. We have successfully injected a session. Further fuzzing values 0x1, 0x4, and 0x5 worked similarly but I can’t explain why. Also, I later found out that the injection works without the ‘0x4’ session table limiter injected through the payload. This is because we are injecting our session on the first entry of the table. As long as we use that injected session cookie the code will not have to look father that the first entry. But if any other request is made with any other session cookie like a regular web request from a browser it will crash since the first entry will not match and there is no limiter. So for stability reasons let’s leave it in the payload. Now what to do with all this?
I actually found this before the session injection. But for clarity’s sake, it is mentioned here, later. Now that we are logged in let’s look for an unfiltered field where we can inject commands into the operating system.
Let’s try injecting the diagnostic ping target field on the POST form /goform/formSetDiagnosticToolsFmPing. It is filtered but only on the front end by JavaScript. We will make manual requests to avoid the filtering and use a session id that is injected using the process developed earlier.
It was a little tricky to show the output of commands as this was intended to take ping target hosts and not display results back. But if there was an error in the execution of the ping command it gets returned as retMsg for JavaScript to display. So we create an error that contains the output we want to see and bob is your uncle. Dissecting further into why this injection was possible we see, as usual with this device software, a chain of hand downs from function to function of the vlu_diagnostic_tools__ping_address POST parameter until it is inevitably appended to the ping command and executed. We will start from the beginning and see some important milestones along this chain and finally where it gets executed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void PING_FUNK(int iParm1)
{
//Definitions
...
uVar1 = websGetVar(iParm1,"subUrl","/error.asp");
memset(&INJECTIBLE_BUFFER_POINTER,0,0x18);
guiUtilGetSessionIdByCookie((int)_pCgrGuiObject,*(char **)(iParm1 + 0xd8));
INJECTIBLE_BUFFER_POINTER = &INJECTIBLE_BUFFER;
guiUtilGetSessionlvlByCookie(_pCgrGuiObject,*(undefined4 *)(iParm1 + 0xd8));
memset(&INJECTIBLE_BUFFER,0,0xc);
INJECTIBLE_BUFFER = (byte *)websGetVar(iParm1,"vlu_diagnostic_tools__ping_address",0xf643d);//<---- Here it fetches the injection string
if (*INJECTIBLE_BUFFER == 0) {
INJECTIBLE_BUFFER = (byte *)(uint)*INJECTIBLE_BUFFER;
}
uVar2 = websGetVar(iParm1,"vlu_diagnostic_tools__ping_packetsize",0xf643d);
uVar2 = websGetVar(iParm1,"vlu_diagnostic_tools__ping_count",0xf643d);
RESPONSE_STATUS = CgrGetSetCfg("diagnostic_tools","diagnostic_tools__ping",4,3,&INJECTIBLE_BUFFER_POINTER,10,0,
0,0,0,0,0);//<---- Here it passes it along by reference to CgrGetSetCfg along with the handler library diagnostic_tools
if ((RESPONSE_STATUS == 0x500 || (RESPONSE_STATUS & 0x10000000) == 0) ||
(RESPONSE_STATUS == 0x501)) {
websRedirect(iParm1,uVar1);
}
else {
memset(acStack1100,0,600);
__dest = *_pCgrGuiObject;
if (*__dest == '\0') {
__src = (char *)GuiGetNotice(0x6b);
strcpy(__dest,__src);
}
uVar2 = guiUtilTransferLFToCh(*_pCgrGuiObject,0x20);
sprintf(acStack1100,"%s?nextUrl=%s&retMsg=%s","error_message_pop.asp",uVar1,uVar2);
websRedirect(iParm1,acStack1100);
strcpy(*_pCgrGuiObject,"");
}
return;
}
This is the stripped-down version, for clarity, of the function that handles the /goform/formSetDiagnosticToolsFmPing callback, our command injection endpoint URI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
uint CgrGetSetCfg(undefined4 param_1,undefined4 param_2,undefined4 INT_SWITCH_FLAG,int param_4,
byte **passed_along_struct,byte param_6,int param_7,int param_8,
undefined4 param_9,undefined4 param_10,undefined4 param_11,int *param_12)
{
//Definitions
...
...
switch(INT_SWITCH_FLAG) {//<- Our ping callback gives a switching value of 4
case 2:...
goto LAB_000197f8;
case 3:...
case 4:
LAB_000197f8:
iVar1 = CgrUICreateRequestPackage
(passed_along_struct,(uint)param_6,param_1,param_2,INT_SWITCH_FLAG,param_4
,param_7,param_9,param_10,param_11);
if (iVar1 == 0) {return 0x10000000;}
uVar2 = CgrUiQuery(iVar1,local_44c,&local_34,&local_30);
do {
if ((uVar2 & 0x10000000) != 0) {...}
if (local_34 == 0) {goto LAB_00019a98;}
uVar2 = *(uint *)(local_34 + 0x34);
if ((uVar2 & 0x11000000) == 0) {...}
uVar4 = CgrResponsePackageGetMessageAddress(local_34,local_2c);
if ((code *)passed_along_struct[4] != (code *)0x0) {
(*(code *)INJECTIBLE_BYTE_POINTER[4])
(uParm1,uParm2,local_2c[0],INJECTIBLE_BYTE_POINTER[5],INT_SWITCH_FLAG);//<- Here it calls the structs' address and passes out the injection string as the last parameter. Notice uParm1 and uParm2 are passed as is where were "diagnostic_tools","diagnostic_tools__ping"
}
if (uVar2 != 0x1000000) goto LAB_00019a98;
if (local_34 != 0) {
CgrFree(local_34,0x2026d,0xf4);
}
uVar2 = CgrUiRead(local_30,*(undefined4 *)(iVar1 + 8),local_44c,&local_34);
} while( true );
case 5:...
case 6:...
case 7:...
case 8:...
case 9:...
case 10:...
case 0xb:...
}
return 0x10000102;
}
Since the passed_along_struct is a struct I can not really make out what is going on really but with some educated conjecture I wager it is calling the right shared library from the first two parameters passed to it and calling it. The first parameter corresponds to a folder name in the modules folder with a similarly named .so file inside it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void CgrDiagnosticToolsProcess
(undefined4 uParm1,undefined4 uParm2,char *pcParm3,undefined4 uParm4,
undefined4 param_5)
{
int iVar1;
char *__s1;
char *pcVar2;
__s1 = pcParm3;
if ((pcParm3 != (char *)0x0) && (__s1 = *(char **)(pcParm3 + 0x48), __s1 != (char *)0x0)) {
__s1 = pcParm3 + 0x48 + (int)__s1;
}
iVar1 = strcmp(__s1,"diagnostic_tools__ping");//<- later along the chain we see our second parameter passed along by CgrGetSetCfg. Looks like the right track.
if (iVar1 == 0) {
CgrProcessDiagnosticToolsFmPing(uParm1,uParm2,pcParm3,uParm4,param_5,0);//This is out function.
}
else {
iVar1 = strcmp(__s1,"diagnostic_tools__ping_result");
if (iVar1 == 0) {
CgrProcessDiagnosticToolsFmPingResult(uParm1,uParm2,pcParm3,uParm4,param_5,0);
}
else {
iVar1 = strcmp(__s1,"diagnostic_tools__tracrt");
if (iVar1 == 0) {
CgrProcessDiagnosticToolsFmTracrt(uParm1,uParm2,pcParm3,uParm4,param_5,0,pcVar2);
}
}
}
return;
}
We see our second parameter passed along by CgrGetSetCfg here. Looks like we are on the right track.
1
pthread_create(apStack44,(pthread_attr_t *)abStack88,CgrDiagToolsRunShCmd,__arg);
Not much worthy of note here. But part of the chain nonetheless.
1
2
3
4
5
6
...
sprintf(local_714,"%s 1>%s 2>%s",puParm1[2],acStack256,acStack256);
...
sscanf((char *)puParm1[2],"ping %s -c %d -s %d",auStack512,&local_34,&local_30);
...
iVar4 = system(local_714);
You might notice that the commands seem out of order. I think this is a decompilation error. I have also checked on IDA and it has the same effect. The second sscanf was supposed to be first in the code. I know that is a lot of “altering facts to fit the case” going on here but hey what are you gonna do :man_shrugging:, If it breaks don’t fix it.
Have ideas to collaborate on? Questions, suggestions, advice, or just want to say hi? Feel free to contact me at these addresses.