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
2 changes: 2 additions & 0 deletions COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ leak). Raw: `raw/fixed-branches-verify.txt`.
per OOM and `mod_php`-only, and on `mod_php` the leak and the stale-`BG(unserialize)`
primitive are mutually exclusive.
- Kernel/allocator portability is not solved; public PoC results vary by host.
`PORTABILITY.md` records the measured matrix behind that sentence: `KeepAlive On`,
proxy buffering and an unsaturated worker pool each break the chain on their own.
- A/C/E prerequisites are application-specific; the matrix records the lab conditions, not a
universal exploitability claim.
- The Nextcloud 33.0.x test deployment was used for configuration validation, not as the RCE
Expand Down
48 changes: 48 additions & 0 deletions PORTABILITY.es.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Portabilidad: por qué la misma PoC da números distintos

Cada celda se midió en el mismo laboratorio — loopback, `php:8.4.26-apache`, tres intentos por
celda salvo que se indique — y responde una pregunta: qué condiciones **sí** condicionan la
cadena y cuáles solo lo parecen.

| Configuración | Resultado | Abortos |
|---|---|---|
| `display_errors=On`, `memory_limit=8M`, 1 worker | **8/8** | 32 (8 corridas) |
| `display_errors=Off`, `memory_limit=8M`, 1 worker | 0/3 | 12 |
| `On`, `512M`, 1 worker, campo de 12 MB | 0/3 | 0 |
| `On`, `512M`, 1 worker, campo de 480 MB | **1/1 PROOF** | 4 |
| `On`, `8M`, 4 workers, exploit estándar sin saturar | 0/3 | 0 |
| `On`, `8M`, 4 workers, saturación (retener N-1) | **6/6 PROOF** | 24 |
| `On`, `8M`, 8 workers, saturación (retener N-1) | **4/4 PROOF** | 16 |
| `On`, `8M`, 16 workers, saturación (retener N-1) | **3/3 PROOF** | 12 |
| `On`, `8M`, 4 workers, proxy nginx, buffering apagado, trickle, 64k | **3/3 PROOF** | 12 |
| `On`, `8M`, 4 workers, proxy nginx, **buffering encendido** | 0/1 | n/d |
| `On`, `8M`, 4 workers, **`KeepAlive On`** | 0/5 | 20 |
| `Off`, `8M`, 1 worker, build debug (primitivo A, oráculo bin-128) | **4/4 OOM-in-cookie** (NB=235, K=4) | n/d |
| `On`, `8M`, 1 worker, imagen oficial, ASLR encendido | **3/3 PROOF RCE** | ~12 |
| `Off`, `8M`, 1 worker, imagen oficial, warmup A+B, ASLR on/off | 15/15 cookie de 4000 B dentro de su ventana (12 on / 3 off); repetición controlada **0/3** — claim de estabilidad retirado | n/d |
| `Off`, `8M`, 1 worker, imagen oficial, ASLR **apagado**, direcciones fijas | **3/3 PROOF RCE** | n/d |
| `Off`, `8M`, 1 worker, imagen oficial, ASLR **encendido** | **0/3** — bloqueado por el leak de `libphp` | n/d |
| `On`, `8M`, 1 worker, kernel pve 7.0, ASLR encendido | 0/3 | n/d |

## Lo que dice la matriz

1. **`memory_limit` no es el candado: lo es `post_max_size`.** Con 512M/480 MB la cadena corre
igual, porque el cuerpo cabe; con los defaults 128M/8M el A de 3.7 MB no agota la memoria y
el primitivo no arranca. La fila 512M/480 MB es una configuración no-default (del tipo que
corre un Nextcloud) y es con `display_errors=On`.
2. **`display_errors=Off` bloquea exactamente una cosa: el leak de `libphp`.** El primitivo A y
el `chunk_base` sobreviven; lo que desaparece es el puntero que convierte una escritura en
heap en un destino de código.
3. **En prefork, saturar workers es obligatorio.** Sin retener a los otros hijos, el OOM cae en
un hijo que no es el que llevará la siguiente petición y la cadena muere en 0/3. Reteniendo
`N-1` sale 6/6, 4/4 y 3/3 con 4, 8 y 16 workers.
4. **Un proxy tiene que dejar de bufferear.** Con buffering apagado y entrega por goteo la
cadena sobrevive; con buffering encendido no, porque la conexión nunca queda en el estado que
el OOM necesita.
5. **`KeepAlive On` la rompe**: 0/5. Es la mitigación más barata de la matriz y no está en PHP.
6. **Una corrida suelta no prueba nada.** La fila 15/15 fue el claim retirado de la cookie:
dentro de su ventana reproducía, en repetición controlada dio 0/3. Cada número marcado
`PROOF` viene de una celda repetida hasta estabilizarse.

