Web shell WordPress in uploads: come trovarle e bloccarle
Come trovare le web shell WordPress in wp-content/uploads con comandi sicuri, bloccare l'esecuzione di PHP su Apache e nginx, rivedere i permessi e chiudere l'ingresso.
Anche in: English Deutsch Français Español Polski Português Türkçe
Se si sospetta una web shell WordPress in uploads, i lavori da svolgere sono tre, e distinti. Si individua il file e lo si conserva. Si impedisce che richieste dirette eseguano PHP in wp-content/uploads. Si chiude la via da cui il file è entrato. Ogni intervento risolve un problema diverso e nessuno di essi, da solo, equivale a un ripristino completo.
Questo articolo riporta i comandi e le impostazioni del server per ciascun intervento e spiega che cosa ciascuno non fa.
Che cos'è una web shell e perché wp-content/uploads è il posto preferito
Una web shell è un piccolo script, di solito in PHP, che offre a un aggressore un pannello di controllo remoto sul server. A seconda della configurazione, consente di eseguire comandi, leggere wp-config.php con la password del database, interrogare il database e caricare altri file.
Gli aggressori preferiscono wp-content/uploads perché WordPress ha normalmente bisogno di scrivervi. In quella cartella finiscono file multimediali, allegati dei moduli ed esportazioni dei plugin, e i suoi file sono serviti direttamente tramite URL. Se un file PHP finisce lì e il server lo esegue, chiunque su Internet può richiamarlo.
Come le web shell arrivano in wp-content/uploads
Le vie d'accesso più comuni sono tutt'altro che sofisticate:
- Un modulo di upload che accetta qualsiasi tipo di file, per esempio un plugin di moduli di contatto o di candidature che salva gli allegati in uploads.
- Un difetto di caricamento arbitrario di file in un plugin o in un tema.
- Un controllo degli accessi errato (broken access control), per cui una funzione pensata per gli amministratori può essere richiamata da chiunque. Il rapporto Patchstack State of WordPress Security in 2026, che è un rapporto di un fornitore, lo indica come la categoria più sfruttata.
- Un account rubato combinato con una debolezza. I controlli predefiniti di WordPress sui tipi di file di norma impediscono a un Editor di caricare un file
.php. Un account rubato diventa una via di upload quando quel ruolo può raggiungere anche un difetto di upload in un plugin, oppure quando la configurazione del sito concede la capabilityunfiltered_upload. Un Amministratore di un sito singolo di solito può installare plugin e usare gli editor di file della bacheca.DISALLOW_FILE_EDITdisattiva questi editor;DISALLOW_FILE_MODSblocca anche l'installazione e l'aggiornamento di plugin e temi dalla bacheca. In una multisite questi poteri spettano solo ai Super Admin.
Le shell raramente si chiamano shell.php. Conviene cercare doppie estensioni come invoice.pdf.php, le estensioni alternative .phtml e .phar, e nomi che imitano file di cache o miniature. Codice PHP nascosto in un file .jpg è un campanello d'allarme, ma non viene eseguito solo perché contiene PHP: lo è soltanto se il server passa quel file a PHP o se un altro script vulnerabile lo include.
I plugin concentrano il rischio. Patchstack attribuisce ai plugin il 91 % delle nuove vulnerabilità dell'ecosistema WordPress del 2025. Per le vulnerabilità sfruttate in modo massiccio, riporta una mediana ponderata di 5 ore prima dello sfruttamento di massa. La nostra guida ai plugin vulnerabili spiega come tenere questo ritmo.
Trovare una web shell WordPress: file PHP in uploads
Questi comandi non modificano alcun file. Vanno eseguiti via SSH dalla root di WordPress, cioè la cartella che contiene wp-config.php. Si parte dalle estensioni più ovvie:
find wp-content/uploads -type f \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) -ls
invoice.pdf.php viene individuato perché termina in .php. Per intercettare varianti come .php5 o image.php.jpg si può ampliare il criterio a -iname '*.ph*', mettendo in conto più falsi positivi.
Poi si elencano i file modificati di recente, oppure più recenti di un upload noto come pulito:
find wp-content/uploads -type f -mtime -7 -ls
find wp-content/uploads -type f -newer wp-content/uploads/2026/09/known-good.jpg -ls
Gli aggressori possono falsificare le date di modifica, che vanno quindi considerate solo indizi. Per trovare codice PHP in file con nome da immagine:
grep -rli --include='*.jpg' --include='*.jpeg' --include='*.png' --include='*.gif' --include='*.webp' --include='*.ico' '<?php' wp-content/uploads
Attenzione: grep legge il contenuto dei file, e la lettura può aggiornare la data di ultimo accesso. Se si sospetta già una compromissione, conviene chiedere prima uno snapshot al provider di hosting.
Non ogni risultato è dannoso. Alcuni plugin creano un segnaposto index.php che contiene solo un commento, e certi plugin di cache o di moduli scrivono PHP nella propria sottocartella. Prima di aprire un file sospetto, se ne salvano i metadati come descritto nella sezione successiva. Poi lo si ispeziona con less oppure xxd file | head -40. Mai aprirlo in un browser, perché potrebbe essere eseguito. Stringhe codificate molto lunghe, eval, base64_decode o assert combinati con variabili della richiesta sono segnali di allarme forti.
Va inoltre cercata nel log degli accessi ogni richiesta verso file PHP in uploads. Una POST verso un file di questo tipo raramente è legittima:
grep -E '"(GET|POST) /wp-content/uploads/[^ ]*\.(php|phtml|phar)' access.log
Ne avete trovata una? Conservare le prove prima di cancellare
Si registrano prima i metadati, poi si copiano il file e i log. Tutto va salvato in una directory privata, esterna a qualsiasi document root servita dal server. Su alcuni hosting condivisi la stessa home directory è raggiungibile dal web: conviene quindi verificare la document root nel pannello di controllo o nella configurazione del vhost.
umask 077
mkdir -p ~/ir-evidence && chmod 700 ~/ir-evidence
f="wp-content/uploads/path/to/suspicious.php"
case_id="webshell-$(date -u +%Y%m%dT%H%M%SZ)-$RANDOM"
stat "$f" > ~/ir-evidence/"$case_id"-stat.txt
sha256sum "$f" > ~/ir-evidence/"$case_id"-sha256.txt
tar -czpf ~/ir-evidence/"$case_id"-file.tar.gz "$f"
tar -czpf ~/ir-evidence/"$case_id"-logs.tar.gz /path/to/access.log* /path/to/error.log*
Il timestamp e il suffisso casuale riducono il rischio di riutilizzare il nome di un file di prove; prima di ripetere l'acquisizione, verificare che non esistano già file con lo stesso nome. Gli hosting ruotano i log, quindi vanno copiati subito. Poi si scaricano le prove in un luogo sicuro fuori dal server e si annotano il percorso e le righe di log corrispondenti.
Le credenziali del database in wp-config.php vanno considerate potenzialmente esposte. Occorre valutare se i dati personali siano stati consultati, divulgati, alterati, persi o resi indisponibili: una shell funzionante è un segnale grave, ma di per sé non dimostra una violazione dei dati personali ai sensi del GDPR. L'art. 33 impone di notificare la violazione all'autorità di controllo senza ingiustificato ritardo e, ove possibile, entro 72 ore dal momento in cui se ne è venuti a conoscenza, a meno che sia improbabile che la violazione presenti un rischio per i diritti e le libertà delle persone fisiche. L'art. 33, par. 5, impone di documentare qualsiasi violazione, anche quelle che non vengono notificate. La nostra guida alle 72 ore riporta i dettagli. Per la bonifica completa si veda il nostro piano di rimozione del malware.
Bloccare l'esecuzione di PHP nella cartella uploads (Apache e nginx)
I file multimediali legittimi non hanno mai bisogno di essere eseguiti come PHP. Su Apache 2.4 si crea wp-content/uploads/.htaccess:
<FilesMatch "\.(?i:php|phtml|phar)$">
Require all denied
</FilesMatch>
Apache rispetta questo file solo se AllowOverride lo consente per quella directory, il che per Require significa All o almeno AuthConfig. Se gli override sono disattivati, si chiede al provider di inserire il blocco in una sezione <Directory> limitata al percorso assoluto, nel filesystem, della directory uploads di questo sito. Non va inserito senza ambito nel virtual host, perché potrebbe bloccare PHP sull'intero sito.
Su nginx si aggiunge questo al blocco server:
location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ { deny all; }
nginx individua per prima la location con il prefisso corrispondente più lungo. Se tale location usa ^~, per esempio location ^~ /wp-content/uploads/, nginx non valuta mai le location con espressioni regolari e la regola viene ignorata: in tal caso la regola di divieto va inserita all'interno di quel blocco. In caso contrario le location regex vengono controllate in ordine e vince la prima che corrisponde, quindi questa regola deve precedere il blocco generale location ~ \.php$. Se WordPress si trova in una sottodirectory come /blog/, o se l'URL di uploads è personalizzato, il percorso va adattato. Si controlla la configurazione effettiva, poi si ricarica:
sudo nginx -T | grep -n 'location'
sudo nginx -t && sudo systemctl reload nginx
Su un hosting gestito, si chiede al supporto di applicare la regola.
Queste regole negano le richieste web dirette che corrispondono alle estensioni elencate, quando il server le applica. Non coprono tutti i suffissi eseguibili né impediscono a un altro script PHP di includere un file caricato. Non eliminano le shell e non impediscono i caricamenti. Conviene chiedere al provider quali suffissi siano associati a PHP ed estendere l'elenco se necessario.
Verificare la regola con un file di prova
Si usa un nome di file nuovo e casuale. Con noclobber attivo il comando si rifiuta di sovrascrivere un file esistente ed elimina solo un file creato da esso stesso:
t="wp-content/uploads/exec-test-$(date +%s)-$RANDOM.php"
if ( set -o noclobber; printf '%s\n' '<?php echo "marker-" . (40 + 2);' > "$t" ); then
curl -sS -i "https://example.com/$t"
rm -- "$t"
else
echo "Not created: $t already exists"
fi
Se il corpo della risposta contiene marker-42, PHP ha eseguito il file, perché quella stringa non compare nel file stesso. Se invece si vede la riga <?php in chiaro con stato 200, PHP non è stato eseguito, ma il server sta esponendo il codice sorgente: è un problema distinto, da segnalare al provider. Un solo stato 200 non prova nulla. Un 403 conta solo se è stato prodotto dalla regola e non da una CDN o da un WAF. Si prova direttamente sul server di origine con curl --resolve example.com:443:ORIGIN_IP e si cerca "access forbidden by rule" (nginx) oppure "client denied by server configuration" (Apache) nell'error log dell'origine.
Permessi e proprietario dei file in WordPress: rivedere, non reimpostare
Permessi e proprietario, insieme, determinano che cosa il processo PHP può modificare. Il proprietario da solo non basta, perché anche i bit del gruppo, quelli di "altri" e le ACL concedono la scrittura. Non va eseguito un chmod ricorsivo sull'intero sito: potrebbe rendere leggibili da tutti file riservati come i backup e togliere il bit di esecuzione agli script di manutenzione.
Prima di tutto si verifica chi possiede i file e con quale utente gira PHP:
stat -c '%U:%G %a %n' wp-config.php wp-content wp-content/uploads
ps -eo user,group,comm | grep -E 'php-fpm|php|httpd|apache2' | sort -u
In molti hosting condivisi PHP gira con l'utente del proprio account. In questa configurazione i normali bit di sola lettura offrono una protezione limitata: il proprietario del file può in genere modificarli per ripristinare la scrittura, salvo altre restrizioni del filesystem o dell'hosting.
Poi si elenca ciò che l'utente PHP può effettivamente scrivere, insieme a tutto ciò che è scrivibile da chiunque. Si sostituisce PHPUSER con l'utente individuato:
sudo -u PHPUSER find . -writable ! -type l -print
find . -perm -o+w ! -type l -ls
I due elenchi vanno esaminati separatamente: i percorsi scrivibili da chiunque vanno in genere corretti, mentre quelli scrivibili da PHP dipendono dal modello di aggiornamento. Un deployment in sola lettura dovrebbe normalmente limitare le scritture di PHP a uploads e alle cartelle di runtime necessarie; gli aggiornamenti dalla bacheca e quelli automatici richiedono permessi di scrittura aggiuntivi. I percorsi esaminati si correggono uno per uno con la notazione simbolica per rimuovere i bit, per esempio chmod g-w,o-w path/to/file. Rimuovere bit non rende mai meno restrittiva una modalità già più rigida. La guida all'hardening di WordPress considera 755 per le directory e 644 per i file come il limite superiore abituale. wp-config.php va impostato a 640 o 600, a seconda di come PHP lo legge, e il sito va poi testato. Mai usare 777.
C'è un limite inevitabile. PHP deve poter scrivere in uploads, quindi un plugin vulnerabile eseguito con quell'utente può comunque scrivervi.
define( 'DISALLOW_FILE_EDIT', true ); in wp-config.php elimina gli editor di file della bacheca. DISALLOW_FILE_MODS va oltre, ma disattiva anche gli aggiornamenti automatici in background. Va usato solo se si aggiorna in altro modo.
Chiudere la falla da cui è entrata la shell, poi continuare a controllare uploads
Eliminare la shell non rimuove la causa. Finché la via d'ingresso resta aperta, una nuova infezione è possibile.
Si confronta l'output di stat salvato con il log degli accessi, cercando il modulo o l'endpoint del plugin che ha ricevuto una POST poco prima della comparsa del file. Poi si aggiorna il plugin oppure lo si riconfigura: si limitano i tipi di file ammessi e, se il plugin lo supporta, si salvano gli allegati fuori dalla web root. Se non è disponibile alcun aggiornamento o mitigazione sicura, il plugin va eliminato. La disattivazione non è un'alternativa affidabile, perché i file restano sul disco e alcuni potrebbero essere ancora raggiungibili direttamente. Dopo di che si cambiano le password degli amministratori, i salt in wp-config.php e le credenziali del database.
Il controllo prosegue nel tempo:
- Eseguire il comando
findcombinato in un cron job giornaliero che invii per e-mail qualsiasi output. - Aggiungere un monitoraggio dell'integrità dei file.
- Eseguire regolarmente
wp-cli core verify-checksums.
Il plugin Security Audit & Compliance di secaudit.eu aiuta nel monitoraggio. Mantiene una baseline di integrità dei file, segnala le modifiche e i contenuti offuscati, e controlla firme di malware e indicatori di compromissione. Rileva e segnala; non rimuove nulla. Per altri segnali d'allarme si veda 12 segnali che il vostro sito WordPress è stato violato.
FAQ
Ogni file PHP in wp-content/uploads è dannoso?
No. I file segnaposto index.php e alcune cartelle di plugin sono legittimi. Si registrano i metadati, si ispeziona il file senza eseguirlo e si verifica a quale plugin appartiene la cartella.
Un plugin di sicurezza o un WAF rende superflua la regola su uploads?
No. Un firewall può non riconoscere un nuovo schema di attacco. La regola sul server è un livello distinto che nega le richieste dirette per le estensioni elencate anche quando un upload riesce a passare.
Bloccare PHP in uploads basta se ho già trovato una shell?
No. Occorre conservare le prove, bonificare il sito e chiudere la via d'ingresso. La shell potrebbe già essere stata usata per inserire file altrove o per leggere il database.
Questo articolo è un'informazione di carattere generale e non costituisce consulenza legale.
Controlla il tuo sito
Security Audit & Compliance è il plugin WordPress di secaudit.eu: cerca malware, spam nascosto, file core modificati e codice vulnerabile, e collega i risultati all’art. 32 del GDPR, all’art. 21 della NIS2 e alla OWASP Top 10.
Scopri cosa controlla il plugin