File esposti per errore: .git, backup .zip, wp-config.php.bak e debug.log

Cartelle .git, archivi del sito, copie di wp-config.php e debug.log serviti a chiunque: cosa rivelano, come trovarli, bloccarli e cosa fare se erano esposti.

Una cartella di file aperta sul web con alcuni file che escono allo scoperto: .git, backup.zip, wp-config.php.bak e debug.log, ciascuno etichettato con codice HTTP 200, mentre una lente d’ingrandimento di uno scanner automatico passa sopra l’elenco

Non tutti gli incidenti cominciano con un exploit. Alcuni cominciano con un file che nessuno ricordava di aver lasciato lì: una copia di wp-config.php salvata prima di una modifica, l’archivio zip del sito creato durante un trasloco, la cartella .git di un deploy, il debug.log attivato per un problema risolto mesi prima. Sono file che il web server serve a chiunque li chieda, e qualcuno li chiede di continuo.

Un file dimenticato nella docroot non ha bisogno di una vulnerabilità per essere letto. Basta conoscerne il nome, e i nomi sono sempre gli stessi.

In questo articolo vediamo quali file finiscono esposti per errore, cosa rivelano, come trovarli sul proprio server e da fuori, come bloccarli nella configurazione di NGINX e Apache e cosa fare se si scopre che erano accessibili: rimuoverli non basta, perché quello che contenevano può essere già stato copiato.

Come li cercano gli attaccanti

Nessuno deve indovinare. Gli scanner automatici percorrono internet con elenchi di percorsi noti, e chiunque abbia letto i log di accesso di un sito pubblico li ha visti: richieste a /.env, /.git/config, /wp-config.php.bak, /backup.zip, /phpinfo.php, una dopo l’altra, dallo stesso indirizzo, a pochi millisecondi di distanza. Quasi tutte ricevono un 404. Quella che conta è l’unica che riceve un 200.

Gli elenchi si costruiscono osservando le abitudini: gli editor che salvano copie con la tilde o l’estensione .swp, i client FTP che rinominano in .old, i pannelli di hosting e i plugin di backup che creano archivi con nomi prevedibili, i nomi che chiunque sceglierebbe per un dump del database, come db.sql o nomesito.sql. Per questo la domanda giusta non è se qualcuno troverà il file, ma quando.

Estratto di un log di accesso NGINX con dieci righe provenienti dallo stesso indirizzo IP nell’arco di due secondi: richieste GET a /.env, /.git/config, /wp-config.php.bak, /wp-config.php~, /backup.zip, /esempio.sql, /wp-content/debug.log, /phpinfo.php, /adminer.php e /.git/HEAD. Nove righe hanno codice 404 e dimensione piccola, in grigio; la riga di /wp-config.php.bak ha codice 200 e 3.214 byte, evidenziata in rosso. Le annotazioni a destra spiegano come leggere la riga: stesso IP, intervallo di pochi millisecondi, codice di stato e dimensione della risposta, che distingue una pagina d’errore da un file vero.
Una scansione tipica nei log: decine di 404 e un solo 200. La dimensione della risposta dice se è stata servita una pagina d’errore o il file vero.

La cartella .git: tutta la storia del sito

Deployare con git pull direttamente nella docroot è comodo e diffuso, e lascia la cartella .git dentro il sito. Se il web server non la blocca, chiunque può scaricarla. Non serve nemmeno che l’elenco dei file sia abilitato: strumenti pubblici come git-dumper partono da .git/HEAD, ricavano i riferimenti e scaricano uno a uno gli oggetti del repository, fino a ricostruirlo per intero.

Il danno non sta tanto nel codice del tema, che spesso è pubblico comunque, quanto nella storia. Una password del database in un wp-config.php committato per errore anni fa e poi rimosso, una chiave API in un file di configurazione, le credenziali SMTP di un vecchio plugin: tutto ciò che è entrato almeno una volta nel repository resta negli oggetti, e il comando git log -p lo mostra in pochi secondi. La correzione strutturale è tenere il repository fuori dalla docroot, come descriviamo nell’articolo sul monitoraggio dell’integrità dei file, oppure pubblicare con git archive o rsync --exclude=.git.

Se la cartella è stata esposta, conviene guardare la storia con gli occhi dell’attaccante. Il comando git log -p -S 'DB_PASSWORD' --all, eseguito nel repository, elenca ogni commit che ha aggiunto o tolto quella stringa; lo stesso vale per AUTH_KEY, SMTP, api_key o token. Ogni segreto trovato va considerato pubblico, anche se è stato rimosso dieci commit fa: riscrivere la storia serve a non ripetere l’errore, non a rimediare a quello già fatto. Lo stesso discorso vale per le cartelle .svn e .hg dei progetti più vecchi.

