Blog··16 min de lecture

PHP2Shell : pourquoi nos agents ont manqué une CVE du runtime PHP, et ce qui a changé

PHP2Shell (CVE-2026-109618/109619) : pourquoi nos agents l’ont d’abord classée en simple divulgation, comment ils prouvent le correctif, nos limites.

Le 21 octobre 2026, le projet PHP a publié les correctifs de deux failles critiques du moteur lui-même, CVE-2026-109618 et CVE-2026-109619, que chercheurs et presse ont baptisées PHP2Shell. Le lendemain matin, nous avons posé une question simple à notre propre outil : un client qui aurait lancé Darkmoon ce jour-là sur un site PHP vulnérable aurait-il été prévenu ? Non. Les agents ont lu la version vulnérable exacte dans les en-têtes de réponse, puis l’ont rangée parmi les simples divulgations d’information. Cet article documente ce résultat négatif, ce que nous avons modifié dans les agents, la validation face à un témoin corrigé, et ce qui ne va pas encore.

Le périmètre, d’emblée. Il s’agit de détecter une CVE du runtime (une faille située sous l’application, dans l’interpréteur qui la sert) et de prouver un niveau de correctif par HTTP. Nous n’avons pas exploité PHP2Shell, nous ne publions aucun élément d’exploitation, et rien ici ne prétend que Darkmoon détecte toutes les vulnérabilités PHP. Chaque validation décrite est un essai unique, sur un lab que nous avons construit.

PHP2Shell en une page

Les deux failles sont publiques et corrigées. Tout ce qui suit provient des avis PHP, de la NVD et du CERT-EU, lus à la source [1][3][4][7]. L’avis de php.net n’emploie pas le surnom ; Wiz, Huntress et Help Net Security, si [8][9][10].

ÉlémentCVE-2026-109618CVE-2026-109619
Titre php-srcRCE via OOM corruptionRCE via stale global state
Cause (avis)longjmp() sur manque de mémoire : des données de requête restent à moitié initialisées, le nettoyage les croit validesun état global pointe encore vers la mémoire libérée d’une requête ; la requête suivante du même worker l’utilise avant réinitialisation
Versions affectées< 8.2.35, < 8.3.36, < 8.4.27, < 8.5.12idem
Versions corrigées8.2.35, 8.3.36, 8.4.27, 8.5.12idem
CVSS v4.0 (PHP, CNA)9,2, AV:N/AC:H/AT:P/PR:N/UI:N9,2, même vecteur
CVSS 3.1 (NVD)9,88,1
CWE (NVD)CWE-416CWE-416

Le mécanisme, au niveau utile à un défenseur. Quand une requête manque de mémoire, PHP l’abandonne en sautant hors du contexte courant. À certains endroits, ce saut survient alors que des structures de requête ne sont qu’à moitié construites, et le nettoyage qui suit les lit comme si elles étaient complètes [3]. La seconde faille concerne un état qui survit à sa requête : un worker conserve un pointeur global vers une mémoire qui appartenait à la requête précédente, et du code exécuté très tôt dans la requête suivante (l’avis cite les diagnostics INI précoces et la progression d’envoi de session) peut la lire ou la libérer [4]. Le correctif de la première supprime ce saut pour les manques de mémoire. Il s’agit de bugs de sûreté mémoire du moteur : le code applicatif n’y change rien, et Wiz note qu’un simple « Hello World » sur une version affectée peut être vulnérable [8].

