English · Ελληνικά
admin:password. Από εκεί, το CVE-2024-37054 οπλοποιεί το ίδιο το model format του MLflow: τα models αποθηκεύονται ως Python pickles, οπότε κάνεις register ένα model, αντικαθιστάς το python_model.pkl του με ένα κακόβουλο cloudpickle, και τη στιγμή που η εφαρμογή τρέχει μια πρόβλεψη το pickle κάνει deserialize και εκτελείται — shell ως svcweb. Το root είναι μια μικρότερη, πιο κοφτερή παγίδα: ο svcweb μπορεί να κάνει sudo ένα script διαχείρισης που τρέχει ως root, και ο φάκελος plugins που φορτώνει αυτό το script είναι εγγράψιμος από την ομάδα. Ρίχνεις ένα αρχείο .pth εκεί και η Python το τρέχει ως root κατά την εκκίνηση του interpreter, πριν το script κάνει απολύτως τίποτα.
SSH και μια πρόσοψη nginx. Το web root κάνει redirect στο smarthire.htb — μια εφαρμογή hiring-portal — οπότε η ενδιαφέρουσα επιφάνεια είναι πιθανότατα αλλού, πάνω στο vhost.
Κάνουμε fuzz για vhosts. Εμφανίζεται ένα subdomain models. — και μια γρήγορη ματιά λέει ότι είναι MLflow, ο server model-registry/experiment-tracking.
Το prompt basic-auth υποκύπτει αμέσως στο admin:password, και το API επιβεβαιώνει MLflow 2.14.1. Αυτή η έκδοση είναι ευάλωτη στο CVE-2024-37054, ένα deserialization RCE. Η ριζική αιτία είναι δομική: το MLflow αποθηκεύει τα models pyfunc ως Python pickles (python_model.pkl), και το φόρτωμα ενός model κάνει unpickle αυτό το αρχείο. Όποιος μπορεί να γράψει τα artifacts ενός model μπορεί επομένως να επιλέξει ποιος κώδικας τρέχει όταν το model φορτωθεί.
Η ίδια η εφαρμογή SmartHire είναι το όχημα παράδοσης. Το PoC του jimmexploit αυτοματοποιεί όλο τον κύκλο: κάνει register έναν λογαριασμό στο smarthire.htb, ανεβάζει ένα CSV για να εκπαιδεύσει ένα model (που δημιουργεί ένα registered model + run στο MLflow), μετά κάνει PUT ένα κακόβουλο python_model.pkl πάνω από το εκπαιδευμένο και χτυπάει το /predict της εφαρμογής ώστε να φορτωθεί το δηλητηριασμένο model. Το payload είναι ένα αντικείμενο cloudpickle του οποίου το __reduce__ επιστρέφει os.system(<reverse shell>):
500 μοιάζει με αποτυχία, αλλά είναι το σημάδι ότι δούλεψε: το pickle κάνει deserialize πριν το MLflow προσπαθήσει να το χρησιμοποιήσει ως model, οπότε το os.system πυροδοτείται κατά το unpickling και το reverse shell είναι ήδη ζωντανό — το MLflow σφάλλει μόνο μετά, όταν διαπιστώνει ότι το αντικείμενό μας δεν είναι πραγματικό model. Αγνόησε το 500 και έλεγξε τον listener σου.Shell ως svcweb, και το user flag είναι εκεί ακριβώς. Αξίζει να σημειωθούν οι συμμετοχές σε ομάδες — έχουν σημασία για το root:
Το sudo -l δείχνει έναν κανόνα, και είναι ένα wildcard πάνω σε ένα Python script που τρέχει ως root:
Η προφανής κίνηση είναι argument injection μέσα από αυτό το *, αλλά ο καθαρότερος δρόμος είναι ο plugin loader του script. Το mlflowctl.py καταχωρεί τον φάκελο plugin του ως site directory (site.addsitedir('/opt/tools/mlflow_ctl/plugins/dev')), και — το κρίσιμο — αυτός ο φάκελος είναι εγγράψιμος από την ομάδα devs, στην οποία ανήκει ο svcweb:
Εδώ είναι το κόλπο. Όταν η Python επεξεργάζεται ένα site directory, διαβάζει κάθε αρχείο .pth μέσα του — και κάθε γραμμή που ξεκινάει με import εκτελείται ως κώδικας, κατά την εκκίνηση του interpreter, πριν τρέξει η main(). Οπότε ένα μονογραμμικό .pth που ρίχνει ο svcweb τρέχει ως root τη στιγμή που ξεκινάει η sudo'd python3.10:
sudo πρέπει να δείχνει σε ένα σταθερό, κλειδωμένο entry point, ποτέ σε έναν interpreter με wildcard.| → svcweb | Το MLflow εκτεθειμένο με default admin:password + models αποθηκευμένα ως pickles → CVE-2024-37054 deserialization RCE (CWE-502 / CWE-1188) |
| → root | NOPASSWD sudo πάνω σε python3.10 <script> του οποίου ο φάκελος plugin site.addsitedir είναι εγγράψιμος από την ομάδα· η Python εκτελεί γραμμές import των .pth κατά την εκκίνηση (CWE-426 / CWE-732) |