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
As usual, one can get a rough idea of what is happening by running
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
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
cat /proc/PID/maps can’t be trusted.
Reversing web_cgi.cgi to find a vulnerability
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
A match results in a call to
- ’/version.txt’ (@ 0x401e80) and a match leads to calling
- ’/common/info.cgi’ (@ 0x401eac) which ends in a call to the imported
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");
_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
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
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
/etc/config.sqlite, with potential problems after a reboot.
Furthermore, the instructions before the vulnerable
(found @ 0x4030d4 - 403100) roughly translate to
memset(key, 0, 0x21); memcpy(key, ptr, 0x20);
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.
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
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 address 0x40202c where function
get_upload_parameters() is called
is easy, since it only requires:
REQUEST_URIenvironment variable that starts with
SOAPActionenvironment variable (which is normally set by
lighttpdwhen one sends a valid request for a SOAP action)
- a non-
GETrequest, so a
POSTrequest, because of the
Content-Lengthheader field /
REMOTE_ADDRenvironment variable (which is set by
Content-Typeheader field /
CONTENT_TYPEenvironment variable that is
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
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.
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
To ease debugging, an easy solution is to patch the binary so that this
check is skipped.
Having set some GET parameters and passing the Cookie check leads to the
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
.bin and dumping the 64 first bytes (the magic string starts at
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.