La prima cosa che fanno quasi tutti, davanti a un server sospetto, è reinstallare tutto. È la reazione comprensibile e, nella maggior parte dei casi, è anche l'errore che fa perdere il controllo del problema.
Il motivo è semplice: se non sai come sono entrati, reinstallare non serve a niente. Ti rientrano dentro lo stesso giorno, attraverso la stessa porta, e stavolta non hai nemmeno i log per capirlo.
La regola che vale più di ogni strumento
Preserva la prova, poi intervieni. Nel momento in cui trovi qualcosa di sospetto, prima di cancellare fai tre cose: copia il file da parte, salva i log, e annota l'ora. Un file infetto che hai cancellato è un file che non potrai più analizzare, e spesso è l'unico pezzo che ti dice chi è entrato.
Solo dopo, e con la prova al sicuro, inizi a pulire.
I segnali che non devi ignorare
Prima di cercare file infetti, impara a riconoscere che è successo qualcosa. I sintomi più silenziosi, in ordine di probabilità:
Il consumo di CPU anomalo. Un miner installato gira a pieno regime e non ha nome riconoscibile. Su un server che dovrebbe stare fermo, un processo che macina la CPU è il primo segnale. Se non sai quale processo è normale per il tuo servizio, non hai una baseline, e questa è già una falla.
Connessioni in uscita sconosciute. Il traffico di uscita è il canale con cui l'attaccante porta fuori i tuoi dati o scarica il secondo stadio. Un server che non ha motivo di contattare il mondo esterno non dovrebbe avere connessioni in uscita permanenti.
File modificati negli ultimi giorni. I file che un attaccante tocca hanno una data di modifica recente, e i file che non dovrebbero esistere hanno un proprietario o una data sbagliata.
Il cron. È il meccanismo di persistenza più comune in assoluto, e anche il più ignorato. Un crontab -l con una riga che non riconosci è un allarme.
I comandi, in ordine di uso
1. Processi: cosa sta realmente girando
# I processi ordinati per consumo CPU, i primi 15
ps aux --sort=-%cpu | head -15
# Solo i processi con un percorso eseguibile, con il comando completo
ps -eo pid,ppid,user,%cpu,%mem,etimes,args --sort=-%cpu | head -20
# I processi lanciati dopo il boot, con il percorso assoluto del binario
ps -eo pid,lstart,args | grep -v "^\s*PID" | head -20
Le colonne che contano sono etimes (da quanti secondi il processo gira) e args (la riga di comando completa). Un processo con etimes basso che consuma molta CPU e ha un nome come [kdevtmpfsi] non è un processo di sistema: è un miner con il nome travestito.
Un dettaglio che vale un controllo a sé: confronta la ppid. Se un processo che dovrebbe essere lanciato da systemd ha come genitore un processo sconosciuto, la catena è sospetta.
2. Il filesystem: i file sospetti
# File modificati negli ultimi 10 giorni, esclusi cache e pseudo-filesystem
find /var/www /home /tmp -mtime -10 \
-type f \( -name "*.php" -o -name "*.js" -o -name "*.sh" \) \
-not -path "*/cache/*" 2>/dev/null | head -50
# Gli stessi file, con la data di modifica in chiaro
find /var/www -mtime -10 -type f -name "*.php" \
-printf '%TY-%Tm-%Td %TH:%TM %u %p\n' 2>/dev/null | sort -r | head -40
Il -printf è la parte che ti serve: %TY-%Tm-%Td la data, %u il proprietario. Il file con data di ieri e proprietario www-data in una directory che dovrebbe essere tua è la pista.
# File senza proprietario (uid non mappato): spesso lasciano questi
find / -nouser -o -nogroup 2>/dev/null | grep -vE "^/(proc|sys|dev|run)" | head -30
# File eseguibili in directory che non dovrebbero contenerne
find /tmp /var/tmp /dev/shm -type f -perm -u+x 2>/dev/null | head -30
# Sorgenti PHP con funzioni sospette: eval, base64_decode su input, gzinflate
grep -rlnE "eval\s*\(|base64_decode\s*\(\s*\\\$_(GET|POST|REQUEST)|assert\s*\(\s*\\\$|gzinflate\s*\(" \
/var/www --include="*.php" 2>/dev/null | head -20
L'ultimo è il coltellino svizzero. Quasi tutto il codice malevolo in PHP passa per una di queste funzioni, perché servono a decodificare ed eseguire qualcosa che staticamente sembra innocuo. Un base64_decode($_POST[...]) seguito da eval è un webshell, e non c'è un caso legittimo in un tema o un plugin.
3. Persistenza: dove si nasconde
# Cron di tutti gli utenti
for u in $(cut -d: -f1 /etc/passwd); do echo "== $u"; crontab -u "$u" -l 2>/dev/null; done
# Cron di sistema
cat /etc/crontab; ls -la /etc/cron.*/ 2>/dev/null
# Script di avvio e profili, eseguiti a ogni login
ls -la /etc/profile.d/ /etc/init.d/ /etc/rc.local 2>/dev/null
grep -rInE "curl|wget|nc |bash -i|sh -c" /etc/profile.d/ /etc/rc.local 2>/dev/null
# Chiavi SSH autorizzate: controlla sempre *chi* può entrare
cat ~/.ssh/authorized_keys 2>/dev/null
find / -name authorized_keys -not -path "*/proc/*" 2>/dev/null | head
Quel for u in ... serve perché un utente non privilegiato con un crontab proprio è il posto dove un attaccante tiene la persistenza quando non può scrivere in /etc.
E le authorized_keys vanno guardate ogni volta, anche se non sospetti niente: è il modo più comune per mantenere l'accesso, e una chiave in più è impossibile da notare a occhio.
4. Rete: da dove arriva e dove va
# Connessioni attive con il processo che le tiene
ss -tunap 2>/dev/null | head -30
# Chi ascolta, e su quali indirizzi
ss -tulnp 2>/dev/null
# Connessioni verso l'esterno, per un server che dovrebbe starsene tranquillo
ss -tnp state established 2>/dev/null | grep -vE "127\.0\.0\.1|::1" | head -20
# DNS: modifiche recenti a resolv.conf sono un classico di DNS hijacking
stat /etc/resolv.conf; cat /etc/resolv.conf
# Hostname sospetto: alcuni malware si impostano un nome riconoscibile
hostnamectl 2>/dev/null | head -5
Se il tuo server risolve i nomi verso un DNS che non riconosci, tutto il traffico in uscita passa da qualcun altro. È un attacco meno rumoroso di tutti, perché non tocca quasi nessun file.
5. La domanda giusta: come sono entrati
Questa è la parte che reinstallando non risolvi mai. I vettori, in ordine di frequenza reale:
# Web: file PHP scritti di recente nella document root
find /var/www -type f -name "*.php" -mtime -30 -printf '%TY-%Tm-%Td %p\n' 2>/dev/null | sort
# Log di accesso: gli status insoliti (200 su .php strani, 4xx da tentativi)
grep -E " (404|403|500) " /var/log/nginx/access.log 2>/dev/null | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# Login SSH falliti: da quanti indirizzi diversi
grep "Failed password" /var/log/auth.log 2>/dev/null | awk '{print $NF}' | sort | uniq -c | sort -rn | head
Quel uniq -c | sort -rn sugli URL richiesti con errore è la tecnica più informativa di tutte. Un attaccante automatizzato che cerca una vulnerabilità lascia un'impronta molto regolare: centinaia di richieste a URL che non esistono. Vedere quegli URL ti dice che cosa stava cercando, e quindi che cosa hai.
L'ordine delle operazioni
Dopo aver raccolto le informazioni, l'ordine conta. Se lo fai in ordine sbagliato, ti cancelli le prove e non impari niente.
Prima. Copia fuori i file sospetti e i log. Metti il server in rete sotto un firewall con solo le connessioni che ti servono davvero, senza toccare il filesystem. Se la compromissione è attiva, ogni minuto conta.
Poi. Ruota le credenziali, tutte: password degli utenti, chiavi SSH, token API, credenziali del database, e le chiavi in cloud. Tutte, non quelle che ti ricordi. Il presupposto è che qualcosa che avevi è stato letto, e non hai modo di sapere cosa.
Poi ancora. Ricostruisci da un'immagine pulita, non riparando. L'approccio "pulisco i file" funziona finché lo trovi; quando non funziona è perché c'è una persistenza che non hai cercato. La ricostruzione è più lenta di dieci minuti e ti toglie il dubbio.
Infine. Chiudi la porta da cui sono entrati. Patch, credenziali ruotate, o il bug del CMS. Senza questo, la ricostruzione è solo un noleggio: il prossimo attacco entra dalla stessa parte.
Il ruolo della difesa preventiva
Tutto questo è utile, ma il vero lavoro è aver reso difficile arrivarci. Le quattro cose che fanno la differenza, in ordine:
Gli aggiornamenti. Il primo vettore di ingresso resta un software con una patch disponibile. Un server con 200 giorni di ritardo sugli aggiornamenti è un server che ha già deciso quando farsi bucare.
L'accesso minimo. Nessuno dovrebbe poter scrivere nella document root tranne l'utente di deploy. Nessun servizio dovrebbe girare come root. Ogni permesso in eccesso è una possibilità in più per l'attaccante.
I log, e qualcuno che li guardi. Un server di produzione senza log consultati è un server che ha violato la privacy di tutti i suoi utenti senza che nessuno lo sappia. Il rilevamento automatico conta più della conservazione.
Un backup che hai provato. Un backup mai ripristinato è un'ipotesi, non un backup. Se non sai quanto tempo ci vuole a tornare in piedi, non hai un piano: hai un desiderio.
Il punto che vale la pena tenere a mente è questo: quando trovi un file infetto, stai guardando il risultato di qualcuno che è stato dentro il tuo server per settimane o per mesi. Il file che trovi non è il punto di ingresso, è una conseguenza. Il lavoro vero non è ripulire, ma capire perché quel file c'era, e chi lo ha messo lì.
Se il tuo server ha ospitato un WordPress, probabilmente la porta d'ingresso è stata una vulnerabilità di un plugin: in quel caso il percorso di ricerca è descritto in Trovare vulnerabilità in WordPress. E se quello che stai guardando è un pannello di amministrazione, le porte d'ingresso più comuni oltre a quella sono proprio le SQL injection, che spiegano anche perché un file finisce per trovarsi dove non dovrebbe.