
PHP CVE-2026-109618/109619 — RCE non authentifiée autonome sur PHP 8.5.11 standard (divulgation mémoire in-band PS(id)→cookie + chaîne UAF), échelles de contrôle complètes, contrôle négatif sur version corrigée en 8.5.12
Package de reproduction en laboratoire et de recherche sur les vulnérabilités pour la classe php2shell de PHP 8.5.11 (CVE-2026-109618, CVE-2026-109619), octobre 2026. Statut : Prouvé — RCE autonome (quatre requêtes HTTP simples, sans débogueur, sans accès local), plus les résultats d'amplification, de lacunes de correctif et d'état obsolète FPM ci-dessous.
Sommaire : Résultats · Chaînes · Amplification · Lacunes de correctif · État obsolète FPM · Contournement WAF · Environnement · Démarrage rapide · Discipline de preuve · Arborescence · Contexte public · Déclaration
| # | Constat | Classification |
|---|---|---|
| 1 | RCE autonome de bout en bout sur PHP 8.5.11 — quatre requêtes HTTP simples, obtention d'adresse in-band via la divulgation PS(id)→cookie, les deux bases à partir d'une seule fuite de PHPSESSID de 512 octets ; 12/12 déclenchements, uid=33, 8 générations ASLR | Prouvé — RCE autonome |
| 2 | Amplification mémoire 52x via des en-têtes multipart dupliqués de 6 octets — contourne post_max_size (un corps de 2,57 Mo provoque une OOM sur une limite de 128M) sur PHP 7.4–8.5.11 | Prouvé — laboratoire, les quatre versions |
| 3 | Trois chemins d'exploitation survivent au correctif de sécurité 8.5.12 (UAF de réentrance var_destroy à n=1015, inadéquation du gestionnaire de sérialisation, injection de progression d'upload) | Prouvé — ASAN sur 8.5.12 vérifié par GPG |
| 4 | État obsolète BG(unserialize) en FPM : le worker survit à une OOM de décodage de session, l'état obsolète est consommé par la fenêtre rfc1867 de la requête suivante, une consommation façonnée par le tas provoque un SIGSEGV du worker | Prouvé — mécanisme + DoS ; RCE non prouvée |
| 5 | Contournement du séparateur Content-Type (multipart/form-data, boundary=X) — toute signature WAF publiée ancrée sur Content-Type est contournable | Prouvé — comportement du parseur SAPI.c |
| 6 | Les deux chaînes CVE sont mortes sur 8.5.12 corrigé (configure identique au token près, source vérifiée par GPG) | Prouvé — contrôle négatif |
| 7 | La théorie publique du « zval mis en cache session.upload_progress » — réfutée sur 4 versions PHP standard + 3 builds vendeur LiteSpeed, tous les recoins de nettoyage/concurrence | Réfutée — exhaustive |
Deux chaînes indépendantes de corruption mémoire atteignent des appels indirects contrôlés par l'attaquant sur l'image standard php:8.5.11-apache ; l'enveloppe de bout en bout combine le mécanisme de la chaîne B avec une divulgation in-band inédite pour un déclenchement entièrement distant.
| Chaîne A — bucket obsolète | Chaîne B — référence pendante | |
|---|---|---|
| Mécanisme | L'abandon sur OOM laisse un bucket HashTable publié mais non initialisé ; l'arrêt le consomme comme un faux zval → faux tableau → pDestructor | Une OOM pré-RINIT laisse PS(http_session_vars) détenant une zend_reference pendante ; une chaîne vivante de 31 octets la récupère ; référence forgée → zend_hash_destroy pDestructor |
| Allocateur | tracked/system (USE_ZEND_ALLOC=0) | Zend MM par défaut standard |
| Terminal | system() de la libc (adresses calibrées via gdb) | zif_system @ libphp+0x5996e0 (sans libc) |
| Débogueur au moment du déclenchement | calibration uniquement | aucun — FIRE_GDB=0 |
| Reproductibilité | 2 générations parentes randomisées | répétée, y compris entre exécutions |
Leak-A arme un PS(id) pendant (la 1001e variable déclenche max_input_vars →
headers_sent=1 ; l'échec de reset_id avec cookie vide maintient send_cookie=1 ;
un champ de 24 MiB provoque une OOM avant le RINIT). Leak-B dépose un tableau de 257 éléments dont
le zend_array chevauche PS(id)−0x10 : ZSTR_LEN lit nTableSize=512 et le
cookie encodé en URL transporte pDestructor (→ base libphp) ainsi que neuf pointeurs
de tas arData (vote majoritaire → base du chunk). Le delta mesuré chunk−libphp
n'est pas constant selon les générations (−0x1a00000 / +0x3600000 / −0x1c00000),
la divulgation est donc réellement porteuse. Le déclenchement s'exécute ensuite avec les
adresses divulguées. Prérequis : display_errors=On, post_max_size supérieur
au champ OOM, memory_limit inférieur à celui-ci, prefork mono-worker.
A status='HTTP/1.1 200 OK' → OOM fatal, worker survives, PS(id) armed
B Set-Cookie: PHPSESSID=<512-byte encoded disclosure>
FIRE → MARKER=uid=33(www-data) gid=33(www-data) groups=33(www-data)
FIRE_OK (×12 across 8 distinct ASLR layouts)
Reçu complet et échelle de contrôle : docs/END-TO-END.md.
Amplification optimisée par en-têtes dupliqués : de courts en-têtes X: a (6 octets sur le fil)
exploitent la taille fixe de 256 octets de SMART_STR_START_SIZE de PHP par en-tête sauvegardé
(~314 B de mémoire pour 6 B sur le fil). Mesuré par bissection exacte du seuil d'OOM sur
8.5.11 ; identique sur 7.4.33 / 8.1.34 / 8.4.26 (51–52x) :
| memory_limit | corps OOM min | amplification |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Tous les corps tiennent dans un post_max_size=8M par défaut — la barrière pratique
d'upload est contournée sur toutes les versions non corrigées. Le framing chunked
contourne en outre la pré-vérification Content-Length (8.85 MB en chunked analysé
et OOM là où le même corps avec Content-Length était rejeté). Le
correctif 8.5.12 ferme les deux vecteurs (filtrage des en-têtes inconnus + application
en flux de post_max_size). La forme « bombe » de tableau imbriqué ne se reproduit pas
de manière exponentielle (max 9.6x, 1.16 MiB — impossible d'OOM même à 8M).
Détails : docs/RELEASE-RECONCILIATION-2026-10-21.md.
Vérifié sur un build ASAN 8.5.12 identique au token et vérifié par GPG — les deux CVE sont corrigées, mais trois chemins distincts vers l'impact survivent au correctif :
var_unserializer.c:906:67) ; les gardes
ZEND_HAS_OOM du correctif sont inertes sur ce chemin ; BG(serialize_lock) n'est
toujours pas pris autour du dispatch de __destruct.| écrite par php_serialize
décodée par le handler php après un session_start() en phase applicative
instancie le gadget aussi bien sur mod_php php_value que sur FPM .user.ini ;
la garde incomplete_classes_only du correctif est limitée à la fenêtre d'upload
pré-RINIT uniquement.