# Repository fuori dalla docroot, file di lavoro nella docroot
git --git-dir=/srv/repo/esempio.git --work-tree=/var/www/esempio.it/htdocs checkout -f main

# Oppure: pubblicare solo i file tracciati, senza .git
git archive main | tar -x -C /var/www/esempio.it/htdocs

Backup, dump e copie di wp-config.php

Il secondo gruppo sono le copie. Un archivio .zip o .tar.gz del sito lasciato nella docroot dopo un trasloco contiene tutto: codice, configurazione, spesso anche un dump del database. Un file .sql contiene gli utenti con gli hash delle password, gli indirizzi email dei clienti, gli ordini. Se il sito è un e-commerce o raccoglie dati tramite moduli, un dump scaricato da un estraneo è a tutti gli effetti una violazione di dati personali, con gli obblighi che descriviamo nell’articolo su sito hackerato e data breach.

Le copie di wp-config.php sono il caso più subdolo. Il file originale è al sicuro perché il web server lo passa a PHP, che lo esegue e non restituisce nulla. Ma wp-config.php.bak, wp-config.php.old, wp-config.php~ o wp-config.php.save non finiscono in .php: il server li tratta come file di testo e li consegna così come sono, con nome del database, utente, password e chiavi di sicurezza. Le copie nascono da editor da terminale, da modifiche fatte «per sicurezza» prima di toccare il file, da client FTP che non sovrascrivono. Vale lo stesso per qualunque file PHP con un suffisso aggiunto, e per le copie di backup che alcuni plugin salvano in cartelle pubbliche dentro wp-content. C’è poi il caso dei plugin di backup che salvano gli archivi in una cartella di wp-content protetta solo da un .htaccess: su Apache funziona, ma se il sito viene migrato su NGINX quel file viene ignorato e gli archivi diventano scaricabili da un giorno all’altro, senza che nessuno abbia toccato nulla. Sui backup in generale, e su dove tenerli, rimandiamo all’articolo sui backup immutabili.

debug.log, phpinfo, .env e adminer

Il terzo gruppo sono gli strumenti di diagnosi dimenticati. Con WP_DEBUG_LOG impostato a true, WordPress scrive gli errori in wp-content/debug.log, come indica la documentazione ufficiale sul debug, cioè in una cartella servita dal web server. Il file rivela percorsi assoluti, nome dell’utente di sistema, plugin installati e relative versioni, a volte frammenti di query con dati dei clienti. Per un attaccante è una mappa del sito, e può crescere fino a diversi gigabyte. Sui server con cPanel c’è un parente stretto: i file error_log che PHP crea nella cartella dello script che ha generato l’errore, sparsi per tutto il sito e anch’essi serviti come testo. E con WP_DEBUG_DISPLAY attivo, gli stessi messaggi finiscono direttamente nelle pagine, visibili a ogni visitatore.

Un phpinfo.php o info.php creato per una verifica mostra la versione esatta di PHP, i moduli, le funzioni disabilitate, i percorsi e le variabili d’ambiente, che su alcune configurazioni includono credenziali. Un file .env, frequente nei progetti basati su Bedrock o in applicazioni affiancate al sito, è per definizione un elenco di segreti. E adminer.php, lo strumento di gestione del database in un solo file, lasciato nella docroot dopo un intervento, offre un modulo di accesso a MySQL a chiunque: con una password debole, o con la password letta da una delle copie di cui sopra, l’attaccante ha il database in mano.

Tabella con sette tipi di file esposti e cosa rivelano, con un indicatore di gravità. La cartella .git rivela codice e storia completa, comprese credenziali rimosse in passato: gravità alta. Archivi zip e tar.gz del sito rivelano tutto il codice e spesso il database: alta. Dump .sql: utenti, hash delle password, dati dei clienti: alta, possibile data breach. Copie di wp-config.php con estensione .bak, .old o tilde: credenziali del database e chiavi: alta. File .env: segreti e chiavi API: alta. adminer.php: accesso diretto al database: alta. debug.log e phpinfo.php: percorsi, versioni, plugin, a volte dati: media, una mappa per l’attacco successivo.
Cosa rivela ciascun file: quasi tutti consegnano credenziali o dati; debug.log e phpinfo forniscono la mappa per l’attacco successivo.

Trovarli sul proprio server

