tl;dr
Unauthenticated code execution on the D-Link DSP-W215 smart plug.
- communication with the plug via SOAP and HNAP
- setting up
qemufor 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
requestSOAP action to the plug (which only relies on theuser name). The plug answers aresult(that is ‘OK’ for a successful loginrequest), acookie, achallengeand apublic key. - The client uses the
public key, thepasswordsubmitted in the login form and thechallengeto build aprivate keywhich is a HMAC obtained from the HMAC-MD5 algorithm. - The client sends a login
loginSOAP action to the plug, by providing among others, thecookie, theuser nameand alogin passwordobtained from theprivate keyandchallenge, again via HMAC-MD5. Furthermore, aHNAP_AUTHtoken must be provided. This token is built from theprivate key,current timeand SOAPaction. This token is required for almost all SOAP actions, not only for thelogin. - At this point, if one is doing all this in a browser, the JavaScript code
in
Login.htmlwill 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 = "http://192.168.0.60/HNAP1";
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
<snip>
root@debian-mips:~#
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=0.0.0.0 netmask=0.0.0.0 gateway=0.0.0.0 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/lighttpd.pid
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\
Host: 127.0.0.42\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
1db
-- ENV VARS --
SERVER_SOFTWARE=lighttpd/1.4.34
SERVER_NAME=127.0.0.42
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.1
SERVER_PORT=80
SERVER_ADDR=0.0.0.0
REQUEST_METHOD=POST
REDIRECT_STATUS=200
REQUEST_URI=/web_cgi.cgi
REMOTE_ADDR=127.0.0.1
REMOTE_PORT=41367
CONTENT_LENGTH=16
SCRIPT_FILENAME=/www/web_cgi.cgi
SCRIPT_NAME=/web_cgi.cgi
DOCUMENT_ROOT=/www
HTTP_HOST=127.0.0.42
CONTENT_TYPE=text/xml
HTTP_CONNECTION=close
HTTP_CONTENT_LENGTH=16
-- STDIN --
aaaabaaacaaadaaa
0
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/ld-uClibc.so.0...(no debugging symbols found)...done.
0x77fe2a80 in _start () from ./lib/ld-uClibc.so.0
(gdb)
Debugging is then done as usual (of course, use c to continue execution).
We are ready to pwn!