Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions CHAIN-D.es.md
Original file line number Diff line number Diff line change
@@ -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.
74 changes: 74 additions & 0 deletions CHAIN-D.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)](#)
Expand All @@ -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`.

---
Expand Down Expand Up @@ -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 |
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)](#)
Expand All @@ -19,10 +19,10 @@ Contact: Telegram [@JocesLab](https://t.me/JocesLab) · email [joshrob.sec@proto
---
</div>

- **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
Expand Down Expand Up @@ -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 |
Expand Down
Loading
Loading