cve

PHP2Shell CVE-2026-109618/109619: PoC, Attacco, Fix & Difesa

PHP2Shell CVE-2026-109618/109619: PoC, Attacco, Fix & Difesa

PHP2Shell: vulnerabilità RCE CVE-2026-109618 e CVE-2026-109619. Analisi dell’attacco, PoC, versioni PHP corrette, verifica dell’esposizione e mitigazioni.

  • Pubblicato il 2026-10-26
  • Tempo di lettura: 11 min

PHP2Shell: CVE-2026-109618 e CVE-2026-109619, le due RCE nel core di PHP #

Il 20 ottobre 2026 il team PHP ha annunciato una release di sicurezza fuori ciclo per il giorno dopo; il 21 ottobre, alle 14:00 UTC, sono uscite le versioni corrette. Secondo ServiceNow, già il 20 ottobre dettagli sulle falle circolavano online prima della disclosure. Le due falle, soprannominate PHP2Shell, sono CVE-2026-109618 e CVE-2026-109619: entrambe CVSS v4.0 9.2 (Critical), entrambe raggiungibili da remoto, senza autenticazione.

Non sono bug di un framework o di un plugin. Sono nel motore che esegue il codice, quindi toccano WordPress, Laravel, Drupal, applicazioni custom e qualsiasi altra cosa giri su PHP.

In questo articolo vediamo cosa succede dentro PHP, perché una gestione degli errori diventa esecuzione di codice, come capire se sei esposto, come rilevare tentativi e come mitigare. È un’analisi per studio e difesa: qui non trovi un exploit funzionante, e c’è un motivo che spieghiamo più sotto.

PHP2Shell in breve #

PHP2Shell è il nome associato a due vulnerabilità RCE non autenticate nel core di PHP: CVE-2026-109618 e CVE-2026-109619. Le versioni corrette sono PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35. La correzione richiede di aggiornare il runtime e riavviare i processi che lo usano. Le mitigazioni di configurazione riducono soltanto specifici percorsi di attacco.

DomandaRisposta
Cos’è PHP2Shell?Due vulnerabilità RCE nel runtime PHP
Quali versioni correggono il problema?8.5.12, 8.4.27, 8.3.36, 8.2.35
WordPress è coinvolto?Sì, se gira su un runtime PHP vulnerabile
Basta disattivare gli errori PHP?No, non corregge la causa
Un WAF basta?No, non sostituisce l’aggiornamento
Come verifico l’esposizione?Controlla il runtime usato dal web server, non solo php -v

Aggiornamento del 26 ottobre 2026: CVE-2026-115560 #

Il 23 ottobre PHP ha pubblicato un avviso informativo su un’ulteriore vulnerabilità, CVE-2026-115560, distinta dalle due di questo articolo e non coperta dalle release del 21 ottobre.

Secondo il team PHP può verificarsi solo con una combinazione insolita di configurazione e codice: l’applicazione imposta session.serialize_handler su php_binary o php_serialize, poi lo cambia a runtime in php con ini_set() prima di session_start(), con l’upload progress delle sessioni ancora attivo.

La mitigazione indicata è session.upload_progress.enabled=0 fino alla prossima release regolare, con cui sarà pubblicato l’advisory completo. In pratica: anche dopo aver aggiornato alle versioni del 21 ottobre, tieni l’upload progress spento.

Cosa sono CVE-2026-109618 e CVE-2026-109619 #

Sono due bug distinti, con due identificativi separati, ma nati dalla stessa causa radice. Entrambi sono classificati come use-after-free (CWE-416) legati a stato corrotto o non reimpostato (CWE-824).

CVE-2026-109618CVE-2026-109619
Nome breveRCE via OOM corruptionRCE via stale global state
TipoUse-after-free / memoria non inizializzataUse-after-free / stato globale obsoleto
Quando si manifestaNella stessa richiestaIn una richiesta successiva sullo stesso worker
AutenticazioneNon richiestaNon richiesta
CVSS v4.09.29.2

