Plug and Play -- Part 2
Oct 9, 2017
6 minute read


Unauthenticated code execution on the D-Link DSP-W215 smart plug.

  • communication with the plug via SOAP and HNAP
  • setting up qemu for debugging

Communicating with the plug

Now that we know from Part 1 that the plug uses SOAP and HNAP as communication protocols, it is time to try and interact with the plug, without using the smartphone apps.

Understanding the protocol

First things one can do is reading Login.html and the associated JavaScript soapclient.js, and look at what is going on thanks to Burp. To sum up what happens after clicking the Login button of the login form shown in Part 1:

  • The client sends a login request SOAP action to the plug (which only relies on the user name). The plug answers a result (that is ‘OK’ for a successful login request), a cookie, a challenge and a public key.
  • The client uses the public key, the password submitted in the login form and the challenge to build a private key which is a HMAC obtained from the HMAC-MD5 algorithm.
  • The client sends a login login SOAP action to the plug, by providing among others, the cookie, the user name and a login password obtained from the private key and challenge, again via HMAC-MD5. Furthermore, a HNAP_AUTH token must be provided. This token is built from the private key, current time and SOAP action. This token is required for almost all SOAP actions, not only for the login.
  • At this point, if one is doing all this in a browser, the JavaScript code in Login.html will request /version.txt. If one is directly communicating with the plug, one can do the same, or perform other SOAP actions. This is also what the smartphone apps do to query the plug.

Reusing existing code

It turns out that a tool for this has already been coded in JavaScript, see this link. Installing nodejs and all necessary modules with npm, modifying lines 12-14 in app.js as

var LOGIN_USER = "pwner";
var LOGIN_PWD = "";
var HNAP_URL = "";