Deux détails de notation comptent pour le tri. Le score du CNA est CVSS v4.0 9,2 pour les deux ; la NVD, avec son propre calcul CVSS 3.1, donne 9,8 et 8,1 [5][6]. Les outils qui lisent la NVD afficheront donc d’autres chiffres pour le même avis. Par ailleurs, au moment où nous écrivons, aucune des deux CVE ne figure au catalogue Known Exploited Vulnerabilities de la CISA (version 2026.10.23) [12]. Cette absence ne prouve pas qu’il n’y a pas d’exploitation : le CERT-EU indique que des preuves de concept initiales « commencent à circuler » [7], et Help Net Security relaie un signalement d’Amazon Threat Intelligence faisant état d’une tentative d’exploitation captée par ses honeypots le 17 octobre, avant la divulgation [10] ; nous ne l’avons pas vérifié nous-mêmes. Le 23 octobre, php.net a publié un autre problème, moins risqué, dans la même zone (CVE-2026-115560, progression d’envoi de session sous une configuration inhabituelle) [2] ; il n’est pas couvert par ce travail.

Conditions d’exploitation, vues du défenseur

  • Ni authentification ni bug applicatif. Il suffit d’atteindre un point d’entrée PHP en HTTP.
  • Tous les SAPI qui servent des requêtes web sont concernés : PHP-FPM, Apache mod_php, php-cgi, FrankenPHP, LiteSpeed. php.net juge mod_php le plus exposé, avec une exploitation démontrée avant l’exécution du moindre code applicatif [1].
  • La réutilisation des workers compte pour CVE-2026-109619 : il faut une requête « productrice » et une requête « consommatrice » traitées par le même worker [4].
  • La fiabilité varie. Selon Wiz, l’exploitation doit s’adapter à la disposition mémoire de chaque cible, et les différences de version, de configuration ou d’application peuvent casser les hypothèses d’un exploit [8]. Ce n’est pas une raison d’attendre : le même billet décrit une RCE non authentifiée sur des images WordPress, Drupal et Joomla standard.
  • Le trafic d’attaque est un trafic multipart inhabituel. Wiz conseille de surveiller des requêtes multipart atypiques suivies d’erreurs de limite mémoire ou de rafales de HTTP 500, des cookies de session démesurés ou malformés, et un renouvellement inexpliqué des workers ; Huntress ajoute les workers PHP qui lancent des shells ou des outils de téléchargement [8][9].

Politique de divulgation de cet article

Les deux CVE sont publiques et corrigées en amont : nous expliquons donc le mécanisme, sans aller au-delà des avis officiels (aucune forme, taille ou séquence de requête qu’ils ne publient pas). Des scanners actifs publics existent, et ils fonctionnent en poussant la cible jusqu’à sa limite mémoire[11]. Raison de plus pour que nos agents ne les utilisent pas contre une cible en production (voir les limites : notre propre garde-fou avait un trou).

Pourquoi les numéros de version trompent

Puisque nos agents ne doivent pas exploiter un bug de corruption mémoire, la preuve doit être la version. Cela paraît trivial, jusqu’à ce qu’on regarde d’où viennent les versions.

  • La CLI n’est pas le runtime web. Le jour même, nous avons vérifié l’une de nos propres applications PHP exposées sur Internet. Les binaires php en ligne de commande présents sur la machine annonçaient des versions sans rapport avec celle qui servait le site. Seule une sonde éphémère exécutée par le SAPI web a donné la vraie réponse (déjà corrigée, en l’occurrence). Notre déploiement relance désormais cette sonde à chaque mise en production et échoue sous le plancher corrigé.
  • Proxys et CDN peuvent mentir dans les deux sens. Un en-tête X-Powered-By périmé peut survivre à une mise à jour derrière un cache : un seul en-tête ne suffit pas.
  • Les distributions rétroportent. Une chaîne comme 8.1.2-1ubuntu2.x ou +debNuM peut contenir le correctif sans que le numéro amont change.
  • Les versions masquées sont courantes. Avec expose_php=Off, il n’y a rien à lire, et deviner à partir de l’OS ou de la version du CMS n’est pas une preuve.

Méthode : un lab, une seule variable

Pour mesurer la détection plutôt que raconter une anecdote, nous avons monté un lab A/B : deux conteneurs issus des images officielles, la dernière 8.3 vulnérable et la première corrigée, qui servent la même application de 20 lignes (une session et un formulaire d’envoi de fichier, les chemins de requête dont parlent les avis). Le conteneur corrigé sert de témoin négatif : si Darkmoon le signale aussi, la détection ne vaut rien.

