
PHP CVE-2026-109618/109619 — самодостаточный RCE без аутентификации на стандартном PHP 8.5.11 (цепочка раскрытия памяти in-band PS(id)→cookie + UAF), полные лестницы контроля, негативный контроль на исправленной версии 8.5.12
Пакет для воспроизведения лабораторной среды и исследования уязвимостей для класса php2shell в PHP 8.5.11 (CVE-2026-109618, CVE-2026-109619), октябрь 2026. Статус: доказано — самодостаточный RCE (четыре обычных HTTP-запроса, без отладчика, без локального доступа), плюс результаты по амплификации, пробелам в патче и устаревшему состоянию FPM, приведённые ниже.
Содержание: Результаты · Цепочки · Амплификация · Пробелы в патче · Устаревшее состояние FPM · Обход WAF · Окружение · Быстрый старт · Дисциплина доказательств · Структура · Публичный контекст · Заявление
| # | Находка | Классификация |
|---|---|---|
| 1 | Сквозной самодостаточный RCE на PHP 8.5.11 — четыре обычных HTTP-запроса, внутриполосное получение адресов через раскрытие PS(id)→cookie, обе базы из одной утечки 512-байтного PHPSESSID; 12/12 срабатываний, uid=33, 8 генераций ASLR | Доказано — самодостаточный RCE |
| 2 | 52-кратная амплификация памяти через дублирующиеся 6-байтные multipart-заголовки — обходит post_max_size (тело 2.57 МБ вызывает OOM при лимите 128M) на PHP 7.4–8.5.11 | Доказано — лаборатория, все четыре версии |
| 3 | Три пути эксплуатации переживают патч безопасности 8.5.12 (UAF повторного входа var_destroy при n=1015, несоответствие serialize-обработчика, инъекция upload-progress) | Доказано — ASAN на проверенном через GPG 8.5.12 |
| 4 | Устаревший BG(unserialize) в FPM: воркер переживает OOM при декодировании сессии, устаревшее состояние потребляется окном rfc1867 следующего запроса, потребление сформированной кучи вызывает SIGSEGV воркера | Доказано — механизм + DoS; RCE не доказан |
| 5 | Обход разделителя Content-Type (multipart/form-data, boundary=X) — любая опубликованная WAF-сигнатура, привязанная к Content-Type, обходится | Доказано — поведение парсера SAPI.c |
| 6 | Обе цепочки CVE мертвы на пропатченном 8.5.12 (идентичная по токенам конфигурация, проверенный через GPG исходный код) | Доказано — негативный контроль |
| 7 | Публичная теория «кэшированного zval session.upload_progress» — опровергнута на 4 стоковых версиях PHP + 3 вендорских сборках LiteSpeed, во всех углах очистки/конкурентности | Опровергнуто — исчерпывающе |
Две независимые цепочки повреждения памяти достигают контролируемых атакующим косвенных вызовов на стоковом образе php:8.5.11-apache; сквозная обёртка объединяет механизм Цепочки B с новой внутриполосной утечкой для полностью удалённого срабатывания.
| Цепочка A — устаревший бакет | Цепочка B — висячая ссылка | |
|---|---|---|
| Механизм | Аварийный выход по OOM оставляет бакет HashTable опубликованным, но неинициализированным; завершение потребляет его как поддельный zval → поддельный массив → pDestructor | OOM перед RINIT оставляет PS(http_session_vars) с висячей zend_reference; живая 31-байтная строка перехватывает её; поддельная ссылка → pDestructor в zend_hash_destroy |
| Аллокатор | tracked/system (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 с пустым cookie сохраняет send_cookie=1;
поле размером 24 MiB вызывает OOM до RINIT). Leak-B размещает массив из 257 элементов, чей
zend_array перекрывает PS(id)−0x10: ZSTR_LEN читает nTableSize=512, а
URL-кодированное cookie несёт 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 байт в сети) эксплуатируют фиксированный размер SMART_STR_START_SIZE в 256 байт на каждый сохранённый заголовок в PHP (~314 Б памяти на 6 Б в сети). Измерено точной бисекцией порога OOM на 8.5.11; идентично на 7.4.33 / 8.1.34 / 8.4.26 (51–52x):
| memory_limit | мин. тело для OOM | усиление |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Все тела укладываются в стандартный post_max_size=8M — практический барьер загрузки преодолён на каждой неисправленной версии. Чанковая передача дополнительно обходит предварительную проверку Content-Length (чанковое тело 8.85 MB разобрано и вызвало OOM, тогда как то же тело с Content-Length было отклонено). Патч 8.5.12 закрывает оба вектора (фильтрация неизвестных заголовков + потоковое применение post_max_size). Форма «бомбы» из вложенных массивов не воспроизводится как экспоненциальная (максимум 9.6x, 1.16 MiB — не может вызвать OOM даже при 8M). Подробности: 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.|, записанная через php_serialize, декодированная обработчиком php после session_start() в фазе приложения, инстанцирует гаджет как на mod_php php_value, так и на FPM .user.ini; защитная проверка incomplete_classes_only из патча ограничена только окном загрузки до RINIT.Проверено в лаборатории на php:8.5.11-fpm: сессия размером 24 MB при memory_limit=32M вызывает OOM внутри php_var_unserialize, и воркер выживает — барьер выхода с кодом 70 применяется только к OOM при запуске. gdb показывает BG(unserialize).level=1 и устаревший указатель на данные, сохраняющийся в окне rfc1867 до RINIT следующего запроса; multipart-поле размером 8191 байт порождает _emalloc на 8216 байт, который перекрывает устаревший указатель и вызывает SIGSEGV воркера в var_tmp_var во время запуска запроса. Измеренное воздействие: воспроизводимый краш воркера (DoS) плюс примитив повреждения памяти; RCE за пределами краша не доказан. Предварительные условия: низкий memory_limit, большая сессия, на которую может влиять атакующий, эндпоинт с session_start(), upload_progress=On и pm=static max_children=1 для надёжного сопоставления в одном воркере. Таким образом, FPM защищён условно, а не категорически, для этого класса.