
The trick at the center of this campaign is not an exploit. It is a feature. Pluto Security’s analysis of a cryptominer operation on the Hugging Face Hub shows that the attacker never needed to break anything. The model-loading library runs the Python that infects a machine exactly as designed.
Three doors, one left open
Loading an open model can run code through three different doors. One is the parser that reads a binary format such as GGUF. That is the surface behind CVE-2024-34359, where a chat template stored in a GGUF file executed code through llama-cpp-python.
Another is the serialization format: pickle, Keras and SavedModel weights can embed executable logic. That is the best-known risk in ML security and the one with the most tooling. The third is the Python import path, trust_remote_code=True, and that is the door this campaign uses.
Serving frameworks have tripped on the same door. CVE-2026-22807 showed vLLM (0.10.1 up to 0.14.0) loading auto_map modules without checking the trust_remote_code setting. A malicious model repository could run code at server startup. Version 0.14.0 fixed it.
The execution chain
A developer calls from_pretrained() on what looks like a small Qwen2 model with trust_remote_code=True. The repository’s config.json uses auto_map to name a module and class, for example modeling_evil2.EvilModel. Transformers downloads the referenced file into its module cache and imports it.
In get_class_in_module(), exec_module() executes the file’s top-level code before getattr() returns the requested class. Any call placed next to the class definition runs first. In the representative build MENXIANGYU/qwen2-tiny, importing the module reaches a function called _boot() before the class definition.
The model.safetensors beside it is just 42 bytes and contains no tensors. The attack involves no poisoned weights or pickle exploit.
Why the scanners stay quiet
Hugging Face does scan repository files. Its documentation says every file smaller than 2 GB is run through the ClamAV antivirus scanner at each commit. ClamAV matches known malware signatures. Partner scanners cover the weight formats. The pickle documentation says the scanning is “not 100% foolproof” and that checking whether something is safe is the user’s responsibility.
A plain Python file named in auto_map is neither a known signature nor a serialized model. As a result, nothing reads what it does. In Pluto Security’s checks, modeling_evil2.py in qwen2-tiny came back safe from every scanner that ran. Antivirus was clean, with JFrog reporting it was not a machine-learning model.
The same held for the wave-one files. The safetensors format, built to remove the execution risk, appears in this campaign only as a 42-byte stand-in.
Phase one: the foothold lands first
The payload writes /tmp/.cch/svc.py, a watchdog that runs as a separate process. Hooks appended to /root/.bashrc, /root/.profile and ~/.bashrc relaunch it. Using the model loader’s own interpreter, it 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 on port 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
Next comes the miner binary, syscore.bin. The payload checks the downloaded model snapshot, then /tmp/.cch/sync left by an earlier infection. Otherwise, it fetches the binary from the Hugging Face repository or a mirror.
When it needs a download, the client disables the host’s configured proxies. If the network allows direct outbound traffic, 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 with memfd_create. It then runs the binary through fexecve as [kworker/u20:1]. That first launch leaves no executable path on disk for a file-based antivirus or EDR scan to inspect. The process list shows what looks like a kernel worker.
The disguise has a tell: unlike a real kernel thread, the process carries userspace arguments, including the mining login. A fallback copy stays in /tmp/.cch/sync for watchdog relaunches.
UPX packs the 40.6 MB binary. Two UPX! markers in the packing metadata, at offsets 0x000000ec and 0x026be650, were changed to PEK!, so upx -d rejects the file as not UPX-packed. The embedded loader still unpacks and runs the miner.
Pluto Security restored both markers and unpacked it to reveal peakminer. That is the same client reported by the live workers on the first-wave wallet. The loader supplies the pool endpoints and the krxXJ2PKQW.worker login at launch. The operator’s account is not embedded in the miner itself.
Phase three: reporting in, and clearing the field
Progress reports go into the local Xinference model registry disguised as model names: qwen2-tiny- followed by base32-encoded status. An operator with access to the exposed API can decode them.
The receipts run from B (startup reached) and P (binary in place) to OK and OK2 (running through the memory-backed or on-disk fallback path), DLN and NM (acquisition or launch failed), OK3 (watchdog restored mining), and H, a heartbeat with GPU-utilization data every fifth watchdog cycle.
Once its own miner is running, the payload removes rival model registrations. It strips their startup commands and kills matching processes. Earlier builds scanned /proc/*/cmdline for 13 known rival miner names.
What this means for engineers
Mining theft is the visible cost. The durable cost is access. SSH keys, shell-profile hooks, a watchdog and reverse shells outlast the miner. Pluto Security notes that killing the miner or deleting the model does not remove persistence.
Its guidance is to review the .py files an auto_map points to before setting the flag, and pin the reviewed revision. It also advises running untrusted code with limited privileges, restricted network access and no production secrets, and requiring authentication on the Xinference API. A clean scan alone is not a reason to trust a repository.