Le versioni corrette sono 8.2.35, 8.3.36, 8.4.27 e 8.5.12. Le branch 8.1 e precedenti sono end of life e non ricevono aggiornamenti regolari dal progetto PHP. Il team ha però pubblicato backport minimali in sorgente per PHP 7.4, 8.0 e 8.1, e alcuni distributori e vendor LTS forniscono backport propri (dettagli nella sezione sulla mitigazione).

La causa radice: longjmp senza cleanup #

Per capire il bug bisogna capire come PHP reagisce a un errore fatale, compreso l’esaurimento della memoria (OOM, out of memory).

Storicamente PHP, quando incontra un errore fatale, non risale lo stack funzione per funzione. Chiama zend_bailout(), che internamente usa un longjmp: un salto diretto a un punto di recupero, saltando tutto ciò che sta in mezzo.

Il problema è quello che viene saltato. Se una funzione aveva appena modificato dello stato e contava di arrivare alla propria fase di cleanup per sistemarlo, quel cleanup non viene mai eseguito. Lo stato resta a metà: puntatori che puntano a memoria già liberata, strutture solo parzialmente inizializzate.

Poi PHP prosegue con una normale sequenza di pulizia, che dà per scontato che ogni struttura sia coerente. Non lo è. Ed è qui che nasce il use-after-free.

PHP2Shell: longjmp senza cleanup in PHP, stato inconsistente e vulnerabilità use-after-free.

Un esempio di vita quotidiana: immagina di aggiornare un registro cartaceo. Scrivi metà della riga, poi qualcuno ti strappa il foglio di mano (il longjmp). Il registro resta con una riga incompleta, e chi lo legge dopo si fida che sia valida.

CVE-2026-109618: la corruzione nella stessa richiesta #

Qui il danno si consuma durante la richiesta che ha causato l’errore. Un OOM può arrivare mentre PHP sta ancora costruendo i dati della richiesta, per esempio durante il parsing di un corpo multipart/form-data, incluso il gestore dell’upload progress delle sessioni (session.upload_progress).

Il risultato è che PHP si ferma con la memoria di richiesta solo parzialmente impostata, ma poi esegue il cleanup come se fosse valida. Da lì, un attaccante che controlla le dimensioni e il contenuto della richiesta può puntare a trasformare la corruzione in esecuzione di codice.

Un dettaglio importante dal lato difensivo: il fatto che la corruzione avvenga prima che giri qualsiasi codice dell’applicazione significa che nessun controllo applicativo, nessun framework e nessuna validazione dell’input può fermarla. Lo conferma anche l’analisi di un laboratorio indipendente: sulla build vulnerabile la richiesta di test muore con un fatal di memoria esaurita “in Unknown on line 0”, cioè prima del codice utente.

VulnCheck ha riprodotto le falle in modo indipendente e la sua analisi preliminare indica sfruttamento riuscito su ARM64 e su x86 senza ASLR, il che dice che la mitigazione ASLR conta, ma non va considerata una difesa sufficiente.

CVE-2026-109619: lo stato globale che sopravvive #

Il secondo bug è più sottile, e più interessante da capire.

Un worker PHP (pensa a un processo di PHP-FPM) non nasce e muore a ogni richiesta: ne serve migliaia. Per questo mantiene dello stato globale che attraversa le richieste: stato di errore, sessioni, oggetti, estensioni, serialize/unserialize.

Se una richiesta finisce male, per esempio con un fatale, parte di questo stato può restare “agganciato” a memoria di richiesta ormai liberata. A quel punto:

  • il puntatore globale esiste ancora
  • la memoria a cui punta non esiste più
  • la richiesta successiva sullo stesso worker lo legge prima che PHP lo reinizializzi

Questo spiega una cosa che sorprende: spegnere display_errors, spegnere log_errors, cambiare error_reporting o usare @ non aiuta. Lo stato viene letto prima che PHP decida se il messaggio vada mostrato.

Spiega anche perché il team PHP insiste che l’unica mitigazione affidabile è aggiornare, e che nessuna modifica di configurazione copre tutti i percorsi.

Chi è esposto #

L’advisory PHP copre qualsiasi installazione che serve richieste web. Esempi citati:

  • PHP-FPM (con Nginx o Apache)
  • Apache con mod_php (rischio più alto)
  • php-cgi
  • FrankenPHP
  • LiteSpeed (lsphp)
  • Software che include php.exe / php-cgi.exe su Windows, spesso senza che l’operatore sappia di avere un server PHP

