From df518d5b1c796210a2bf7e027db985f2c087953b Mon Sep 17 00:00:00 2001 From: JoshRob Date: Mon, 9 Nov 2026 22:39:32 -0600 Subject: [PATCH] =?UTF-8?q?1.14.0:=20chain=20D,=20measured=20=E2=80=94=20t?= =?UTF-8?q?he=20stale-store=20claim=20does=20not=20hold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHAIN-D.es.md | 78 +++++++++++++++++++++++++++++++++++++ CHAIN-D.md | 74 +++++++++++++++++++++++++++++++++++ README.es.md | 6 +-- README.md | 8 ++-- raw/chain-d-eg-shutdown.txt | 54 +++++++++++++++++++++++++ 5 files changed, 213 insertions(+), 7 deletions(-) create mode 100644 CHAIN-D.es.md create mode 100644 CHAIN-D.md create mode 100644 raw/chain-d-eg-shutdown.txt diff --git a/CHAIN-D.es.md b/CHAIN-D.es.md new file mode 100644 index 0000000..b36b93d --- /dev/null +++ b/CHAIN-D.es.md @@ -0,0 +1,78 @@ +# Cadena D, medida: el object store en el shutdown +[English](CHAIN-D.md) · [Español](CHAIN-D.es.md) + + +Una de las cadenas que circulan para estas CVE dice que el shutdown de la petición lee un +`EG(objects_store)` obsoleto que dejó una petición muerta a medio arranque, y que la siguiente +petición en el mismo worker puede consumirlo. Medimos ese paso. No se comporta así. + +Este documento dice exactamente hasta dónde llega la medición, porque la afirmación vecina —el +arreglo de buckets de objetos liberado y reutilizado— es otra cosa y **no** se aborda aquí. + +## Lo que se midió + +`php:8.4.26-apache`, mod_php, prefork, 128M, un worker (pid 17), breakpoint en +`php_request_shutdown` (`main/main.c:1891`). Tres peticiones en ese mismo worker: + +1. un GET de calentamiento → 200 +2. un cuerpo chunked de 70 MB que muere al arrancar la petición con + `Allowed memory size of 134217728 bytes exhausted` → 500 +3. un GET después del OOM → 200, el worker sigue vivo + +En el breakpoint, una observación por petición: + +``` +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +``` + +El puntero del store, el siguiente slot libre, la capacidad y la cabeza de la lista libre son +idénticos en las tres. `top=1` con el handle 0 reservado significa que no hay objetos vivos en el +shutdown, y las dos primeras entradas de buckets leen cero. `current_execute_data`, la cima y el +final de `vm_stack` también son idénticos. + +O sea: la cabecera del object store en el shutdown es la misma, haya muerto o no la petición +durante el arranque. El encuadre de "store obsoleto que queda para la siguiente petición" no se +sostiene. + +Transcripción: [`raw/chain-d-eg-shutdown.txt`](raw/chain-d-eg-shutdown.txt). + +## Dónde se detiene esto + +Dos límites, y pesan más que el resultado: + +- Medimos la **cabecera del store y sus primeras entradas** de buckets, no el arreglo completo de + 1024. +- Las tres observaciones se hicieron con el store **vacío en el shutdown**. Una petición que llega + al shutdown con objetos vivos es otro estado, y esta medición no dice nada de él. + +## Lo que dice el trabajo de otros + +El material publicado no afirma todo lo mismo, y la diferencia decide si sobrevive a esta +medición: + +- **segfo** confirma un use-after-free de `object_buckets` con caída, y marca el RCE como no + demostrado. Es el arreglo de buckets, no la cabecera del store: fuera de lo que medimos. +- **cookie-bakery** publica una cadena completa: OOM, un bucket marcado `IS_OBJECT`, + `zend_objects_store_del`, un pivote por `free_obj`, ROP y `system`. Corre con ASLR apagado y su + propia documentación pide leaks para funcionar bajo ASLR. Una cadena funcional con ASLR off y + nuestro resultado de cabecera idéntica no están en conflicto: hablan de memoria distinta. +- **ikow** describe "stale `EG()` / object store" como RCE sin dependencias. El repositorio trae + exploits de las cadenas A, B y C; el código de la D no está publicado ahí. +- El parche que reinicia `object_buckets` durante el shutdown es consistente con la variante de + buckets. No es evidencia de una cabecera obsoleta. + +## La conclusión + +"Cadena D funciona porque `EG(objects_store)` está obsoleto" es la versión que no se sostiene. La +versión que vale la pena perseguir es el arreglo de buckets, que alguien ya llevó a código con +ASLR apagado y que necesita un leak para cruzar a ASLR encendido. + +Es un resultado negativo sobre un paso concreto, publicado porque una matriz que solo lista +aciertos no es una matriz. También cambia nuestra propia fila de `UNSUPPORTED` a algo más +estrecho: no sostenido **como afirmación de store obsoleto**, con la variante de buckets +explícitamente abierta. diff --git a/CHAIN-D.md b/CHAIN-D.md new file mode 100644 index 0000000..537350a --- /dev/null +++ b/CHAIN-D.md @@ -0,0 +1,74 @@ +# Chain D, measured: the object store at shutdown +[English](CHAIN-D.md) · [Español](CHAIN-D.es.md) + + +One of the chains circulating for these CVEs says the request shutdown reads a stale +`EG(objects_store)` left behind by a request that died mid-startup, and that the next request +on the same worker can consume it. We measured that step. It does not behave that way. + +This document says exactly how far the measurement goes, because the neighbouring claim — the +object bucket array being freed and reused — is a different thing and is **not** addressed here. + +## What was measured + +`php:8.4.26-apache`, mod_php, prefork, 128M, one worker (pid 17), breakpoint at +`php_request_shutdown` (`main/main.c:1891`). Three requests on that same worker: + +1. a warmup GET → 200 +2. a chunked 70 MB body that dies at request startup with + `Allowed memory size of 134217728 bytes exhausted` → 500 +3. a GET after the OOM → 200, the worker is still alive + +At the breakpoint, one observation per request: + +``` +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +``` + +The store pointer, the next-free slot, the capacity and the free-list head are identical in all +three. `top=1` with handle 0 reserved means no live objects at shutdown, and the first two +bucket entries read as zero. `current_execute_data`, `vm_stack` top and end are identical too. + +So: the object store header at shutdown is the same whether or not the request died during +startup. The "stale store left behind for the next request" framing is not supported. + +Transcript: [`raw/chain-d-eg-shutdown.txt`](raw/chain-d-eg-shutdown.txt). + +## Where this stops + +Two limits, and they matter more than the result: + +- We measured the store **header and its first bucket entries**, not the whole 1024-entry array. +- All three observations were made with the store **empty at shutdown**. A request that reaches + shutdown holding live objects is a different state, and this measurement says nothing about it. + +## What the other work says + +The published material does not all make the same claim, and the difference decides whether it +survives this measurement: + +- **segfo** confirms a use-after-free of `object_buckets` with a crash, and marks RCE unproven. + That is the bucket array, not the store header — outside what we measured. +- **cookie-bakery** publishes a full chain: OOM, a bucket marked `IS_OBJECT`, + `zend_objects_store_del`, a `free_obj` pivot, ROP and `system`. It runs with ASLR off and its + own documentation asks for leaks to work under ASLR. A working ASLR-off chain and our + identical-header result are not in conflict: they are about different memory. +- **ikow** describes "stale `EG()` / object store" as dependency-free RCE. The repository ships + exploits for chains A, B and C; the chain D code is not published there. +- The patch that resets `object_buckets` during shutdown is consistent with the bucket variant. + It is not evidence for a stale store header. + +## The takeaway + +"Chain D works because `EG(objects_store)` is stale" is the version that does not hold. The +version worth pursuing is the bucket array, which someone has already taken to code with ASLR +off, and which needs a leak to cross into ASLR on. + +This is a negative result about a specific step, published because a matrix that only lists +successes is not a matrix. It also changes our own row from `UNSUPPORTED` to something narrower: +unsupported **as a stale-store claim**, with the bucket variant explicitly still open. diff --git a/README.es.md b/README.es.md index fab7ec4..622e534 100644 --- a/README.es.md +++ b/README.es.md @@ -3,7 +3,7 @@ # php2shell — Verificación Independiente y Auditoría de Límites de Explotación ### CVE-2026-109618 · CVE-2026-109619 · CVE-2026-115560 -[![Release](https://img.shields.io/badge/Release-v1.13.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.13.0) +[![Release](https://img.shields.io/badge/Release-v1.14.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.14.0) [![Target](https://img.shields.io/badge/Objetivos_PHP-8.2_·_8.3_·_8.4_·_8.5-334155?style=flat-square)](#) [![CVSS](https://img.shields.io/badge/CVSS_v4.0-9.2_CRÍTICO-b91c1c?style=flat-square)](#) [![ASLR](https://img.shields.io/badge/ASLR-Ventana_Cookie_1.88_MB-d97706?style=flat-square)](#) @@ -22,7 +22,7 @@ Contacto: Telegram [@JocesLab](https://t.me/JocesLab) · correo [joshrob.sec@pro - **Estado**: Publicación v1.9.2, 2026-11-09 (verificación del estado de parches contra php.net y php-src, más el puntero de v1.0.0 a la última; calibración en imágenes oficiales de distribución, pruebas de aislamiento de stdout en FastCGI, desambiguación de arquitecturas SAPI, alcance de ASLR medido por tamaño de lectura, transcripciones por corrida de la cadena 9bie y arnés de medición publicado). - **Alcance**: Ramas de PHP 8.4.x y adyacentes (8.2, 8.3, 8.5), imágenes oficiales en contenedores, código público de prueba de concepto (PoC) y reproducción independiente en laboratorio. - **Alcance aplicativo**: Se utilizó un entorno de prueba de Nextcloud 33.0.x (PHP 8.4.21, Apache `mod_php` prefork) para validar la exposición de configuración y nivel de parche. La prueba de RCE se obtuvo sobre la imagen oficial `php:8.4.26-apache`. El fallo reside en el inicio de la petición de PHP, no en el código de Nextcloud. -- **Artefactos**: `PORTABILITY.es.md` (la matriz medida de qué condiciona la cadena), `TIMELINE.es.md` (cronología de la divulgación con fuentes), `CHAIN-E.es.md` (el brazo FPM, medido: layout, precondiciones y las rutas que no funcionaron), `RELIABILITY.es.md` (qué decide si dispara, y con qué frecuencia), versiones formales en PDF (`REPORT.pdf` y `RESUMEN.pdf`), instrumentación de laboratorio reproducible (`tooling/labkit/` y `tooling/docker-compose.yml`), herramienta de diagnóstico no destructiva (`tooling/checker/`), calculadoras offline de alcanzabilidad y reglas de detección. +- **Artefactos**: `PORTABILITY.es.md` (la matriz medida de qué condiciona la cadena), `TIMELINE.es.md` (cronología de la divulgación con fuentes), `CHAIN-E.es.md` (el brazo FPM, medido: layout, precondiciones y las rutas que no funcionaron), `RELIABILITY.es.md` (qué decide si dispara, y con qué frecuencia), `CHAIN-D.es.md` (el object store en el shutdown, medido: resultado negativo con su alcance), versiones formales en PDF (`REPORT.pdf` y `RESUMEN.pdf`), instrumentación de laboratorio reproducible (`tooling/labkit/` y `tooling/docker-compose.yml`), herramienta de diagnóstico no destructiva (`tooling/checker/`), calculadoras offline de alcanzabilidad y reglas de detección. - **Asistencia de IA**: Se utilizaron múltiples modelos de IA y subagentes especializados bajo dirección humana para revisión, orquestación de laboratorio y comprobación documental; cada hallazgo fue contrastado contra comandos, salidas reales o fuentes públicas. Ver `METHODOLOGY.md`. --- @@ -108,7 +108,7 @@ La evaluación empírica de la primitiva E-OOM en PHP-FPM 8.4.26 (`fpmdbg` detr | CVE-2026-115560 | `php_binary` + runtime `ini_set()` | Fallo de decode observado; sin impacto en seguridad de memoria | `NO CONCLUYENTE` | Sección 12 | | Chunked `post_max_size` | Sin parche (8.2.34 / 8.3.35 / 8.4.26) | 30M parsea completo, 70M causa OOM en límite de 128M | `BYPASS CONFIRMADO` | Sección 12 | | Chunked `post_max_size` | Parchado (8.4.27 / 8.5.12) | Rechazado: `Actual POST length does not match Content-Length` | `PARCHADO` | Sección 12 | -| Cadena D (`EG` en shutdown) | `mod_php` sin parche | `EG(objects_store)` idéntico antes/después de OOM; sin crash | `NO SOPORTADO` | Sección 12 | +| Cadena D (`EG` en shutdown) | `mod_php` sin parche | cabecera de `EG(objects_store)` idéntica antes/después de OOM, store vacío en el shutdown; sin crash | `NO SOSTENIDO como store obsoleto` — la variante de buckets es otra y sigue abierta | Sección 12, `CHAIN-D.es.md` | | Control `mod_php` 8.4.27 | Límites por defecto (128M / 8M) | Ejecución limpia, sin corrupción de memoria | `PARCHADO (0/5)` | Sección 7 | | Cadenas A/C/E (Petición) | 8.4.26 / 8.4.27 / 8.5.12 | RCE condicionado a fallos aplicativos (LFI / gadget / handler) | `DEPENDE DE APP` | Sección 3 | | Cadena A con `strict=1` | Todas las versiones | Archivo con SID elegido ausente; inyección bloqueada | `PREVENIDO` | Sección 3 | diff --git a/README.md b/README.md index fac33dc..6b1ca16 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # php2shell — Independent Verification & Exploitation Boundary Audit ### CVE-2026-109618 · CVE-2026-109619 · CVE-2026-115560 -[![Release](https://img.shields.io/badge/Release-v1.13.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.13.0) +[![Release](https://img.shields.io/badge/Release-v1.14.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.14.0) [![Target](https://img.shields.io/badge/PHP_Targets-8.2_·_8.3_·_8.4_·_8.5-334155?style=flat-square)](#) [![CVSS](https://img.shields.io/badge/CVSS_v4.0-9.2_CRITICAL-b91c1c?style=flat-square)](#) [![ASLR](https://img.shields.io/badge/ASLR-Cookie_Window_1.88_MB-d97706?style=flat-square)](#) @@ -19,10 +19,10 @@ Contact: Telegram [@JocesLab](https://t.me/JocesLab) · email [joshrob.sec@proto --- -- **Status**: public release v1.13.0, 2026-11-09 (portability matrix and disclosure timeline published, patch-state check against php.net and php-src, plus the v1.0.0→latest pointer; official stock distribution image calibration, FastCGI stdout isolation proofs, SAPI architecture disambiguation, measured ASLR scope per read size, per-run 9bie transcripts, and the published measurement harness). +- **Status**: public release v1.14.0, 2026-11-09 (portability matrix and disclosure timeline published, patch-state check against php.net and php-src, plus the v1.0.0→latest pointer; official stock distribution image calibration, FastCGI stdout isolation proofs, SAPI architecture disambiguation, measured ASLR scope per read size, per-run 9bie transcripts, and the published measurement harness). - **Scope**: PHP 8.4.x and adjacent branches (8.2, 8.3, 8.5), official PHP container images, public proof-of-concept code, and independent lab reproduction. - **Application scope**: a Nextcloud 33.0.x test deployment (PHP 8.4.21, Apache `mod_php` prefork) was used to validate the exposed configuration and patch level. The RCE proof was obtained on the official `php:8.4.26-apache` image. The bug is in PHP request startup, not Nextcloud code. -- **Artifacts**: `PORTABILITY.md` (the measured matrix of what gates the chain), `TIMELINE.md` (disclosure timeline with sources), `CHAIN-E.md` (the FPM arm, measured: layout, preconditions and the paths that did not work), `RELIABILITY.md` (what decides whether it fires, and how often), formal typography PDF releases (`REPORT.pdf` and `RESUMEN.pdf`), reproducible lab instrumentation (`tooling/labkit/` and `tooling/docker-compose.yml`), non-destructive diagnostic tooling (`tooling/checker/`), offline reachability calculators, and detection rules. +- **Artifacts**: `PORTABILITY.md` (the measured matrix of what gates the chain), `TIMELINE.md` (disclosure timeline with sources), `CHAIN-E.md` (the FPM arm, measured: layout, preconditions and the paths that did not work), `RELIABILITY.md` (what decides whether it fires, and how often), `CHAIN-D.md` (the object store at shutdown, measured: a negative result with its scope), formal typography PDF releases (`REPORT.pdf` and `RESUMEN.pdf`), reproducible lab instrumentation (`tooling/labkit/` and `tooling/docker-compose.yml`), non-destructive diagnostic tooling (`tooling/checker/`), offline reachability calculators, and detection rules. - **AI-assisted**: multiple AI models and specialized subagents were used under human direction for review, lab orchestration and documentation checks; every finding was validated against commands, outputs or public sources. See `METHODOLOGY.md`. ## Executive summary @@ -107,7 +107,7 @@ Empirical evaluation of the E-OOM primitive on PHP-FPM 8.4.26 (`fpmdbg` behind N | CVE-2026-115560 | `php_binary` config + runtime `ini_set()` | Bounded attempt: decode failure observed, no memory safety impact | `INCONCLUSIVE` | Section 12 | | Chunked `post_max_size` | Unpatched (8.2.34 / 8.3.35 / 8.4.26) | 30M parses full, 70M OOMs on 128M limit (worker lives) | `BYPASS CONFIRMED` | Section 12 | | Chunked `post_max_size` | Patched (8.4.27 / 8.5.12) | Rejected: `Actual POST length does not match Content-Length` | `PATCHED` | Section 12 | -| Chain D (`EG` at shutdown) | Unpatched `mod_php` | `EG(objects_store)` identical before/after OOM; no crash | `UNSUPPORTED` | Section 12 | +| Chain D (`EG` at shutdown) | Unpatched `mod_php` | `EG(objects_store)` header identical before/after OOM, store empty at shutdown; no crash | `UNSUPPORTED as a stale-store claim` — the bucket-array variant is separate and open | Section 12, `CHAIN-D.md` | | `mod_php` 8.4.27 Control | Default Limits (128M / 8M) | Clean execution, no memory corruption | `PATCHED (0/5)` | Section 7 | | A/C/E Request-level chains | 8.4.26 / 8.4.27 / 8.5.12 | RCE conditional on application sinks (LFI / gadget / handler) | `APP DEPENDENT` | Section 3 | | Chain A with `strict=1` | All versions | File under attacker-chosen SID absent; injection blocked | `PREVENTED` | Section 3 | diff --git a/raw/chain-d-eg-shutdown.txt b/raw/chain-d-eg-shutdown.txt new file mode 100644 index 0000000..cc20513 --- /dev/null +++ b/raw/chain-d-eg-shutdown.txt @@ -0,0 +1,54 @@ +chain D / EG(objects_store) at shutdown — measurement of 2026-10-24 +=================================================================== + +Question: is the object store left stale or dangling when a request dies in the middle of +its startup, so that the next request on the same worker can consume it? + +Target +------ +php:8.4.26-apache (mod_php, prefork), memory_limit 128M, post_max_size 8M +Worker pid 17, same worker for all three requests +Breakpoint: php_request_shutdown, main/main.c:1891 + +Sequence on that worker +----------------------- +1. warmup GET -> HTTP 200 +2. chunked 70M at startup -> HTTP 500, "Allowed memory size of 134217728 bytes exhausted + (tried to allocate 102400 bytes) in Unknown on line 0" +3. GET after the OOM -> HTTP 200 (the worker survived) + +At the shutdown breakpoint, three observations, one per request: + +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 +SHUTDOWN ob=0x7f1b30451000 top=1 size=1024 flh=-1 err=30719 +0x7f1b30451000: 0x0000000000000000 0x0000000000000000 + +Second run, same shape, with the VM stack included: + +SHUTDOWN ob=0x7f0ed8651000 top=1 size=1024 flh=-1 err=30719 ced=(nil) vs=0x7f0ed8611000 vst=0x7f0ed8611020 vse=0x7f0ed8651000 (x3) + +Reading +------- +- The store pointer (`ob`), its next-free slot (`top=1`), its capacity (`size=1024`) and the + free-list head (`flh=-1`) are identical in all three shutdowns. +- `top=1` with handle 0 reserved means the store holds no live objects at shutdown, and the + first two bucket entries read as zero. That is consistent across the healthy request, the + one that died at startup, and the one that followed. +- `current_execute_data`, `vm_stack` top and end are also identical. +- No crash, no dangling pointer, and the worker kept serving. + +What this measures, and what it does not +---------------------------------------- +Measured: the state of the object store header at shutdown is the same whether or not the +request died in the middle of startup. The "stale EG(objects_store)" framing, where the +pointer or its bookkeeping is left behind for the next request, is not supported by this. + +Not measured: the contents of the entire 1024-entry bucket array, and the case where the +request reaches shutdown with live objects. Other work covers that variant and reaches further +than this measurement does; this file does not contradict it. + +Raw source: private audit repository, evidence/fase8-multisapi/evidence-chaind/ +(eg.log, eg_a.log, summary.txt, summary2.txt).