
PHP CVE-2026-109618/109619 — RCE não autenticado autónomo em PHP 8.5.11 de origem (divulgação de memória in-band PS(id)→cookie + cadeia UAF), escadas de controlo total, controlo negativo de versão corrigida em 8.5.12
Pacote de reprodução de laboratório e pesquisa de vulnerabilidade para a classe php2shell do PHP 8.5.11 (CVE-2026-109618, CVE-2026-109619), outubro de 2026. Status: Comprovado — RCE autocontido (quatro requisições HTTP simples, sem depurador, sem acesso local), além de amplificação, lacunas de patch e descobertas de estado obsoleto do FPM abaixo.
Conteúdo: Resultados · Cadeias · Amplificação · Lacunas de patch · Estado obsoleto do FPM · Bypass de WAF · Ambiente · Início rápido · Disciplina de prova · Estrutura · Contexto público · Declaração
| # | Descoberta | Classificação |
|---|---|---|
| 1 | RCE autocontido de ponta a ponta no PHP 8.5.11 — quatro requisições HTTP simples, obtenção de endereço in-band via PS(id)→divulgação por cookie, ambas as bases a partir de um único vazamento de PHPSESSID de 512 bytes; 12/12 disparos, uid=33, 8 gerações de ASLR | Comprovado — RCE autocontido |
| 2 | Amplificação de memória de 52x via cabeçalhos multipart duplicados de 6 bytes — derrota post_max_size (corpo de 2,57 MB causa OOM em um limite de 128M) no PHP 7.4–8.5.11 | Comprovado — laboratório, todas as quatro versões |
| 3 | Três caminhos de exploração sobrevivem ao patch de segurança 8.5.12 (UAF de reentrada em var_destroy com n=1015, incompatibilidade de handler de serialização, injeção de progresso de upload) | Comprovado — ASAN no 8.5.12 verificado por GPG |
| 4 | BG(unserialize) obsoleto no FPM: o worker sobrevive a um OOM de decodificação de sessão, o estado obsoleto é consumido pela janela rfc1867 da próxima requisição, o consumo moldado no heap causa SIGSEGV no worker | Comprovado — mecanismo + DoS; RCE não comprovado |
| 5 | Bypass do separador de Content-Type (multipart/form-data, boundary=X) — toda assinatura de WAF publicada ancorada em Content-Type é contornável | Comprovado — comportamento do parser SAPI.c |
| 6 | Ambas as cadeias de CVE mortas no 8.5.12 corrigido (configure idêntico em tokens, código-fonte verificado por GPG) | Comprovado — controle negativo |
| 7 | A teoria pública de "zval em cache session.upload_progress" — refutada em 4 versões padrão do PHP + 3 builds de fornecedor LiteSpeed, todos os cantos de limpeza/concorrência | Refutado — exaustivo |
Duas cadeias independentes de corrupção de memória alcançam chamadas indiretas controladas pelo atacante na imagem padrão php:8.5.11-apache; o wrapper de ponta a ponta combina o mecanismo da Cadeia B com uma nova divulgação in-band para um disparo totalmente remoto.
| Cadeia A — bucket obsoleto | Cadeia B — referência pendente | |
|---|---|---|
| Mecanismo | O bailout de OOM deixa um bucket de HashTable publicado mas não inicializado; o shutdown o consome como um zval falso → array falso → pDestructor | OOM pré-RINIT deixa PS(http_session_vars) mantendo um zend_reference pendente; uma string viva de 31 bytes o recupera; referência forjada → zend_hash_destroy pDestructor |
| Alocador | rastreado/sistema (USE_ZEND_ALLOC=0) | Zend MM padrão de fábrica |
| Terminal | system() da libc (endereços calibrados via gdb) | zif_system @ libphp+0x5996e0 (sem libc) |
| Depurador no momento do disparo | apenas calibração | nenhum — FIRE_GDB=0 |
| Reprodutibilidade | 2 gerações pai randomizadas | repetida, inclusive entre execuções |
Leak-A arma um PS(id) pendente (a 1001ª variável dispara max_input_vars →
headers_sent=1; a falha do reset_id com cookie vazio mantém send_cookie=1;
um campo de 24 MiB causa OOM antes do RINIT). Leak-B posiciona um array de 257 elementos cujo
zend_array se sobrepõe a PS(id)−0x10: ZSTR_LEN lê nTableSize=512 e o
cookie codificado em URL carrega pDestructor (→ base do libphp) mais nove ponteiros de heap
arData (voto majoritário → base do chunk). O delta medido entre chunk e libphp
não é constante entre gerações (−0x1a00000 / +0x3600000 / −0x1c00000),
portanto a divulgação é genuinamente essencial. O disparo então ocorre com os
endereços vazados. Pré-requisitos: display_errors=On, post_max_size acima
do campo de OOM, memory_limit abaixo dele, prefork com um único 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)
Escada completa de recibo e controle: docs/END-TO-END.md.
Amplificação otimizada de cabeçalhos duplicados: cabeçalhos curtos X: a (6 bytes no wire)
exploram o SMART_STR_START_SIZE fixo de 256 bytes do PHP por cabeçalho salvo
(~314 B de memória por 6 B no wire). Medido por bisseção exata do limiar de OOM em
8.5.11; idêntico em 7.4.33 / 8.1.34 / 8.4.26 (51–52x):
| memory_limit | corpo mínimo para OOM | amplificação |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Todos os corpos cabem dentro de um post_max_size=8M padrão — a barreira prática de upload é derrotada em todas as versões sem patch. O enquadramento chunked
também contorna a pré-verificação de Content-Length (8.85 MB em chunked foi parseado
e sofreu OOM onde o mesmo corpo com Content-Length foi rejeitado). O
patch 8.5.12 fecha ambos os vetores (filtragem de cabeçalhos desconhecidos + aplicação de post_max_size em streaming). A forma de "bomba" de array aninhado não
se reproduz como exponencial (máx. 9.6x, 1.16 MiB — não consegue causar OOM nem em 8M).
Detalhes: docs/RELEASE-RECONCILIATION-2026-10-21.md.
Verificado em um build ASAN 8.5.12 verificado por GPG e idêntico em token — os dois CVEs estão corrigidos, mas três caminhos separados para impacto sobrevivem ao patch:
var_unserializer.c:906:67); as guardas
ZEND_HAS_OOM do patch são inertes neste caminho; BG(serialize_lock) ainda
não é adquirido em torno do despacho de __destruct.| escrita por php_serialize
decodificada pelo handler php após um session_start() na fase de aplicação
instancia o gadget tanto em php_value do mod_php quanto em .user.ini do FPM;
a guarda incomplete_classes_only do patch está limitada apenas à janela de upload
pré-RINIT.