Secondo il CERT-AGID l’esecuzione di codice da remoto è confermata dal produttore su Apache con mod_php, mentre su PHP-FPM è possibile in specifiche condizioni di sfruttamento.

Per CVE-2026-109619 contano soprattutto i modelli con worker persistenti (FPM, mod_php), perché è lo stesso processo a servire richieste diverse. Un php-cgi classico, che nasce e muore a ogni richiesta, è molto meno esposto a questa seconda falla.

Come capire se sei vulnerabile #

Il primo controllo è la versione. Ricorda però che molte distribuzioni Linux applicano le patch senza cambiare il numero di versione upstream, quindi una versione “vecchia” può essere già corretta.

bash
# Show the PHP runtime version
php -v

# Show version, SAPI and memory limit in one go
php -r 'echo PHP_VERSION, " ", PHP_SAPI, " memory_limit=", ini_get("memory_limit"), PHP_EOL;'

# Debian/Ubuntu: check whether the distro backported the fix
apt changelog php8.3-fpm | grep -i "CVE-2026-1096"

# RHEL/Fedora family: same check
rpm -q --changelog php | grep -i "CVE-2026-1096"

php -v non basta: controlla il runtime del web server #

php -v mostra la versione dell’interprete da riga di comando, che può essere diversa da quella usata dal web server (più versioni installate, container, pannelli di controllo). Quella che conta è la versione che serve davvero le richieste.

bash
# Is PHP loaded as an Apache module (mod_php)?
apachectl -M 2>/dev/null | grep -i php

# Print the real binary and version behind every running PHP-FPM master
for p in $(pgrep -f 'php-fpm: master'); do
  bin=$(sudo readlink -f /proc/$p/exe)
  echo "$p -> $bin"
  "$bin" -v | head -n 1
done

Se la distribuzione applica le patch in backport, il riferimento è l’avviso di sicurezza del distributore, non il numero di versione.

Se vuoi sapere cosa vede un esterno sui tuoi asset, controlla l’header che espone la versione. Fallo solo su sistemi tuoi o autorizzati.

bash
# Check whether the web server leaks the PHP version
curl -sI https://example.com | grep -i "x-powered-by"

Un X-Powered-By: PHP/8.x.y visibile è un’informazione regalata a chi fa ricognizione. Disattivalo con expose_php = Off in php.ini. Per trovare esposizioni su larga scala si usano motori come Shodan, ed è esattamente quello che faranno gli attaccanti nei giorni dopo una disclosure; e per mappare i servizi di un tuo perimetro puoi partire da Nmap.

Dopo ogni verifica, ricordati che aggiornare il pacchetto non basta: il worker già in memoria continua a girare con il vecchio binario finché non lo riavvii.

bash
# Restart PHP-FPM after patching (unit name varies by distro/version)
sudo systemctl restart php8.3-fpm

# Confirm the master process started AFTER the update
ps -o pid,lstart,cmd -C php-fpm8.3

Come rilevare tentativi di sfruttamento #

Una RCE in PHP ha quasi sempre lo stesso effetto visibile: il processo web genera un figlio che non dovrebbe esistere. Questa è la tua osservazione più robusta, perché non dipende dalla firma di uno specifico exploit.

I processi padre da tenere d’occhio sono php-fpm (su Debian/Ubuntu con il numero di versione, es. php-fpm8.3), php-cgi, httpd/apache2 con mod_php, lsphp e frankenphp.

bash
# List shells/interpreters spawned by PHP-FPM workers
ps -eo pid,ppid,user,cmd --forest | grep -A2 "php-fpm" | grep -E "sh|bash|dash|python|perl|nc|curl|wget"

Per un monitoraggio continuo, una regola auditd sull’esecuzione di binari da parte dell’utente del web server:

bash
# Log every execve performed by the web server user (adjust uid for your setup)
sudo auditctl -a always,exit -F arch=b64 -S execve -F uid=www-data -k php_exec

# Review hits
sudo ausearch -k php_exec -i | tail -n 50

