0.前言
在二手市场买了一个光猫,但是通过恢复出厂设置,管理员账号并不是默认密码,是没办法获取到之前运营商设置的随机密码,因为按reast只能恢复用户设置内容,最后通过工具恢复,感谢这位恩山论坛朋友帮我恢复出厂设置。
光猫是G7615V2电信江苏地区的(管理员密码是随机的),于是对这个zteOnu工具感兴趣,同时感谢开源作者的贡献。

1.项目地址
GitHub - Septrum101/zteOnu: A tool that can open ZTE onu device factory mode
下载软件:Releases · Septrum101/zteOnu
2.实现原理
猜测项目原始来源:应该有人反编译过中兴某一个专门用来进行出产设置的机器,是用来调试中兴光猫的。
这个工具的实现的具体流程如下(AI画的):
python
┌──────────────┐ HTTP (TCP 8080) ┌──────────────┐
│ 本机脚本 │ ←------------------------------------------→ │ 光猫 Web │
└────┬─────────┘ └────┬─────────┘
│ │
│ 1. 激活工厂接口 │
│ 2. 请求 FactoryMode │
│ 3. 拿随机数→算密钥 │
│ 4. 加密发送账号密码 │
│ 5. 拿到临时 Telnet 账号/密码 │
├─────── 以下走 Telnet (TCP 23) ─────────┤
│ 6. 登录临时 shell │
│ 7. 5 条 sendcmd 改写 TelnetCfg 表 │
│ 8. reboot │
│ ▼
│ Flash 中的 db 被固化
│ 9. 重启完成 → Telnet 23 端口永久开放
中兴光猫内部其实是预留了一个工厂接口用来开启临时的Telnet.
这个接口只能允许特定的网口mac申请访问,也相当一个密码验证,写死在机器里面了,
这个MAC地址是:00:07:29:55:35:57
不排除还有其他的MAC地址也可以通过验证,希望有能力大佬反编译中兴机器的固件,看看代码怎么写的
这里说明一下,目前大部分光猫是运行的liunx系统,一般系统调试会通过Telnet进行无线设置,比如文件删除移动,调用系统工具等,并且Telnet通过账号,密码访问liunx主机,这个Telnet在liunx是权限大小的,好比Win shell.
这里以中兴默认路由器192.168.1.1地址80端口为例子
首先进入重置工厂模式 (reset), POST方式访问地址http://192.168.1.1:80/webFac
Body内容是 SendSq.gch
然后请求进入工厂模式, POST方式访问地址http://192.168.1.1:80/webFac
Body内容是 RequestFactoryMode.gch
再获取随机数 & 密钥索引, POST方式访问地址http://192.168.1.1:80/webFac
Body内容是SendSq.gch?rand=3
发rand随机数,获取到后,有点复杂,最后通过一个特定解密函数获取到一个密钥
获取交流密钥后,确定接口固件版本,比如version61
中间判断有点多就不写了
然后登录工厂模式
于是访问http://192.168.1.1:80/CheckLoginAuth.gch?\&version61\&user=telecomadmin\&pass=nE7jA%5m
然后会得到相应响应判断是否成功
关键一步获取临时的Telnet账号密码
访问http://192.168.1.1:80/FactoryMode.gch?mode=2\&user=notused
然后光猫会返回:
FactoryMode.gch?user=临时的Telnet账号&pass=临时的Telnet密码
后面就可以通过这个账号登录Telnet,然后重新设置,开启Telnet。
3.使用命令
zteOnu.exe --telnet --user 运营商(默认)超级账号 --pass 运营商(默认)超级密码 --ip 192.168.1.1 --port 80
4.Python版本(AI写的没有测试过)
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ZTE ONU Permanent Telnet Enabler -- Python Port
Author : github.com/thank243 (Go) → Python by <your-name>
Usage : python zte_telnet.py -i 192.168.1.1 -u telecomadmin -p nE7jA%5m --telnet
"""
import argparse
import base64
import binascii
import socket
import time
import struct
from typing import Tuple, Optional
import requests
from Crypto.Cipher import AES
# ---------- AES-ECB ----------
BS = 16
def pad(s: bytes) -> bytes:
return s + b'\x00' * (BS - len(s) % BS)
def aes_ecb_encrypt(data: bytes, key: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(pad(data))
def aes_ecb_decrypt(data: bytes, key: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data).rstrip(b'\x00')
# ---------- 密钥池 ----------
AES_KEY_POOL = bytes.fromhex(
"7B56B0F7DA0E6852C819F32B849079E562F8EAD2649387DF73D7FBCCAAFE7543"
"1C29DF4C522C6E7B453D1FF1DEBC27858A4591BE3813DE673208541175F4D3B4"
"A4B312866723994C617FB1D230DF47F17693A38C95D359BF878EF3B3E4764988"
)
AES_KEY_POOL_NEW = bytes.fromhex(
"8C2365D1FC32453711287163072069141473E7D453132436C2B5E1FCCF8A9A41"
"893C49CF5C728C9EEB750D3FD1FECC57657A35213E68537E9702487471953453"
"84B4C3E2D6273DE65D729CBC3D03FD76C19C25A89247E4180F243F4F67EC97F4"
)
def get_key_pool(version: int, r: int, new_r: int) -> bytes:
if version == 1:
idx = r
pool = AES_KEY_POOL[idx:idx + 24]
else:
idx = ((0x1000193 * r) & 0x3F ^ new_r) % 60
pool = AES_KEY_POOL_NEW[idx:idx + 24]
return bytes((b ^ 0xA5) & 0xFF for b in pool)
# ---------- HTTP 交互 ----------
class FactoryClient:
def __init__(self, ip: str, port: int, user: str, pwd: str):
self.ip = ip
self.port = port
self.user = user
self.pwd = pwd
self.s = requests.Session()
self.s.headers["User-Agent"] = "curl/8.8.0-DEV"
self.base = f"http://{ip}:{port}"
self.key = b''
def reset(self):
self.s.get(self.base + "/")
resp = self.s.post(self.base + "/webFac", data="SendSq.gch")
if resp.status_code == 400:
return
resp.raise_for_status()
def req_factory_mode(self):
try:
self.s.post(self.base + "/webFac", data="RequestFactoryMode.gch", timeout=3)
except requests.exceptions.RequestException:
pass
def send_sq(self) -> Tuple[int, int]:
r = int(time.time()) % 60
resp = self.s.post(self.base + "/webFac",
data=f"SendSq.gch?rand={r}\r\n")
if resp.status_code != 200:
raise RuntimeError(resp.text)
if "newrand" in resp.text:
version = 2
new_r = int(resp.text.split("=")[1])
self.key = get_key_pool(version, r, new_r)
else:
version = 1
self.key = get_key_pool(version, r, 0)
return version, r
def check_login_auth(self):
cmd = f"CheckLoginAuth.gch?&version61&user={self.user}&pass={self.pwd}"
payload = aes_ecb_encrypt(cmd.encode(), self.key)
resp = self.s.post(self.base + "/webFacEntry", data=payload)
if resp.status_code == 401:
raise RuntimeError("错误的工厂用户名或密码")
if resp.status_code != 200:
raise RuntimeError(resp.text)
aes_ecb_decrypt(resp.content, self.key) # 仅校验解密是否成功
def send_info(self):
magic = base64.b64decode("AAAAAGAIAACTBwAAOggAALoAAACQBwAAxAcAAMoGAACVBAAATggAAM0BAAAnCA==")
cmd = b"SendInfo.gch?info=12|" + magic
payload = aes_ecb_encrypt(cmd, self.key)
resp = self.s.post(self.base + "/webFacEntry", data=payload)
if resp.status_code == 401:
raise RuntimeError("SendInfo 校验失败")
resp.raise_for_status()
def factory_mode(self) -> Tuple[str, str]:
cmd = b"FactoryMode.gch?mode=2&user=notused"
payload = aes_ecb_encrypt(cmd, self.key)
resp = self.s.post(self.base + "/webFacEntry", data=payload)
resp.raise_for_status()
dec = aes_ecb_decrypt(resp.content, self.key)
user, pwd = dec.decode().split("&user=")[1].split("&pass=")
return user, pwd
def full_flow(self) -> Tuple[str, str]:
print("-" * 35)
print("step [0] reset factory ... ", end="", flush=True)
self.reset()
print("ok")
print("step [1] request factory mode ... ", end="", flush=True)
self.req_factory_mode()
print("ok")
print("step [2] send sq ... ", end="", flush=True)
ver, _ = self.send_sq()
print("ok")
print("step [3] login auth ... ", end="", flush=True)
if ver == 2:
self.send_info()
self.check_login_auth()
print("ok")
print("step [4] enter factory mode ... ", end="", flush=True)
u, p = self.factory_mode()
print("ok")
print("-" * 35)
return u, p
# ---------- Telnet ----------
class ZTETelnet:
def __init__(self, ip: str, port: int, user: str, pwd: str):
self.ip = ip
self.port = port
self.user = user
self.pwd = pwd
self.sock: Optional[socket.socket] = None
def open(self):
self.sock = socket.create_connection((self.ip, self.port), 10)
time.sleep(0.5)
self.wait(b"login:")
self.send(self.user.encode())
self.wait(b"Password:")
self.send(self.pwd.encode())
self.wait(b"#")
def wait(self, pat: bytes):
buf = b''
while pat not in buf:
tmp = self.sock.recv(2048)
if not tmp:
raise RuntimeError("Telnet 断开")
buf += tmp
def send(self, data: bytes):
self.sock.sendall(data + b"\n")
def perm(self):
cmds = [
"sendcmd 1 DB set TelnetCfg 0 Lan_Enable 1",
"sendcmd 1 DB set TelnetCfg 0 TSLan_UName root",
"sendcmd 1 DB set TelnetCfg 0 TSLan_UPwd Zte521",
"sendcmd 1 DB set TelnetCfg 0 Max_Con_Num 3",
"sendcmd 1 DB set TelnetCfg 0 InitSecLvl 3",
"sendcmd 1 DB save"
]
for c in cmds:
self.send(c.encode())
time.sleep(0.2)
print("永久 Telnet 已写入")
def reboot(self):
self.send(b"reboot")
print("设备正在重启 ...")
def close(self):
if self.sock:
self.sock.close()
# ---------- main ----------
def main():
ap = argparse.ArgumentParser(description="ZTE ONU 永久开启 Telnet")
ap.add_argument("-i", "--ip", default="192.168.1.1")
ap.add_argument("-P", "--http-port", type=int, default=8080)
ap.add_argument("-t", "--telnet-port", type=int, default=23)
ap.add_argument("-u", "--user", default="telecomadmin")
ap.add_argument("-p", "--pass", default="nE7jA%5m")
ap.add_argument("--telnet", action="store_true", help="执行永久 Telnet 开启并重启")
args = ap.parse_args()
fac = FactoryClient(args.ip, args.http_port, args.user, args.pass)
for retry in range(10):
try:
tl_user, tl_pass = fac.full_flow()
break
except Exception as e:
print(e, f" 重试 ({retry + 1}/10)")
time.sleep(0.5)
else:
print("失败 10 次,退出")
return
if not args.telnet:
print(f"临时 Telnet 账号:\n user: {tl_user}\n pass: {tl_pass}")
return
print("登录临时 Telnet ...")
tn = ZTETelnet(args.ip, args.telnet_port, tl_user, tl_pass)
tn.open()
tn.perm()
tn.reboot()
tn.close()
print("永久 Telnet 已启用 → root / Zte521")
if __name__ == "__main__":
main()