ModelScan 0.8.x arbitrary file write via io.open alias bypass and NEWOBJ_EX kwargs injection
Proof of concept for the Protect AI / huntr.com Model Format Vulnerability program.
Vulnerability
Two independent weaknesses combined into an arbitrary file write chain.
Weakness 1: io.open alias bypass for builtins.open
ModelScan blocks builtins.open at CRITICAL severity. However, io.open is the
identical function object (io.open is open evaluates to True in Python), and the
io module is entirely absent from the unsafe_globals blocklist.
Referencing builtins.open as io.open produces the global ('io', 'open') in the
pickle stream. This is not matched by the blocklist rule "builtins": ["open"].
ModelScan reports clean despite calling the exact same blocked function.
Weakness 2: NEWOBJ_EX opcode enables kwargs in pickle without operator.methodcaller
Pickle protocol 4 includes the NEWOBJ_EX opcode (0x92), which calls
cls.new(cls, *args, **kwargs) with full keyword-argument support. This opcode
is not covered in any known ModelScan bypass research.
Using NEWOBJ_EX to construct functools.partial with a file= keyword argument
allows calling builtins.print(content, file=fileobj) at deserialization time,
writing arbitrary content to the opened file.
builtins.print is not in the blocklist. Only eight specific builtins are listed:
eval, compile, getattr, apply, exec, open, breakpoint, import.
('io', 'open') not in unsafe_globals
('functools', 'partial') not in unsafe_globals
('builtins', 'print') not in unsafe_globals
ModelScan verdict: No issues found.
Affected
Scanner: ModelScan 0.8.8 (latest as of July 2026)
Extensions: .pkl .pickle .joblib .dill .dat .data
Trigger: pickle.loads() and joblib.load()
Reproduction
Install requirements:
pip install modelscan joblib
Generate the PoC files:
python3 poc_io_open_filewrite.py
This creates poc_filewrite_bypass.pkl and poc_filewrite_bypass.joblib and
confirms that the target file is written at deserialization time.
Scan with ModelScan:
modelscan -p poc_filewrite_bypass.pkl
Expected output: No issues found
Attack scenarios
Writing to ~/.ssh/authorized_keys with an attacker public key gives persistent
SSH access. Writing to /etc/cron.d/ (as root) gives scheduled code execution.
Writing to ~/.bashrc or ~/.profile gives code execution on the next shell session.
Replace WRITE_TARGET and WRITE_CONTENT in the PoC script with the desired path
and content.
The blocklist approach has two gaps demonstrated here.
First, it matches function references by module name and attribute name, but does
not account for the same function being accessible under multiple module paths.
builtins.open and io.open refer to the same underlying C function but only the
former is blocked.
Second, the NEWOBJ_EX opcode (protocol 4) is not analyzed for kwargs-based
dangerous construction. It enables creating functools.partial with arbitrary
keyword arguments including file handles, bypassing the requirement for
operator.methodcaller or operator.attrgetter.