国内某知名路由器固件存在隐藏后门,可用任意用户名登管理员账户
原创
suntiger
suntiger
二进制空间安全
2026年7月20日 15:25
北京
在小说阅读器读本章
去阅读
将二进制空间安全设为\"星标⭐️\"
第一时间收到文章更新
事件背景
根据公开报道(https://kb.cert.org/vuls/id/213560),该路由器厂商至少有5个版本固件在Web管理服务器/bin/httpd的login()函数中,嵌入了未向用户披露的第二条认证通道。攻击者如果掌握配置项sys.rzadmin.password中的隐藏密码,可在 username填写任务字符串的情况下获得role=2的管理员权限,并创建合法session,该步骤与用户自己设置的管理员口令没有任何联系。
对于普通企业和家庭用户而言,该后门在管理界面中完全不可见、关不掉,甚至修改管理员登录密码也无法消除风险,在开启远程Web管理时,攻击暴露面将从局域网扩散到互联网,高风险大幅度增加。
login()双路径认证架构
根据公开信息, 该后门位于Web服务器二进制/bin/httpd的login()函数,当用户访问路由器管理页面,例如:192.168.0.1, 提交用户名和密码后,login()函数理论上会走合规正常流程,例如:
用户 password │ ▼PasswordToMd5() ← MD5 哈希 │ ▼check_rand_key() ← 随机数/会话密钥校验 │ ▼prod_encode64() ← 与存储值比对 │ ▼成功 → 创建 Session → 分配 role
但如果上述正常验证流程失败,函数并不总是返回密码错误,而是进入隐藏分支:
if (normal_auth_failed) { char backdoor_pwd[256]; GetValue(\"sys.rzadmin.password\", backdoor_pwd);
if (strcmp(backdoor_pwd, user_supplied_password) == 0) { /* username 在此分支不参与校验 */ session.role = 2; /* 管理员 */ create_valid_session(); return SUCCESS; }}return AUTH_FAILED;
当攻击者拿到role=2的管理员账号权限后,可以更改路由器的DNS、防火墙、端口转发、无线密钥等, 为进一步进行横向渗透提供了有利条件。
受影响固件与暴露面评估
目前已经确认的固件版本包括:
| 型号系列 | 固件版本字符串 |
| FH1201 | US\_FH1201V1.0BR\_V1.2.0.14(408)\_EN\_TD |
| W15E | US\_W15EV1.0br\_V15.11.0.5(1068\_1567\_841)\_EN\_TDE |
| AC10 | US\_AC10V1.0re\_V15.03.06.46\_multi\_TDE01 |
| AC5 | US\_AC5V1.0RTL\_V15.03.06.48\_multi\_TDE01 |
| AC6 V2 | US\_AC6V2.0RTL\_V15.03.06.51\_multi\_T |
需要注意的是,上面未被列出的固件并不能视为安全,同厂商/bin/httpd代码树可能在其它区域版本复用。
根据该漏洞形成原理,概念型攻击链流程如下:
(1).通过固件逆向、配置提取、内部泄露等方式,获取到目标版本固件的隐藏密码。
(2).访问https://
(3).POST login:username=任意,password=<隐藏密码>。
(4).获取role=2管理员权限Session,登录后获取Web管理权限。
企业分层防御与缓解措施
在无官方补丁的前提下,企业策略必须区分缓解和根治,如果要根治,目前只能依赖换硬件或等待厂商固件更新。官方推荐的缓解措施主要包括:关闭远程Web管理和修改默认LAN IP。目的是阻断WAN侧利用以及降低对192.168.0.1的扫描命中。
在网络层之外,安全团队可在现有的SIEM/NDR能力之上叠加以下低误报导向的检测线索,主要包括:
(1).同一源IP对管理口多次失败后单次成功。
(2).非变更窗口的DNS服务器修改、静态路由、DMZ、端口转发新增。
(3).无线SSID密码或WPS状态异常变更。
(4).管理Session创建时间与已知变更工单不匹配。
企业对自身资产隐患进行排查阶段性工作可按以下方案进行:
阶段一:资产盘点
该阶段目标是把\"可能受影响\"变成可枚举资产列表,动作包括:从CMDB、DHCP指纹、网关ARP表收集Tenda设备,登录管理页收集完整固件版本字符串,标记是否曾开启远程Web管理、是否使用默认192.168.0.1。最终收集完成三项物料清单,分别为:受影响/待确认/不受影响,以及责任人。
阶段二:暴露面收敛
对已命中或待确认设备:立即关闭远程管理;将管理口迁入独立 VLAN,ACL 仅允许 jump host / 运维网段;修改LAN网关地址降低扫描噪音;对解包得到的 /bin/httpd 运行strings特征扫描。
阶段三:根治与持续运营
根治路径只有更换已确认不受影响设备或应用厂商修复固件,持续运营建议:新采购路由器纳入固件strings抽检;禁止Internet-facing远程Web管理写入基线;关注官方补丁,
务必对 /bin/httpd 做 bindiff,确认 login() 中 sys.rzadmin.password 分支已移除,而非仅改默认密码值。
固件与资产自动化核查
安全团队从设备或官网下载固件,解包得到/bin/httpd,需批量筛查是否包含rzadmin配置键,运行实例如下:
cd tendaBackdoorArticle/srcpython tenda_backdoor_assessor.py --scan-firmware ./firmware_samples/httpd
预期扫描出结果如下:
=== Tenda CVE-2026-11405 Assessment: FAIL ===Target: firmware_samples/httpd
[1] [CRITICAL] backdoor_indicator: 检测到后门配置键 sys.rzadmin.password(httpd) → 标记为 CVE-2026-11405 疑似受影响固件,禁止上线并计划换机
在内网盘点时确认网关管理口是否可从运维网段意外暴露,并尝试从 HTTP 首页提取固件版本线索。
python tenda_backdoor_assessor.py --probe 192.168.0.1:80
预期输出结果如下:
[1] [MEDIUM] exposure: 管理口 TCP 80 可达 → 确认是否应从WAN暴露;建议关闭远程Web管理
检测脚本代码如下:
#!/usr/bin/env python3# -*- coding: utf-8 -*-\"\"\"Tenda CVE-2026-11405 固件与资产自动化核查工具(防御向)功能: 1. 固件二进制 strings 扫描 — 检测 sys.rzadmin.password 等后门特征 2. 受影响固件版本字符串匹配 3. 路由器管理口暴露面探测(HTTP 可达性,不尝试后门密码) 4. 综合 JSON 报告 + 控制台 PASS/FAIL用法: python tenda_backdoor_assessor.py --config routers.yaml python tenda_backdoor_assessor.py --scan-firmware ./firmware_samples/httpd python tenda_backdoor_assessor.py --probe 192.168.0.1:80\"\"\"from __future__ import annotationsimport argparseimport jsonimport reimport socketimport sysfrom dataclasses import asdict, dataclass, fieldfrom pathlib import Pathfrom typing import Anyfrom urllib.error import URLErrorfrom urllib.request import Request, urlopentry: import yamlexcept ImportError: yaml = None # type: ignoreDEFAULT_INDICATORS = ( \"sys.rzadmin.password\", \"rzadmin\", \"GetValue\", \"PasswordToMd5\", \"check_rand_key\", \"prod_encode64\",)DEFAULT_AFFECTED_FIRMWARE = [ \"US_FH1201V1.0BR_V1.2.0.14(408)_EN_TD\", \"US_W15EV1.0br_V15.11.0.5(1068_1567_841)_EN_TDE\", \"US_AC10V1.0re_V15.03.06.46_multi_TDE01\", \"US_AC5V1.0RTL_V15.03.06.48_multi_TDE01\", \"US_AC6V2.0RTL_V15.03.06.51_multi_T\",]FIRMWARE_VERSION_RE = re.compile( r\"US_(?:FH1201|W15E|AC10|AC5|AC6)[^\\s\\\"']{8,80}\", re.IGNORECASE,)@dataclassclass Finding: category: str severity: str # CRITICAL | HIGH | MEDIUM | LOW message: str recommendation: str = \"\" context: dict[str, Any] = field(default_factory=dict)@dataclassclass AssessmentReport: target: str findings: list[Finding] = field(default_factory=list) passed: bool = True def add(self, finding: Finding) -> None: self.findings.append(finding) if finding.severity in (\"CRITICAL\", \"HIGH\", \"MEDIUM\"): self.passed = Falsedef load_config(path: Path) -> dict[str, Any]: if yaml is None: raise SystemExit(\"需要 PyYAML: pip install pyyaml\") with path.open(encoding=\"utf-8\") as f: return yaml.safe_load(f) or {}def extract_strings(data: bytes, min_len: int = 6) -> list[str]: \"\"\"从二进制中提取可打印 ASCII 字符串。\"\"\" pattern = re.compile(rb\"[\\x20-\\x7e]{%d,}\" % min_len) return [m.group().decode(\"ascii\", errors=\"ignore\") for m in pattern.finditer(data)]def scan_firmware_file(path: Path, indicators: tuple[str, ...]) -> AssessmentReport: report = AssessmentReport(target=str(path)) try: data = path.read_bytes() except OSError as exc: report.add( Finding( category=\"firmware_io\", severity=\"HIGH\", message=f\"无法读取固件文件: {exc}\", recommendation=\"确认路径与文件权限\", ) ) return report strings = extract_strings(data) joined = \"\
\".join(strings) hits = [s for s in indicators if s in joined] if \"sys.rzadmin.password\" in hits: report.add( Finding( category=\"backdoor_indicator\", severity=\"CRITICAL\", message=f\"检测到后门配置键 sys.rzadmin.password({path.name})\", recommendation=\"标记为 CVE-2026-11405 疑似受影响固件,禁止上线并计划换机\", context={\"matched_indicators\": hits}, ) ) elif len(hits) >= 3: report.add( Finding( category=\"backdoor_indicator\", severity=\"HIGH\", message=f\"检测到 {len(hits)} 个 login() 相关特征字符串\", recommendation=\"对 /bin/httpd 做 bindiff 或逆向确认是否存在隐藏认证分支\", context={\"matched_indicators\": hits}, ) ) for ver in FIRMWARE_VERSION_RE.findall(joined): report.add( Finding( category=\"firmware_version\", severity=\"MEDIUM\", message=f\"固件内嵌版本字符串: {ver}\", recommendation=\"与 CERT/CC 受影响列表交叉核对\", context={\"version_string\": ver}, ) ) if not report.findings: report.add( Finding( category=\"backdoor_indicator\", severity=\"LOW\", message=\"未命中已知后门特征(不代表安全,可能为不同 SDK 分支)\", recommendation=\"结合设备型号与供应链信息继续排查\", ) ) return reportdef scan_firmware_dirs(dirs: list[str], indicators: tuple[str, ...]) -> AssessmentReport: report = AssessmentReport(target=\"firmware_batch\") paths: list[Path] = [] for d in dirs: base = Path(d) if not base.exists(): report.add( Finding( category=\"firmware_io\", severity=\"MEDIUM\", message=f\"固件目录不存在: {base}\", recommendation=\"创建目录并放入待测 httpd 或完整固件镜像\", ) ) continue paths.extend(p for p in base.rglob(\"*\") if p.is_file()) if not paths: report.add( Finding( category=\"firmware_io\", severity=\"MEDIUM\", message=\"固件目录为空,跳过 strings 扫描\", recommendation=\"将 /bin/httpd 或 squashfs 解包文件放入 firmware_scan_dirs\", ) ) return report for path in paths: sub = scan_firmware_file(path, indicators) report.findings.extend(sub.findings) if not sub.passed: report.passed = False return reportdef match_affected_firmware(version: str, affected: list[str]) -> bool: version_norm = version.strip() return any(version_norm == item or item in version_norm for item in affected)def probe_mgmt_port(host: str, port: int, timeout: float = 3.0) -> dict[str, Any]: \"\"\"探测管理口 TCP 可达性与 HTTP 响应头(不提交登录表单)。\"\"\" result: dict[str, Any] = {\"host\": host, \"port\": port, \"tcp_open\": False, \"http_status\": None} sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: sock.connect((host, port)) result[\"tcp_open\"] = True except OSError: return result finally: sock.close() scheme = \"https\" if port == 443 else \"http\" url = f\"{scheme}://{host}:{port}/\" try: req = Request(url, headers={\"User-Agent\": \"TendaAssessor/1.0 (defensive inventory)\"}) with urlopen(req, timeout=timeout) as resp: result[\"http_status\"] = resp.status body = resp.read(8192).decode(\"utf-8\", errors=\"ignore\") if \"tenda\" in body.lower(): result[\"vendor_hint\"] = \"Tenda-like UI detected\" fw = re.search(r\"(?:firmware|version)[^<]{0,40}([0-9A-Za-z_().]+)\", body, re.I) if fw: result[\"firmware_guess\"] = fw.group(1)[:80] except URLError as exc: result[\"http_error\"] = str(exc.reason) if hasattr(exc, \"reason\") else str(exc) return resultdef assess_router_target( host: str, port: int, affected: list[str], expect_remote: bool,) -> AssessmentReport: report = AssessmentReport(target=f\"{host}:{port}\") probe = probe_mgmt_port(host, port) if probe.get(\"tcp_open\"): report.add( Finding( category=\"exposure\", severity=\"MEDIUM\" if not expect_remote else \"HIGH\", message=f\"管理口 TCP {port} 可达\", recommendation=\"确认是否应从 WAN 暴露;CERT/CC 建议关闭远程 Web 管理\", context=probe, ) ) else: report.add( Finding( category=\"exposure\", severity=\"LOW\", message=f\"管理口 {host}:{port} 不可达(可能离线或非 Tenda)\", context=probe, ) ) return report fw_guess = probe.get(\"firmware_guess\", \"\") if fw_guess and match_affected_firmware(fw_guess, affected): report.add( Finding( category=\"affected_firmware\", severity=\"CRITICAL\", message=f\"管理页疑似匹配受影响固件: {fw_guess}\", recommendation=\"立即关闭远程管理、隔离 VLAN,计划换机或等待官方补丁\", context={\"firmware_guess\": fw_guess}, ) ) if expect_remote: report.add( Finding( category=\"exposure\", severity=\"CRITICAL\", message=\"配置标记 expect_remote_mgmt=true — WAN 侧管理口暴露\", recommendation=\"关闭远程 Web 管理并修改默认 LAN IP(CERT/CC 官方缓解)\", ) ) return reportdef run_full_assessment(config_path: Path) -> AssessmentReport: cfg = load_config(config_path) affected = list(cfg.get(\"affected_firmware\", [])) indicators = tuple(cfg.get(\"backdoor_indicators\", DEFAULT_INDICATORS)) report = AssessmentReport(target=str(config_path)) fw_report = scan_firmware_dirs(cfg.get(\"firmware_scan_dirs\", []), indicators) report.findings.extend(fw_report.findings) for item in cfg.get(\"router_targets\", []): host = item.get(\"host\", \"\") port = int(item.get(\"mgmt_port\", 80)) expect = bool(item.get(\"expect_remote_mgmt\", False)) if not host: continue sub = assess_router_target(host, port, affected, expect) report.findings.extend(sub.findings) if not sub.passed: report.passed = False if fw_report.passed and all( f.severity not in (\"CRITICAL\", \"HIGH\", \"MEDIUM\") for f in report.findings ): report.passed = True return reportdef print_report(report: AssessmentReport) -> None: status = \"PASS\" if report.passed else \"FAIL\" print(f\"\
=== Tenda CVE-2026-11405 Assessment: {status} ===\") print(f\"Target: {report.target}\
\") for i, f in enumerate(report.findings, 1): print(f\"[{i}] [{f.severity}] {f.category}: {f.message}\") if f.recommendation: print(f\" → {f.recommendation}\") print()def main() -> int: parser = argparse.ArgumentParser(description=\"Tenda CVE-2026-11405 防御向核查工具\") parser.add_argument(\"--config\", type=Path, help=\"YAML 配置文件路径\") parser.add_argument(\"--scan-firmware\", type=Path, help=\"单个固件/httpd 文件 strings 扫描\") parser.add_argument(\"--probe\", help=\"探测管理口 host:port,例 192.168.0.1:80\") parser.add_argument(\"--json-out\", type=Path, help=\"输出 JSON 报告路径\") args = parser.parse_args() if args.scan_firmware: report = scan_firmware_file(args.scan_firmware, DEFAULT_INDICATORS) elif args.probe: host, _, port_str = args.probe.partition(\":\") port = int(port_str or \"80\") report = assess_router_target(host, port, DEFAULT_AFFECTED_FIRMWARE, False) elif args.config: report = run_full_assessment(args.config) else: parser.print_help() return 1 print_report(report) if args.json_out: payload = {\"passed\": report.passed, \"target\": report.target, \"findings\": [asdict(f) for f in report.findings]} args.json_out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding=\"utf-8\") print(f\"JSON report written to {args.json_out}\") return 0 if report.passed else 2if __name__ == \"__main__\": sys.exit(main())
(全文完)
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论