25,46 dollari. È quanto è costata in media, in chiamate a un modello linguistico, ciascuna delle 101 scansioni completate da una campagna che fra luglio e settembre 2026 ha compromesso almeno 27 aziende di e-commerce. La più economica è costata 3,13 dollari, la più cara 79,31. Con quel budget l’operatore ha rubato più di 600.000 carte di pagamento non scadute a due sole vittime e ha installato skimmer sulla pagina di checkout di altri siti. Dove l’accesso è riuscito, di solito è bastato meno di un giorno, spesso poche ore.

I dati vengono dal rapporto pubblicato il 22 settembre da Gambit Security, che ha ricostruito l’operazione dai log degli stessi agenti. È il primo caso documentato che ho letto in cui la parte difficile di un attacco a un negozio online, cioè trovare la falla, risalire la catena e piazzare il codice, la fa quasi tutta un agente AI. L’umano sceglie i bersagli e dà ordini di una riga.

Lo tratto come un caso studio per chi ha un WooCommerce o gestisce il negozio di un cliente. Nella catena c’è un passaggio che riguarda WordPress in modo diretto, ma la lezione più utile è un’altra: quasi ogni anello corrisponde a un controllo che su WordPress si verifica in una giornata.

Cosa ha fatto la campagna, in numeri

Gambit descrive tre harness open source usati in parallelo: Strix, Cairn e Hermes. Sono i programmi che fanno girare un modello in un ciclo di azioni, con strumenti veri in mano: terminale, browser, scanner. I numeri principali del rapporto sono questi.

  • Scala: fra il 10 e il 15 settembre 105 progetti d’attacco aperti e almeno 27 aziende compromesse. La campagna era attiva da luglio e, alla data del rapporto, ancora in corso.
  • Danni: oltre 600.000 carte non scadute prese a due aziende, di cui 488.372 emesse negli Stati Uniti. Skimmer ordinati su 27 vittime e confermati su 19. Accessi parziali a un gruppo alberghiero della Fortune 500, a una grande compagnia aerea statunitense, a un distributore industriale e a un retailer di moda.
  • Costi: 7.005,71 dollari spesi su OpenRouter in quattro settimane, secondo il saldo al 25 agosto. Gambit stima un costo totale fra 12.000 e 18.000 dollari.
  • Ritmo: solo Strix, dal 23 al 31 agosto, ha fatto 146 esecuzioni in modalità deep contro 138 host. Sono 633 ore di scansione concentrate in 195 ore reali.

Strix girava su GLM 5.2 e poi su DeepSeek v4 Pro, Cairn su DeepSeek v4.1 Flash. Hermes aveva 121 skill, di cui 78 d’attacco, più una per togliere i filtri al modello. Secondo Gambit, l’operatore usava opus-4.6 perché i modelli più recenti avevano rifiutato le sue richieste. In 260 sessioni ha scritto 1.951 prompt, del tipo «vai a cercare wp2shell» o «guarda se entri nel backend». Il resto lo faceva l’agente.

Per una PMI conta un dato solo. Il costo in modelli per bersaglio, circa 25 dollari, è meno di un pranzo di lavoro. Non serve più che il tuo negozio valga la fatica di un criminale competente. Basta che compaia in una lista.

Come sceglievano i bersagli

La lista l’operatore la prendeva da un servizio di ranking del traffico web, categoria shopping. Poi faceva una cosa che mi ha sorpreso: scartava i negozi costruiti sulle grandi piattaforme ospitate o open source e teneva quelli con codice scritto su misura. In una sessione ha incollato 301 risultati con l’ordine «run these, use the proxy, high severity only». Alcuni bersagli li ha scelti a mano, perché aveva già una password di amministratore.

La logica è economica. Una piattaforma diffusa è controllata da migliaia di ricercatori. Un checkout scritto da un’agenzia nel 2019 e mai più toccato non l’ha mai guardato nessuno.

Qui sta il punto che riguarda chi lavora con WooCommerce. Gambit scrive che dalla lista venivano scartati i negozi su piattaforme ospitate o open source: un WooCommerce standard, con buona probabilità, non c’era. La mia lettura è che ogni personalizzazione senza manutentore avvicina il tuo negozio al profilo che l’agente cercava. Lo snippet nel functions.php che modifica il checkout, il plugin custom per il gestionale, il campo di upload aggiunto al profilo cliente, il pannello fatto in casa accanto a WordPress: sono la parte del sito che nessuno ha mai controllato.

La catena d’attacco, anello per anello

Gambit pubblica una catena completa contro una delle vittime. La riporto perché passa da un blog WordPress e perché ogni passaggio ha un controllo corrispondente. La colonna di destra è la mia traduzione per un sito WordPress con WooCommerce.

