PHP2Shell est le nom donné à deux failles de PHP publiées le 21 octobre 2026, CVE-2026-109618 et CVE-2026-109619. Elles permettent, par une simple requête HTTP et sans authentification, d’exécuter du code sur un serveur qui fait tourner PHP. PHP alimentant environ 70 % des sites web dont la technologie serveur est connue (WordPress, Drupal, Joomla, et une foule d’applications), la portée est considérable : des sites gouvernementaux, Wikimedia et l’Internet Archive ont été mis hors ligne par précaution, et certains spécialistes l’ont comparée à Heartbleed.

Ce billet explique ce que sont ces failles, sans entrer dans les détails d’exploitation, en partant de la façon dont PHP gère la mémoire d’une requête. Il décrit ensuite comment vérifier qu’un serveur est réellement protégé, ce qui demande un peu plus qu’un numéro de version, et ce que j’ai fait sur mes propres serveurs. Les faits viennent des avis de sécurité de PHP et de la page Wikipédia consacrée à l’affaire.

Les faits

CVE-2026-109618CVE-2026-109619
intitulé de l’avis« RCE via OOM corruption »« RCE via stale global state »
naturecorruption mémoire lors d’un manque de mémoire pendant le nettoyage d’une requêteétat interne propre à une requête qui survit après la libération de sa mémoire
CVSS 3.19,8 (sans authentification, sans interaction)8,1 (complexité d’attaque élevée)
CVSS 4.09,29,2
versions touchéesPHP 5.4 à 8.5PHP 5.4 à 8.5
corrigé dans8.2.35, 8.3.36, 8.4.27, 8.5.12idem
Date (2026)Événement
aoûtdécouverte par deux équipes, avec l’aide d’outils d’analyse assistés par IA ; un des deux bugs avait été repéré dès 2024 lors d’un audit, sans correctif faute d’accord sur la solution
17 octobrepremière tentative d’exploitation observée par un pot de miel, quatre jours avant la publication
20 octobreannonce publique, fuites (vidéo de démonstration publiée par erreur), correctifs déjà visibles chez un distributeur
21 octobre, 13 h 34 UTCavis et versions corrigées publiés ; exploitation observée le jour même
23 octobreavis complémentaire CVE-2026-115560, lié à la progression des envois de fichiers en session

Deux éléments de ce calendrier méritent attention. La faille a été exploitée avant sa publication : quand le correctif sort, il n’y a plus de période de grâce. Et le premier bug avait été repéré deux ans plus tôt, sans être corrigé faute de démonstration d’exploitabilité : c’est l’arrivée d’outils capables de construire cette démonstration qui a tout changé. On peut s’attendre à voir ce scénario se répéter sur d’autres vieux bugs « théoriques ».

Comprendre : la vie d’une requête dans PHP

Pour saisir ce que veulent dire « corruption lors d’un manque de mémoire » et « état global périmé », il faut savoir comment PHP traite une requête, en particulier avec PHP-FPM, le mode le plus courant derrière nginx.

  • des processus réutilisés : PHP-FPM démarre un groupe de processus (workers) qui traitent chacun une requête après l’autre, des centaines ou des milliers de fois avant d’être recyclés (pm.max_requests). Ce n’est pas un processus neuf par requête ;
  • un cycle en quatre temps : au démarrage du processus, PHP initialise ses extensions une fois pour toutes (phase MINIT). Puis, pour chaque requête : initialisation propre à la requête (RINIT), exécution du script, puis nettoyage (RSHUTDOWN) qui doit remettre le processus dans un état propre pour la requête suivante ;
  • une mémoire par requête : le gestionnaire de mémoire de PHP (le Zend Memory Manager) alloue la mémoire du script dans une zone propre à la requête, libérée d’un bloc à la fin. C’est rapide et cela évite la plupart des fuites, mais cela suppose que rien ne garde, au-delà de la requête, un pointeur vers cette zone ;
  • une limite de mémoire : memory_limit borne la mémoire d’une requête. Quand un script la dépasse, PHP interrompt l’exécution… au beau milieu de ce qu’il faisait, y compris pendant le nettoyage.

Les deux bugs, à ce niveau d’explication

Le premier bug concerne justement ce cas : un dépassement de mémoire survenant pendant le nettoyage d’une requête laisse certaines structures internes dans un état incohérent, à moitié libérées. Un attaquant qui sait provoquer ce dépassement au bon moment, avec une requête construite pour cela, obtient une corruption de la mémoire du processus.

Le second bug relève de la même famille : un élément d’état interne appartenant à une requête n’est pas réinitialisé et survit à la libération de sa mémoire. La requête suivante, traitée par le même processus, trouve une référence vers une zone libérée puis réutilisée, éventuellement remplie par l’attaquant. C’est une utilisation après libération (use-after-free), classique dans les programmes écrits en C.

Dans les deux cas, une corruption de mémoire maîtrisée permet de détourner le flot d’exécution du processus PHP, donc d’exécuter du code avec ses droits : ceux de l’utilisateur du serveur web, qui peut lire les fichiers des sites, leurs identifiants de base de données, et souvent écrire dans leurs répertoires. Le nom « PHP2Shell » résume le résultat.

Pourquoi tous les modes sont touchés

Les deux bugs sont dans le cœur de PHP, pas dans un module particulier. PHP-FPM, Apache avec mod_php, php-cgi, FrankenPHP, LiteSpeed : tout ce qui sert des requêtes web est concerné. La ligne de commande l’est moins, puisqu’aucun attaquant distant n’y envoie de requêtes. Et le fait que les processus FPM soient réutilisés d’une requête à l’autre est précisément ce qui rend exploitable un état qui « survit » à une requête.

Pourquoi un pare-feu applicatif ne suffit pas

