YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
NEW pickle RCE gadget: _posixsubprocess.fork_exec β the C fork+exec primitive under subprocess, in NEITHER scanner's blocklist
Category: Pickle deserialization RCE / model-scanner bypass (new gadget)
Affected scanners (bypassed): picklescan 1.0.5, modelscan 0.8.8
Trigger runtime: CPython 3.12.13 (_posixsubprocess C extension); reproduced via pickle.load
Payload: 141-byte pickle protocol 4 (evil.pkl, payload id)
Summary
_posixsubprocess.fork_exec is the low-level fork()+exec() primitive that the
entire stdlib subprocess machinery funnels through:
# CPython Lib/subprocess.py
from _posixsubprocess import fork_exec as _fork_exec
...
self.pid = _fork_exec(args, executable_list, close_fds, tuple(sorted(...)),
cwd, env_list, p2cread, p2cwrite, c2pread, c2pwrite,
errread, errwrite, errpipe_read, errpipe_write,
restore_signals, start_new_session, process_group,
gid, gids, uid, umask, preexec_fn, _USE_VFORK)
It is a module-level callable in the C extension _posixsubprocess and is
present in neither scanner's dangerous-globals list. A pickle whose only
global is _posixsubprocess.fork_exec spawns an arbitrary process directly on
pickle.load, achieving command execution while both scanners report 0 issues.
The gadget hands fork_exec a fully-constant argument tuple β argv
[b'/bin/sh', b'-c', b'<cmd>'], keep the always-open stdio fds (0,1,2), point
the error pipe at fd 2 β so no runtime file-descriptor allocation is needed
and the entire exploit is self-contained pickle data.
Why this bypasses both scanners
Both scanners are opcode allow/deny lists keyed on the (module, name) of
GLOBAL/STACK_GLOBAL opcodes. They block the high-level process modules but
not the syscall wrapper they all delegate to:
- modelscan 0.8.8 β
unsafe_globalsblocksos,posix,nt,subprocess,pty,runpy,socket, β¦ but has no_posixsubprocessentry. The single global_posixsubprocess.fork_execis unlisted β "No issues found! π". - picklescan 1.0.5 β
_unsafe_globalsblocksos:'*',posix:'*',nt:'*',subprocess:'*',pty:'*',ctypes:'*', β¦ but has no_posixsubprocessentry._posixsubprocess.fork_execis reported only as Suspicious (not Dangerous) β Infected files: 0.
The blocklists cover every user-facing way to spawn a process, yet miss the common C primitive underneath them all.
Distinctness
Distinct module and distinct primitive from every previously-filed stdlib gadget
(uuid._get_command_stdout, mailcap.findmatch, multiprocessing MakeProxyType,
operator.methodcaller+ctypes.CDLL, numpy.ctypeslib, marshal.loads+FunctionType,
codeop.compile_command, doctest, pathlib+SourceFileLoader, dataclasses._create_fn,
logging.config.fileConfig / dictConfig, pydoc.pipepager, timeit, cProfile,
os.spawnv, inspect.getattr_static, typing.get_type_hints). This is the raw
fork_exec C entry point β no eval, no string-resolution, no higher-level module.
Root cause
The scanners deny the process-spawning modules (os/posix/subprocess/pty)
but not the shared C implementation _posixsubprocess.fork_exec that those
modules import and call. A pickle can name it directly and pass a constant argv,
so the exec target rides entirely in pickle data reachable from a single REDUCE.
PoC
python gen_poc.py 'id' # writes evil.pkl
python -c "import pickle; pickle.load(open('evil.pkl','rb'))"
Verified execution (real pickle.load, CPython 3.12.13)
The child ran id, output captured via id > /tmp/POSIXSUB_PROOF 2>&1:
PROOF: uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),...
Negative control
A benign pickle ({"weights":[1,2,3],"name":"model"}) creates no proof file β
confirming execution comes from the fork_exec reduce, not from loading itself.
modelscan 0.8.8
--- Summary ---
No issues found! π
picklescan 1.0.5
----------- SCAN SUMMARY -----------
Scanned files: 1
Infected files: 0
Suspicious globals: 1 # _posixsubprocess.fork_exec -> Suspicious (NOT Dangerous)
Dangerous globals: 0
Opcode evidence
The only global in the stream is _posixsubprocess / fork_exec; argv rides as
inert SHORT_BINBYTES:
STACK_GLOBAL # '_posixsubprocess' + 'fork_exec'
SHORT_BINBYTES b'/bin/sh'
SHORT_BINBYTES b'-c'
SHORT_BINBYTES b'id > ...'
Suggested fix
Add _posixsubprocess (*, or at least fork_exec) to both scanners'
dangerous-globals blocklists alongside os/posix/subprocess/pty. Consider
also _winapi.CreateProcess on Windows. Users: never pickle.load/joblib.load
untrusted artifacts.
Files
evil.pklβ 141-byte proof-of-concept pickle (payloadid)gen_poc.pyβ regenerates the pickle for an arbitrary command