← tous les articles
phprceuse-after-freecve-2026-109618cve-2026-109619vulnérabilité· 10 min de lecture

PHP2Shell

PHP2Shell (CVE-2026-109618 et CVE-2026-109619) est une paire de vulnérabilités critiques de PHP.

Lux

PHP2Shell : comment une erreur de mémoire dans PHP mène à l'exécution de code à distance

En bref

  • Deux vulnérabilités critiques du moteur PHP, CVE-2026-109618 et CVE-2026-109619, notées 9,2 en CVSS v4.0, permettent dans certaines configurations une exécution de code à distance sans authentification (PHP).
  • Le surnom « PHP2Shell » vient de Wiz Research, l'une des équipes qui les ont signalées (Wiz).
  • Les versions corrigées sont 8.2.35, 8.3.36, 8.4.27 et 8.5.12, publiées le 21 octobre 2026. Mettre à jour est la seule correction complète (PHP).
  • Le Centre canadien pour la cybersécurité indique que la CISA a ajouté les deux CVE à son catalogue KEV (vulnérabilités exploitées) le 26 octobre 2026 (Centre canadien).

Pourquoi c'est différent d'une faille « classique »

La plupart des failles PHP qu'on lit dans l'actualité concernent une application : un plugin WordPress, un module Drupal, un formulaire mal filtré. Ici, le défaut se trouve dans l'interpréteur PHP, c'est-à-dire le programme en C qui exécute les scripts. Wiz résume la conséquence ainsi : l'exploitation ne dépend pas de l'application, et une simple page « Hello World » sur une version vulnérable peut être concernée (Wiz).

Autre point important : sur Apache avec mod_php, une des chaînes démontrées s'exécute avant même que le code de l'application ne tourne (PHP Foundation). Un code applicatif propre, une authentification ou une validation des entrées ne protègent donc pas.

Quelques notions avant d'entrer dans le détail

Le cycle de vie d'une requête PHP

Sur un serveur web, PHP ne démarre pas un nouveau processus pour chaque visite. Des workers (processus php-fpm, processus Apache avec mod_php, etc.) restent en vie et traitent des requêtes les unes après les autres. Pour chaque requête, PHP :

  1. initialise ses structures internes (« startup ») ;
  2. lit le corps de la requête, y compris les fichiers envoyés en multipart/form-data ;
  3. exécute le script ;
  4. nettoie tout (« shutdown ») et libère la mémoire de la requête.

Cette mémoire propre à la requête est gérée par l'allocateur de PHP. À la fin de la requête, elle est rendue d'un bloc. Tant que plus rien ne pointe dessus, c'est sans danger.

memory_limit et la sortie de secours par longjmp()

Quand une requête dépasse memory_limit, PHP déclenche une erreur fatale « out of memory » (OOM). Pour abandonner la requête, il n'exécute pas proprement la fin de chaque fonction en cours : il saute directement à un point de reprise grâce à la fonction C longjmp() (avis GHSA-w8hw-w9fx-j79w).

Une analogie : c'est comme quitter un chantier par l'issue de secours. On sort vite, mais les outils restent là où ils étaient, et certains panneaux « en cours » restent accrochés.

Use-after-free

Un use-after-free (CWE-416) survient quand un programme continue d'utiliser une adresse mémoire après l'avoir libérée. Si un attaquant parvient à faire réutiliser cette zone par des données qu'il contrôle, le programme va lire ses données à lui en les prenant pour des structures internes légitimes. Bien orchestré, cela peut aller jusqu'à l'exécution de code.

La progression d'upload de session

Depuis PHP 5.4, la fonctionnalité session upload progress permet de suivre l'avancement d'un envoi de fichier. Elle est activée par défaut (session.upload_progress.enabled) et s'appuie sur la session PHP pendant la lecture du corps de la requête (manuel PHP, Recorded Future). Elle intervient donc très tôt, pendant le traitement du multipart, avant l'exécution du script. C'est un élément central de plusieurs chemins d'attaque.

Le schéma commun : un état laissé derrière soi

Les deux CVE regroupent plusieurs bugs, mais ils suivent tous le même motif, décrit par Wiz : une erreur fatale, surtout un OOM, détruit la mémoire de la requête, alors qu'une partie de l'état du moteur pointe encore dessus. Une requête suivante, traitée par le même worker, réutilise cet état périmé (Wiz).

