Pluto Research tracked a cryptomining and backdoor campaign distributed through model repositories on Hugging Face. The investigation traced related accounts and models, analyzed the malicious payload, and followed the campaign’s return within days of takedown.
The campaign and linked models collectively recorded more than 32,000 downloads. That figure measures downloads, not confirmed infections or unique victims.
This report examines how the malicious code executes, the evidence connecting the repositories, and the indicators defenders can use to investigate potential exposure.
The campaign and linked models recorded more than 32,000 downloads The campaign returned within days of takedown, showing that removing the identified repositories did not end the activity.
Model hubs are a malware channel. We tracked 31 malicious repositories across seven Hugging Face accounts. The 19 second-wave repositories have been downloaded more than 29,000 times, and they were still live eight days after we reported them. With linked models, downloads pass 32,000.
The code runs before the model does. trust_remote_code=True imports the repository’s Python with the caller’s privileges before any weights load. No malicious weights are needed.
Self-hosted AI infrastructure is the target. The payloads were written for GPU hosts running self-hosted inference, especially Xinference and GPUStack, along with rented GPU machines, shared research notebooks and CI jobs that pull small test models without anyone reading the code.
The scanners look at the wrong files. The Hub runs every file through antivirus, and partner scanners inspect weight formats, but nothing analyzes what the Python does. The flagship payload scanned clean, and one scanner reported it was “not a machine-learning model.”
Victims are mining into the operator’s wallets right now. The first-wave wallet has received 2,276 PRL on-chain, and its live workers include a batch of notebook-style GPU machines that joined within 90 seconds of each other.
The operator adapts to takedowns. After the first wave was removed, the replaced the hardcoded SSH key with reverse shells. One later stage only answers to allow-listed IP addresses. Backdoor access outlasts the miner.
AI model hubs are becoming malware-distribution channels. Attackers are exploiting the trust placed in open-weight models to compromise ML environments before a model even begins running, while existing scanners focus on model files and signatures and do not examine what the code around them actually does.
We found one such campaign on Hugging Face. It has been turning other people’s GPUs into miners, using models that look like small Qwen2 test checkpoints. Loading one with trust_remote_code=True runs the publisher’s Python on your machine. Across two waves and 31 repositories, that is all it took.
We uncovered the first wave by scanning model repositories for executable code, then traced it across accounts and platforms and reported it to Hugging Face and ModelScope. Both platforms took the accounts down. Within days a second wave appeared under four new accounts, with revised loaders and reverse shells in place of the hardcoded SSH key. One wallet from the first wave is still earning today.
Why Loading a Model Can Execute Code
Open models can run code on your machine through three different doors. Most of the security tooling points at one of them.
The parser. Some models ship as a single binary in a format like GGUF, and a C or C++ parser such as the one in llama.cpp has to read it before inference starts. Any parser of untrusted binary input is an attack surface, and so is the metadata it reads: CVE-2024-34359 showed a chat template stored in a GGUF file executing code through llama-cpp-python. It is not the door we found open.
The serialization formats. Weight files in pickle, Keras or SavedModel formats can embed executable logic, so loading them can run code. This is the best-known risk in ML security and has the most tooling. Hugging Face runs malware scanning and pickle import scanning on uploaded files, and integrates third-party scanners from Protect AI and JFrog . safetensors was built to remove the risk entirely.
The Python import ( trust_remote_code=True ). Some models need a Python implementation that Transformers does not include. Loading them requires permission to run code supplied by the repository, through trust_remote_code=True or an interactive confirmation. This is documented behavior, not a default. Serving frameworks have made the same mistake: CVE-2026-22807 showed vLLM (0.10.1 up to 0.14.0) loading auto_map modules without checking the trust_remote_code setting, so a malicious model repository could run code at server startup. It was fixed in 0.14.0. Fine-tuning tools have made it too: Pillar Security found that Unsloth Studio inspected a selected model’s configuration with trust_remote_code=True hardcoded, running repository code without the user ever enabling the flag. It was fixed in 2026.6.9. The Transformers security policy says exactly where the responsibility sits: “please always verify the content of the modeling files when using this argument.”
The repository’s config.json uses auto_map to name the Python module and class to load, for example modeling_custom.CustomModel . Transformers downloads the referenced file into its module cache and imports it. The critical step is in get_class_in_module() :
Python executes a module’s top-level statements to define its classes. exec_module() therefore runs any call placed to those definitions before getattr() returns the model class. If the publisher put malicious code there, one loading call runs it. No poisoned weights or pickle exploit is involved: the attacker uses the permission to run a custom implementation.
We built a scanner for the files that trust_remote_code actually downloads and runs: every .py file referenced by auto_map in a config.json , plus any Python file sitting beside model weights. A model definition should describe layers and a forward pass. If it imports subprocess , opens a socket or calls memfd_create , someone put that there on purpose.
While scanning recently uploaded public repositories, one hit stood out: dsxk4mr8tq/qwen2-tiny-gpt2 . A random-string account name, and a file called modeling_evil.py matching subprocess , socket , ctypes and memfd_create .
The file was 305 lines with error handling, fallback infrastructure and anti-detection built in. The account had six repositories, all created within 2.5 hours on October 12, 2026, with the same code under six model names.
The real break came from qwen2-tiny-lite . It held two backup files the attacker had left behind, modeling_evil.py.bak-src and modeling_evil.py.bak-sync : older versions of the same malware with different download URLs. Those URLs did not point at dsxk4mr8tq . They pointed at hhzz667 on Hugging Face and wssb123e on ModelScope.
The attacker had copied production code into a new test account, updated the URLs in the main file and forgot the backups. Their own version history connected the accounts.
The initial targeted scan uncovered the first wave. We then expanded our investigation, scanning a much larger set of models and examining more traffic. Those broader scans uncovered the second wave and linked findings, including similar operations using different miners and delivery methods.
Real Victims, Real Payouts
A payload that only mines would be a nuisance. This one is paying.
All six analyzed mining builds one Kryptex login, and the first-wave payout wallet was still active on public pool dashboards on October 21, long after its repositories were removed.
On-chain. The wallet had received 2,276 PRL across 716 transactions and sent 2,241 PRL onward. That is realized profit, not a pending balance.
Live workers. On Kryptex it showed two online workers reporting peakminer/2.16.5 , the same miner we later recovered from the second-wave binary, with 1,008 PRL (roughly $2,237) in 30-day rewards at capture time. On luckypool, a cohort of workers with default MAC-style names had been connecting since September 23 on the first wave’s own stratum port.
A cluster joins in real time. While we were analyzing the wallet, nine workers joined within 90 seconds of each other. Their names read like a shared notebook cluster: dhumphreys-notebook-2 , tsakaguchi-notebook-1 , albertzhou-notebook-1 , gonzano-notebook-1 , cprat-notebook-1 , plus a10040gb , a10040gb2 , a10040gb3 and mig (an A100 Multi-Instance GPU partition). That is the profile of a shared GPU cluster, not a mining farm. They stopped submitting shares six minutes later, which fits an administrator killing the jobs.
We also found a separate hl family : models such as MONIADF-2/qwen2-hl with similar import-time execution and watchdog techniques, which we track separately from the core campaign. Its own MoneroOcean wallet showed 13 workers, including hl-de8647d4 , whose name matches the family’s prefix. That wallet’s realized value is small (roughly $26 paid in a month), so for that line the activity matters more than the money.
Anatomy of the Payload
Take MENXIANGYU/qwen2-tiny , a representative mining build, listed at 28,412 downloads on October 28. Its configuration looks like any Qwen2 model, but model.safetensors contains no tensors and is just 42 bytes. The payload is in modeling_evil2.py , and auto_map points to modeling_evil2.EvilModel . Importing the module reaches _boot() before the class definition:
Phase one: the foothold goes in before the miner
The payload first writes /tmp/.cch/svc.py , a watchdog that runs as a separate process. Hooks in shell startup files can launch it again:
Using the model loader’s own interpreter, the watchdog restarts a missing miner every 120 seconds. Three builds ( qwen2-tiny-5 , -6 and -9 ) add a “step zero”: a separate process connects a shell to 212.32.253.3:443 or :9443 and retries after every disconnect. A successful connection gives the attacker an interactive shell with the files and credentials of the compromised process.
Phase two: a miner disguised as a kernel worker
comes syscore.bin . The payload checks the downloaded model snapshot, then /tmp/.cch/sync left by an earlier infection. If neither has the binary, it fetches it from the Hugging Face repository or a mirror. When a download is needed, the client disables the host’s configured proxies:
The empty dictionary ignores HTTP_PROXY and HTTPS_PROXY . If direct outbound traffic is allowed, the download skips that proxy’s inspection and logs. It cannot bypass a network that enforces proxy use.
For the first launch, the payload copies the binary into a memory-backed file descriptor and executes it as [kworker/u20:1] :
This is the step that keeps the miner out of sight. memfd_create creates an anonymous file that exists only in memory, and fexecve runs the binary straight from it. The first launch leaves no executable path on disk for a file-based antivirus or EDR scan to inspect, and the process list shows what looks like a kernel worker. A fallback copy does stay in /tmp/.cch/sync for watchdog relaunches, so the disk still holds evidence for anyone who looks for it. The disguise also has a tell: unlike a real kernel thread, the process carries userspace arguments, including the mining login.
For the person who loaded the model, nothing looks wrong. The model loads, there is no prompt and no error, and nothing new appears among the programs they recognize. Earlier builds also throttled the miner to stay under monitoring thresholds. The first sign is usually a GPU that is busier than it should be, or a cloud bill.
The 40.6 MB binary is packed with UPX, a tool that compresses an executable and bundles a small loader that unpacks it in memory when the program starts. Analysts normally run upx -d to restore the executable for inspection.
In this sample, two UPX! markers in the packing metadata were changed to PEK! . These signatures are how the unpacking tool recognizes its own format, so upx -d rejects the file as not packed by UPX:
The embedded loader still unpacks and runs the miner, because the changed markers do not break its execution path. The modification blocks routine unpacking without changing or encrypting the mining code.
We reverse-engineered the payload, restored both markers and unpacked it to reveal peakminer , the same mining client reported by the live workers on the first-wave wallet shown earlier. The loader supplies its pool endpoints and the krxXJ2PKQW.worker login at launch; the operator’s account is not embedded in the miner itself.
Phase three: reporting success and keeping the compute
Progress reports go into the local Xinference registry disguised as model names: qwen2-tiny- followed by base32-encoded status. An operator with access to the exposed API can retrieve and decode them to check each infection.
Once its own miner is running, the payload removes rival model registrations, strips their startup commands and kills matching processes, freeing compute for its own miner. Earlier builds scanned /proc/*/cmdline for 13 known rival miner names.
A Campaign That Adapts
The payload described above developed through several revisions. The first wave shows how the operator assembled the mining chain. The second shows which capabilities persisted and how access and delivery changed after takedown.
Wave one: from SSH backdoor to fileless miner (September 21 to October 12)
The account hhzz667 (“hhzz svg”) was created on April 29, 2026, and sat dormant for five months. Then it moved fast.
Phase 1, September 21. qwen2-shell , 37 lines. A pure SSH backdoor: an ed25519 key planted in authorized_keys , and a beacon to an Alibaba Cloud server in China. The README was copied word for word from the official Qwen2.5-0.5B model card.
Phase 2, September 21-22. qwen2-tiny , -mini , -nano , -legacy , 102 lines. Key injection in six paths, including the xinference user’s , plus .bashrc injection. The code also dropped a self-test marker, /tmp/.qa_pwned , containing PWNED: uid= : the attacker was QA-testing their own payload.
Phase 3, September 22. qwen2-final , 305 lines. The first full fileless miner, with a kernel-thread disguise, a competing-miner killer, Xinference-based status reporting and dual pools. This build used clean names: Qwen2Config , modeling_qwen2.py . The EvilConfig naming only existed in the dev account.
Phase 4, October 12. dsxk4mr8tq , the throwaway account our scanner caught. Same code, updated URLs.
throughout were in Simplified Chinese: “daemon disguise name,” “blend into Xinference directory,” “kill rival processes.” Wave one totals 12 repositories, more than 700 downloads recorded before removal and two miner binaries.
Wave two: revised miners and broader access
The final first-wave build already combined memory-backed execution, process disguise, competing-miner removal, and Xinference reporting. The second-wave mining builds retained that foundation, and three of them added reconnecting reverse shells, extending the campaign’s earlier use of planted SSH keys for remote access. The two waves also money: the decoded loader in wjsbg/qwen2-final pays out to the same first-wave wallet that victims are still mining into.
The takedown, and what came after
We reported the first wave to Hugging Face and ModelScope, and the accounts were removed. The second wave followed within days: 19 repositories across five accounts, with more aggressive and more varied payloads.
MENXIANGYU (13 repositories): the toolkit lab. qwen2-tiny carries the production miner. Others are payload iterations, import-time canaries, a diagnostic collector, a gated dropper, the oversized zz_ext kit with recon, miners and a reverse shell, and a container-escape script.
wjsbg : an execution canary, a payload that reclaims hosts from rival miners, and qwen2-final , a miner paying the first-wave wallet.
soumikmahato : a “parasit” toolkit with timestamp forgery, a process renamed python3 and a Cloudflare tunnel beacon, behind four legitimate-looking cover repositories.
r1r13 : GPU platform discovery for LLaMA-Factory deployments and Tencent Cloud metadata collection.
htt0118 : a new account created on October 25, carrying a near-identical copy of the qwen2-tiny-6 payload, with the same pool login and reverse-shell receivers.
A header in the wave-two payload references “report §9.1” and states that “ammunition must be self-hosted.” Repository-hosted binaries and mirrors continued from wave one. What changed was access: the planted SSH key, one grep away from detection, gave way to reverse shells. The operator was adapting to the takedown.
Publishing also continued after our disclosure. Three new campaign repositories appeared between October 20 and 25, one of them on the new htt0118 account. Linked families added five more in the same window.
One of them marks an escalation. MENXIANGYU/qa-whl-1021 , published 8.5 hours after our disclosure email, is a container-to-host escape script. From inside a privileged container it activates LVM volumes, mounts candidate host disks, finds the host root or GPUStack’s /var/lib/gpustack , and gives the attacker root login on the host. That names GPUStack as a second target platform beside Xinference, and moves the operator from the process that loaded the model to the machine underneath it.
One later dropper restricts its second stage at :8898/lf_py.py by source IP. Other requests get a 403 and the text static ip. . An analyst fetching the same URL can miss the payload served to an allowed host.
Where the Scanners Stop
None of the 12 first-wave repositories was flagged. The second-wave flagship scanned clean while it collected 28,412 downloads.
Hugging Face does scan repository files, and says so in its documentation. Its malware scanning page states that every file smaller than 2 GB is run through the ClamAV antivirus scanner at each commit. ClamAV matches known malware signatures. The pickle scanning page adds that this “doesn’t cover pickle exploits,” which is why the Hub also extracts and displays the imports inside pickle files. Partner scanners Protect AI Guardian (pickle, Keras and other model exploits) and JFrog (malicious behavior in model files) cover the weight formats. The same pickle page is candid the limits: the scanning is “not 100% foolproof,” and checking whether something is safe is the user’s responsibility.
So a Python file in a repository is scanned, but by a signature matcher, not by anything that reads what the code does.
The campaign sits in that gap. A plain Python file referenced by auto_map is neither a known malware signature nor a serialized model. In our checks, modeling_evil2.py inside qwen2-tiny returned safe from every scanner that ran: antivirus clean, and JFrog reporting it was not a machine-learning model. The same held for the wave-one files.
Hugging Face’s guidance for this surface is explicit and puts the work on the user: verify the code before you enable the flag. That works only if people actually read the code. In practice model cards and tutorials copy trust_remote_code=True , and the flag meant as an informed opt-in becomes a checkbox.
Reviewing the .py files an auto_map points to, before the flag is set, would have caught every payload in this campaign. A static check at upload time for subprocess , socket , ctypes or raw-IP network calls in files referenced by auto_map would have caught them earlier still.
Our assessment is a financially motivated, Chinese-speaking operator or small group. Wave-one code carries Simplified Chinese , the C2 runs on Alibaba Cloud, the miner binary is mirrored on ModelScope and hf-mirror.com , the targeting assumes Xinference and Tencent Cloud, and activity clusters between 18:38 and 21:19 UTC, which is late night in China Standard Time.
The money points the same way: Pearl and Monero wallets, real pool payouts and commodity tooling, with throwaway accounts, a leaked token and development files named evil . Language and hosting place the operator in the Chinese-speaking ML ecosystem. The wallets and tradecraft show the goal is profit.
One line of the wave-two family is different. The soumikmahato “parasit” build carries Indonesian-language and a different build system. Either a second operator in the same niche, or one operator running a deliberately different-looking line for redundancy, fits the evidence.
If you loaded one of the listed models with custom code enabled, check the machine for compromise even if loading failed or GPU usage looks normal. The payload runs before model initialization, and its backdoors can remain after mining stops.
Check the host against the indicators below. Look for /tmp/.cch/ , unexpected svc.py processes, and a process named [kworker/u20:1] carrying mining arguments. Inspect Xinference registrations for encoded qwen2-tiny- or ezv- names, and review connections to the listed servers and pools. Use the additional paths and hashes for the other builds. No single missing indicator rules out compromise.
Contain affected machines and preserve evidence. Isolate them from production networks and restrict outbound access. Before deleting anything, capture process details, connections, logs, model cache files, startup changes and registry entries. Check other machines that loaded the same repositories.
Remove every route back in. Check shell startup files, cron jobs, SSH authorized_keys and model registrations for unauthorized changes, then remove the malicious loaders, miners, watchdogs and backdoors. The payload also deletes competing miners and their startup entries, so review deletions as well as additions. Killing the miner or deleting the model does not remove persistence. Rebuild from a trusted image if you cannot establish the host’s integrity.
Replace exposed credentials. From a clean machine, revoke or rotate tokens, cloud credentials, passwords and SSH keys the compromised process could reach, and review their recent use. Removing an attacker’s key from authorized_keys does not invalidate credentials they may have copied.
Secure the replacement environment. Require authentication on the Xinference API and limit its reachability. Review custom model code and pin the reviewed revision before loading it. Run untrusted code with limited privileges, restricted network access and no production secrets. The analyzed payload returned clean scanner results, so a clean scan alone is not a reason to trust it.
Nothing in this campaign required skill or money. The operator needed a few free accounts, plain Python files in repositories named after a popular model family, and one flag that users are used to setting. Scanners built for other threats returned clean results. When the first wave was removed, the second was up within days, with the payload adjusted to what the takedown had burned. One operator ran 31 repositories across seven accounts, and one wallet was still collecting payouts after the repositories behind it were gone.
Cryptomining is the mildest thing this entry point can deliver. The same mechanism can run anything the loading process is allowed to do, and in this research we found other, unrelated publishers using it for credential theft, reverse shells and persistent backdoors. The machines it reaches make that worse: GPU servers, shared research clusters and automated pipelines tend to hold credentials, data and network access worth far more than the compute. And the access these payloads plant does not expire when the miner stops.
We think malicious models remain an underestimated threat. They are cheap to publish, easy to disguise, and sit in a part of the supply chain where most teams still assume that a familiar model name means safe code. We expect to see more campaigns like this one, and we are tracking them closely.
Until detection catches up, treat model code like any other software dependency: review it, pin it and isolate its runtime. Those controls hold when the repository name appears, and they let teams keep using open models safely.
Indicators of Compromise
Repository status and download counts as of October 28, 2026. Network infrastructure last observed October 21. Labels follow our investigation: proven means shared infrastructure or payload identity, linked means shared technique without proven shared ownership.
Repositories by account
Miner binary download locations
Host artifacts and hashes
Quick checks on a suspect host
This analysis is part of Pluto Security’s ongoing research into the security of the AI ecosystem. Pluto provides workspace security platforms engineered for the AI era, giving enterprise security teams continuous visibility and governance over models, personal assistants, extensions, tools, and autonomous workloads. To learn how to secure open models and AI tooling in your environment, us at pluto.security .
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