La ricerca dall’interno è la più completa, perché vede anche i file con nomi che nessuno scanner proverebbe. Tre comandi coprono la gran parte dei casi: cartelle di versionamento e file di ambiente, archivi e copie per estensione, strumenti di diagnosi per contenuto. Conviene stampare anche data e dimensione, perché un archivio da due gigabyte dell’anno scorso si riconosce subito.

cd /var/www/esempio.it/htdocs

# Repository e file di ambiente
find . \( -name .git -o -name .svn -o -name .hg -o -name '.env*' \) -printf '%TY-%Tm-%Td  %p\n'

# Archivi, dump, copie di backup e log
find . -type f \( -iname '*.zip' -o -iname '*.tar' -o -iname '*.tar.gz' -o -iname '*.tgz' \
  -o -iname '*.rar' -o -iname '*.7z' -o -iname '*.sql' -o -iname '*.sql.gz' \
  -o -iname '*.bak' -o -iname '*.old' -o -iname '*.orig' -o -iname '*.save' \
  -o -iname '*.swp' -o -name '*~' -o -iname '*.log' -o -name error_log \) \
  -printf '%TY-%Tm-%Td  %10s  %p\n' | sort

# phpinfo e adminer, anche con nomi diversi
grep -rlE --include='*.php' 'phpinfo[[:space:]]*\(' . | grep -v '^./wp-\(admin\|includes\)/'
grep -rli --include='*.php' 'adminer' . | head

Nell’output ci saranno anche file legittimi, per esempio archivi zip caricati come allegati negli upload o log di plugin protetti da un .htaccess. Ogni riga va comunque verificata: un file legittimo può essere esposto lo stesso, se la protezione su cui conta non funziona sul server in uso, come accade ai .htaccess sotto NGINX.

Una ricerca fatta una volta sola protegge fino alla prossima modifica urgente. Gli stessi comandi, messi in uno script eseguito ogni settimana da cron e configurato per inviare un’email solo quando l’output non è vuoto, trasformano il controllo in un’abitudine che non dipende dalla memoria di nessuno. Chi ha già un controllo di integrità dei file può aggiungere questi schemi all’elenco dei file nuovi da segnalare.

Verificarli da fuori, come farebbe uno scanner

Il controllo dall’esterno risponde a una domanda diversa: non «il file c’è?», ma «il web server lo consegna?». Un ciclo con curl sugli stessi percorsi che provano gli scanner, stampando codice di stato e dimensione, basta a scoprire la maggior parte dei problemi. La dimensione è importante perché molti siti rispondono 200 anche alle pagine inesistenti, mostrando la pagina 404 del tema: un 200 con la stessa dimensione della home è un falso allarme, un 200 da 3 KB su wp-config.php.bak no.

SITO=https://www.esempio.it
for p in .git/HEAD .git/config .env wp-config.php.bak wp-config.php.old wp-config.php~ \
         wp-config.php.save wp-content/debug.log backup.zip sito.zip database.sql \
         phpinfo.php info.php adminer.php; do
  printf '%s  %s\n' "$(curl -s -o /dev/null -w '%{http_code} %{size_download}' "$SITO/$p")" "$p"
done

# Una .git esposta risponde con il riferimento al ramo corrente
curl -s "$SITO/.git/HEAD"
ref: refs/heads/main

Il ciclo va ripetuto su ogni nome a dominio servito dallo stesso server, sottodomini compresi. Le sorprese peggiori arrivano da lì: un staging. creato per un restyling e mai spento, una cartella /vecchio/ o /old/ con una copia completa del sito precedente, mai aggiornata. Non sono solo file esposti: sono installazioni WordPress intere, con plugin fermi a versioni vulnerabili, che condividono il server e spesso il database con il sito principale.

Bloccarli nel web server

Cancellare i file è necessario, ma non impedisce che ne compaiano di nuovi alla prossima modifica fatta di fretta. Una regola nel web server rende innocui quelli futuri. Su NGINX va messa nel blocco server prima della location che passa i file PHP a FPM, perché le location con espressione regolare vengono valutate nell’ordine in cui compaiono e vince la prima che corrisponde:

# NGINX: file nascosti (tranne .well-known), copie, archivi, dump e log
location ~ /\.(?!well-known/) { deny all; }
location ~* \.(bak|old|orig|save|swp|sql|sql\.gz|log|zip|tar|tgz|tar\.gz|rar|7z)$ { deny all; }
location ~ ~$ { deny all; }
location ~ /error_log$ { deny all; }
# Apache 2.4, nella configurazione del virtual host o in .htaccess
RedirectMatch 404 /\.(git|svn|hg|env)
<FilesMatch "(\.(bak|old|orig|save|swp|sql|log|zip|tar|tgz|gz|rar|7z)|~)$">
    Require all denied
