对中兴光猫zteOnu.exe项目的简单分析(提供下载地址)

0.前言

在二手市场买了一个光猫,但是通过恢复出厂设置,管理员账号并不是默认密码,是没办法获取到之前运营商设置的随机密码,因为按reast只能恢复用户设置内容,最后通过工具恢复,感谢这位恩山论坛朋友帮我恢复出厂设置。

3550387095https://www.right.com.cn/forum/?815290

光猫是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()
相关推荐
千码君201613 小时前
Go语言:常量设置的注意事项
go·const·iota·常量·var·编译期可计算·无类型常量
qinyuan151 天前
gorm读取PostgreSQL的json数据类型
后端·go
mit6.8241 天前
[OP-Agent] 可扩展架构 | 插件管理器plugins.go
go·1024程序员节
代码扳手1 天前
Go + gRPC + HTTP/3:解锁下一代高性能通信
go
mit6.8242 天前
[OP-Agent] `opa run` | 交互模式(REPL) | 服务模式(HTTP API) | runtime包
go·1024程序员节
梁梁梁梁较瘦3 天前
边界检查消除(BCE,Bound Check Elimination)
go
梁梁梁梁较瘦3 天前
指针
go
梁梁梁梁较瘦3 天前
内存申请
go
半枫荷3 天前
七、Go语法基础(数组和切片)
go