
PHP CVE-2026-109618/109619 — 기본 PHP 8.5.11에서 자체 포함된 비인증 RCE(인밴드 PS(id)→쿠키 메모리 노출 + UAF 체인), 전체 제어 사다리, 8.5.12에서의 수정 버전 음성 대조
PHP 8.5.11 php2shell 클래스(CVE-2026-109618, CVE-2026-109619)에 대한 실험실 재현 및 취약점 연구 패키지, 2026년 10월. 상태: 입증됨 — 자체 완결형 RCE (네 개의 일반 HTTP 요청, 디버거 없음, 로컬 접근 없음), 그리고 아래의 증폭, 패치 갭, FPM 스테일 상태 발견 사항 포함.
목차: 결과 · 체인 · 증폭 · 패치 갭 · FPM 스테일 상태 · WAF 우회 · 환경 · 빠른 시작 · 증명 규율 · 구성 · 공개 맥락 · 성명
| # | 발견 사항 | 분류 |
|---|---|---|
| 1 | PHP 8.5.11에서의 종단 간 자체 완결형 RCE — 네 개의 일반 HTTP 요청, PS(id)→쿠키 노출을 통한 인밴드 주소 소싱, 단일 512바이트 PHPSESSID 유출로부터 두 베이스 모두 획득; 12/12 발화, uid=33, 8개 ASLR 세대 | 입증됨 — 자체 완결형 RCE |
| 2 | 중복된 6바이트 멀티파트 헤더를 통한 52배 메모리 증폭 — PHP 7.4–8.5.11에서 post_max_size 무력화 (2.57 MB 본문이 128M 제한을 OOM으로 만듦) | 입증됨 — 실험실, 네 버전 모두 |
| 3 | 세 가지 익스플로잇 경로가 8.5.12 보안 패치를 견딤 (n=1015에서 var_destroy 재진입 UAF, serialize 핸들러 불일치, 업로드 진행 주입) | 입증됨 — GPG 검증된 8.5.12에서 ASAN |
| 4 | FPM 스테일 BG(unserialize): 워커가 세션 디코드 OOM에서 살아남고, 스테일 상태가 다음 요청의 rfc1867 윈도우에 의해 소비되며, 힙 형태의 소비가 워커를 SIGSEGV로 만듦 | 입증됨 — 메커니즘 + DoS; RCE 미입증 |
| 5 | Content-Type 구분자 우회 (multipart/form-data, boundary=X) — 공개된 모든 Content-Type 기반 WAF 시그니처가 우회 가능 | 입증됨 — SAPI.c 파서 동작 |
| 6 | 패치된 8.5.12에서 두 CVE 체인 모두 사망 (토큰 동일 configure, GPG 검증 소스) | 입증됨 — 음성 대조 |
| 7 | 공개된 "캐시된 zval session.upload_progress" 이론 — 4개 스톡 PHP 버전 + 3개 LiteSpeed 벤더 빌드, 모든 정리/동시성 코너에서 반박됨 | 반박됨 — 철저히 |
두 개의 독립적인 메모리 손상 체인이 스톡 php:8.5.11-apache 이미지에서
공격자 제어 간접 호출에 도달하며, 종단 간 래퍼는 체인 B의 메커니즘과
완전 원격 발화를 위한 새로운 인밴드 노출을 결합합니다.
| 체인 A — 스테일 버킷 | 체인 B — 댕글링 참조 | |
|---|---|---|
| 메커니즘 | OOM 베일아웃이 HashTable 버킷을 게시된 채로 미초기화 상태로 남김; 종료 시 이를 가짜 zval로 소비 → 가짜 배열 → pDestructor | RINIT 전 OOM이 PS(http_session_vars)를 댕글링 zend_reference를 보유한 채로 남김; 살아있는 31바이트 문자열이 이를 재점유; 위조된 참조 → zend_hash_destroy pDestructor |
| 할당자 | 추적/시스템 (USE_ZEND_ALLOC=0) | 스톡 기본 Zend MM |
| 종단 | libc system() (gdb 보정 주소) | zif_system @ libphp+0x5996e0 (libc 없음) |
| 발화 시 디버거 | 보정 전용 | 없음 — FIRE_GDB=0 |
| 재현성 | 2개의 무작위 부모 세대 | 반복됨, 교차 실행 포함 |
Leak-A는 매달린 PS(id)를 무장합니다(1001번째 변수가 max_input_vars를 초과시켜 →
headers_sent=1; 빈 쿠키 reset_id 실패는 send_cookie=1을 유지;
24 MiB 필드는 RINIT 이전에 OOM을 발생시킴). Leak-B는 zend_array가
PS(id)−0x10과 겹치는 257개 요소 배열을 배치합니다: ZSTR_LEN은 nTableSize=512를 읽고
URL 인코딩된 쿠키는 pDestructor(→ libphp 베이스)와 아홉 개의 arData
힙 포인터(다수결 → 청크 베이스)를 담습니다. 측정된 청크−libphp 델타는
세대에 걸쳐 일정하지 않으며(−0x1a00000 / +0x3600000 / −0x1c00000),
따라서 이 정보 노출은 실제로 핵심적인 역할을 합니다. 이후 공격은
유출된 주소를 사용하여 실행됩니다. 전제 조건: display_errors=On, post_max_size가
OOM 필드보다 크고, memory_limit이 그보다 작으며, 단일 워커 prefork.
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)
전체 영수증 및 제어 사다리: docs/END-TO-END.md.
최적화된 중복 헤더 증폭: 짧은 X: a 헤더(와이어 6바이트)는 저장된 헤더당 PHP의 고정 256바이트 SMART_STR_START_SIZE를 악용합니다(와이어 6바이트당 약 314B 메모리). 8.5.11에서 정확한 OOM 임계값 이분법으로 측정했으며, 7.4.33 / 8.1.34 / 8.4.26에서도 동일합니다(51–52배):
| memory_limit | 최소 OOM 본문 | 증폭 |
|---|---|---|
| 8M | 153 KB | 54.8배 |
| 32M | 636 KB | 52.8배 |
| 128M | 2.57 MB | 52.3배 |
| 256M | 5.14 MB | 52.2배 |
모든 본문이 기본 post_max_size=8M 안에 들어갑니다 — 실질적인 업로드 장벽이 무력화됩니다 패치되지 않은 모든 버전에서. 청크 프레이밍은 추가로 Content-Length 사전 검사를 우회합니다(8.85 MB 청크가 파싱되어 OOM이 발생했으며, 동일한 본문을 Content-Length와 함께 보내면 거부됨). 8.5.12 패치는 두 벡터를 모두 차단합니다(알 수 없는 헤더 필터링 + 스트리밍 post_max_size 적용). 중첩 배열 "폭탄" 형태는 지수적으로 재현되지 않습니다(최대 9.6배, 1.16 MiB — 8M에서도 OOM 불가). 세부 사항: docs/RELEASE-RECONCILIATION-2026-10-21.md.
GPG 검증되고 토큰이 동일한 8.5.12 ASAN 빌드에서 검증됨 — 두 CVE는 수정되었지만, 패치 이후에도 영향을 미치는 세 가지 별개의 경로가 남아 있습니다:
var_unserializer.c:906:67에서 폴트); 패치의 ZEND_HAS_OOM 가드는 이 경로에서 무효이며; BG(serialize_lock)은 여전히 __destruct 디스패치 주변에서 획득되지 않습니다.session_start() 이후 php 핸들러가 디코딩하는 php_serialize로 작성된 | 키 세션은 mod_php php_value와 FPM .user.ini 모두에서 가젯을 인스턴스화합니다; 패치의 incomplete_classes_only 가드는 RINIT 이전 업로드 윈도우에만 적용됩니다.php:8.5.11-fpm에서 실험실 검증됨: memory_limit=32M에서 24 MB 세션이 php_var_unserialize 내부에서 OOM을 일으키고 워커는 살아남습니다 — exit-70 장벽은 시작 OOM에만 적용됩니다. gdb는 BG(unserialize).level=1과 오래된 데이터 포인터가 다음 요청의 rfc1867 RINIT 이전 윈도우까지 지속되는 것을 보여줍니다; 8191바이트 멀티파트 필드는 오래된 포인터와 겹치는 8216바이트 _emalloc을 생성하고 요청 시작 중 var_tmp_var에서 워커에 SIGSEGV를 발생시킵니다. 측정된 영향: 반복 가능한 워커 크래시(DoS) 및 메모리 손상 프리미티브; 크래시를 넘어선 RCE는 입증되지 않았습니다. 전제 조건: 낮은 memory_limit, 공격자가 영향을 준 대형 세션, session_start() 엔드포인트, upload_progress=On, 그리고 안정적인 동일 워커 페어링을 위한 pm=static max_children=1. 따라서 FPM은 이 클래스에 대해 조건부로 보호되며, 범주적으로 보호되지는 않습니다.
Content-Type: multipart/form-data, boundary=X(쉼표 구분자)는 PHP에서 허용됩니다 — SAPI.c는 ;, ,, 공백에서 자릅니다. Content-Type: multipart/form-data;에 고정된 모든 공개된 php2shell WAF 시그니처는 우회 가능합니다. 방어자 지침은 docs/WAF-BYPASS.md에 있습니다.
display_errors=Off, 높은 memory_limit은 모두 실제 장벽으로 남아 있음 — docs/RELEASE-RECONCILIATION-2026-10-21.md의 플릿 연구 참조);| 속성 | 필수 값 |
|---|---|
| 호스트 | 네이티브 Linux x86_64 |
| 대상 | PHP 8.5.11, Apache 모듈 (Chain B는 stale-state 작업을 위해 FPM에서도 실행) |
| 이미지 | php@sha256:974e3a920309308e1690ed607e6cc3061d6906f90cc8d30fe519daa48bccf101 |
| Chain A 할당자 | USE_ZEND_ALLOC=0, USE_TRACKED_ALLOC=1 (의도적으로 비기본값) |
| Chain B 할당자 | 기본 Zend MM |
| PHP 제한 (체인) | memory_limit=16M, post_max_size=32M, upload_max_filesize=32M |
| Apache | prefork, 워커 정확히 하나 |
| 디버거 | Python 지원이 포함된 호스트 GDB (교정/영수증 전용) |
소유하고 격리된 x86_64 Linux 랩에서만 실행하십시오:
sudo ./scripts/preflight.sh
sudo ./scripts/reproduce.sh
러너는 환경을 검증하고, 바이트 단위로 동일한
5,767,505바이트 픽스처를 생성하며, 안전한 type-0x7 슬롯을 보정한 다음,
독립적인 마커 바이트, 워커 전환, 상태, 트레이스 검사를 포함한
새 워커 부정 → 중단 → 마커를 실행하고, 전체 SHA-256
매니페스트를 작성합니다. 가드된 배치 미스는 기록되며 새 워커마다
한 번 재시도됩니다 — 미스는 실행으로 보고되지 않습니다. 보존된 판정은
runs/DATE/VERDICT.json의 모든 게이트가 true일 때만 유효합니다. 발사 없이
재검증하려면: python3 scripts/verify_run.py runs/DATE.
sudo ./scripts/lab-stop.sh # stop container, keep state
sudo ./scripts/lab-reset.sh # remove PoC container + network only
| 사례 | 기대 효과 | RCE 오라클 |
|---|---|---|
| 교정 | Type-0x7 스테일 슬롯 관찰됨; 워커 생존 | 없음 |
| 부정 | 가짜 구조체 존재; 디스패치 없음; 동일 워커 | 마커 부재 |
| 중단 | Refcount 감소; 정확한 abort@plt; SIGABRT | 마커 부재 |
| 마커 | 정확한 대상 디스패치; /tmp 아래 명령 출력 | 정확한 고유 바이트 |
HTTP 오류, 빈 응답, PHP 치명적 오류 또는 워커 크래시는 명령 실행 증거로 인정되지 않습니다. FPM 스테일 상태 발견이 크래시 원시 요소로 보고되는 이유는 정확히 이 기준을 통과하지 못했기 때문입니다.
/tmp 아래에만 쓰는 제한된 id/printf 계열 명령으로 한정됨.app/ Minimal PHP/Apache target
tools/ php2shell_test.py (v1+v2) + domain_php2shell_scan.py
ninebie/ Chain B PoC (send/env scripts, fire entry point, receipts)
docs/ Proof boundary, port write-ups, reconciliation log
evidence/reference/ Chain A two-generation proof summary + provenance
evidence/validation-*/ Chain A compact evidence, two fresh generations
scripts/ Chain A lab, calibration, body, runner, verifier
tests/ Deterministic fixture and safety tests
artifacts/, runs/ Generated locally; ignored by Git