Esta es la respuesta a "por qué la PoC pública funciona en mi máquina y en la tuya no": la
diferencia suele ser el pool de workers, el buffering del proxy o `KeepAlive`, no el build de PHP.
49 changes: 49 additions & 0 deletions PORTABILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Portability: why the same PoC gives different numbers

Every cell below was measured in the same lab — loopback, `php:8.4.26-apache`, three attempts
per cell unless stated — and answers one question: which conditions actually gate the chain,
and which ones only look like gates.

| Configuration | Result | Aborts |
|---|---|---|
| `display_errors=On`, `memory_limit=8M`, 1 worker | **8/8** | 32 (8 runs) |
| `display_errors=Off`, `memory_limit=8M`, 1 worker | 0/3 | 12 |
| `On`, `512M`, 1 worker, 12 MB field | 0/3 | 0 |
| `On`, `512M`, 1 worker, 480 MB field | **1/1 PROOF** | 4 |
| `On`, `8M`, 4 workers, standard exploit, no saturation | 0/3 | 0 |
| `On`, `8M`, 4 workers, saturation (hold N-1) | **6/6 PROOF** | 24 |
| `On`, `8M`, 8 workers, saturation (hold N-1) | **4/4 PROOF** | 16 |
| `On`, `8M`, 16 workers, saturation (hold N-1) | **3/3 PROOF** | 12 |
| `On`, `8M`, 4 workers, nginx proxy, buffering off, trickle, 64k | **3/3 PROOF** | 12 |
| `On`, `8M`, 4 workers, nginx proxy, **buffering on** | 0/1 | n/a |
| `On`, `8M`, 4 workers, **`KeepAlive On`** | 0/5 | 20 |
| `Off`, `8M`, 1 worker, debug build (primitive A, bin-128 oracle) | **4/4 OOM-in-cookie** (NB=235, K=4) | n/a |
| `On`, `8M`, 1 worker, official image, ASLR on | **3/3 PROOF RCE** | ~12 |
| `Off`, `8M`, 1 worker, official image, A+B warmup, ASLR on/off | 15/15 cookie at 4000 B inside its window (12 on / 3 off); controlled repetition **0/3** — stability claim withdrawn | n/a |
| `Off`, `8M`, 1 worker, official image, ASLR **off**, fixed addresses | **3/3 PROOF RCE** | n/a |
| `Off`, `8M`, 1 worker, official image, ASLR **on** | **0/3** — blocked by the `libphp` leak | n/a |
| `On`, `8M`, 1 worker, kernel pve 7.0, ASLR on | 0/3 | n/a |

## What the matrix says

1. **`memory_limit` is not the gate; `post_max_size` is.** At 512M/480 MB the chain runs the
same, because the body fits; with the default 128M/8M the 3.7 MB A never exhausts memory and
the primitive never starts. The 512M/480 MB row is a non-default configuration (the kind a
Nextcloud deployment runs), and it is `display_errors=On`.
2. **`display_errors=Off` blocks exactly one thing: the `libphp` leak.** Primitive A and the
`chunk_base` survive; what disappears is the pointer that turns a heap write into a code
target.
3. **On prefork, worker saturation is mandatory.** Without holding the other children, the OOM
lands on a child that is not the one that will carry the next request, and the chain dies at
0/3. Holding `N-1` gives 6/6, 4/4 and 3/3 at 4, 8 and 16 workers.
4. **A proxy has to stop buffering.** With nginx buffering off and trickle delivery the chain
survives; with buffering on it does not, because the connection never sits in the state the
OOM needs.
5. **`KeepAlive On` breaks it**: 0/5. This is the cheapest mitigation on the matrix and it is
not in PHP at all.
6. **A single measured run proves nothing.** The 15/15 row was the withdrawn cookie claim: in
its window it reproduced, in controlled repetition it gave 0/3. Every number here that says
`PROOF` came from a cell where the counts were repeated until they were stable.

