LWa7ch's picture
Upload README.md with huggingface_hub
4c8de46 verified
|
Raw
History Blame Contribute Delete
4.57 kB
# 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__.
### Combined chain
io.open(path, 'a') -> file_obj
NEWOBJ_EX(
functools.partial,
args=(builtins.print, content),
kwargs={file: file_obj, end: ''}
) -> partial_obj
partial_obj() -> print(content, file=file_obj)
-> writes content to path
Globals extracted by ModelScan:
('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.
## Payload structure
io.open(path, 'a') -> file_obj [memo 6]
builtins.print -> print_func [memo 9]
functools.partial -> partial_cls [memo 12]
NEWOBJ_EX(partial_cls,
(print_func, content),
{file: file_obj, end: ''}) -> partial_obj
EMPTY_TUPLE
REDUCE(partial_obj, ()) -> print(content, file=file_obj) -> file write
STOP
## Root Cause
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.
## Fix
Add to unsafe_globals:
"io": ["open"],
"_io": ["open"],
"functools": "*",
Or alternatively, build a whitelist approach rather than a denylist.
## Files
- poc_io_open_filewrite.py: generates and verifies both PoC files
- poc_filewrite_bypass.pkl: malicious pickle, passes ModelScan, writes to disk
- poc_filewrite_bypass.joblib: same payload with .joblib extension
## Ethics
Payload writes a benign string to /tmp/ml_filewrite_indicator.
Submitted exclusively through the Protect AI / huntr.com bug bounty program.