Analyse technique

Comment fonctionne PHP2Shell

Une cause, deux CVE, quatre bugs : PHP ne nettoie pas de façon fiable après une erreur fatale — surtout en cas de manque de mémoire — au milieu d’une requête.

Dernière vérification 2026-10-22

Cause

Quand PHP atteint memory_limit, il lève une erreur fatale et remonte avec longjmp(). Cela saute le code qui aurait dû finir d’initialiser ou de réinitialiser l’état. Deux choses tournent mal :

  • Données à moitié construites (CVE-2026-109618). De la mémoire allouée mais pas encore initialisée est ensuite lue pendant l’arrêt de la requête comme si elle était valide.
  • Globales résiduelles (CVE-2026-109619). Les pointeurs globaux propres à la requête (état d’erreur, de session, d’objet, d’extension) ne sont pas effacés, mais la mémoire vers laquelle ils pointent est libérée à la fin de la requête. La requête suivante sur le même worker les utilise avant l’initialisation normale.

Les attaquants atteignent ces chemins avec des requêtes multipart forgées qui poussent PHP en OOM pendant l’analyse des entrées — avant votre routeur, votre framework ou votre authentification. C’est pourquoi un script « Hello World » est aussi exposé que WordPress.

La chaîne d’exploitation

  1. Étape 1

    Divulguer

    Utiliser la fuite de mémoire pour lire la mémoire vive du worker et contourner l’ASLR.

  2. Étape 2

    Corrompre

    Utiliser un use-after-free (désérialiseur ou backtrace) pour écrire des données contrôlées.

  3. Étape 3

    Exécuter

    Transformer la corruption en exécution de code sous l’utilisateur du worker — avant toute connexion.

L’exploitation ne se fait pas en un coup : l’attaquant sonde à répétition pour s’adapter à la disposition mémoire de chaque cible. Les chercheurs ont regroupé la chaîne dans un script nommé php2shell (non publié) et l’ont démontré sur les images de conteneur wordpress, drupal et joomla alors à jour.

Les quatre bugs

CVE-2026-109618

Use-after-free dans le désérialiseur de progression d’envoi

Le module de session décode des données pour le suivi de progression d’envoi avant la fin de l’initialisation de la requête et tient sa comptabilité dans BG(unserialize). Un échec d’allocation détruit le tas de la requête mais laisse cette globale en place. La requête suivante sur le même worker traite de la mémoire réoccupée par l’attaquant comme un état actif du désérialiseur.

CVE-2026-109618

Divulgation de la mémoire du processus à distance

Lors de l’analyse multipart RFC 1867 précoce, le code de progression d’envoi peut publier un identifiant de session dans PS(id). Après une interruption OOM, ce pointeur reste pendant. Une requête multipart ultérieure fait confiance au pointeur et à la longueur et encode en URL ce qui s’y trouve désormais dans un cookie PHPSESSID — même si l’application n’appelle jamais session_start().

CVE-2026-109619

Use-after-free dans les backtraces d’erreur fatale

EG(last_fatal_error_backtrace) survit à un OOM qui détruit le tas de la requête. Un diagnostic précoce dans la requête suivante suit le pointeur pendant. Le plus direct sur PHP 8.5 avec fatal_error_backtraces activé.

CVE-2026-109619

Confusion de types dans la mémoire partagée d’OPcache

Dans zend_accel_script_persist(), CG(map_ptr_last) avance avant que la frontière partagée ne soit publiée. Un échec d’allocation entre les deux laisse des emplacements de map-pointer sur lesquels les workers sont en désaccord ; un worker peut alors réutiliser un emplacement pour un autre type d’objet.

Le rattachement des bugs aux CVE suit l’article de Wiz Research. Les avis de PHP décrivent les classes de façon plus large et signalent d’autres variantes.

Exposition selon la configuration serveur

D’après les avis de PHP. « Exposé » signifie vulnérable selon l’analyse du code source, sans exploit générique publié pour cette SAPI.

Configuration…109618…109619Remarques
Apache + mod_phpRCE confirméeRCE confirméeLes deux CVE ont été confirmées jusqu’à la RCE avec les limites par défaut. Configuration la plus à risque.
PHP-FPM (nginx, Apache, Caddy…)ExposéRCE confirméeRCE via le cycle de vie des sessions confirmée sur FPM. Chemin OOM confirmé via des chemins de code applicatifs ; aucun exploit FPM générique publié.
FrankenPHP / LiteSpeed LSAPIProbableProbableL’avis les considère comme probablement exposés. Les workers de longue durée rendent l’état résiduel entre requêtes plus pertinent, pas moins.
CLI (cron, workers de file, artisan)Non traitéNon traitéNon traité par les avis et non joignable par des requêtes HTTP en usage normal. Mettez à jour quand même — c’est le même paquet.

Fiches CVE

CVE-2026-109618GHSA-w8hw-w9fx-j79wCVSS 4.0 · 9.2

RCE par corruption en cas de mémoire épuisée (OOM)

Quand la mémoire est épuisée, PHP interrompt le contexte courant avec longjmp(). Si cela survient alors que la mémoire de la requête n’est que partiellement initialisée, l’arrêt de la requête lit ensuite des données invalides ou s’y branche. Plusieurs chemins d’analyse multipart peuvent pousser PHP en OOM dans des limites normales, avant même l’exécution du code applicatif.

Impact :
Exécution de code sans authentification sous l’utilisateur du worker web PHP.
Démontré :
RCE pré-authentification sur WordPress, Drupal et Joomla (mod_php, limites par défaut).
Concerné
<8.2.35, <8.3.36, <8.4.27, <8.5.12 (et branches en fin de vie jusqu’à 5.4)
CWE
CWE-416, CWE-755, CWE-824, CWE-1325, CWE-1419
CVE-2026-109619GHSA-6wmf-4rv5-p3rpCVSS 4.0 · 9.2

RCE par état global résiduel entre requêtes

Une erreur fatale (y compris OOM) peut sauter la réinitialisation propre à chaque requête : l’état global d’erreur, de session, d’objet ou d’extension continue alors de pointer vers de la mémoire libérée à la fin de la requête. La requête suivante sur le même worker peut utiliser cette mémoire libérée avant l’initialisation normale.

Impact :
Use-after-free menant à la divulgation de la mémoire du worker ou à l’exécution de code.
Démontré :
RCE pré-authentification sur WordPress et Drupal ; divulgation de mémoire à distance indépendante de l’application.
Concerné
<8.2.35, <8.3.36, <8.4.27, <8.5.12 (et branches en fin de vie jusqu’à 5.4)

Vecteur (les deux) : CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. Joignable par le réseau, sans privilèges ni interaction utilisateur ; « AC:H / AT:P » traduit le fait que l’exploit doit être ajusté à la cible. Les premiers rapports issus de la fuite citaient CVSS 10.0 — ce chiffre est faux.

Ce que change le correctif

Le correctif n’utilise plus longjmp() en cas de manque de mémoire. Atteindre memory_limit déclenche désormais un OOM souple levé au prochain point sûr de la VM, là où l’état est cohérent. Un second seuil, plus élevé, déclenche un OOM strict qui termine le processus avec _exit() — sans gestionnaires d’arrêt, sans état à moitié nettoyé.

Comme il corrige le schéma de défaillance plutôt que des points d’entrée isolés, l’équipe PHP indique qu’il couvre aussi les variantes connues.

Effet secondaire : les chemins de code qui comptaient sur l’exécution de fonctions d’arrêt après un OOM se comporteront différemment une fois la limite stricte atteinte.

Voir aussi