Diagramme (source ci-dessous)
Source du diagramme
flowchart TD
    A["Requête 1 (attaquant)<br/>multipart forgé"] --> B["PHP initialise un état interne<br/>(session, unserialize, backtrace...)"]
    B --> C["Allocation qui échoue<br/>memory_limit atteint"]
    C --> D["Erreur fatale OOM<br/>sortie par longjmp()"]
    D --> E["Mémoire de la requête libérée"]
    E --> F["Mais un pointeur global<br/>vise encore cette mémoire"]
    F --> G["Requête 2 (même worker)"]
    G --> H{"Code qui lit l'état<br/>avant sa réinitialisation"}
    H --> I["Lecture de mémoire libérée<br/>= fuite d'informations"]
    H --> J["Utilisation de mémoire libérée<br/>réoccupée par l'attaquant<br/>= corruption contrôlée"]
    I --> K["Contournement de l'aléa<br/>des adresses"]
    J --> L["Exécution de code<br/>avec les droits du worker"]
    K --> L

Comment provoquer l'OOM au bon moment

Il faut d'abord faire manquer de mémoire à PHP à un instant précis, alors que la requête semble respecter les limites. L'avis officiel liste plusieurs défauts du parseur multipart qui le permettent (GHSA-w8hw-w9fx-j79w) :

  • une requête multipart sans longueur de contenu exploitable peut passer le contrôle initial de post_max_size ;
  • chaque partie peut contenir un nombre illimité d'en-têtes, qui coûtent bien plus de mémoire dans PHP que sur le réseau ;
  • les champs fichier vides ne comptent pas dans max_file_uploads mais créent quand même des tableaux de métadonnées, et des noms de champs imbriqués multiplient ce travail.

Autrement dit, quelques kilo-octets bien construits suffisent à faire gonfler la consommation mémoire côté serveur jusqu'à la limite.

CVE-2026-109618 : corruption lors d'un OOM

Description officielle : la gestion d'un OOM peut interrompre PHP pendant que des données internes de la requête ne sont que partiellement initialisées. Le nettoyage de la requête peut ensuite les traiter comme valides (PHP). L'avis GHSA détaille deux effets :

1. Divulgation de mémoire du worker. PHP analyse le corps multipart au tout début de la requête. Le traitement de la progression d'upload peut initialiser la session très tôt et enregistrer un identifiant de session dans un pointeur interne. Si un OOM interrompt la requête avant le nettoyage, ce pointeur reste accroché à de la mémoire désormais libérée. Une requête multipart suivante atteint le nettoyage de la progression d'upload avant que le pointeur soit réinitialisé : PHP lit alors le contenu du tas comme s'il s'agissait d'une chaîne, puis l'encode dans un cookie PHPSESSID. D'après Wiz, cela fonctionne même si l'application n'appelle jamais session_start() (Wiz).

Concrètement, l'attaquant récupère des morceaux de la mémoire du serveur — pouvant contenir mots de passe, jetons d'authentification, clés d'API, certificats — simplement en lisant le cookie renvoyé.

2. Corruption via un use-after-free. Le même motif, appliqué aux structures de dé-sérialisation de la session, permet de faire traiter de la mémoire réoccupée par l'attaquant comme des structures internes valides (Wiz).

PHP précise que la chaîne complète jusqu'au RCE a été validée sur Apache mod_php avec les réglages par défaut, le nettoyage s'exécutant même quand le démarrage échoue. Sur PHP-FPM, l'analyse du code suggère une sensibilité mais aucun exploit complet n'a été démontré ; FrankenPHP et LiteSpeed/LSAPI sont jugés probablement concernés, sans validation (GHSA-w8hw-w9fx-j79w).

CVE-2026-109619 : état global périmé

Description officielle : un état interne propre à la requête peut survivre après la libération de sa mémoire. Une requête suivante traitée par le même worker peut utiliser cet état périmé avant que l'initialisation normale ne le réinitialise (PHP).

L'avis GHSA décrit deux cas confirmés (GHSA-6wmf-4rv5-p3rp) :

  • Backtrace d'erreur fatale (PHP 8.5). Quand fatal_error_backtraces est actif, PHP conserve la trace de la dernière erreur fatale dans un état global de l'exécuteur. Un OOM détruit le tas sans vider cet état ; une requête suivante, via un diagnostic précoce, touche ce pointeur périmé. C'est la base de l'exploitation confirmée sur mod_php.
  • Progression d'upload de session. Un OOM ou une autre erreur fatale pendant le traitement d'un upload peut laisser des variables globales de session ou du décodeur pointer vers de la mémoire libérée. Une opération de progression d'upload ultérieure les lit. L'avis rapporte un RCE validé contre mod_php et FPM.

Il existe aussi un chemin par les diagnostics INI : des avertissements émis en appliquant certaines valeurs INI avant l'initialisation de la requête (par exemple via .htaccess ou php_value sous Apache) peuvent déclencher l'accès à l'état périmé, même si le message n'est ni affiché ni journalisé.

Condition commune à tous ces cas : la requête « productrice » et la requête « consommatrice » doivent tomber sur le même worker.

