Le rapport php2shell de Calif Research analyse deux vulnérabilités PHP permettant l’exécution de code à distance, CVE-2026-109618 et CVE-2026-109619. PHP les a corrigées dans sa mise à jour de sécurité du 21 octobre 2026. L’étude a été menée par Calif avec la participation d’OpenAI et de Wiz. Ryan Austin, de Calif, est crédité pour la recherche et la rédaction.

Le plus ancien chemin identifié remonte à PHP 5.4.0. La méthode disponible dépend de la version de PHP et du SAPI utilisé. Selon le rapport, les chemins concernés restent toutefois exploitables dans les configurations par défaut. Les chercheurs ont confirmé une RCE pré-authentification contre WordPress 7.1.2 et Drupal 11.4.3. Joomla 6.1.4 et MediaWiki 1.46.2 figurent également parmi les applications testées. Le rapport cite Roundcube 1.7.4 et Magento 2.4.9. Il mentionne aussi phpMyAdmin 5.2.3 et Nextcloud 35.0.1. Laravel 13.11 et Symfony 8.1 sont inclus, ainsi que WHMCS sans numéro de version. Certains chemins permettent de viser des scripts PHP arbitraires.

L’analyse replace ces résultats dans l’histoire des failles liées à la limite mémoire de PHP. Stefan Esser avait signalé en 2004 la première vulnérabilité PHP distante de cette famille. Il n’avait pas publié d’exploit, jugeant le problème suffisamment grave. CORE et Immunity ont ensuite développé des exploits non publics. De fausses annonces circulaient par ailleurs sur IRC. Calif indique qu’une précédente étude sur les use-after-free de PHP n’avait trouvé aucun vecteur exploitable par défaut parmi les classes encore sérialisables ou non sérialisables concernées.

En 2020, jr@coredu.edu a signalé le bug PHP #78876. Des variables longues pouvaient provoquer un manque de mémoire et empêcher le nettoyage de fichiers temporaires. La chronologie du rapport attribue toutefois ce signalement à jr@coredu.mp. Un audit mené par OSTIF et Quarkslab a retrouvé une condition liée en 2024, sans démontrer son exploitation. Un correctif PHP associé à #78876 avait par ailleurs atténué le même type de problème dans HHVM. Son historique Git porte le tag `security-audit-2024`, bien que le correctif date de 2020.

OpenAI a invité Calif à participer à l’initiative Patch the Planet le 30 juillet 2026. Stefan Esser a mis l’équipe en relation avec Volker Dusch, membre de l’Ecosystem Security Team de la PHP Foundation. Calif a signalé le premier bug à PHP le 11 août, fourni une preuve de concept RCE le 14 août, puis reçu la confirmation de PHP le 15 août. Le second bug a été signalé le 21 août. Ryan Austin avait publié le hash d’une découverte le 5 août. Les versions corrigées sont sorties le 21 octobre.

Le premier chemin d’exploitation repose sur une pression mémoire contrôlée. Avant de parser le corps POST, PHP compare `Content-Length` à `post_max_size`. Il ne vérifie pas de la même façon le nombre réel d’octets consommés. Une requête multipart HTTP/1.1 en mode chunked ou une requête multipart HTTP/2 sans longueur déclarée peut donc dépasser la limite apparente. Un filtre de décompression produit un écart comparable entre la taille compressée et le flux décodé.

Un second déclencheur utilise des parties multipart avec `filename=""`. PHP ne les compte pas dans `max_file_uploads`, mais crée malgré tout six champs `$_FILES` pour chaque partie : `name`, `full_path`, `type`, `tmp_name`, `error` et `size`. Des noms de champs fortement imbriqués obligent PHP à construire des tables de hachage distinctes pour chaque champ. Des centaines de petits enregistrements peuvent dépasser la limite mémoire par défaut de 128 MiB. Le corps complet reste pourtant sous la valeur par défaut de 8 MiB de `post_max_size`. Une allocation ultérieure déclenche alors une erreur fatale contrôlée.

