Type
Analyse
Publié le
Durée
5 min de lecture
Auteur
rootzero
Read in English
Read in English →

PHP2Shell : deux bugs de mémoire dans le cycle de vie des requêtes PHP

Pourquoi un formulaire multipart ou deux requêtes sur le même worker peuvent mener à une RCE dans PHP : mémoire, état global, preuves et correctifs.

  • PHP
  • RCE
  • Mémoire
  • Vulnérabilités

Divulgation et correctifs : 21 octobre 2026. Analyse rédigée le 4 novembre 2026. « PHP2Shell » désigne deux défauts distincts du moteur PHP, CVE-2026-109618 et CVE-2026-109619. Le premier concerne la sortie brutale d’une allocation mémoire pendant qu’une requête est partiellement construite. Le second concerne un état global du processus qui survit à la requête alors que la mémoire qu’il désigne a été libérée. L’équipe PHP confirme une exécution de code sans authentification dans certaines configurations, avec les droits du worker web. Les deux avis portent un score CVSS 4.0 de 9,2.

Le point commun n’est ni WordPress ni une fonction PHP dangereuse appelée par le développeur. Il est dans la frontière entre préparation, traitement, nettoyage et réutilisation d’un processus PHP. Un correctif applicatif ne remplace donc pas la mise à jour du runtime.

CVE-2026-109618 : quitter au milieu d’une initialisation

PHP peut interrompre un traitement sur manque de mémoire par un mécanisme apparenté à longjmp(). Celui-ci saute plusieurs cadres d’appel. Si le code avait déjà publié un pointeur vers une structure de requête mais n’avait pas fini d’initialiser cette structure, les opérations de nettoyage peuvent ensuite la traiter comme valide. Lire, libérer ou parcourir ce contenu indéterminé crée une corruption mémoire. L’avis PHP décrit une RCE confirmée sur Apache mod_php : ce SAPI exécute le nettoyage même si le démarrage de la requête échoue, avant tout code de l’application.

Pourquoi un client distant peut-il provoquer l’épuisement ? Le parseur multipart/form-data peut consommer beaucoup plus de mémoire que la taille du message sur le réseau : nombre non borné d’en-têtes par partie, métadonnées de fichiers vides, noms de champs imbriqués. Une longueur absente ou inutilisable pouvait également passer le premier contrôle post_max_size. L’attaquant contrôle la forme de l’entrée et atteint une allocation au mauvais moment. Un OOM ne donne pas mécaniquement une RCE ; l’exploitation dépend de la disposition du tas, de la version, des extensions et de l’enchaînement précis du nettoyage. PHP indique cependant qu’une RCE HTTP complète a été validée sur mod_php avec des limites par défaut.

La distinction par environnement compte. Sur PHP-FPM, un échec pendant le démarrage termine le worker, ce qui évite ce chemin précis de nettoyage ; des OOM après démarrage restent possibles selon l’application. L’avis ne revendique pas une chaîne RCE FPM complète pour cette CVE seule. Pour FrankenPHP, l’analyse de source suggère une exposition analogue à mod_php, sans validation équivalente publiée. (Portée par SAPI)

CVE-2026-109619 : un pointeur traverse deux requêtes

Un worker PHP peut servir plusieurs requêtes à la suite. Certaines variables globales du moteur référencent pourtant des objets alloués pour une seule requête. Le contrat est simple : remettre ces variables à zéro avant de libérer la mémoire. Le défaut apparaît si une erreur fatale abandonne l’opération avant cette remise à zéro, ou si une étape tardive de nettoyage remplit de nouveau la variable après son nettoyage normal. La requête suivante lit alors un pointeur devenu invalide : c’est un use after free.

L’avis de l’équipe PHP distingue un producteur de l’état périmé et un consommateur lors d’une requête ultérieure sur le même worker. Deux chemins sont confirmés : certains diagnostics de configuration émis avant l’initialisation normale, et le mécanisme de suivi de progression des uploads de session. Sur PHP 8.5, fatal_error_backtraces fournit un producteur confirmé. Un second upload peut consommer l’ancien état de session sans que l’application appelle elle-même session_start(). La RCE du cycle de session a été validée sur mod_php et FPM, cette dernière sous condition d’une opération applicative appropriée après démarrage.

Ce modèle explique pourquoi un test avec une seule requête échoue souvent à reproduire le problème : il faut provoquer l’état périmé, conserver le worker, puis lui faire traiter l’entrée consommatrice. Il explique aussi pourquoi masquer les avertissements avec display_errors=Off ne protège pas : le moteur accède au pointeur avant de décider d’afficher ou de journaliser le message.

Comment chercher et vérifier sans confondre plantage et exploitation

Une revue de code part des appels qui peuvent interrompre le démarrage ou le nettoyage, puis inventorie les pointeurs globaux vers des allocations de requête. Chaque chemin d’erreur doit être examiné : l’état est-il remis à zéro, et le worker survit-il ? Les tests pertinents comparent une version vulnérable et une version corrigée, isolées, avec instrumentation mémoire. Un crash, un 502 ou une fuite de mémoire est un signal d’enquête, pas une preuve de prise de contrôle. La preuve d’une RCE doit montrer la transition du contenu contrôlé vers l’exécution, avec ses conditions de configuration.

Pour les exploitants, les journaux peuvent aider à trouver des bursts de requêtes multipart, des OOM, des erreurs fatales, des redémarrages de workers et des anomalies autour de PHP_SESSION_UPLOAD_PROGRESS. Leur absence ne prouve pas l’absence de tentative. Le trafic peut traverser plusieurs workers, et le point d’échec peut précéder la journalisation PHP.

Correctifs et mesures d’attente

Les versions 8.2.35, 8.3.36, 8.4.27 et 8.5.12 contiennent les corrections. Les branches 8.1 et antérieures n’ont pas de nouvelle release PHP officielle ; les distributions peuvent intégrer des backports. Il faut vérifier le paquet réellement déployé, pas uniquement la chaîne php -v. Redémarrer tous les workers après mise à jour est nécessaire pour éliminer l’ancien état en mémoire.

En attente d’un correctif, l’équipe PHP propose des réductions de surface : désactiver session.upload_progress.enabled au niveau administrateur, imposer des limites sur les requêtes multipart et, pour certains chemins OOM, retirer la limite PHP de mémoire par requête tout en conservant une isolation système adaptée. Aucune de ces options ne couvre toutes les variantes ; augmenter ou supprimer memory_limit peut aussi dégrader la disponibilité. Le 23 octobre, PHP a ajouté une recommandation provisoire de désactiver le suivi de progression face à un cas distinct, CVE-2026-115560, dépendant d’une combinaison rare de sérialiseurs de session.

La leçon de PHP2Shell tient dans une invariant de durée de vie : un worker réutilisé ne doit jamais transporter un pointeur vers la mémoire d’une requête achevée. La mise à jour corrige ce contrat dans le moteur ; filtrer une seule forme de requête ne suffit pas à le rétablir.

Comparer ces invariants de durée de vie avec les RCE de compilation Ruby dans GitLab aide à distinguer les mécanismes derrière une même étiquette « RCE ». Notre note « La preuve avant l’alerte » détaille la différence entre un indice reproductible, une vulnérabilité confirmée et une conséquence métier démontrée.

Sources principales : avis PHP sur l’OOM, avis PHP sur l’état global, annonce de l’équipe PHP.

#rootzero

Une question sur votre exposition ?

Écrivez-nous. Une personne de l’équipe technique vous répond, sources à l’appui.