Plug and Play -- Part 3
Oct 11, 2017
8 minute read


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/, 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]
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 by lighttpd when one sends a valid request for a SOAP action)
  • a non-GET request, so a POST request, because of the Content-Type (see below)
  • a Content-Length header field / CONTENT_LENGTH environment variable
  • a REMOTE_ADDR environment variable (which is set by lighttpd)
  • a Content-Type header field / CONTENT_TYPE environment variable that is multipart/form-data


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 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.


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.