Anello documentato Controllo su WordPress e WooCommerce
SQL injection non autenticata sul campo email del login Nessun form di login custom senza query preparate. Usa $wpdb->prepare() e le API di WordPress, mai una stringa SQL concatenata.
Codice OTP letto in chiaro dal database, MFA aggirata 2FA con un plugin mantenuto che non salva i codici leggibili. Se il database esce, il secondo fattore deve restare utile.
Upload arbitrario in un campo immagine senza controllo dell’estensione Ogni campo di upload custom passa da wp_check_filetype_and_ext(). PHP non deve essere eseguibile dentro wp-content/uploads.
Esecuzione di codice sull’host, poi root con sudo NOPASSWD python3.12 L’utente del web server non ha sudo. Se il VPS lo gestisci tu, controlla sudo -l per quell’utente.
Mount NFS interno con no_root_squash, wp-config.php letto da lì Le credenziali del database di WordPress non stanno su uno share leggibile da altre macchine.
Utente amministratore WordPress creato direttamente nel database Controllo periodico degli amministratori e avviso quando ne compare uno nuovo.
Upload di un plugin, esecuzione di codice sull’host del blog DISALLOW_FILE_MODS in produzione, se gli aggiornamenti passano da un deploy.
Dump di AWS Secrets Manager (46 segreti) e chiave di cifratura di Magento Le chiavi API di pagamento e di servizio con i permessi minimi, ruotate dopo ogni incidente sospetto.

L’ultimo passo della catena riguarda Magento: l’agente ha estratto la chiave di cifratura e ha decifrato la colonna cc_number_enc, cifrata in Blowfish-ECB. Su WooCommerce il caso equivalente è raro, perché i gateway seri non salvano il numero di carta sul tuo database. Questo però non ti protegge dallo skimmer, che la carta la legge nel browser, prima che parta verso il gateway.

Una nota sul blog WordPress della vittima. Era un sito secondario e ha fatto da ponte: il suo wp-config.php stava su uno share NFS, e dal suo host l’agente è arrivato ai segreti AWS. È lo stesso schema che avevo descritto parlando di agenti AI fuori dalla sandbox: il perimetro vero di un sito è quello delle credenziali che condivide, non quello del suo dominio.

Dove si nascondeva lo skimmer

Questa è la parte che uso già come lista di controllo. Gambit elenca i metodi di iniezione osservati, e insieme al ricercatore Varys ha trovato più di 100 siti infetti oltre a quelli iniziali.

  • Codice aggiunto in coda a un file jQuery o Bootstrap, con la data di modifica originale ripristinata.
  • Un loader nella forma new Function(atob('...'.slice(7)))(), con sette caratteri di spazzatura davanti al base64 per confondere i filtri.
  • Un tag <script> in più nella pagina, oppure dentro il blocco di Google Analytics, spinto fuori dalla vista con circa cento tabulazioni.
  • Campi del database, come le descrizioni prodotto, e la cache lato server della pagina di checkout.
  • Persistenza fuori dall’applicazione: un bucket S3 dietro la CDN, un initContainer di Kubernetes, un cron job nella cartella dei log che ogni due minuti rimetteva lo skimmer dopo un redeploy.

Controllare solo i file del tema, quindi, non basta. Se pulisci il file e non trovi il cron che lo reinserisce, domani mattina lo skimmer è di nuovo lì.

Come cercarlo su un sito WordPress

Questi controlli richiedono WP-CLI e un accesso SSH al server. Sono tutti in sola lettura. Prima di lanciare qualsiasi comando su un sito di produzione, verifica di essere nella cartella giusta: ne ho parlato nella guida sugli script WP-CLI lanciati sul sito sbagliato.

1. File del core e dei plugin contro i checksum ufficiali

wp core verify-checksums --include-root
wp plugin verify-checksums --all
Confronto dei file installati con i checksum pubblicati da WordPress.org.

Il primo comando scarica i checksum MD5 della versione installata e li confronta con i file presenti. Per sicurezza lavora senza caricare WordPress, come spiega la documentazione di WP-CLI. L’opzione --include-root segnala anche i file estranei nella cartella principale. Il secondo fa lo stesso con i plugin, ma solo con quelli distribuiti da WordPress.org. I plugin premium, WooCommerce Subscriptions o un gateway comprato altrove, vanno confrontati con lo zip originale del fornitore. Il tema custom non ha nessun checksum di riferimento: per quello serve un repository git.

2. Il loader offuscato e i domini noti

grep -rnE "new Function\(atob|eval\(atob|\.slice\(7\)\)" wp-content --include='*.js' --include='*.php'
grep -rnE "static-js|netlfjs|x1opay|b8t\.shop|js-static|jsnetlify|netlifyjs|newssjs" wp-content
Ricerca dei pattern di iniezione descritti da Gambit e dei domini degli indicatori di compromissione.