This is the answer to "why does the public PoC work on my machine and not on yours": the
difference is usually the worker pool, the proxy buffering, or `KeepAlive`, not the PHP build.
4 changes: 2 additions & 2 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.9.2_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.9.2)
[![Release](https://img.shields.io/badge/Release-v1.10.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.10.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**: 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), 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
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.9.2_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.9.2)
[![Release](https://img.shields.io/badge/Release-v1.10.0_(2026--11--09)-0284c7?style=flat-square&logo=github)](https://github.com/JoshRob297/php2shell-research/releases/tag/v1.10.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.9.2, 2026-11-09 (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.10.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**: 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), 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 @@ -75,7 +75,7 @@ Standard out-of-the-box PHP configurations use `memory_limit = 128M` and `post_m

### 2. The Session HashTable is out of the cookie window, but the channel leaks heap
Using non-intrusive GDB worker probing (`php_session_reset_id` & `php_session_track_init`), we tracked `PS(id)` and session `HashTable` allocations across requests. Due to the LIFO release order of superglobals, the `HashTable` is consistently positioned **before** the `PS(id)` pointer or separated across different 2 MB allocator chunks (delta > 4.5 MB), so the forward cookie window cannot reach session data. Testing with custom destructor objects (`pDestructor`) confirmed that userland destructor ordering cannot reorder the pre-request allocations done during `rfc1867_post_handler`.
The forged cookie does over-read past its own filler: at the 4000/5000-byte read size the measured values carry 525-841 bytes outside the session alphabet, including **47 pointer-like values** inside the id's 2 MB chunk (Zend MM bin-64 free-list links), disclosing the chunk base. The distance from that base to libphp text varied between `0x289bc00` and `0x2a7dc00` (1.88 MB) across 20 ASLR-on runs, with the low 12 bits constant (`delta % 0x1000 == 0xc00`), so **at this read size** the leak narrows libphp to a ~481-page window without pinning it. That is a property of the 4000-byte read, not of the channel: with a forged `zend_string` of `len=0xC8000` and `display_errors=On` the same cookie returns ~800 KB and the chain pins libphp. `9bie/php2shell-bypass-aslr-exp` reports 10/10 on the stock profile; under `display_errors=On` we replicated the chain 8/8 with ASLR on and 5/5 with ASLR off, while under `display_errors=Off` with ASLR on it stops at stage 1 in 0/3 runs in the same lab profile (`raw/9bie-display-off-3run.txt`); the author's 128M/150-worker Off profile was not reproduced here (`POCS.md`, 9bie entry; raw sweeps `raw/9bie-aslr-on-8run.txt` and `raw/9bie-aslr-off-5run.txt`, stage-2 base resolution in `raw/rce-on-aslr-on-8426.txt`). The session-table read remains unreachable and the channel stays `mod_php`-only and single-shot per OOM. See `validation/`.
The forged cookie does over-read past its own filler: at the 4000/5000-byte read size the measured values carry 525-841 bytes outside the session alphabet, including **47 pointer-like values** inside the id's 2 MB chunk (Zend MM bin-64 free-list links), disclosing the chunk base. The distance from that base to libphp text varied between `0x289bc00` and `0x2a7dc00` (1.88 MB) across 20 ASLR-on runs, with the low 12 bits constant (`delta % 0x1000 == 0xc00`), so **at this read size** the leak narrows libphp to a ~481-page window without pinning it. That is a property of the 4000-byte read, not of the channel: with a forged `zend_string` of `len=0xC8000` and `display_errors=On` the same cookie returns ~800 KB and the chain pins libphp. `9bie/php2shell-bypass-aslr-exp` reports 10/10 on the stock profile; under `display_errors=On` we replicated the chain 8/8 with ASLR on and 5/5 with ASLR off, while under `display_errors=Off` with ASLR on it stops at stage 1 in 0/3 runs in the same lab profile (`raw/9bie-display-off-3run.txt`); the author's 128M/150-worker Off profile was not reproduced here (`POCS.md`, 9bie entry; the full matrix is in `PORTABILITY.md`; raw sweeps `raw/9bie-aslr-on-8run.txt` and `raw/9bie-aslr-off-5run.txt`, stage-2 base resolution in `raw/rce-on-aslr-on-8426.txt`). The session-table read remains unreachable and the channel stays `mod_php`-only and single-shot per OOM. See `validation/`.

### 3. The leak channel is `mod_php`-only
On FPM, payload A makes the worker exit (pool logs `child ... exited with code 70`, nginx returns 502); the follow-up request runs in a new process with a fresh 32-byte id and never reuses the stale one.
Expand Down
Loading
Loading