is all one needs to do to start interacting with the plug. One can then use a few functionalities that were enumerated in Part 1 when requesting /HNAP1, such as probing the plug’s temperature sensor with GetCurrentTemperature or measuring power consumption with GetCurrentPowerConsumption. This all works pretty fine (I even checked that plugging a 20W(max) lamp in the plug, the power consumption is almost 0W when the lamp is off, and approximately 17W when it’s on. One drawback of using the JavaScript code is that requests are performed asynchronously. One could try and fix this. But thinking ahead, writing exploits in JavaScript is not my cup of tea. So we’re better off rewriting a client in Python (whose source will be given in Part 3).

Setting up a debugging environment

Running qemu

It is always nice to be able to combine static and dynamic analysis. For this, one can download Debian MIPS images for QEMU. I tried the first suggested combination of files vmlinux-2.6.32-5-4kc-malta and debian_squeeze_mips_standard.qcow2, since the kernel version is close to the one running on the plug. But this failed for debugging either with strace or gdb (it seems ptrace is problematic). Note that to install Debian packages, the sources.list entries must be modified according to point to Debian archives.

I finally used the second combination of vmlinux-3.2.0-4-4kc-malta and debian_wheezy_mips_standard.qcow2 files, this time leading to success. With this setup, no modification is required to the sources.list and one can install packages as usual with apt-get install. In all that follows, running qemu is done via the following command:

qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta\
-hda debian_wheezy_mips_standard.qcow2 -append "root=/dev/sda1 console=tty0"\
-net user,hostfwd=tcp::2222-:22,hostfwd=tcp::4444-:4444,\
hostfwd=tcp::8080-:80,hostfwd=tcp::2323-:23 -net nic

The first port forwarding allows for ssh access via

ssh -p2222 root@localhost # password: root

Local port 4444 will be used for remote debugging, port 8080 for interacting with lighttpd, and port 2323 for telnet access (telnetd is not running by default on the plug, but our aim is to launch it).

Getting things to work

After scp’ing the squashfs-root directory extracted in Part 1 to root@debian-mips:~/, one can try to launch (the chroot being mandatory to allow import of the proper libraries)

root@debian-mips:~/squashfs-root# chroot . www-ro/web_cgi.cgi
Segmentation fault

It took me some time to realize that the problem is not coming from qemu, libraries or whatever else. It comes from web_cgi.cgi itself! Indeed, looking at the disassembly of main(), one can decompile the first two C instructions by hand, which look like:

char *request_uri = getenv("REQUEST_URI");
strncasecmp(request_uri, "/mplist.txt", 11);

So, without a REQUEST_URI environment variable, request_uri is set to NULL by getenv(), hence the segfault in strncasecmp(). Setting the REQUEST_URI environment variable leads to a first successful execution

root@debian-mips:~/squashfs-root# REQUEST_URI=/common/info.cgi chroot . www-ro/web_cgi.cgi
model=DSP-W215 product=mydlink Wi-Fi Smart Plug brand=D-Link version=2.02 build=04 name=DSP-W215 macaddr=4d:43:41:44:44:52 ipaddr= netmask= gateway= wireless=Yes

So we already learn that lighttpd will partly communicate with web_cgi.cgi via environment variables. Browsing the disassembly of web_cgi.cgi, one notices some fgetc(stdin) and fread(..., stdin) calls, showing that communication also takes place via the standard input. To confirm this, let us make lighttpd work. A few commands have to be executed to set up things right:

cd ~/squashfs-root
mkdir var/run
touch var/run/
cp -r www-ro/* www/
cp www/web_cgi.cgi www/web_cgi.cgi.orig
cp -r mnt/* etc/

One must then edit etc/lighttpd/conf.d/cgi.conf, uncomment line 28 and replace /HNAP1/ by /HNAP1 on the same line. One can finally launch lighttpd with

cd ~/squashfs-root && chroot . usr/bin/lighttpd -f etc/lighttpd/lighttpd.conf

I then wrote www/my_web_cgi.c to test things (note that one must use a statically compiled binary because the chroot lighttpd is running in does not contain the libraries present in qemu’s /lib):

// compile with gcc my_web_cgi.c -o web_cgi.cgi -static
#include <stdio.h>
#define BUF_SIZE 256
int main(int argc, char *argv[], char *envp[]) {
    char buf[BUF_SIZE] = ""; 
    char **p;
    int len;
    printf("\n-- ENV VARS --\n\n");
    for(p = envp; *p != 0; p++)
        printf("%s\n", *p);
    printf("\n-- STDIN --\n\n");
    while((len = fread(buf, 1, BUF_SIZE, stdin)) != 0)
        fwrite(buf, 1, len, stdout);

and interacted with it like as follows (directly in qemu):

python -c 'print "POST /web_cgi.cgi HTTP/1.1\r\n\
Content-Type: text/xml\r\n\
Connection: close\r\n\
Content-Length: 16\r\n\r\n\
aaaabaaacaaadaaa"' | nc 0 80
HTTP/1.1 200 OK
Connection: close
Transfer-Encoding: chunked
Date: Sun, 08 Oct 2017 11:45:57 GMT 
Server: lighttpd/1.4.34


-- ENV VARS --


-- STDIN --



Now that the transfer of information from lighttpd to web_cgi.cgi is clear, one can replace the custom web_cgi.cgi script by the original one.

Give me a debugger and I shall pwn the world

Final step before pwning can start is to have a gdbserver running in qemu. I went for the lazy option to download an already statically compiled gdbserver binary, found here. This binary must be put in the directory ~/squashfs-root and run with, e.g.,

env REQUEST_URI=/common/info.cgi chroot . ./gdbserver localhost:4444 www/web_cgi.cgi
Process www/web_cgi.cgi created; pid=1234
Listening on port 4444

Then, on the host machine, having a copy of the qemu ~/squashfs-root directory, one can go to this directory and launch gdb-multiarch with

gdb-multiarch www/web_cgi.cgi
(gdb) set sysroot .
(gdb) target remote localhost:4444
Remote debugging using localhost:4444
Reading symbols from ./lib/ debugging symbols found)...done.
0x77fe2a80 in _start () from ./lib/

Debugging is then done as usual (of course, use c to continue execution). We are ready to pwn!