CVE-2026-115560 : un troisième chemin, non corrigé le 21 octobre

Le 23 octobre 2026, PHP a publié un avis informatif sur CVE-2026-115560, un RCE distinct que les correctifs du 21 octobre ne corrigent pas. Il demande une combinaison peu courante : l'application doit régler session.serialize_handler sur php_binary ou php_serialize en configuration, puis le basculer sur php à l'exécution via ini_set() avant session_start(), la progression d'upload restant activée (PHP, Centre canadien).

Mesure provisoire recommandée en attendant un correctif :

session.upload_progress.enabled=0

Les installations qui ont déjà appliqué ce réglage pour CVE-2026-109619 n'ont rien de plus à faire.

Impact et portée

  • RCE non authentifié. Wiz a démontré un RCE non authentifié contre WordPress, Drupal et Joomla (109618) et contre Drupal et WordPress (109619). Le code s'exécute avec les droits du processus PHP (Wiz).
  • Fuite de mémoire même sans exécution de code, exposant secrets et jetons d'autres requêtes.
  • Angle mort de journalisation. L'exploitation opère sous la couche applicative : les journaux de l'application peuvent ne presque rien montrer (Wiz, Patchstack).
  • Surface d'exposition. Tous les SAPI servant des requêtes web sont concernés : PHP-FPM, Apache mod_php (risque le plus élevé), php-cgi, FrankenPHP, LiteSpeed. Wiz indique que toute version depuis PHP 5.4 est touchée par au moins un des défauts (NCSC-NL, Wiz).

Exploitation observée

  • Avant même le correctif, Patchstack a reçu de ses hébergeurs partenaires plusieurs signalements d'exploitation active (21 octobre, 07:34 UTC) ; Cloudflare a signalé des attaques précoces et déployé des règles WAF (Patchstack).
  • La divulgation a été perturbée par une fuite le 20 octobre : les identifiants CVE ont circulé avec une heure de publication et un score (10,0) erronés, poussant PHP à avancer son pré-annonce et sa publication. Le vrai score est 9,2 (PHP Foundation).
  • La CISA a inscrit les deux CVE à son catalogue des vulnérabilités exploitées (KEV) le 26 octobre 2026 (Centre canadien).

Que faire

La seule correction complète est la mise à jour (PHP) :

BrancheVersion corrigée
8.58.5.12
8.48.4.27
8.38.3.36
8.28.2.35
  • PHP 8.1 et antérieur ne reçoivent plus de mises à jour du projet PHP ; des rétroportages existent pour 7.4, 8.0 et 8.1 (sources uniquement) ou via les distributions et éditeurs.
  • Après mise à jour, redémarrer tous les workers et vérifier qu'aucun ancien processus ne subsiste.
  • Inventorier chaque installation PHP, y compris les conteneurs, environnements de test et appliances qui embarquent PHP.

Mesures provisoires si la mise à jour est impossible immédiatement (elles réduisent l'exposition sans rendre l'installation sûre) (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp) :

; CVE-2026-109618
memory_limit=-1            ; de préférence via php_admin_value / config FPM
post_max_size=8M           ; relever seulement si des uploads sont nécessaires

; CVE-2026-109619
session.upload_progress.enabled=0
fatal_error_backtraces=0   ; PHP 8.5 uniquement
  • Appliquer ces réglages là où ils prennent effet avant l'analyse de la requête (php.ini, pool FPM, config serveur), pas via ini_set().
  • Limiter au frontal la taille décodée des requêtes, le nombre de parties multipart, d'en-têtes et la profondeur d'imbrication.
  • Pour les limites conteneur/cgroup, utiliser memory.max et éviter RLIMIT_AS / RLIMIT_DATA, qui déclenchent justement le chemin vulnérable.
  • Isoler les systèmes exposés (VPN, filtrage), surveiller requêtes multipart inhabituelles suivies d'erreurs mémoire, réponses 500 répétées, cookies PHPSESSID anormaux, redémarrages de workers inexpliqués.

Repères chronologiques

Date (2026)Événement
19 aoûtPremière vulnérabilité découverte par Wiz (outils assistés par IA) (Wiz)
20–21 aoûtRCE non authentifié confirmé sur Drupal puis WordPress (Wiz)
24 aoûtSignalement à l'équipe sécurité de PHP (Wiz)
20 oct.Fuite partielle des identifiants CVE avant la date prévue (PHP Foundation)
21 oct.Publication des versions corrigées ; exploitation déjà observée (PHP, Patchstack)
23 oct.Avis informatif sur CVE-2026-115560 (PHP)
26 oct.Ajout au catalogue KEV de la CISA (Centre canadien)

Sources

Markdown supporté · Ctrl+Entrée pour publierSe connecter pour publier

chargement de la discussion…