La première faille se trouve dans `_zend_hash_str_add_or_update_i`. PHP incrémente `nNumUsed` et `nNumOfElements` avant d’allouer et d’affecter la clé du bucket. Si cette allocation provoque une erreur fatale, le bucket est déjà visible pour la destruction, même si sa clé, son hash et sa valeur n’ont pas été initialisés. L’enregistrement des variables de requête atteint ce code avec les POST encodés en URL comme avec les données multipart RFC1867, avant l’exécution du script cible. Lors de l’arrêt de mod_php, PHP détruit la table corrompue et interprète le bucket périmé comme une structure valide.

PHP-FPM traite différemment un échec pendant le démarrage de la requête. Il termine la requête FastCGI, arrête le module PHP et quitte le worker avec `FPM_EXIT_SOFTWARE`. Le worker ne conserve donc pas l’état de la requête défaillante. Le premier chemin vise ainsi Apache avec mod_php. Pour cette variante, le rapport indique une exploitation contre des scripts arbitraires à partir de PHP 7.3.

Les chercheurs ont ensuite généralisé le motif : un état devient visible avant la fin d’une opération faillible, puis une erreur fatale empêche le retour à un état cohérent. Une analyse assistée par LLM a transformé cette idée en règle de recherche dans `SKILL.md` et a conduit à une seconde découverte dans le suivi de progression des uploads de Session. Le parsing multipart peut initialiser Session avant l’activation normale des modules. Si une erreur survient après `FILE_START`, le callback de nettoyage n’est pas appelé. `PG(modules_activated)` reste à false et les globals de Session conservent des pointeurs vers le tas de la requête, déjà libéré. Une requête multipart suivante, avec un identifiant de Session et `PHP_SESSION_UPLOAD_PROGRESS` mais sans partie fichier, atteint le nettoyage sans réinitialiser cet état. Cela crée un use-after-free entre deux requêtes sous mod_php. PHP-FPM quitte le worker lors de l’échec initial et ne conserve pas cet état.

Un autre chemin atteint PHP-FPM après un démarrage réussi. Le désérialiseur générique stocke les informations de références et de destructeurs dans les globals de l’extension Basic. Une erreur fatale pendant un décodage déclenché par l’application peut laisser `BG(unserialize).data` pointer vers une zone mémoire de requête libérée, avec un niveau d’imbrication encore non nul. La requête suivante démarre le traitement du suivi d’upload de Session avant l’initialisation normale de Basic et peut réutiliser cette zone. Les références sérialisées `R:` et `r:` peuvent alors viser des entrées sélectionnées de l’ancienne liste de références. Une valeur négative de `used_slots` dans un bloc de destructeurs permet de sélectionner une adresse avant le tableau attendu. Des clés sérialisées en double atteignent `var_push_dtor_value()` et copient un zval légitime dans l’emplacement choisi. Le rapport décrit une RCE dépendante de l’application sous mod_php et PHP-FPM, via des opérations comme `session_start()`, `session_decode()`, `unserialize()` et le `maybe_unserialize()` de WordPress.

Calif précise qu’aucun harnais de fuzzing particulier ni modèle exceptionnellement puissant n’a été nécessaire. Les chercheurs se sont appuyés sur leur connaissance des anciennes vulnérabilités de PHP et sur des questions ciblées posées aux LLM. Des prompts génériques ne retrouvent pas de manière fiable les anciennes primitives d’exploitation peu documentées.

Le rapport recommande d’installer immédiatement la mise à jour de sécurité PHP d’octobre 2026. En attendant, les déploiements PHP-FPM peuvent désactiver `session.upload_progress.enabled` et définir `memory_limit=-1`. La même valeur est recommandée comme mesure temporaire pour mod_php. Ces réglages ne remplacent pas un correctif. La suppression de la limite mémoire augmente le risque d’épuisement des ressources.