La seconda riga cerca i domini che Gambit pubblica come indicatori di compromissione: static-js[.]com, cdn[.]netlfjs[.]com, x1opay[.]co, b8t[.]shop, js-static[.]com, jsnetlify[.]com, netlifyjs[.]com e newssjs[.]com. Aspettati qualche falso positivo, perché molti plugin legittimi usano atob. Ogni risultato va aperto e letto.

Sulla data di modifica ho un consiglio preciso. Lo skimmer rimette la data originale sul file jQuery, quindi cercare con -mtime non lo trova. Su Linux, però, la change time (ctime) si aggiorna anche quando qualcuno ripristina la data con touch. Il comando find wp-content wp-includes -name "*.js" -ctime -30 elenca i file JavaScript toccati negli ultimi 30 giorni, anche quelli che fingono di essere vecchi. Un file di jQuery in quella lista, senza un aggiornamento che lo spieghi, merita attenzione.

3. Il database

wp db search 'atob(' --all-tables-with-prefix
wp db search '<script' --all-tables-with-prefix
for d in static-js netlfjs x1opay b8t.shop js-static jsnetlify netlifyjs newssjs; do
  wp db search "$d" --all-tables-with-prefix
done
Script e domini noti in tutte le tabelle del sito, meta e opzioni comprese.

wp db search cerca la stringa in tutte le colonne di testo. Copre quindi anche la descrizione breve dei prodotti, i dati dei page builder salvati nei meta e le opzioni, dove finiscono gli script aggiunti dai plugin che inseriscono codice nell’header. Troverai righe legittime, come Google Tag Manager. Lo scopo è sapere quali script ci sono e chi li ha messi: uno script senza proprietario è già un problema, anche se oggi è innocuo.

4. Amministratori, cron e cache

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp cron event list --fields=hook,next_run_relative,recurrence
sudo crontab -l -u www-data; ls -la /etc/cron.d
Utenti con pieni poteri, eventi pianificati di WordPress e cron del sistema.

Un amministratore recente che nessuno ricorda è il segnale più chiaro. Nel caso documentato l’utente è stato creato direttamente nel database, quindi senza email di benvenuto né log di WordPress. Sui cron cerca hook che non riconosci e ricorrenze più fitte del normale. Alla fine svuota le cache, del plugin e della CDN, e ricontrolla il sorgente del checkout da un browser in incognito: in un caso lo skimmer viveva solo nella copia in cache.

Il checkout ospitato non chiude la questione

La difesa che sento proporre più spesso è «tanto il pagamento lo fa Stripe». È una buona scelta e riduce molto il rischio, perché il numero della carta viene scritto dentro un iframe del gateway e il tuo sito non lo vede mai. Ma non chiude la questione, per due motivi.

Il primo è tecnico. Uno script che gira sulla tua pagina non legge dentro l’iframe, però può coprirlo con un form finto identico, raccogliere la carta e poi mostrare un errore. Il cliente riprova, il secondo tentativo va a buon fine e nessuno si accorge di niente. Il gateway protegge il suo campo, non la pagina che lo contiene.

Il secondo è normativo. PCI DSS v4.0.1 ha tolto i requisiti 6.4.3 e 11.6.1, quelli sugli script della pagina di pagamento, dal questionario SAQ A usato da chi incorpora un form del gateway. Al loro posto ha aggiunto un criterio di idoneità: il merchant deve aver confermato che il suo sito non è vulnerabile ad attacchi via script. La FAQ 1588 del PCI SSC precisa che la conferma può arrivare in due modi: applicare tecniche come quelle dei requisiti 6.4.3 e 11.6.1, oppure ottenere dal gateway la conferma che la sua soluzione, installata secondo le istruzioni, protegge la pagina da quegli attacchi. Il blog del PCI SSC aggiunge che il cambiamento non riduce i requisiti di fondo. Se incorpori il form del gateway, gli script sulla tua pagina restano un problema tuo.

Per una PMI su WooCommerce il minimo sensato è questo.

  1. Pagina di checkout pulita. Niente chat, popup, script di heatmap o widget di recensioni sulla pagina di pagamento. Ogni script in meno è una porta in meno.
  2. Inventario degli script. Un elenco scritto di cosa carica il checkout e perché. È la sostanza del requisito 6.4.3, anche se non devi certificarti.
  3. Content Security Policy in modalità report. Parti da Content-Security-Policy-Report-Only sulla sola pagina di checkout, con un endpoint report-uri che riceve le violazioni. Raccogli per una settimana i domini che compaiono, poi passa in modalità bloccante.
  4. Subresource Integrity sulle librerie da CDN. L’attributo integrity fa rifiutare al browser un file diverso dall’hash atteso, come spiega MDN. Sui file del tuo stesso server serve a poco, perché chi modifica il file può modificare anche l’hash nella pagina: lì valgono i checksum.
  5. Meno amministratori, tutti con 2FA. L’account del vecchio sviluppatore e quello creato per un plugin vanno chiusi.

