tl;dr
Unauthenticated code execution on the D-Link DSP-W215 smart plug.
- vulnerability in
web_cgi.cgi
’s configuration upgrade functionality - root shell on the plug
Analysis and reversing of web_cgi.cgi
Exploit mitigations
As usual, one can get a rough idea of what is happening by running strings
on the binary. One can furthermore see what kind of binary one is facing
file web_cgi.cgi
web_cgi.cgi: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
and
checksec web_cgi.cgi
Arch: mips-32-big
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
If a vuln is found, it should be possible to exploit it and party like it’s 1999! No exploit mitigation whatsoever is implemented! This binary has both stack and heap marked as RWX as seen from
readelf -l ./web_cgi.cgi | grep RWE
DYNAMIC 0x000180 0x00400180 0x00400180 0x00128 0x00128 RWE 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
And this seems to be generic to all binaries running on the plug, e.g. (this was run after the exploit succeeded)
cat /proc/self/maps
00400000-00495000 r-xp 00000000 1f:08 256 /bin/busybox
004a4000-004a6000 rw-p 00094000 1f:08 256 /bin/busybox
004a6000-004a8000 rwxp 00000000 00:00 0 [heap]
<snip>
7fae0000-7faf5000 rwxp 00000000 00:00 0 [stack]
(for different runs, only the stack position is randomized, so ASLR is far
from being fully implemented; furthermore
cat /proc/sys/kernel/randomize_va_space
returns a value of 1).
In fact, this is irrelevant, because after experimenting, it turns out that
the QCA9531 cpu of the plug does not have a NX bit, so the informations
given by cat /proc/PID/maps
can’t be trusted.
Reversing web_cgi.cgi to find a vulnerability
The main()
function compares REQUEST_URI
to (in parenthesis are given
the addresses of the jalr t9
instruction, where t9
contains the address
of a string comparison function):
- ’/mplist.txt’ (@ 0x401e54) (as already mentionned in
Part 2).
A match results in a call to
create_mplist()
- ’/version.txt’ (@ 0x401e80) and a match leads to calling
create_version_txt()
- ’/common/info.cgi’ (@ 0x401eac) which ends in a call to the imported
function
show_info_cgi()
These functions do not seem to contain any user data, so do not look interesting. Let’s move on.
The final comparison is to the string ‘/HNAP1’ (@ 0x401ef8), a match resulting in more things and comparisons to happen. These will be detailed later when we’ll try to find a path to the vuln. One interesting looking path is the one going through calls to functions:
get_upload_parameters()
(called @ 0x40202c)checkValidUpgrade()
(called @ 0x402064)save_upload_file()
(called @ 0x40209c)
There is nothing interesting in the first two functions, but digging the last one leads to a little gem, found @ 0x40311c. Translated in C, this reads:
_system("ccrypt -d -K %s %s", key, "/tmp/root_uImage");
where _system
is an imported function, that seems to accept a format string,
a variable number of arguments, makes a string out of them, and finally calls
system()
with the resulting string.
From a security point of view, the above instruction is bad!
First (and this is a classic of ctfs/wargames), if one had local access to a
SUID binary doing such a call to system()
, all that is needed to escalate
privileges is to create a custom binary ccrypt
that launches a shell, in
/tmp/mydir
, and to execute export PATH=/tmp/mydir:$PATH
.
Running this binary would then lead to a higher privilege shell.
Second, things will get worse if the decryption key
that is on the stack
is a string under the attacker’s control that is not sanitized. Given the
security bugs found up to this point, the probability that this is the case
is 2, and indeed, no sanitization of the decryption key in (what turns
out to be) the configuration upgrade of the plug is the first of two
vulnerabilities that will lead to code execution (the second one will be
discussed in
Part 4).
Being careful
Some care must be taken when trying to exploit this vulnerability, because
after this call to _system()
, there is another such call @ 0x403154 (and
a call to restoreDevicePwd()
in between):
_system("cp -f %s %s", "/tmp/root_uImage", "/etc/config.sqlite");
If we (and we will), mess up the content of /tmp/root_uImage
, this will be
copied to /etc/config.sqlite
, with potential problems after a reboot.
Furthermore, the instructions before the vulnerable _system()
call
(found @ 0x4030d4 - 403100) roughly translate to
memset(key, 0, 0x21);
memcpy(key, ptr, 0x20);
where ptr
points to the content of a buffer on the stack. This means that
the decryption key is 32 characters long. This is enough to accomodate
;rm /tmp/*_u*;telnetd -F;reboot;
which is exactly 32 characters long. Note that this is very conservative.
The rm /tmp/*_u*
makes sure the second _system()
call will fail, so the
reboot
instruction is not needed, but is here for cautiousness.
A final remark is that we use telnetd -F
so that telnetd
will run in the
foreground (see the
busybox documenation).
Making one’s way to the vulnerability
All that is left to do is to combine reversing and debugging to find a path to the identified vulnerability.
Reaching 0x40202c
Reaching address 0x40202c where function get_upload_parameters()
is called
is easy, since it only requires:
- a
REQUEST_URI
environment variable that starts with/HNAP1
- a
SOAPAction
environment variable (which is normally set bylighttpd
when one sends a valid request for a SOAP action) - a non-
GET
request, so aPOST
request, because of theContent-Type
(see below) - a
Content-Length
header field /CONTENT_LENGTH
environment variable - a
REMOTE_ADDR
environment variable (which is set bylighttpd
) - a
Content-Type
header field /CONTENT_TYPE
environment variable that ismultipart/form-data
get_upload_parameters()
As can be seen by reversing and debugging the function
get_upload_parameters()
, this function takes a buffer address as input,
fills it with the GET parameters of the requested url and returns the number
of ‘&’ characters found (i.e. the number of GET parameters minus one). A few
more things should be noted.
In practice, this buffer is of size 15000 and located on the stack. It is
the variable referenced by IDA as var_3AB8= -0x3AB8
(in main). No buffer
overflow seems to occur here.
Furthermore, when using the url
/HNAP1?foo=bar&AAAA=BBBB&morefoo=morebar&...
neither ‘foo’ nor ‘bar’ appears in the buffer, whose content starts with strings
‘AAAA’ and ‘BBBB’, and goes on with ‘morefoo’, etc.
The variable referenced by IDA as var_3A86= -0x3A86
is the address of ‘BBBB’
in the above example, and is what matters for the exploit.
checkValidUpgrade()
The extern function checkValidUpgrade()
is called @ 0x402064, with a first
argument being the content of the REMOTE_ADDR
environment variable, and a
second argument being the value of the Cookie
(also found in environment
variables). This function is defined in libhnap.so
. Passing this check is
automatic if one respects the SOAP and HNAP protocols, as described in
Part 2.
To ease debugging, an easy solution is to patch the binary so that this
check is skipped.
save_upload_file()
Having set some GET parameters and passing the Cookie check leads to the
function save_upload_file()
to be called (@ 0x40209c).
The string ‘BBBB’ passed as a GET parameter is compared to both ‘UpgradeFW’
and ‘UpgradeConfig’ strings. Reaching the vulnerability requires a
configuration upgrade, so that the relevant url for the exploit will be
/HNAP1?&do=UpgradeConfig
(where the ‘do’ could be replaced by anything
else). A buffer of size 0x84=132 is allocated on the heap, zeroed out, and
filled with the 132 first bytes of the POST request’s content. Part of this
content is copied onto the stack.
In the case of an UpgradeConfig
, the string ‘HONEYBEE-CONFIG-DSP-W215’ is
checked to be present and starting at byte 12 (counting from 0). Note that
for a firmware upgrade, the corresponding magic string is
‘HONEYBEE-FIRMWARE-DSP-W215’, and this can be checked by looking at the
firmware’s .bin
and dumping the 64 first bytes (the magic string starts at
byte 16):
00000000: 4800 0000 1f5c 2395 003f b000 000e 0000 H....\#..?......
00000010: 484f 4e45 5942 4545 2d46 4952 4d57 4152 HONEYBEE-FIRMWAR
00000020: 452d 4453 502d 5732 3135 0000 0000 0000 E-DSP-W215......
00000030: 322e 3032 2e30 3400 0000 0000 0000 0000 2.02.04.........
The second word (4 bytes) in the 132 bytes is the size of the configuration file that one is uploading to the plug, and whose content starts after the first 132 bytes sent (in the case of the firmware upgrade, the size is stored in the third word, e.g. 0x3fb000 in what is shown above).
Sending random content for the configuration file results in a return to
main()
and a ‘checksum error’ message being printed.
I did not try to understand the logic behind this but only wanted to get the
easiest possible way to the vulnerability. For this it turns out that it
suffices to use a null size of the configuration file, and one reaches the
vulnerability mentionned earlier in this post. The decryption key is found
to be 32 bytes long and to start at byte 84 of the POST request’s content.
The final exploit and a video of the exploit running on the plug can be found
here and
there.
When connecting to the plug via telnet
, one simply has to
use the credentials Admin:5up
found in
Part 1.