# labs/php2shell/docker-compose.yml (notre lab interne ; extrait : commentaires d'en-tête,
# noms de conteneurs et politique de redémarrage omis)
name: darkmoon-labs-php2shell
services:
  php2shell-vulnerable:
    image: php:8.3.35-apache          # dernière 8.3 avant le correctif
    ports: ["127.0.0.1:19110:80"]
    volumes: ["./app:/var/www/html:ro"]
  php2shell-patched:
    image: php:8.3.36-apache          # témoin négatif : ne doit PAS être signalé
    ports: ["127.0.0.1:19111:80"]
    volumes: ["./app:/var/www/html:ro"]
# app/index.php : 20 lignes, session_start() et un formulaire d'envoi multipart

Les deux campagnes ont reçu la même consigne, au mot près, qui ne mentionne ni PHP ni aucune CVE ; le même modèle (claude-opus-4-6) ; le même conteneur d’outils. Seuls changeaient les fichiers des sept agents de la famille PHP. La consigne, conservée en anglais comme elle a été envoyée :

TARGET: http://127.0.0.1:19110 and http://127.0.0.1:19111. Two internet-facing
web applications of a customer (authorized lab, full scope, black-box). Run a
complete security assessment of both, dispatch the right specialists, report
every vulnerability with evidence, and finalize the campaign.

D’abord, un résultat négatif

La campagne 1 (22 octobre, 08:17 UTC) a tourné avec les fichiers d’agents en service depuis juillet. Bilan : 39 findings, dont 0 critique, 0 élevé, 14 moyens, 20 faibles et 5 informatifs. L’agent PHP a bien lu PHP/8.3.35, et l’a classé ainsi :

// campagne 1 (agents en service depuis juillet), extrait du finding enregistré
{ "title": "Server Version and Technology Stack Disclosure via HTTP Headers",
  "severity": "info", "cvss_score": 0.0, "cve": null, "status": "confirmed",
  "endpoint": "http://127.0.0.1:19110/", "discovered_by_agent": "php" }
evidence.raw_response: "Server: Apache/2.4.69 (Debian)  X-Powered-By: PHP/8.3.35"

Trois causes, toutes visibles dans les journaux de session :

  • Ce que sait le modèle. Il a été entraîné avant le 21 octobre : il ne peut pas savoir que la 8.3.35 est devenue vulnérable la veille.
  • Ce que sait le scanner. Les templates Nuclei locaux dataient du 26 septembre et tournaient sans vérification de mise à jour. Un rejeu du 23 octobre n’a trouvé aucun template citant l’une ou l’autre CVE dans ce dossier. En amont, un template avait déjà été publié (vulnx l’indiquait le 22 octobre) : une boîte à outils à jour l’aurait eu, la nôtre non, comme beaucoup d’installations réelles.
  • Un outil disponible, jamais appelé. vulnx, qui interroge une base de vulnérabilités à jour, était installé et autorisé ; les deux sessions de l’agent PHP de la campagne 1 l’ont appelé zéro fois. Les agents se contentaient de la passe Nuclei générique commune à tous, -tags exposure,misconfig,tech-detect.

La donnée était là. Aucun maillon ne la reliait à une CVE publiée la veille.

Ce que nous avons changé dans les agents

Darkmoon repose sur un orchestrateur (pentest) qui identifie la cible et lance des agents spécialisés ; un agent est un fichier de consigne Markdown exécuté par le moteur dans les limites d’une liste d’outils autorisés (voir comment les empreintes techniques déclenchent les agents PHP et l’architecture des agents spécialisés). Nous n’avons touché qu’aux fichiers d’agents, en deux commits, tous deux publiés sur le dépôt public[13][14] :