Poi ci sono i segnali nei log, tipici di una corruzione di memoria che non va a buon fine al primo colpo:

bash
# PHP-FPM workers crashing (memory corruption often ends in SIGSEGV first)
grep -E "exited on signal (11|6)" /var/log/php8.3-fpm.log

# Fatal OOM errors raised outside any application file
grep -E "Allowed memory size .* exhausted in Unknown on line 0" /var/log/php8.3-fpm.log /var/log/nginx/error.log

Una raffica di questi messaggi da uno stesso IP è un indizio forte. Un singolo evento può invece essere un normale OOM.

Se hai il sospetto di una compromissione, cerca file PHP comparsi dove non dovrebbero, in particolare nelle directory scrivibili dal web server come le cartelle di upload. Una web shell lasciata come persistenza è il classico secondo passo, e capire come funzionano i caricamenti non voluti ti aiuta a riconoscerli: ne parliamo nella guida agli attacchi di file upload.

bash
# PHP files modified in the last 3 days inside upload folders
find /var/www -type f \( -name "*.php" -o -name "*.phtml" -o -name "*.phar" \) \
  -path "*upload*" -mtime -3 -ls

Se trovi qualcosa, il consiglio è non limitarsi a cancellare il file: cambia segreti, variabili d’ambiente, password del database e chiavi API che l’applicazione poteva leggere, e ricostruisci l’ambiente da zero. Nel modello della Cyber Kill Chain sei già oltre la fase di exploitation, e quello che segue è post-exploitation.

Mitigazione: patch prima di tutto #

La mitigazione corretta è una sola: aggiornare a una release corretta e riavviare i worker.

BranchRelease corretta
8.58.5.12
8.48.4.27
8.38.3.36
8.28.2.35 (fine supporto il 31 dicembre 2026)
8.1 e precedentiNessun aggiornamento regolare: usa un backport del tuo distributore o vendor LTS, i backport minimali del team PHP (build da sorgente), oppure migra

Per chi compila da sorgente, il team PHP ha pubblicato tre branch con il backport minimo: PHP 7.4, PHP 8.0 e PHP 8.1. Prima di scegliere, controlla sempre l’advisory del tuo fornitore.

Alcuni punti pratici che fanno perdere tempo:

  • Container con tag fissi: un FROM php:8.4.26-apache non si aggiorna da solo. Cambialo in 8.4.27-apache. I tag mobili come php:8.4-apache puntano già alle immagini corrette, ma devi comunque ricostruire.
  • Dipendenze bloccate: se in composer.json hai "php": "8.4.26" esatto, un redeploy non cambia versione. Usa un intervallo come "^8.4".
  • Distribuzioni in ritardo: il 21 ottobre alcune distro (per esempio Amazon Linux) risultavano ancora in attesa di fix su diverse branch. Controlla lo stato della tua prima di dare il sistema per sicuro.

Se non puoi aggiornare subito #

Quando la patch non è immediatamente possibile, esistono impostazioni che rendono l’attacco più difficile, senza risolverlo. Vanno applicate in php.ini, in un pool FPM o nella configurazione del server, cioè prima del parsing della richiesta: ini_set() e .user.ini non sono adatti.

ini
; Set these in php.ini, an FPM pool or the server config: ini_set() and .user.ini do not work

; Request size limits: 8M if the app needs no uploads, 32M if it does
post_max_size = 8M
; Keep upload_max_filesize below post_max_size
upload_max_filesize = 4M

; Disable session-based upload progress (also the advisory mitigation for CVE-2026-115560)
session.upload_progress.enabled = 0

; Remove the configurable memory limit as an OOM trigger (partial measure)
memory_limit = -1

; PHP 8.5 only: blocks the confirmed fatal backtrace path
fatal_error_backtraces = 0

Su PHP-FPM, meglio impostarle nel pool con php_admin_value, così l’applicazione non può sovrascriverle:

ini
; PHP-FPM pool: applications cannot override php_admin_value
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[upload_max_filesize] = 4M
php_admin_value[session.upload_progress.enabled] = 0

Ognuna ha un costo, e conviene conoscerlo:

ImpostazioneEffetto collaterale
post_max_size / upload_max_filesizeLe richieste e gli upload più grandi vengono rifiutati: se servono file grossi, usa 32M
session.upload_progress.enabled = 0La barra di avanzamento basata sulle sessioni PHP smette di funzionare
memory_limit = -1Una singola richiesta può consumare tutta la RAM dell’istanza: rischio di OOM killer e di denial of service. Non esclude ogni possibile fallimento di allocazione: non è una soluzione sicura
fatal_error_backtraces = 0Gli errori fatali non includono più il backtrace (solo PHP 8.5)

Aggiungi anche limiti a livello di web server o proxy su dimensione della richiesta decodificata, numero di campi, parti multipart, header e profondità di annidamento, anche per richieste senza un Content-Length utilizzabile. Per la disponibilità, usa container o cgroup. Non sostituire memory_limit con limiti di sistema come RLIMIT_AS o RLIMIT_DATA: possono generare proprio il fallimento di allocazione che la falla sfrutta.

Sono misure parziali, non sostituti della patch.

Per CVE-2026-109619, riavviare i worker azzera lo stato in memoria che la falla abusa. È una misura tampone utile, non una correzione. Riguarda anche il codice dell’applicazione: errori fatali e timeout ricorrenti sono il modo in cui lo stato “sporco” viene creato, quindi correggerli riduce la superficie.

Quale PoC usare per studiare #

Su GitHub circolano già repository con exploit completi “one-click” per queste CVE, alcuni con centinaia di stelle. Il CERT-AGID segnala che l’efficacia di questi PoC pubblici non è ancora verificata a fondo, ma che aumentano comunque il rischio di tentativi di attacco. E c’è un problema in più: dopo ogni CVE famosa compaiono repository falsi, con backdoor o infostealer nascosti dentro lo “script exploit”. Il nome del repo non garantisce nulla.

Non eseguire PoC trovati online su sistemi che non controlli, e non farlo nemmeno sulla tua macchina principale.

Per questo abbiamo pubblicato una nostra versione, hack-ita/php2shell-cve-2026-109618-109619. È una versione migliorata di quelli già esistenti: parte dalla catena di sfruttamento di riferimento (lo stage 3 del PoC pubblico di eness, l’unico che implementa davvero il UAF documentato) e la estende con le tre fasi mancanti automatizzate — rotazione del worker, leak dell’heap base via OOM, leak di zval_ptr_dtor da libphp — più fallback automatico tra più profili di offset, scanner multi-sorgente per i puntatori trapelati nelle risposte HTTP, parser del Transfer-Encoding: chunked, conferma via timing per il caso blind, randomizzazione di boundary e User-Agent per non lasciare firme statiche, e supporto a proxy HTTP CONNECT per ispezionare tutto in Burp.

Il codice è su licenza MIT, con docs/vulnerability.md che spiega la causa radice riga per riga e docs/detection.md con regole pronte per Snort, Suricata, ModSecurity, Cloudflare, Apache e lighttpd. Usalo solo in ambienti isolati e autorizzati: capire il meccanismo è il punto, non aprire shell su macchine altrui.

Come studiarla in lab #

Il percorso didatticamente più solido è partire dal codice, non dall’exploit.

bash
# Get the source and check out the last vulnerable release of the 8.4 branch
git clone https://github.com/php/php-src.git
cd php-src
git checkout php-8.4.26

# Build with debug symbols and AddressSanitizer to observe memory errors
./buildconf --force
./configure --enable-debug --enable-address-sanitizer --disable-all --enable-session
make -j"$(nproc)"

Poi confronta con la release corretta e leggi la differenza.

bash
# Compare against the first patched release of the same branch
git diff php-8.4.26 php-8.4.27 --stat
git log php-8.4.26..php-8.4.27 --oneline | grep -i "bailout"

Il commit di fix si intitola “Avoid state corruption via bailout” e introduce un percorso di uscita pulito (UnwindExit), il tracciamento dei fallimenti di allocazione, il reset delle variabili globali e una nuova batteria di test oom_rfc1867_*. Leggere quei test ti dice esattamente quali scenari gli sviluppatori considerano pericolosi, ed è un ottimo esercizio di lettura di una patch di sicurezza per chi punta a fare exploit development su basi solide.