Les fournisseurs de pare-feu applicatif ont publié des règles temporaires dès les premiers jours. Elles ont leur utilité contre les outils d’exploitation publiés, mais une faille de corruption mémoire déclenchée par la forme d’une requête se prête mal au filtrage : il suffit souvent de varier la charge utile. Les mainteneurs de PHP l’ont dit clairement : aucune mesure de contournement n’est complète, seule la mise à jour protège. L’agence de sécurité américaine (NSA) est allée jusqu’à recommander de mettre hors ligne les sites qui ne pouvaient pas être mis à jour immédiatement.

Vérifier un serveur : la version ne suffit pas

php -v                                           # le binaire en ligne de commande
rpm -qa 'php*-fpm' 'php*-common' --qf '%{NAME} %{VERSION} installé %{INSTALLTIME:date}\n'
systemctl show -p ActiveEnterTimestamp php-fpm   # PHP-FPM a-t-il redémarré APRÈS la mise à jour ?
needs-restarting -s | grep -i php                # un processus utilise-t-il encore d'anciennes bibliothèques ?
ls /opt/remi/*/root/usr/sbin/php-fpm 2>/dev/null # versions parallèles (collections logicielles)
  • la version installée doit être au moins 8.2.35, 8.3.36, 8.4.27 ou 8.5.12. Pour les versions plus anciennes, plus maintenues par le projet, seuls des correctifs en source existent ; les distributions et certains fournisseurs (cPanel, CloudLinux) en ont fait des paquets ;
  • le processus en cours doit avoir redémarré après la mise à jour. Un PHP-FPM lancé avant l’installation continue d’exécuter l’ancien code en mémoire, quel que soit le paquet sur le disque. needs-restarting -s liste les services concernés ;
  • toutes les copies de PHP : un serveur peut en avoir plusieurs (le paquet principal, des collections parallèles dans /opt/remi, des conteneurs qui embarquent leur propre PHP). Chacune doit être vérifiée ;
  • les numéros rétroportés : sur une distribution qui corrige sans changer de version (RHEL et dérivées pour leur PHP d’origine), c’est le journal des modifications du paquet qu’il faut lire (rpm -q --changelog), pas le numéro de version.

Ce que j’ai trouvé sur mes serveurs

# main
php-fpm 8.5.12  installé le 21 oct. 2026 à 21:10   (avis publié à 15:34, heure de Paris)
# homelab
php-fpm 8.4.27  installé le 21 oct. 2026 à 20:30

Mes deux serveurs qui font tourner PHP utilisent les paquets du dépôt Remi, qui suit de près les versions officielles : les versions corrigées étaient installées le soir même de la publication, dans les cinq à six heures. Le processus PHP-FPM de l’un d’eux avait redémarré depuis, et celui de l’autre aussi, mais needs-restarting le signalait encore pour une bibliothèque mise à jour ensuite : un redémarrage de plus a réglé ce point. C’est la vérification qu’on oublie le plus souvent.

La faille complémentaire : la progression des envois de fichiers

; 99-local.ini : désactiver la progression des envois en session (CVE-2026-115560)
session.upload_progress.enabled = Off

Le 23 octobre, l’équipe de PHP a publié un avis sur une troisième faille, CVE-2026-115560, distincte des deux premières : une exécution de code qui dépend de la configuration, liée à la fonction de suivi de la progression des envois de fichiers en session. Quand elle est active (c’est la valeur par défaut), PHP écrit dans la session, pendant un envoi, des informations dont une partie vient de la requête. Si aucune application ne s’en sert, la désactiver ne coûte rien. J’ai vérifié qu’aucun code PHP de mes sites n’utilisait cette fonction (recherche de PHP_SESSION_UPLOAD_PROGRESS et upload_progress), puis je l’ai désactivée par la configuration commune déployée par Ansible.

Détecter une tentative

journalctl -u php-fpm --since "-7 days" | grep -E "exited on signal (11|6)|SIGSEGV|SIGABRT"
# WARNING: [pool www] child 12345 exited on signal 11 (SIGSEGV) after 3.2 seconds from start

Une tentative d’exploitation ratée d’une corruption mémoire se traduit souvent par le plantage du processus visé. Des processus PHP-FPM qui meurent sur un signal 11 (erreur de segmentation) ou 6 (abandon), surtout en série depuis une même adresse dans les journaux du serveur web, sont un signal à surveiller. Une règle d’alerte sur ces messages, dans un outil comme Wazuh, coûte peu.

Les leçons

  • la vitesse de mise à jour compte plus que jamais : exploitation avant publication, outils d’exploitation dans les jours suivants. Un dépôt de paquets qui suit de près les versions amont, et des mises à jour automatiques de sécurité, font la différence ;
  • une mise à jour n’est appliquée qu’au redémarrage du processus qui utilise le code. Vérifiez-le, ne le supposez pas ;
  • les versions en fin de vie (PHP 8.1 et antérieures) n’ont reçu que des correctifs en source. Un serveur sur une version ancienne, c’est une compilation à faire soi-même au pire moment ;
  • réduire ce qui est exposé : une fonction inutilisée (la progression des envois) désactivée, c’est une faille de moins le jour où elle est découverte ;
  • et une évolution de fond : les outils d’analyse assistés par IA découvrent et démontrent des failles anciennes plus vite qu’avant. Les bugs « jamais exploités » ne le resteront peut-être pas.

Sources : avis de sécurité du projet PHP (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp) ; page Wikipédia « PHP2Shell » ; Help Net Security et Recorded Future, octobre 2026.


Ce billet fait partie d’une série de tutoriels d’administration système (tag « pas à pas »).
Série « Pas à pas », tutoriel 1/45. · Tutoriel suivant → (à paraître le 30/10) · English version