</FilesMatch>
<Files "error_log">
    Require all denied
</Files>

Se il sito offre legittimamente file zip da scaricare, l’estensione va tolta dall’elenco o la regola limitata ai percorsi fuori dagli upload. phpinfo.php e adminer.php non si possono bloccare per estensione, perché sono PHP validi: vanno cancellati. Per il log di debug la soluzione giusta è non scriverlo nella docroot. WP_DEBUG_LOG accetta anche un percorso di file, e in produzione gli errori non devono mai comparire nelle pagine:

// wp-config.php: log fuori dalla docroot, errori mai mostrati ai visitatori
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/var/log/php/esempio.it-debug.log' );
define( 'WP_DEBUG_DISPLAY', false );

La cartella del log deve essere scrivibile dall’utente PHP del sito e, finita la diagnosi, WP_DEBUG va rimesso a false. Tutte queste regole trovano posto accanto alle altre della checklist di hardening.

Se erano esposti: rimuovere non basta

Quando la ricerca trova un file esposto, la prima tentazione è cancellarlo e passare oltre. Prima conviene rispondere a una domanda: qualcuno l’ha scaricato? I log di accesso lo dicono, se sono conservati abbastanza a lungo. Si cercano le richieste a quei percorsi con codice 200 e una dimensione compatibile con il file:

# Richieste servite con successo ai percorsi sensibili: IP, data, percorso, byte
zgrep -hE '"GET /(\.git/|\.env|wp-config\.php[^" ]+|wp-content/debug\.log|[^" ]*\.(sql|zip|tar\.gz))[^"]*" 200 ' \
  /var/log/nginx/access.log* | awk '{print $1, $4, $7, $10}'

Se il file conteneva segreti, va dato per compromesso tutto ciò che conteneva, anche se i log non mostrano download: i log possono essere stati ruotati, o la richiesta può essere passata da un percorso diverso. Per una copia di wp-config.php significa cambiare la password dell’utente MySQL e rigenerare le chiavi e i salt, per esempio con wp config shuffle-salts, come spieghiamo nell’articolo su wp-config.php e i salt. Per un .env o una .git significa revocare ogni chiave API e ogni token presenti nella storia, non solo nell’ultima versione. Per un dump del database significa valutare la notifica della violazione e, se il dump contiene gli hash delle password, forzarne il cambio agli utenti.

Infine, un file esposto è spesso il punto d’ingresso di un’infezione successiva: con la password del database un attaccante può creare un amministratore fantasma o inserire codice nelle opzioni, senza toccare un solo file. Se dai log risulta che il file è stato scaricato, il passo successivo è controllare utenti, opzioni e file del sito come dopo qualsiasi compromissione, a partire dalle prime due ore di risposta.

Sequenza in cinque passaggi dopo la scoperta di un file esposto. Uno: conservare una copia fuori dalla docroot e annotare data e dimensione. Due: cercare nei log di accesso le richieste con codice 200 e dimensione compatibile. Tre: rimuovere il file e aggiungere la regola di blocco nel web server. Quattro: ruotare i segreti, cioè password MySQL, chiavi e salt con wp config shuffle-salts, chiavi API e token presenti anche nella storia git. Cinque: verificare da fuori con curl e, se il file è stato scaricato, controllare utenti, opzioni e file come dopo una compromissione. Un riquadro laterale ricorda che un dump del database scaricato è una possibile violazione di dati personali.
Rimuovere il file è il terzo passo, non il primo: prima si capisce se è stato scaricato, dopo si ruotano i segreti che conteneva.

Se hai trovato file di questo tipo sul tuo sito e non sai se qualcuno li ha già usati, o se il sito mostra già segni di compromissione, scrivici: analizziamo log, file, database e sistema, cron e processi compresi, e ripuliamo ogni installazione WordPress con una pulizia garantita in 6, 12 o 24 ore, lasciando le regole di blocco al loro posto.

IL PROSSIMO PASSO

Il tuo sito
è infetto?

Ogni ora conta: il malware continua a colpire visitatori e clienti, e Google, l’hosting e le blacklist non aspettano. Scegli il piano, paga online e condividi gli accessi: la bonifica parte subito, a prezzo fisso e con tempi garantiti.

Richiedi la bonifica

Ti porta ai piani: bonifica entro 6, 12 o 24 ore dalla ricezione degli accessi.

MANAGED SERVER S.R.L.info@rimozionemalware.com +39 02 5656 9681

Lunedì – venerdì · 09:30 – 19:30

Preferisci un modulo online?
Contattaci su managedserver.it ↗