Commit (ASCIT31/Dark-Moon)DateContenuStatut
9dbb9c422 oct. 2026bloc PHP RUNTIME CVE CHECK dans php, wordpress, drupal, joomla, magento, moodle, prestashop ; le signal PHP de l’orchestrateur transmet la ligne X-Powered-By / Server brute ; un script générateur ; un garde-fou de non-régressionimplémenté, testé en lab, publié
2b20a2c25 oct. 2026bloc TARGETED VULN SWEEP dans 36 agents web et services, une ligne TECH PROFILE par agent (tags Nuclei et requête vulnx) ; script générateur ; garde-fou de non-régressionimplémenté, testé sur un passage d’agent, publié
Comparaison avant et après de l’architecture des agents PHP de Darkmoon. Avant : l’orchestrateur lance les agents de la famille PHP, qui font une passe Nuclei générique et classent la version PHP en simple information. Après : deux blocs ajoutés dans les agents, un contrôle des CVE du runtime et un balayage ciblé, qui appellent vulnx puis, pour le balayage, Nuclei ciblé ; le contrôle du runtime pousse un finding critique fondé sur la version.
Figure 1 · Ce qui a changé dans les agents de la famille PHP entre les fichiers de juillet et le 25 octobre 2026. Seuls les fichiers de consigne et leurs générateurs ont changé ; le moteur, la liste d’outils autorisés et les scanners sont les mêmes binaires.Source : commits 9dbb9c4 et 2b20a2c sur github.com/ASCIT31/Dark-Moon ; fichiers conf/agents/*.md.

Bloc 1 : le contrôle des CVE du runtime PHP (voie rapide)

Un bloc identique à l’octet près, produit par tools/agent-build/php_runtime_cves.py et inséré entre marqueurs dans les sept agents de la famille PHP. Les CVE du runtime vivent sous l’application : un agent WordPress ou Drupal qui étudie le CMS et ses extensions ne regarderait jamais l’interpréteur. Le bloc s’exécute une fois par hôte et port PHP, avant les modules applicatifs, en six étapes : relever passivement la version sur deux réponses servies par PHP ; comparer par branche aux versions corrigées ; ne jamais exploiter ; qualifier ; pousser un finding avec la preuve brute et la remédiation de php.net ; puis lancer une recherche vulnx bornée pour les CVE du runtime plus récentes que la table.

Bloc 2 : le balayage ciblé (généralisation)

Une table écrite à la main marche pour une CVE et vieillit dès qu’on l’écrit. Trois jours plus tard, nous avons généralisé la méthode plutôt que la table : chaque agent web ou de service qui identifie un produit et une version embarque le même balayage, cadré par une seule ligne de profil (pour l’agent PHP, nuclei tags: php,apache,fpm et vulnx query: php). Il ne se déclenche qu’une fois un produit confirmé, jamais sur une cible nue ; interroge d’abord vulnx, peu coûteux, qui dit si un template Nuclei existe ; ne lance Nuclei que sur les identifiants de CVE trouvés ; ne se rabat sur une passe par tags, bornée aux sévérités critique et élevée, que si aucune version n’a pu être lue ; et tient dans un budget de 90 secondes. Un produit qui relève d’un autre agent est signalé à l’orchestrateur, pas scanné sur place. Les 15 agents qui n’identifient pas de produit web versionné (fournisseurs cloud, IaC et gestion de configuration, Docker et Kubernetes, GitHub, identité et Active Directory, firmware, mobile, LLM, l’agent navigateur et l’orchestrateur lui-même) sont exclus à dessein.

Pourquoi vulnx d’abord : des mesures uniques prises pendant la mise au point donnent environ 2 s pour une recherche vulnx, environ 4,6 s pour Nuclei limité à un identifiant de CVE, et 45 à 48 s pour la passe par tags la plus lourde essayée (1 283 templates WordPress contre une cible qui n’en relevait pas). Trier par coût garde le balayage dans son budget.

Frontière open source

Les deux blocs, leurs scripts générateurs et le garde-fou de non-régression font partie de l’édition open source, github.com/ASCIT31/Dark-Moon (GPL-3.0, CLI et agents). Les mêmes fichiers d’agents sont recopiés à l’identique dans nos dépôts de release et Pro. Le tableau de bord web dans lequel les clients Pro consultent ces findings ne fait pas partie de l’édition open source, et rien dans ce travail n’en dépend.

Détection et qualification

Logigramme du contrôle des CVE du runtime PHP. Une surface PHP est identifiée ; sans version visible, rien n’est poussé ; si deux réponses divergent, le résultat reste non confirmé ; sinon la version est comparée par branche aux correctifs ; au-dessus, rien n’est poussé ; un suffixe de distribution donne « vulnérable, à vérifier », sévérité élevée ; en dessous, l’agent pousse un finding critique, fondé sur la version, jamais exploité, puis lance une recherche vulnx bornée de CVE plus récentes.
Figure 2 · Chemin de décision entre une empreinte et un finding qualifié, tel qu’il est écrit dans le bloc PHP RUNTIME CVE CHECK. Le témoin corrigé de notre lab sort par « rien de poussé ». La recherche vulnx bornée de CVE du runtime plus récentes suit la comparaison sur toutes les branches ; elle n’est dessinée qu’une fois.Source : conf/agents/php.md, bloc darkmoon:php-runtime-cves (commit 9dbb9c4).

La ligne la plus lourde de conséquences du bloc est une règle de qualification. Darkmoon plafonne normalement à « faible » tout finding qu’il ne peut pas démontrer, règle que nous gardons parce qu’une simple correspondance de version n’est en général pas une preuve. Appliqué ici, ce plafond aurait noyé une RCE critique parmi les divulgations d’en-têtes. Le bloc pose donc une exception explicite et étroite : quand l’avis de l’éditeur nomme exactement la version observée, l’avis tient lieu d’impact démontré. Le statut estconfirmed (version-matched), jamais exploited ; la sévérité est celle de l’avis. L’exception a trois garde-fous : la version doit être lue sur deux réponses PHP (sinon le finding reste non confirmé), un suffixe de distribution la ramène à « potentiellement vulnérable, élevé » avec la consigne de vérifier l’avis de l’éditeur, et une version masquée ne produit aucun finding.

Résultats de validation

EssaiDate (UTC)Ce qui a tournéRésultat
Campagne 1, référence22 oct., 08:17campagne complète, agents de juillet39 findings, 0 critique ; PHP 8.3.35 classé en info sur 19110
Campagne 2, après bloc 122 oct., 09:08même consigne, modèle, outils ; 7 agents PHP remplacés37 findings, 1 critique : PHP2Shell sur 19110 seulement ; rien sur PHP2Shell pour 19111
Rejeu23 oct., 07:36les commandes curl de l’agent, à la main19110 PHP/8.3.35, 19111 PHP/8.3.36 ; 0 template Nuclei local pour ces CVE
Balayage, agent PHP seul25 oct.bloc 2, agent php lancé directement sur le conteneur vulnérablevulnx search et vulnx id sur les deux CVE, Nuclei positif, finding critique poussé, aucun scan à l’aveugle
Balayage, campagne complète25 oct.orchestrateur sur le conteneur vulnérablel’orchestrateur n’a pas lancé l’agent PHP ; il a trouvé lui-même CVE-2026-109618 par de larges passes Nuclei et l’a classée en déni de service ; voir les limites
Garde-fou de non-régression25 oct., 18:35tools/check-no-regression.sh sur le dépôt publicPASS : bloc 1 dans les 7 agents, bloc 2 présent dans tous les agents profilés et absent ailleurs

Le finding critique de la campagne 2, tel qu’enregistré :

// campagne 2 (7 agents de la famille PHP remplacés, rien d'autre), extrait du finding enregistré
{ "title": "PHP runtime vulnerable to PHP2Shell unauthenticated RCE
            (CVE-2026-109618, CVE-2026-109619): PHP 8.3.35",
  "severity": "critical", "cvss_score": 9.2, "status": "confirmed",
  "plugin_or_component": "php 8.3.35 (mod_php / Apache 2.4.69)",
  "endpoint": "http://127.0.0.1:19110/", "mitre_attack_id": "T1190" }
evidence.commands: curl -sk -o /dev/null -D - --max-time 15 http://127.0.0.1:19110/
                   curl -sk -o /dev/null -D - --max-time 15 http://127.0.0.1:19110/index.php
evidence.explanation: "... consistently identified via the X-Powered-By header on two
  independent PHP-served routes ... Not exploited by design ..."

Sur le témoin corrigé, l’agent a écrit à 09:17:34 que la 8.3.36 est la version corrigée et qu’aucun finding de runtime n’était à pousser ; la synthèse de la campagne précise que le port 19111 est corrigé. La recherche vulnx bornée a listé les deux CVE (1,9 s) et l’agent en a conclu qu’aucune autre CVE du runtime ne s’appliquait.

Nous publions aussi les imperfections visibles dans les journaux, car elles font partie du résultat. Les commandes du bloc utilisent timeout, absent de la liste d’outils autorisés de Darkmoon : le premier appel a échoué et l’agent a relancé avec curl --max-time. Le premier envoi du finding critique a été refusé faute de description, puis corrigé 25 secondes plus tard. Le modèle a d’abord écrit « This is the fixed version », puis « wait, let me re-check », avant de conclure correctement. Enfin, dans la campagne 2, l’orchestrateur, qui lit le fichier de l’agent avant de le lancer, a ajouté de lui-même dans le contexte transmis pour 19111 que cette cible tourne en version corrigée ; pour la cible vulnérable, le contexte ne contenait que des faits. Dans les deux cas, la connaissance venait du nouveau bloc, pas de la consigne.

Limites et faux positifs

Ce que cette détection ne voit pas

  • Les versions masquées. Avec expose_php=Off et aucune version ailleurs, le bloc 1 ne signale rien, par construction. Un sondage actif répondrait, à un coût que nous refusons sur des cibles en production.
  • Les rétroportages. Un paquet de distribution corrigé peut porter un vieux numéro amont. Le bloc le signale « potentiellement vulnérable » : c’est un faux positif chaque fois que l’éditeur a bien rétroporté le correctif, et seul son avis tranche.
  • Les en-têtes périmés. La double lecture repère un proxy qui injecte un en-tête incohérent, pas un proxy qui réécrit toutes les réponses de la même façon.
  • Un essai par condition. Ce sont des démonstrations reproductibles sur un lab, pas des statistiques. Les sorties du modèle varient d’un essai à l’autre.

Ce que nous avons trouvé de travers dans notre propre travail

  • La règle « non destructif » n’est pas imposée par la ligne de commande. Le balayage interdit d’ajouter les tags intrusive ou dos, mais le template Nuclei amont de CVE-2026-109618 est un template de détection active par plantage qui porte lui-même le tag intrusive, et une passe par tags produit (-tags php,...) le sélectionne. C’est exactement ce qui s’est produit le 25 octobre contre notre lab. La voie précise (nuclei -id <CVE>) sélectionnerait le même template, puisque le bloc laisse à l’agent le soin de juger si un template est non destructif. Le correctif consiste à exclure explicitement ces tags ; il est conçu, pas encore implémenté.
  • Le texte du finding a repris des affirmations tierces non vérifiées. Dans ce même essai du 25 octobre, le finding poussé portait le 9,8 de la NVD au lieu du 9,2 de l’avis inscrit dans le bloc, et sa description affirmait une « exploitation active » sur la foi de la sortie de vulnx (données VulnCheck KEV), que nous n’avons pas vérifiée et que le KEV de la CISA ne mentionne pas. La sévérité était juste ; le récit allait plus loin que nos preuves.
  • L’agent a pris la voie de repli. Dans le même essai, la version était connue : la doctrine demande Nuclei sur les seuls identifiants de CVE trouvés ; l’agent a lancé la passe par tags bornée. Le résultat était juste, le chemin n’était pas celui écrit.
  • Sans lancement de l’agent, pas de bloc. Sur une cible unique et simple, l’orchestrateur fait parfois le travail lui-même au lieu de lancer un spécialiste. Dans la campagne complète du 25 octobre, il a lancé de larges passes Nuclei (l’une avec un délai de dix minutes), trouvé la CVE grâce au template et l’a classée en déni de service. Les blocs n’aident que si l’agent spécialisé tourne vraiment.
  • Le bruit hors sujet demeure. Les deux campagnes signalent un « envoi de fichier sans restriction », de sévérité moyenne, sur les deux conteneurs. L’application ne stocke jamais le fichier ; la synthèse de la campagne 2 le dit, mais le finding reste.
  • Les scanners ont aussi leurs faux positifs. Le README du scanner d’Assetnote note qu’un PHP corrigé peut renvoyer la même réponse HTTP 500 vide qu’un PHP vulnérable lorsqu’une sonde dépasse memory_limit [11]. Détection par plantage et détection par version échouent à des endroits différents : c’est pourquoi nos findings portent ensemble la preuve de version et l’avis.

Prouver le correctif et remédier

Ce que nous faisons sur nos propres systèmes, et ce que dit la remédiation du finding. D’abord, prouver la version du runtime qui sert les requêtes, pas celle d’un binaire présent sur la machine :

# 1. ce que dit le SAPI web (en-têtes), sur deux routes servies par PHP
curl -sk -o /dev/null -D - https://app.example/            | grep -i -E 'x-powered-by|server'
curl -sk -o /dev/null -D - https://app.example/index.php   | grep -i -E 'x-powered-by|server'

# 2. si expose_php=Off masque la version : une sonde éphémère au nom aléatoire,
#    servie par le MÊME vhost (déposer, lire une fois, supprimer ; jamais de phpinfo() en ligne)
<?php echo PHP_VERSION, ' ', PHP_SAPI, ' upload_progress=', ini_get('session.upload_progress.enabled');

# 3. comparer par branche : corrigé si >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12
#    (paquets de distribution : lire l'avis de l'éditeur, le correctif peut être rétroporté)
  • Mettre à jour vers 8.2.35, 8.3.36, 8.4.27, 8.5.12 ou plus, ou vers le rétroportage de l’éditeur pour les branches en fin de vie (PHP a publié des rétroportages minimaux en sources pour 7.4, 8.0 et 8.1), puis redémarrer tous les workers. Seule la mise à jour corrige complètement [1].
  • En attendant, réduire l’exposition (php.net, mesures toutes partielles) : memory_limit=-1 fixé là où l’application ne peut pas le modifier ; un post_max_size bas (8M, ou 32M si des envois sont nécessaires) avec upload_max_filesize en dessous ; session.upload_progress.enabled=0 dans php.ini ou la configuration du pool, pas via ini_set() ni .user.ini ; en 8.5, fatal_error_backtraces=0. Ne pas remplacer par RLIMIT_AS ou RLIMIT_DATA. Le CERT-EU ajoute des limites de taille de corps au niveau du serveur web ou du proxy, et des limites mémoire de conteneur ou de cgroup [7].
  • En faire un critère de mise en production. Notre propre déploiement interroge le SAPI web après chaque release et échoue sous le plancher corrigé ; nous avons aussi vérifié à la main qu’un corps de requête trop gros est refusé par le serveur web (HTTP 413) avant que PHP ne l’analyse. Masquer la version (expose_php=Off) relève de la bonne hygiène et ne corrige rien.

Un écart relevé en écrivant cet article : php.net cite memory_limit=-1 parmi les mesures d’attente, et le texte de remédiation de notre bloc publié ne le mentionne pas. Il sera ajouté à la prochaine révision du bloc.

Perspectives

Chaque point porte son statut réel.

  • Envisagé : exclure les templates intrusive,dos,fuzz dans toutes les commandes du balayage ; remplacer timeout par l’enveloppe autorisée dans le bloc 1 ; ajouter memory_limit=-1 à la remédiation.
  • Envisagé : faire lancer l’agent PHP par l’orchestrateur dès qu’une version du runtime PHP est vue, pour que les blocs s’exécutent aussi sur les cibles simples.
  • Envisagé : publier le lab. Il vit aujourd’hui dans un dépôt interne ; l’extrait compose ci-dessus, avec une petite page qui ouvre une session et affiche un formulaire d’envoi, suffit à le reconstruire.
  • Question ouverte : quand retirer le bloc 1. Une fois le balayage éprouvé sur davantage de CVE, une table écrite à la main deviendra une dette de maintenance ; d’ici là, c’est le seul chemin validé face à un témoin négatif.

Ressources complémentaires

Références

  1. PHP Group, « Security advisory: Critical remote code execution vulnerabilities, update now », archive des actualités php.net, 21 octobre 2026 (entrée 2026-10-21-5), et « Upcoming Critical PHP Security Release », 20 octobre 2026. www.php.net/archive/2026.php#2026-10-21-5
  2. PHP Group, « Informational advisory: Session upload progress » (CVE-2026-115560), php.net, 23 octobre 2026. www.php.net/archive/2026.php#2026-10-23-1
  3. Avis php-src GHSA-w8hw-w9fx-j79w, « RCE via OOM corruption » (CVE-2026-109618). github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w
  4. Avis php-src GHSA-6wmf-4rv5-p3rp, « RCE via stale global state » (CVE-2026-109619). github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp
  5. NIST NVD, CVE-2026-109618 (fiche lue via l’API NVD 2.0 le 25 octobre 2026). nvd.nist.gov/vuln/detail/CVE-2026-109618
  6. NIST NVD, CVE-2026-109619 (fiche lue via l’API NVD 2.0 le 25 octobre 2026). nvd.nist.gov/vuln/detail/CVE-2026-109619
  7. CERT-EU, Security Advisory 2026-018, « Critical Vulnerabilities in PHP », v1.2, 23 octobre 2026. cert.europa.eu/publications/security-advisories/2026-018
  8. Wiz Research, billet de recherche « PHP2Shell » (découverte, chronologie, pistes de détection). www.wiz.io/blog/php2shell-php-rce
  9. Huntress (J. Hammond), « Rapid Response: php2shell, Leaked Critical PHP Vulnerabilities », 20 et 21 octobre 2026. www.huntress.com/blog/php2shell-leaked-critical-php-vulnerabilities
  10. Help Net Security (M. Zorz), « PHP2Shell: Critical PHP vulnerabilities patched, update now! », 21 et 22 octobre 2026. www.helpnetsecurity.com/2026/10/21/php2shell-php-vulnerabilities-patched-cve-2026-109618-cve-2026-109619/
  11. Assetnote, php2shell-scanner (README : conception des sondes, impact réseau, faux positifs). github.com/assetnote/php2shell-scanner
  12. CISA, catalogue Known Exploited Vulnerabilities, flux JSON (version 2026.10.23, consultée le 25 octobre 2026). www.cisa.gov/known-exploited-vulnerabilities-catalog
  13. Darkmoon, commit 9dbb9c4, « feat(agents): PHP runtime CVE check — detect PHP2Shell », ASCIT31/Dark-Moon, 22 octobre 2026. github.com/ASCIT31/Dark-Moon/commit/9dbb9c4
  14. Darkmoon, commit 2b20a2c, « feat(agents): signal-driven targeted vuln sweep across the web/service agents », ASCIT31/Dark-Moon, 25 octobre 2026. github.com/ASCIT31/Dark-Moon/commit/2b20a2c

Next
Lancez-le sur votre propre lab

L'édition open source de Darkmoon (GPL-3.0, CLI) s'auto-héberge. Clonez-la, pointez-la sur une cible qui vous appartient et lisez chaque ligne.