tl;dr
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 theuser name
). The plug answers aresult
(that is ‘OK’ for a successful loginrequest
), acookie
, achallenge
and apublic key
. - The client uses the
public key
, thepassword
submitted in the login form and thechallenge
to build aprivate 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, thecookie
, theuser name
and alogin password
obtained from theprivate key
andchallenge
, again via HMAC-MD5. Furthermore, aHNAP_AUTH
token must be provided. This token is built from theprivate key
,current time
and 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.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 = "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!