Il contesto
Uno studio professionale con una ventina di persone tra soci, collaboratori e personale amministrativo. Il sito WordPress presentava lo studio, pubblicava articoli di approfondimento e raccoglieva richieste di contatto attraverso un modulo. Girava su un server virtuale non gestito, installato nel 2019 da un consulente esterno che nel frattempo aveva smesso di seguirlo: CentOS 7, Apache, un PHP più recente di quello della distribuzione preso da un repository di terze parti, MariaDB e nessun pannello di controllo. Per riservatezza, nome dello studio, area di attività e dettagli che permetterebbero di identificarlo sono stati omessi o modificati.
Il dettaglio decisivo di questa storia è la data. CentOS Linux 7 ha raggiunto la fine del ciclo di vita il 30 giugno 2024: come spiegato dal progetto CentOS nell’annuncio delle date di fine supporto, da quel giorno non vengono più pubblicati aggiornamenti, e i pacchetti restano solo in un archivio congelato. Nel nostro caso, però, il server era fermo da molto prima: le ultime patch installate risalivano alla fine del 2020, quando un conflitto tra i pacchetti di PHP del repository esterno e quelli di sistema aveva fatto fallire un aggiornamento e nessuno aveva più riprovato.
I sintomi
Il primo segnale non è arrivato dal sito, ma dal fornitore del server virtuale: una segnalazione di abuso che indicava tentativi di accesso SSH verso altri server partiti dall’indirizzo IP dello studio. Nei giorni successivi alcune email inviate dal sito, come le conferme del modulo di contatto, avevano cominciato a finire nello spam o a essere respinte, perché l’indirizzo era finito in una lista di blocco. Il sito era più lento del solito, ma all’apparenza normale: nessuna pagina modificata, nessun redirect.
Un collaboratore dello studio aveva provato a «aggiornare tutto», ma yum si era fermato con un errore sui mirror, spariti proprio perché la distribuzione non era più supportata. A quel punto lo studio ci ha contattato e ha scelto il servizio con intervento entro 12 ore, chiedendoci anche di capire se il problema riguardasse solo il sito o l’intero server.
Cosa abbiamo trovato a livello applicativo
L’analisi dei log di Apache, fatta come descritto nell’articolo sui log di accesso come prova del vettore d’attacco, ha portato rapidamente a un plugin per le gallerie fotografiche fermo a una versione di tre anni prima, con una vulnerabilità nota e pubblica che permetteva a chiunque, senza autenticazione, di caricare un file con estensione arbitraria. A metà luglio, da un indirizzo mai visto prima, una richiesta POST all’endpoint di caricamento del plugin era seguita pochi secondi dopo da una GET verso un file PHP nella cartella degli upload.
Quel file era una webshell: poche righe offuscate che eseguivano comandi ricevuti in un parametro. Accanto ce n’erano altre due, con nomi che imitavano miniature di immagini, e un file .htaccess che riabilitava l’esecuzione di PHP nella cartella. Il database era sostanzialmente pulito: nessun amministratore aggiunto e nessun codice nelle opzioni o negli articoli. Chi era entrato non era interessato al sito in sé: gli serviva un punto d’appoggio sul server. E con i permessi dell’utente apache, con cui girava il sito, si può fare poco. Il passo successivo era diventare root.
Cosa abbiamo trovato a livello di sistema
Quando un attaccante può essere diventato root, gli strumenti del sistema compromesso non sono più affidabili: un rootkit può far mentire ps, ls e ss. Per questo non abbiamo lavorato sul server acceso. Abbiamo chiesto al fornitore uno snapshot del disco, lo abbiamo montato in sola lettura su una macchina di analisi pulita e abbiamo confrontato ogni file con i pacchetti originali. Le indagini sui log e sui file sospetti sono state accelerate da modelli di intelligenza artificiale open-weight eseguiti in locale, senza che nessun dato dello studio uscisse dalla nostra infrastruttura.
# Immagine del disco montata in sola lettura su /mnt/caso
cat /mnt/caso/etc/centos-release
rpm --root /mnt/caso -q polkit sudo kernel
# Librerie caricate in ogni processo, se il file esiste
cat /mnt/caso/etc/ld.so.preload
# File dei pacchetti con contenuto diverso dall'originale (esclusi i file di configurazione)
rpm --root /mnt/caso -Va 2>/dev/null | grep -E '^..5' | grep -v ' c /'
# Persistenze: unità systemd, cron di sistema, chiavi di root
ls -la --time-style=full-iso /mnt/caso/etc/systemd/system/ /mnt/caso/etc/cron.d/
cat /mnt/caso/root/.ssh/authorized_keys
Il quadro che ne è uscito era completo. Il pacchetto polkit era alla versione precedente alla correzione di PwnKit (CVE-2021-4034), una vulnerabilità locale di pkexec che permette a qualunque utente del sistema di ottenere i privilegi di root; anche sudo era precedente alla correzione di Baron Samedit (CVE-2021-3156). Nel log /var/log/secure, a due minuti dalla prima esecuzione della webshell, c’era un messaggio di pkexec su una variabile d’ambiente dal «contenuto sospetto»: è una delle tracce che lo avviso originale dei ricercatori di Qualys indica come compatibili con lo sfruttamento di quella vulnerabilità. Nella cartella temporanea dell’utente apache restavano i file di lavoro di uno degli exploit pubblici.
Da root, l’attaccante aveva costruito una persistenza su più livelli:
- una libreria condivisa in
/usr/lib64, con un nome simile a quello di una libreria di sistema, elencata in/etc/ld.so.preload: veniva caricata in ogni programma e nascondeva i propri file, i processi e le connessioni dell’attaccante; - le versioni di
ps,topenetstatsostituite con copie modificate, cherpm -Vasegnalava con un’impronta diversa dall’originale; - un’unità systemd abilitata all’avvio, con un nome da servizio di sistema, che lanciava un eseguibile nascosto in
/var/lib; - un file in
/etc/cron.dche ogni dieci minuti riscaricava l’eseguibile se mancava; - una chiave SSH estranea aggiunta in fondo a
/root/.ssh/authorized_keys, in un server in cui l’accesso di root con chiave era ancora permesso.
L’eseguibile era un agente con due funzioni: un miner di criptovaluta limitato a una parte della CPU, per non dare nell’occhio, e un modulo che scansionava altri server alla ricerca di password SSH deboli, cioè esattamente l’attività segnalata dal fornitore. Le connessioni in uscita verso un server di comando sulla porta 443 erano invisibili dal server stesso, perché la libreria precaricata le filtrava, ma comparivano chiaramente nel traffico registrato dal fornitore. Chi ha letto il nostro articolo sui cryptominer nei server riconoscerà lo schema.
Come l’abbiamo rimosso
Qui c’è la decisione che distingue questo caso da una normale pulizia. Su un sistema in cui l’attaccante è stato root non esiste un modo affidabile per dire «ora è pulito»: può aver modificato il kernel, i pacchetti, i log, perfino il database dei pacchetti usato per le verifiche. E anche se avessimo rimosso tutto ciò che avevamo trovato, il sistema operativo sarebbe rimasto esposto alla stessa vulnerabilità e a quelle future, perché non riceve più correzioni. Rimettere in piedi il sito su quel server avrebbe significato ripulire una casa lasciando la porta senza serratura.
Abbiamo quindi separato nettamente due attività, e lo abbiamo messo per iscritto con lo studio prima di cominciare:
- la bonifica di WordPress, coperta dal servizio entro 12 ore: dall’immagine del disco abbiamo estratto solo i dati, cioè il dump del database, gli upload e il tema su misura. Il core è stato ricostruito dai pacchetti ufficiali, i plugin reinstallati dal repository ufficiale (tranne quello vulnerabile, sostituito), il tema confrontato riga per riga con la copia consegnata dallo sviluppatore. Dagli upload abbiamo eliminato ogni file eseguibile e controllato che le immagini fossero davvero immagini; nel database abbiamo verificato utenti, opzioni, trigger e contenuti;
- la migrazione su un nuovo server, concordata come attività separata e fuori dalla garanzia: un server virtuale nuovo con AlmaLinux 9, una distribuzione supportata, installato da zero, su cui il sito ripulito è stato pubblicato e verificato prima del cambio dei DNS.
Prima di spegnere il vecchio server abbiamo conservato le prove: l’immagine del disco con la relativa impronta, i log, la libreria malevola, l’eseguibile e l’elenco degli indirizzi contattati, che abbiamo passato al fornitore per chiudere la segnalazione di abuso. Il vecchio server è stato isolato dalla rete e poi distrutto dopo trenta giorni, d’accordo con lo studio.
Tutte le credenziali sono state cambiate, partendo dal presupposto che ogni segreto presente sul server fosse stato letto: password del database, chiavi e salt di WordPress, password degli utenti del sito, credenziali SMTP salvate nel plugin di posta, chiavi SSH, accessi al pannello del fornitore. Poiché il modulo di contatto raccoglieva dati personali di potenziali clienti, abbiamo fornito allo studio gli elementi tecnici per valutare, con il proprio consulente privacy, gli obblighi di notifica.
L’hardening
Il nuovo server è stato configurato per non ripetere nessuno degli errori del vecchio:
- PHP dai moduli ufficiali della distribuzione, senza repository esterni, e aggiornamenti di sicurezza installati automaticamente, con un avviso quando serve un riavvio;
- accesso SSH solo con chiave, root escluso, porta raggiungibile solo dagli indirizzi dello studio e del supporto;
- SELinux in modalità enforcing, che sul vecchio server era stato disattivato «perché dava problemi»;
- esecuzione di PHP bloccata negli upload, come spiegato nell’articolo sulle webshell negli upload, e permessi dei file separati tra chi li possiede e chi li esegue, secondo lo schema dell’articolo sull’hardening di wp-config.php;
- un firewall che limita anche il traffico in uscita: un sito vetrina non ha bisogno di aprire connessioni SSH verso Internet;
- monitoraggio dell’integrità dei file, dei processi e delle connessioni, con avvisi inviati fuori dal server.
Il risultato
WordPress ripulito era pronto entro le 12 ore previste dal servizio, già pubblicato sul nuovo server in un indirizzo di prova. La migrazione, con il cambio dei DNS e le verifiche su modulo di contatto, email e certificato, si è chiusa il giorno successivo. Il fornitore ha archiviato la segnalazione di abuso, l’indirizzo del nuovo server non compare in nessuna lista di blocco e nei trenta giorni successivi il monitoraggio non ha registrato modifiche non autorizzate né connessioni anomale.
Lo studio ha ricevuto un rapporto con la ricostruzione dell’attacco, l’elenco degli indicatori di compromissione, le evidenze conservate e le misure adottate, insieme a un breve piano di manutenzione: chi aggiorna cosa, con quale frequenza e chi controlla gli avvisi.
Cosa insegna questo caso
Un sistema operativo fuori supporto non è solo «vecchio»: è un sistema in cui ogni vulnerabilità locale scoperta da quel momento in poi resta aperta per sempre. Nel nostro caso le correzioni per PwnKit e Baron Samedit esistevano, ma non erano mai state installate; su CentOS 7, dopo il 30 giugno 2024, per la prossima vulnerabilità di quel tipo non ci sarà nemmeno una correzione da installare, se non passando al supporto esteso a pagamento di Red Hat o a un’altra distribuzione. E quando una vulnerabilità nel sito si combina con una nel sistema, un plugin trascurato diventa l’accesso di root all’intero server.
La seconda lezione riguarda la fiducia: un server su cui un attaccante è stato root non si pulisce, si ricostruisce. È più lavoro, ma è l’unico modo per poter dire con onestà che il sistema è sotto controllo.
Rimozionemalware.com è il servizio di bonifica dei siti WordPress di Managed Server S.r.l., che dal 2005 gestisce server e hosting: lavoriamo sia sull’applicazione sia sul sistema, con tempi garantiti di 6, 12 o 24 ore per ogni installazione di WordPress, calcolati dalla ricezione degli accessi. Quando un caso richiede attività che vanno oltre la bonifica, come una migrazione, lo diciamo prima e le concordiamo a parte. Se il tuo server è ancora su una distribuzione fuori supporto, o sospetti una compromissione, puoi scriverci.