Errori comuni #

  • “Ho display_errors spento, quindi sono a posto.” No: lo stato viene letto prima della decisione di mostrare l’errore.
  • “Ho aggiornato il pacchetto.” Se non hai riavviato PHP-FPM o Apache, il processo in memoria è ancora quello vecchio.
  • “Uso un WAF.” Può filtrare traffico noto, ma la corruzione avviene prima del codice applicativo e dipende da dimensioni e struttura della richiesta, non da una stringa riconoscibile.
  • “La mia versione è vecchia, quindi sono vulnerabile.” Non per forza: guarda il changelog della distro per il backport.
  • "php -v dice 8.4.27, quindi sono a posto." Quello è il PHP da riga di comando: controlla il runtime usato dal web server.
  • “Sono su PHP 8.1, ho tempo.” È end of life: nessun aggiornamento regolare. Valuta i backport del tuo fornitore o quelli minimali del team PHP, oppure migra.

FAQ #

Cos’è PHP2Shell? È il nome con cui la community indica la coppia di vulnerabilità CVE-2026-109618 e CVE-2026-109619, due RCE non autenticate nel core di PHP, entrambe CVSS v4.0 9.2.

Qual è la differenza tra CVE-2026-109618 e CVE-2026-109619? La prima è una corruzione dovuta a un errore di memoria esaurita che si consuma nella stessa richiesta. La seconda è uno stato globale rimasto agganciato a memoria liberata, che una richiesta successiva sullo stesso worker legge prima della reinizializzazione.

Quali versioni di PHP sono corrette? 8.2.35, 8.3.36, 8.4.27 e 8.5.12. Le branch 8.1 e precedenti non ricevono aggiornamenti regolari, ma esistono backport minimali del team PHP (7.4, 8.0, 8.1) e backport di alcuni distributori e vendor LTS.

Serve essere autenticati per sfruttarle? No. Basta una richiesta HTTP costruita ad hoc che raggiunga un processo PHP vulnerabile.

PHP2Shell interessa WordPress? WordPress può essere interessato quando gira su un runtime PHP vulnerabile. Aggiornare WordPress, il tema o i plugin non corregge da solo una vulnerabilità del motore PHP: occorre aggiornare il runtime o verificare la correzione del proprio fornitore.

Le mitigazioni di configurazione bastano? No. Rendono l’attacco più difficile ma, secondo il team PHP, nessuna configurazione copre tutti i percorsi: l’unica soluzione affidabile è la patch.

Cos’è CVE-2026-115560? Un’ulteriore vulnerabilità annunciata dal team PHP il 23 ottobre 2026, non corretta dalle release del 21 ottobre. Richiede una combinazione insolita di configurazione e codice applicativo; la mitigazione indicata è session.upload_progress.enabled=0 fino alla prossima release regolare.

Posso provare un PoC trovato online per verificare se sono vulnerabile? Solo su un lab isolato e tuo. Molti repository post-CVE sono falsi o contengono malware: per verificare un sistema di produzione controlla la versione del runtime reale e l’avviso del distributore.

Come verifico se sono stato compromesso? Cerca figli anomali dei processi PHP (shell, curl, wget), crash dei worker nei log FPM, errori di memoria “in Unknown on line 0” e file .php nuovi nelle cartelle di upload. Se trovi qualcosa, ruota tutti i segreti.

Conclusione #

PHP2Shell è un buon caso di studio perché non nasce da un errore di programmazione evidente, ma da una scelta di design vecchia di anni: gestire gli errori fatali con un salto che salta anche la pulizia. Per chi studia offensive security, la lezione è che le falle più pesanti stanno spesso nelle ipotesi implicite del runtime. Per chi difende, l’ordine è semplice: patch, riavvio dei worker, monitoraggio dei processi figli del web server, rotazione dei segreti se c’è un dubbio.

Fonti e advisory di sicurezza #

#PHP2Shell #CVE-2026-109618 #CVE-2026-109619 #Remote Code Execution #Vulnerabilità PHP

lascia un messaggio

Non sono un robot