文章总结: 该文章披露了某知名路由器固件在Web管理服务器login()函数中存在隐藏后门,攻击者利用配置项sys.rzadmin.password中的隐藏密码,可在username字段填写任意字符串的情况下获取管理员权限。受影响固件包括多个型号版本,后门在管理界面不可见且无法通过修改密码消除。文章提供了关闭远程管理、修改默认IP等缓解措施,并给出了固件资产自动化核查的检测脚本。 综合评分: 90 文章分类: 漏洞分析,逆向分析,安全工具,安全意识,实战经验
国内某知名路由器固件存在隐藏后门,可用任意用户名登管理员账户
原创
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 = "\n".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"\n=== Tenda CVE-2026-11405 Assessment: {status} ===") print(f"Target: {report.target}\n") 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())
(全文完)
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:二进制空间安全 suntiger suntiger《国内某知名路由器固件存在隐藏后门,可用任意用户名登管理员账户》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论