Load/Mine: Malicious Hugging Face Models Backdoor GPU Hosts
Malicious Hugging Face model repositories ran cryptominers and backdoors on GPU hosts that loaded them; the campaign returned after each takedown.
TL;DR
- A campaign tracked by Pluto Research planted cryptomining and backdoor payloads in 31 model repositories across seven Hugging Face accounts, disguised as small Qwen2 test models. Loading one with custom code enabled runs the publisher's Python on the host before any model weights are read. Pluto published the analysis on October 29, 2026.
- The campaign and its linked models recorded 32,376 downloads, 29,818 of them across the 31 malicious repositories. Victims are real and current: the operator's wallet has received about 2,276 PRL (Pearl) on-chain, roughly $5,000, and live mining workers were still reporting at capture.
- The targets are self-hosted GPU infrastructure: Xinference and GPUStack deployments, rented GPU machines, shared research notebooks and CI jobs that pull small test models. The second wave returned within days of a takedown, and one repository carries a container-to-host escape script.
- No CVE is assigned, and the incident is not in CISA's Known Exploited Vulnerabilities catalog as of November 3, 2026. Review every
.pyfile that anauto_mapentry references before enablingtrust_remote_code, and check hosts that loaded any listed model. "What should I do if I installed it?" carries the containment steps.
Am I affected?
You are affected when all of these hold:
- Someone on your infrastructure loaded a model from one of the campaign's repositories with custom code enabled, through
trust_remote_code=Trueor an interactive confirmation. The repository names and accounts are listed in "How do I check if I was compromised?". - Your environment is the campaign's target class: a host running self-hosted inference, especially Xinference or GPUStack, a rented GPU machine, a shared research notebook, or a CI job that pulls small test models without review.
- The payload runs at import time, before any weights load, so a failed model load still executes it. A 42-byte
model.safetensorswith no tensors inside is the flagship repository's entire weight file.
The accounts in the first wave were removed after Pluto reported them. The second wave appeared within days under new accounts and was still live eight days after being reported, so a takedown notice is not evidence the exposure ended.
| Group | Repositories | Recorded downloads | Status on October 28, 2026 |
|---|---|---|---|
| First wave, accounts hhzz667 and dsxk4mr8tq | 12 | 728 | removed |
| Second wave, five accounts including MENXIANGYU and wjsbg | 19 | 29,090 | live |
| Linked models, six accounts | 13 | 2,558 | live |
Check your caches and hosts:
# Any of the campaign's models in the local Transformers cache
ls ~/.cache/huggingface/hub 2>/dev/null |
grep -iE "MENXIANGYU|wjsbg|htt0118|soumikmahato|r1r13|hhzz667|dsxk4mr8tq|MONIADF-2|antwoinhjro654|vaishupv|andrew3d|oifewuheu|KzuryHD"
# Watchdog and miner artifacts on the host
ls -la /tmp/.cch 2>/dev/null
# A kernel-worker name that carries userspace arguments is the miner
ps aux | grep -F '[kworker/u20:1]'
# Persistence hooks written by the payload
grep -n "svc.py" ~/.bashrc ~/.profile /root/.bashrc 2>/dev/null
A real kernel worker thread shows no command-line arguments; the disguised miner carries the mining login as its arguments, which is the tell Pluto describes.
How do I fix it?
No package update fixes this, because the delivery is a documented feature used as designed. The fix is a review discipline on every model that carries an auto_map.
- Inventory every model your pipelines load with
trust_remote_code=True, then read each Python file that itsconfig.jsonauto_mapreferences before the next load. That review would have caught every payload in this campaign, as Pluto notes. - Prefer models whose weights ship in
safetensorswith noauto_mapentry, and pin model revisions by commit hash in jobs and notebooks. - On Xinference and GPUStack hosts, restrict the model registry and API to trusted networks. The campaign's payloads report into the Xinference registry and one stage targets GPUStack's host directories, so treat an exposed panel as in scope.
- Block and monitor the indicators in "How do I check if I was compromised?" at your egress, including the reverse-shell receivers and the pools.
Hugging Face's guidance already places the work on the user: verify the code before enabling the flag. Model cards and tutorials that copy trust_remote_code=True turn an informed opt-in into a checkbox, which is exactly what this campaign counted on.
What does the malicious code do?
The payload is ordinary Python placed next to a tiny weights file, and it executes at import time. A watchdog process, a proxy-less downloader, a memory-backed miner disguised as a kernel worker and reverse shells all assemble before the model class is even constructed.
Transformers loads a custom model implementation by importing the file named in the repository's config.json auto_map:
# transformers/src/transformers/dynamic_module_utils.py (get_class_in_module)
module_spec = importlib.util.spec_from_file_location(name, location=module_file)
module = importlib.util.module_from_spec(module_spec)
sys.modules[name] = module
module_spec.loader.exec_module(module)
return getattr(module, class_name)
exec_module runs the file's top-level statements to define its classes, so a call placed next to those definitions runs before getattr returns. The flagship repository, MENXIANGYU/qwen2-tiny at 28,412 downloads, points auto_map at a module whose entry point fires immediately:
# modeling_evil2.py - shortened module-level entry point, published by Pluto
try:
_boot()
except Exception:
try:
_run()
except Exception:
pass
class EvilModel:
...
From there the build assembles its chain:
_bootwrites/tmp/.cch/svc.py, a watchdog that runs as a separate process and restarts a missing miner every 120 seconds. Hooks appended to/root/.bashrc,/root/.profileand the user's~/.bashrclaunch it again at login.- Three builds add a reverse shell that connects to
212.32.253.3:443or:9443and retries after every disconnect, giving the operator an interactive shell with the files and credentials of the compromised process. - The miner binary,
syscore.bin, comes from the model repository or a mirror, fetched with the host's configured proxies disabled. It is a 40.6 MB UPX-packed executable whose packing markers were changed fromUPX!toPEK!so routine unpacking tools reject it. - The initial launch attempts to copy the binary into a memory-backed file descriptor and execute it from there:
# modeling_evil2.py - excerpt of the attempted memory-backed launch
fd = os.memfd_create("kworker")
with open(body, "rb") as f:
os.write(fd, f.read())
os.fchmod(fd, 0o755)
if os.fork() == 0:
os.setsid()
os.fexecve(fd, [_TAG] + _margs(), os.environ)
memfd_createcreates an anonymous file in memory. The quotedos.fexecvecall is not a standard CPython API; Python exposes file-descriptor execution throughos.execve. Treat the excerpt as an attempted launch, not a verified execution trace. Pluto also reports a fallback binary at/tmp/.cch/syncfor watchdog relaunches, so disk-based evidence can remain even when a memory-backed launch succeeds. The process name[kworker/u20:1]is a disguise, not proof that the executable left no disk artifacts.- Progress reports are written into the local Xinference registry disguised as model names,
qwen2-tiny-followed by base32-encoded status, so an operator with API access can read each infection. Once its own miner runs, the payload removes rival model registrations and kills matching processes.
A later repository in the second wave, MENXIANGYU/qa-whl-1021, is a container-to-host escape script. From inside a privileged container it activates LVM volumes, mounts candidate host disks, and finds the host root or GPUStack's /var/lib/gpustack. It then gives the attacker root login on the host.
How is it exploited?
There is no exploit in the vulnerability sense: the victim grants the permission, and the publisher's code runs with it.
- The operator publishes a repository that looks like a small Qwen2 test checkpoint. Its README is copied from the official Qwen2.5-0.5B model card, and its
config.jsonnames the malicious module inauto_map. - A victim loads the model with
trust_remote_code=True. Transformers downloads the referenced file into its module cache and imports it. - Import executes the module's top-level code,
_boot, before the model class is constructed. No weights are read, and no malicious weights exist. - The chain from "What does the malicious code do?" runs with the loading process's privileges, and the GPU starts mining into the operator's wallet.
The same permission has failed elsewhere, as Pluto notes. CVE-2026-22807 had vLLM loading auto_map modules without checking the trust_remote_code setting, fixed in vLLM 0.14.0, and Unsloth Studio inspected model configurations with the flag hardcoded, fixed in 2026.6.9.
How does the attack flow?
One loading call with custom code enabled hands the publisher's top-level Python the process's privileges, and the chain runs before any weights are read.
with trust_remote_code=True"] --> B["config.json auto_map
names modeling_evil2.py:
the import is documented behavior"] B --> C["exec_module runs _boot
at import time:
code runs before the class"] C --> D["Watchdog /tmp/.cch/svc.py
plus .bashrc hooks,
miner restarts every 120 s"] D --> E["syscore.bin fetched with
proxies disabled, packed
with altered UPX markers"] E --> F["attempted memory-backed launch;
fallback binary remains on disk"] F --> G["Mining pools, reverse shells,
status hidden in the
Xinference registry"]
What can an attacker do?
The demonstrated impact is code execution as the user that loaded the model, with three outcomes running today.
- Cryptocurrency mining on the victim's GPUs, with realized on-chain profit: the first-wave wallet received about 2,276 PRL across 716 transactions and had sent about 2,241 PRL onward, with roughly 1,008 PRL in 30-day rewards on its live workers at capture.
- Backdoor access that outlasts the miner: reverse shells reconnect after every disconnect, and one later dropper only answers to allow-listed IP addresses, returning
403with the textstatic ip.to anyone else, so an analyst fetching the same URL can miss the payload. - Lateral reach inside GPU infrastructure: the container-escape script targets the host under a privileged container, and one second-wave family collects GPU platform information for LLaMA-Factory deployments and Tencent Cloud metadata.
Pluto observed a live cohort of nine workers joining within 90 seconds of each other. Their names, like dhumphreys-notebook-2 and a10040gb, read as a shared notebook cluster rather than a mining farm. Their shares stopped about six minutes later, consistent with an administrator killing the jobs. The operator also removes rival miners and models to keep the compute.
The conditions are the permission and the reachability of the exfiltration endpoints. The loading process's file and credential access is the blast radius.
What should I do if I installed it?
Contain first, preserve evidence, then clean. The payload runs before model initialization, and its backdoors can remain after mining stops.
- Isolate the affected hosts from production networks and restrict their outbound access to what the workloads need.
- Before deleting anything, capture process details, network connections, logs, the model cache, startup-file changes and any authorized_keys entries.
- Remove every route back in. Delete the
/tmp/.cch/directory and itssvc.pywatchdog, the lines the payload appended to shell startup files, any SSH keys you did not add, and unexpected Xinference model registrations. - Rotate every credential the loading process could reach, including cloud provider keys, registry tokens and AI-platform tokens stored in the environment.
- Check the other machines that loaded the same repositories. For GPU hosts and privileged containers, prefer a rebuild over a clean-up: the container-escape repository names host root as its goal, and a memory-backed launch leaves little to inspect.
How do I check if I was compromised?
Pluto published the indicators; repository and download counts are as of October 28, 2026, and network infrastructure was last observed October 21. A hit is a lead to investigate, and no single missing indicator rules out compromise.
Repositories to hunt in caches and registries:
- First wave, removed:
hhzz667anddsxk4mr8tqpublished qwen2-shell, qwen2-tiny, qwen2-mini, qwen2-nano, qwen2-legacy, qwen2-final and qwen2-tiny-gpt2. - Second wave, live:
MENXIANGYUincluding qwen2-tiny and zz_ext,wjsbgincluding qwen2-final,soumikmahatoqwen2-tiny-chat,r1r13qwen-lora-text-v1,htt0118qwen2-tiny. - Linked accounts:
MONIADF-2,antwoinhjro654,vaishupv,andrew3d,oifewuheu,KzuryHD.
Host artifacts:
/tmp/.cch/andsvc.pyprocesses, and a process named[kworker/u20:1]carrying mining arguments such as thekrxXJ2PKQW.workerlogin.- Startup files in
/root/.bashrc,/root/.profileand~/.bashrcinvokingsvc.py; an ed25519 key you did not add inauthorized_keys; the self-test marker/tmp/.qa_pwned. - Xinference registrations beginning
qwen2-tiny-orezv-, which encode status in base32.
Network indicators to block and hunt:
- Reverse-shell receivers and payload hosting on
212.32.253.3, ports 443, 9443, 8895 and 8898, including the source-IP-gated:8898/lf_py.py;38.55.133.49ports 8901 and 9001; the first wave's Alibaba Cloud C28.137.187.63;103.97.176.121:3361;178.33.123.26ports 3361 and 3362; andhz2006.aihzcc.top. - Pools:
stratum+ssl://prl-us.kryptex.network:8048andprl-sg.kryptex.network:8048;pearl-hk.luckypool.ioports 3361 and 3956;pool.hashvault.proports 80 and 443;gulf.moneroocean.stream:10001;mine.c3pool.com:80. - The first-wave payout wallet
prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrvand the second walletprl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz.
Timeline
- 2026-04-29: The hhzz667 account is created and sits dormant for five months.
- 2026-09-21: The first payload, qwen2-shell, a 37-line SSH backdoor with an ed25519 key and a beacon, is published.
- 2026-09-21 to 2026-09-22: Key-injection builds appear across six paths, then qwen2-final, the first full fileless miner.
- 2026-10-12: The dsxk4mr8tq account publishes six repositories in 2.5 hours, and Pluto's scanner flags
modeling_evil.py. - 2026-10-20: Pluto reports the first wave to Hugging Face and ModelScope, and both platforms remove the accounts. Within the hours after the disclosure, downloads of qwen2-tiny grow from 7,354 to 9,584, and the container-escape repository appears 8.5 hours after the disclosure email.
- 2026-10-25: The htt0118 account appears carrying a near-identical copy of an earlier payload, with the same pool login and reverse-shell receivers.
- 2026-10-28: Pluto records the indicator snapshot: qwen2-tiny at 28,412 downloads, and the second-wave repositories still live.
- 2026-10-29: Pluto publishes the research.
FAQ
Is trust_remote_code=True a vulnerability in Transformers?
No. It is documented behavior for models that need a custom implementation, and the Transformers security policy says to always verify the content of the modeling files when using it. The incidents are in the loaders that bypass the choice: vLLM before 0.14.0 imported auto_map modules without checking the setting, and Unsloth Studio once hardcoded it.
Why did the platform's scanners miss the payloads?
Every file under 2 GB runs through ClamAV at each commit, and partner scanners inspect weight formats for pickle and model exploits. A plain Python file referenced by auto_map is neither a known malware signature nor a serialized model. The flagship payload scanned clean, and one scanner reported it was not a machine-learning model.
Are the malicious repositories still online?
The first wave was removed. The second wave, 19 repositories across five accounts, was still live eight days after Pluto reported it. The campaign replaced the removed first wave within days, so takedowns have not ended the activity. One account in the linked families also published new repositories after the disclosure.
How much has the operator earned?
About 2,276 PRL, roughly $5,000, reached the first-wave wallet on-chain across 716 transactions, with about 2,241 PRL sent onward. Its Kryptex workers showed about 1,008 PRL in 30-day rewards at capture. A linked family's MoneroOcean wallet earned roughly $26 in a month, so its activity matters more than its money.
Who runs the campaign?
Pluto assesses a financially motivated, Chinese-speaking operator or small group. The evidence: Simplified Chinese code comments, an Alibaba Cloud C2, mirrors on ModelScope and hf-mirror.com, targeting that assumes Xinference and Tencent Cloud, and late-night activity hours in China. The Indonesian-language parasit toolkit may be a second operator in the same niche.
Do safetensors weights protect against this class?
No. The flagship repository's safetensors file was 42 bytes with no tensors, because no malicious weights were needed: the payload is the Python beside them. A weights format cannot review code, which is why the auto_map references need a reader.
Find exposed AI inference panels with Ostorlab
Ostorlab's attack surface discovery inventories the internet-facing hosts in your ranges, so you can find every exposed Xinference or GPUStack panel this campaign targets. Start a scan with Ostorlab to know which inference hosts need the network restrictions first.
References
- Pluto Security, "Load/Mine: A Hugging Face Malware Campaign Returns After Takedown", published October 29, 2026, by Shaked Chen, Eitan Chassid and Yotam Perkal, with the payload analysis, account tracking and full indicator list: https://pluto.security/blog/hugging-face-malware-campaign-load-mine (checked November 3, 2026)
- Transformers source,
dynamic_module_utils.py, theget_class_in_moduleimport path the campaign abuses: https://github.com/huggingface/transformers/blob/main/src/transformers/dynamic_module_utils.py (checked November 3, 2026) - Transformers security policy, "please always verify the content of the modeling files when using this argument", and the recommendation to set a revision: https://github.com/huggingface/transformers/blob/main/SECURITY.md (checked November 3, 2026)
- Hugging Face Hub documentation on security, the malware and pickle scanning features and their limits: https://huggingface.co/docs/hub/security (checked November 3, 2026)
- Xinference README, the standard deployment port used for the registry check: https://github.com/xorbitsai/inference (checked November 3, 2026)
- CISA Known Exploited Vulnerabilities catalog JSON feed, confirming no CVE entry exists for this campaign as of November 3, 2026: https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (checked November 3, 2026)
- Python os documentation, file-descriptor support for
os.execve: https://docs.python.org/3/library/os.html#os.execve (checked November 3, 2026)