Un backup nello stesso database non è un backup

C’è un dettaglio del rapporto che mi ha fatto cambiare un’abitudine. Hermes aveva una skill chiamata «Database Wipe After Extraction»: dopo aver copiato i dati delle carte, svuotava i campi con i dati di pagamento nelle tabelle Magento sales_flat_order_payment e sales_flat_quote_payment. In un negozio di biciclette la pulizia è andata oltre e ha cancellato 180 tabelle il cui nome conteneva ZQ o Backup. Fra queste c’erano i backup che gli amministratori della vittima avevano fatto dentro lo stesso database.

È una pratica diffusa anche su WordPress. Prima di un aggiornamento si duplica la tabella degli ordini con un suffisso _backup, oppure un plugin salva le copie nella stessa istanza MySQL o nella cartella wp-content. Contro un errore umano funziona. Contro chi ha le credenziali del database no, perché quelle copie le vede e le cancella con la stessa query.

Un backup è tale se soddisfa tre condizioni:

  • sta su un sistema diverso, con credenziali diverse da quelle del sito;
  • il sito può scriverci ma non cancellare, per esempio un bucket con versioning o con blocco degli oggetti;
  • è stato ripristinato almeno una volta su un ambiente di prova, con una misura del tempo necessario.

Gambit chiude il rapporto con una domanda che trovo più utile di qualsiasi lista di plugin: qual è il tuo minimum viable business? Se domattina il sito fosse da ricostruire, cosa serve per ricominciare a vendere entro sera? Per molte PMI la risposta è corta: catalogo, prezzi, ordini aperti, clienti e un gateway configurato. Sapere dove stanno quelle cinque cose, e da quale copia si rimettono in piedi, è un lavoro di mezza giornata.

Gambit cita anche un dato di a16z: circa l’87% delle vulnerabilità sfruttate viene attaccato entro il giorno della divulgazione. Con quei tempi la patch arriva spesso dopo l’attacco. Il processo per applicarle, che ho descritto nell’articolo sulle patch dei plugin WordPress, resta indispensabile. Ma deve stare accanto a un ripristino verificato, non al suo posto.

Il piano di una giornata

Se gestisci uno o più WooCommerce, ecco come distribuirei il lavoro. I tempi valgono per un sito medio con accesso SSH.

  1. 30 minuti: checksum di core e plugin, più il grep dei pattern e dei domini. Annota ogni file che non torna.
  2. 30 minuti: le ricerche nel database con wp db search, l’elenco degli amministratori, i cron di WordPress e di sistema. Chiudi gli account che nessuno sa spiegare.
  3. 1 ora: inventario degli script sulla pagina di checkout. Togli quelli che non servono al pagamento e attiva la CSP in modalità report.
  4. 1 ora: rivedi il codice custom che tocca login, upload e checkout. Cerca le query SQL concatenate e i campi di upload senza controllo dell’estensione.
  5. 2 ore: sposta i backup su un sistema separato con blocco della cancellazione e fai un ripristino di prova. Scrivi quanto ci hai messo.

Se dal punto 1 o 2 esce qualcosa di sospetto, fermati. Non cancellare il file: copialo, annota data e percorso, cambia le password del database e degli amministratori, ruota le chiavi del gateway e avvisa chi gestisce i pagamenti. La pulizia viene dopo, perché uno skimmer rimosso senza aver trovato la persistenza torna entro pochi minuti.

Chiusura operativa

La campagna descritta da Gambit ha colpito aziende molto più grandi di una PMI ticinese. Ma il suo costo marginale per bersaglio, circa 25 dollari di modelli, rende conveniente attaccare chiunque abbia un checkout con codice su misura. Le difese che contano non sono nuove: file verificati, script inventariati, pochi amministratori con 2FA, backup fuori dalla portata del sito. La differenza è che adesso bisogna farle davvero, perché dall’altra parte non c’è più una persona stanca che a un certo punto smette.

Da fare questa settimana, in ordine:

  • lancia wp core verify-checksums e wp plugin verify-checksums --all su ogni negozio che gestisci;
  • cerca nei file con grep e nel database con wp db search il pattern atob( e i domini elencati sopra;
  • fai l’elenco degli script caricati dalla pagina di checkout e togli quelli che non servono al pagamento;
  • verifica che almeno un backup stia fuori dal database e fuori da wp-content, e ripristinalo una volta.

Fonti