Thursday, October 24, 2013

Netgear Root Compromise via Command Injection

At the end of my post on the Netgear wndr3700v4's authentication bugs, I said to expect followup posts. Once the web interface is unlocked, any further bugs that normally require authentication become fair game. Well good news, everyone!!

Previously, I talked about the net-cgi executable in the wndr3700's firmware. ;net-cgi is a multi-call binary, a little like busybox. As such it has a lot of functionality baked in. One of its more interesting functions is called cmd_ping6(). Here's what it looks like:

This is a function that will ping whatever hostname or IPv6 address is passed in as the char *host argument. What you see here is such an unbelievably common pattern, that the first thing you should do with a router's firmware is check to see if there's a ping diagnostic page, and verify how ping gets executed.

What is happening here, as it so often does, is the host string gets copied into a shell command on the stack using sprintf(). This is probably the most straightforward buffer overflow vulnerability you will ever see. Sadly, you shouldn't exploit it. It is a tempting one to exploit because it is so clean and simple and because popping root with a MIPS ROP payload is sexy. But that would be silly, because right after it there is a call to system(). The system() function passes whatever string it is given to an invocation of /bin/sh. This is a command injection vulnerability in its purest form and is trivially exploitable. If the address string that gets passed in is something like "; evil_command; #", the ping6 command will be terminated prematurely, and evil_command will be executed right after it.

But what if the target isn't configured to use IPv6? Who cares? The ping6 command doesn't actually need to succeed. As long as the injected command string gets passed to system(), that's all that matters.

So how does this function get invoked?

Working backwards, cmd_ping6() gets called by cgi_commit().

The cgi_commit() function gets called by sub_4052d0(), which is the output function for the apply.cgi mime handler (I explained previously how the mime handler table works).

How does cgi_commit() know to call cmd_ping6()? That happens when when apply.cgi is requested as a post, and the post data contains "submit_flag=ping6".

sub_43a60() cgi handler gets called when submit_flag is "ping6".

There is a page that sends a post request to apply.cgi with the ping6 submit flag. That page is "ping6_traceroute6_hidden_info.htm".

This page would normally be protected behind password authentication. With the authentication bypass I wrote about previously, this page becomes accessible to anyone. As you can see, this is a form that allows you to ping an IP address or hostname. Rather than submit an IP address, you can send a shell command, such as `reboot`.

Send a shell command instead of an IP address.

Form is submitted with "ping6_text=`reboot`"

This is an easy test because the effect is immediate and easily observed, and you have very little shell syntax to troubleshoot.

This page is not exposed to the user via any reference in the web interface, and it even has "hidden" in the name. Hidden as it may be, it can't hide from Python. I've written up some proof-of-concept exploit code that will access this page and start up a telnet server, allowing you to log in unauthenticated as root.

"Hidden" pages can't hide from Python! Authentication is disabled, then command injection exploited gain root.

The exploit code does the following:
  1. Fingerprint the device to ensure it's vulnerable.
  2. Disable authentication.
  3. Inject a command to open a hole in iptables, and start a telnet server listening on the internet on port 2323[1].
  4. Re-enable authentication, restoring the device to its original state.
You can download the proof-of-concept exploit code[2] from my GitHub repo. You'll need Bowcaster installed.

[1] Netgear routers usually already have a telnet server listening on the LAN on port 23 that accepts a hardcoded backdoor password.
[2] Don't attempt to test this against devices you don't own. That's illegal in most jurisdictions.