РАЗБОР
Статья
Часть 1. PHP2Shell (CVE-2026-109618 и 109619)
[ обложка статьи ]
Режим чтения
Что вас ждёт в статье:
Краткая сводка
1. Как PHP обрабатывает запрос
2. CVE-2026-109618: поломка при нехватке памяти
3. CVE-2026-109619: use-after-free между запросами
4. В чём разница и почему это класс
Заключение
Источники
Краткая сводка
Две критические уязвимости в ядре PHP, и обе дают удалённое выполнение кода без авторизации. В этой части разберём только технику: как именно PHP ломается изнутри. Почему из-за этого падала половина интернета и как это выглядело со стороны, поговорим во второй части.
Обе уязвимости растут из того, как движок обращается с памятью. PHP написан на C, где разработчик управляет ею вручную, и обе ошибки — классические грабли этого подхода:
В первом случае память закончилась в самый неподходящий момент.
Во втором — программа попыталась прочитать данные из места, которое уже очистили.
Разберём по очереди, но для начала посмотрим, какой путь проходит обычный запрос, когда попадает внутрь PHP.
Обе уязвимости растут из того, как движок обращается с памятью. PHP написан на C, где разработчик управляет ею вручную, и обе ошибки — классические грабли этого подхода:
В первом случае память закончилась в самый неподходящий момент.
Во втором — программа попыталась прочитать данные из места, которое уже очистили.
Разберём по очереди, но для начала посмотрим, какой путь проходит обычный запрос, когда попадает внутрь PHP.
| Идентификаторы | CVE-2026-109618 и CVE-2026-109619 (связка «PHP2Shell») |
| Классы | 109618 — порча памяти при нехватке памяти (OOM); 109619 — use-after-free между запросами (+ раскрытие памяти) |
| Продукт | Ядро PHP; под ударом mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed |
| Уязвимые версии | 8.2 до 8.2.35, 8.3 до 8.3.36, 8.4 до 8.4.27, 8.5 до 8.5.12; 7.4–8.1 — лишь сорс-бэкпорты; 5.4–7.3 EOL |
| Исправлено в | 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 (21 октября 2026) |
| CVSS | 9.2 Critical по 4.0, unauth RCE (не 10.0 — та цифра гуляла до релиза по ошибке) |
| Нашли | LCFR (Calif.io) — эксплойт и разбор; видео — Kirill Firsov; благодарности PHP: Calif, OpenAI, Wiz и др. |
1. Как PHP обрабатывает запрос
Чтобы понять обе уязвимости, надо на секунду заглянуть внутрь. Когда на сайт прилетает запрос, его обрабатывает не сам скрипт напрямую. Сначала запрос проходит через прослойку Server API: через этот переходник веб-сервер общается с движком PHP. Вариантов несколько: mod_php (PHP встроен прямо в Apache), PHP-FPM, php-cgi, а ещё FrankenPHP и LiteSpeed. Обёртки разные, а задача одна — довести запрос пользователя до ядра.
Ядро PHP написано на C и памятью управляет вручную: движок сам запрашивает у системы куски под данные и сам же их потом освобождает. Скорость от этого огромная, а платить за неё приходится максимальной концентрацией, ведь любой косяк с памятью в C ведёт не к аккуратному сообщению об ошибке, а к жёсткой порче данных на самом низком уровне.
Движок (это и есть ядро, Zend Engine) на каждый запрос проходит один и тот же цикл: инициализация, выполнение скрипта, завершение, очистка. При инициализации он выделяет память под всё, что понадобится: суперглобальные массивы вроде $_POST и $_FILES, тело запроса, внутренние структуры. Дальше крутится сам PHP-скрипт, а на завершении всё это освобождается, чтобы воркер взялся за следующий запрос чистым. Обе уязвимости живут ровно на границах этого цикла: одна срабатывает, когда инициализация срывается из-за нехватки памяти, вторая — когда очистка отработала не до конца и мусор уехал в следующий запрос.
Рис. 1. Путь запроса: от SAPI к ядру на C — и два слабых места ручной работы с памятью
Для второй уязвимости важно ещё что один рабочий процесс обслуживает много запросов подряд, потому что так эффективнее и не нужно каждый раз всё поднимать заново. Значит, от запроса к запросу в воркере может что-то оставаться.
Отсюда и два слабых места: что будет, если память кончится прямо посреди обработки, и что от прошлого запроса останется следующему. Логин, чтобы их задеть, не нужен, поскольку вредоносный запрос добивает до уязвимого кода раньше, чем стартует логика сайта.
Опаснее остальных mod_php. Здесь интерпретатор PHP встроен прямо в процесс Apache и живёт в его памяти, а запрос обрабатывает тот же процесс, что принял соединение. Поэтому вредоносный ввод добирается до уязвимого кода на самом раннем этапе, ещё до того, как отработает хоть строчка PHP сайта. PHP-FPM устроен по-другому. Это отдельный пул процессов, и для первой дыры он оказался менее уязвим, потому что цепочка очистки там срабатывает не так охотно. Но «менее» не значит «никак», так что FPM-админам расслабляться тоже рано.
Ядро PHP написано на C и памятью управляет вручную: движок сам запрашивает у системы куски под данные и сам же их потом освобождает. Скорость от этого огромная, а платить за неё приходится максимальной концентрацией, ведь любой косяк с памятью в C ведёт не к аккуратному сообщению об ошибке, а к жёсткой порче данных на самом низком уровне.
Движок (это и есть ядро, Zend Engine) на каждый запрос проходит один и тот же цикл: инициализация, выполнение скрипта, завершение, очистка. При инициализации он выделяет память под всё, что понадобится: суперглобальные массивы вроде $_POST и $_FILES, тело запроса, внутренние структуры. Дальше крутится сам PHP-скрипт, а на завершении всё это освобождается, чтобы воркер взялся за следующий запрос чистым. Обе уязвимости живут ровно на границах этого цикла: одна срабатывает, когда инициализация срывается из-за нехватки памяти, вторая — когда очистка отработала не до конца и мусор уехал в следующий запрос.
Рис. 1. Путь запроса: от SAPI к ядру на C — и два слабых места ручной работы с памятью
Для второй уязвимости важно ещё что один рабочий процесс обслуживает много запросов подряд, потому что так эффективнее и не нужно каждый раз всё поднимать заново. Значит, от запроса к запросу в воркере может что-то оставаться.
Отсюда и два слабых места: что будет, если память кончится прямо посреди обработки, и что от прошлого запроса останется следующему. Логин, чтобы их задеть, не нужен, поскольку вредоносный запрос добивает до уязвимого кода раньше, чем стартует логика сайта.
Опаснее остальных mod_php. Здесь интерпретатор PHP встроен прямо в процесс Apache и живёт в его памяти, а запрос обрабатывает тот же процесс, что принял соединение. Поэтому вредоносный ввод добирается до уязвимого кода на самом раннем этапе, ещё до того, как отработает хоть строчка PHP сайта. PHP-FPM устроен по-другому. Это отдельный пул процессов, и для первой дыры он оказался менее уязвим, потому что цепочка очистки там срабатывает не так охотно. Но «менее» не значит «никак», так что FPM-админам расслабляться тоже рано.
2. CVE-2026-109618: поломка при нехватке памяти
Первая уязвимости срабатывает, когда у PHP заканчивается память прямо посреди обработки запроса.
В этот момент PHP не может спокойно доделать начатое и бросает обработку на полпути. Данные запроса заполнены лишь частично: что-то уже записано, что-то нет, остальное мусор. Дальше включается cleanup и идёт по ним как по валидным, принимая мусор за чистую монету. Так память и портится.
Причём закончить память можно специально. Хитро собранный multipart-запрос (тот самый механизм, которым браузеры загружают файлы) способен выжать её досуха, формально не нарушая настроенных лимитов. Лимиты вроде стоят, а дыру это не закрывает.
Рис. 2. CVE-2026-109618: аллокация падает на полпути, а cleanup принимает недозаполненные данные за валидные
Раз уж multipart и лимиты здесь ключ к дыре, чуть подробнее о них. Когда браузер загружает файл, тело запроса приходит в формате multipart/form-data — набор частей, у каждой свой заголовок и содержимое. PHP разбирает их и раскладывает по $_FILES и $_POST. Предохранители у движка есть: memory_limit, post_max_size, upload_max_filesize. Только исчерпать память можно так, что формально ни один лимит не нарушен, а сбой аллокации всё равно случается в неудачный момент. Поэтому совет «просто поставь лимит пожёстче» тут помогает лишь частично.
Опасна здесь именно уборка, ведь завершая сорванный запрос, PHP идёт освобождать всё, что успел выделить: вызывает деструкторы, чистит структуры. Если часть этих структур заполнена лишь наполовину, уборщик принимает недописанные поля за валидные указатели и длины. Он освобождает не тот кусок памяти или проходит по битой длине, и получается порча кучи, через которую при должной сноровке протаскивают выполнение кода.
И снова в зоне риска mod_php: там cleanup отрабатывает, даже если сам старт запроса провалился. PHP-FPM эту конкретную дыру задевает меньше, но, повторюсь, «меньше» не значит «никак».
В этот момент PHP не может спокойно доделать начатое и бросает обработку на полпути. Данные запроса заполнены лишь частично: что-то уже записано, что-то нет, остальное мусор. Дальше включается cleanup и идёт по ним как по валидным, принимая мусор за чистую монету. Так память и портится.
Причём закончить память можно специально. Хитро собранный multipart-запрос (тот самый механизм, которым браузеры загружают файлы) способен выжать её досуха, формально не нарушая настроенных лимитов. Лимиты вроде стоят, а дыру это не закрывает.
Рис. 2. CVE-2026-109618: аллокация падает на полпути, а cleanup принимает недозаполненные данные за валидные
Раз уж multipart и лимиты здесь ключ к дыре, чуть подробнее о них. Когда браузер загружает файл, тело запроса приходит в формате multipart/form-data — набор частей, у каждой свой заголовок и содержимое. PHP разбирает их и раскладывает по $_FILES и $_POST. Предохранители у движка есть: memory_limit, post_max_size, upload_max_filesize. Только исчерпать память можно так, что формально ни один лимит не нарушен, а сбой аллокации всё равно случается в неудачный момент. Поэтому совет «просто поставь лимит пожёстче» тут помогает лишь частично.
Опасна здесь именно уборка, ведь завершая сорванный запрос, PHP идёт освобождать всё, что успел выделить: вызывает деструкторы, чистит структуры. Если часть этих структур заполнена лишь наполовину, уборщик принимает недописанные поля за валидные указатели и длины. Он освобождает не тот кусок памяти или проходит по битой длине, и получается порча кучи, через которую при должной сноровке протаскивают выполнение кода.
И снова в зоне риска mod_php: там cleanup отрабатывает, даже если сам старт запроса провалился. PHP-FPM эту конкретную дыру задевает меньше, но, повторюсь, «меньше» не значит «никак».
3. CVE-2026-109619: use-after-free
Вторая уязвимости из другой оперы, хотя итог тот же. Это use-after-free, т.е. обращение к памяти, которую уже освободили.
Воркер обслуживает много запросов подряд и в конце каждого освобождает занятую память. Но иногда внутреннее состояние всё ещё держит указатель на уже отданный кусок, и такой указатель считается испорченным. Если следующий запрос на том же воркере дотянется до этой памяти раньше, чем её переинициализируют, — здравствуй, use-after-free. А прервать уборку может фатальная ошибка или таймаут, и тогда протухшее состояние живёт дольше, чем должно.
Рис. 3. CVE-2026-109619: хвост от прошлого запроса достаётся следующему на том же воркере
Воркеры переиспользуют ради оптимизации: поднимать интерпретатор, подключать расширения и прогревать кэш на каждый чих слишком дорого, поэтому один процесс обслуживает сотни и тысячи запросов. Обратная сторона в том, что любое состояние, которое забыли обнулить между запросами, становится мостиком от одного пользователя к другому.
Обидно, что спусковыми крючками работают штуки, включённые по умолчанию. Session upload progress показывает прогресс загрузки файла и лезет в сессионные структуры в самом начале обработки, когда испорченные указатели ещё не переинициализированы. В PHP 8.5 к нему добавилась трассировка фатальных ошибок: она тоже срабатывает рано, бьёт по той же больной области и тоже включена из коробки.
Есть ещё раскрытие памяти, где через обращение к освобождённой памяти 109619 позволяет вычитать то, что лежало там от прошлого запроса: чужие сессии, токены, куски чужих данных. Уязвимость опасна и без полноценного RCE, утечку памяти PHP подтверждает как отдельное её последствие.
Эта утечка ещё и открывает дорогу к RCE. В разборе Varonis всё выглядит так: воркер держит указатель на освобождённый кусок, туда попадают данные атакующего, а PHP принимает их за настоящий идентификатор сессии и отдаёт клиенту в cookie PHPSESSID. Память процесса уходит прямо в ответ сервера. Кроме секретов, атакующий получает представление о том, где что лежит в памяти, и следующий шаг становится практичным. Поэтому Varonis советует: если сервер был открыт до патча, считай утёкшими пароли, токены и API-ключи из памяти воркера и меняй их.
Воркер обслуживает много запросов подряд и в конце каждого освобождает занятую память. Но иногда внутреннее состояние всё ещё держит указатель на уже отданный кусок, и такой указатель считается испорченным. Если следующий запрос на том же воркере дотянется до этой памяти раньше, чем её переинициализируют, — здравствуй, use-after-free. А прервать уборку может фатальная ошибка или таймаут, и тогда протухшее состояние живёт дольше, чем должно.
Рис. 3. CVE-2026-109619: хвост от прошлого запроса достаётся следующему на том же воркере
Воркеры переиспользуют ради оптимизации: поднимать интерпретатор, подключать расширения и прогревать кэш на каждый чих слишком дорого, поэтому один процесс обслуживает сотни и тысячи запросов. Обратная сторона в том, что любое состояние, которое забыли обнулить между запросами, становится мостиком от одного пользователя к другому.
Обидно, что спусковыми крючками работают штуки, включённые по умолчанию. Session upload progress показывает прогресс загрузки файла и лезет в сессионные структуры в самом начале обработки, когда испорченные указатели ещё не переинициализированы. В PHP 8.5 к нему добавилась трассировка фатальных ошибок: она тоже срабатывает рано, бьёт по той же больной области и тоже включена из коробки.
Есть ещё раскрытие памяти, где через обращение к освобождённой памяти 109619 позволяет вычитать то, что лежало там от прошлого запроса: чужие сессии, токены, куски чужих данных. Уязвимость опасна и без полноценного RCE, утечку памяти PHP подтверждает как отдельное её последствие.
Эта утечка ещё и открывает дорогу к RCE. В разборе Varonis всё выглядит так: воркер держит указатель на освобождённый кусок, туда попадают данные атакующего, а PHP принимает их за настоящий идентификатор сессии и отдаёт клиенту в cookie PHPSESSID. Память процесса уходит прямо в ответ сервера. Кроме секретов, атакующий получает представление о том, где что лежит в памяти, и следующий шаг становится практичным. Поэтому Varonis советует: если сервер был открыт до патча, считай утёкшими пароли, токены и API-ключи из памяти воркера и меняй их.
4. В чём разница и почему это класс
Итак, 109618 ломается внутри одного запроса когда не вовремя кончилась память, обработка оборвалась, а уборка пошла по недозаполненным данным как по валидным. У 109619 проблема тянется из прошлого запроса: освобождённая память, на которую всё ещё указывает состояние, достаётся следующему запросу на том же воркере.
Если считать точнее, то, по разбору Varonis, в раскрытии PHP описаны четыре бага, объединённые в два CVE. Три из них стартуют одинаково, запрос исчерпывает память на раннем этапе, PHP его обрывает, очистка не запускается, а устаревшие указатели остаются в живом воркере. Дальше пути расходятся. Один ведёт к утечке памяти, другой к её порче через состояние десериализатора, третий к use-after-free в трассировке фатальных ошибок. Четвёртый баг стоит особняком и связан с OPcache, общим кэшем скомпилированного кода: сбой аллокации в неудачный момент сбивает учёт занятой памяти, и другой воркер может получить указатель неверного типа. Как именно это эксплуатировали, публично не раскрыто.
Баг этот старый. Самый ранний уязвимый путь, который нашли исследователи, ведёт аж в PHP 5.4.0, а это примерно 2012 год. Предпосылка бага больше десяти лет тихо пролежала в коде и ждала того, кто сложит кусочки. Для больших C-проектов это обычная история: в обычной жизни тот самый неудачный сценарий (нехватка памяти ровно в нужный момент или конкретная последовательность запросов) просто не случается, поэтому ошибки с памятью умеют прятаться годами. Пока кто-то не воспроизведёт его нарочно.
Если считать точнее, то, по разбору Varonis, в раскрытии PHP описаны четыре бага, объединённые в два CVE. Три из них стартуют одинаково, запрос исчерпывает память на раннем этапе, PHP его обрывает, очистка не запускается, а устаревшие указатели остаются в живом воркере. Дальше пути расходятся. Один ведёт к утечке памяти, другой к её порче через состояние десериализатора, третий к use-after-free в трассировке фатальных ошибок. Четвёртый баг стоит особняком и связан с OPcache, общим кэшем скомпилированного кода: сбой аллокации в неудачный момент сбивает учёт занятой памяти, и другой воркер может получить указатель неверного типа. Как именно это эксплуатировали, публично не раскрыто.
Баг этот старый. Самый ранний уязвимый путь, который нашли исследователи, ведёт аж в PHP 5.4.0, а это примерно 2012 год. Предпосылка бага больше десяти лет тихо пролежала в коде и ждала того, кто сложит кусочки. Для больших C-проектов это обычная история: в обычной жизни тот самый неудачный сценарий (нехватка памяти ровно в нужный момент или конкретная последовательность запросов) просто не случается, поэтому ошибки с памятью умеют прятаться годами. Пока кто-то не воспроизведёт его нарочно.
Заключение
Обе уязвимости не связаны с дырявым кодом конкретного сайта. Это классика системного программирования: не вовремя кончившаяся память и указатель на уже освобождённую память. Багам этого рода столько же лет, сколько ручному управлению памятью в C.
Опасными их делает место, где они живут, PHP написан на C, крутится под гигантской долей веба и обрабатывает запросы от кого угодно без спроса. В такой среде ошибка с памятью, которой на десктопе грош цена, превращается в пробитие без авторизации на миллионах серверов.
Чистый код сайта тут не спасает. По данным Varonis, выполнение кода показали на актуальных контейнерных образах WordPress, Drupal и Joomla, а уязвимым может оказаться даже приложение уровня Hello World. Ошибка в плагине или слабый логин для атаки не нужны, а срабатывает она раньше кода приложения, так что его логи могут почти ничего не показать.
Механику обеих дыр мы разобрали, на этом техническая часть закончена. Дальше речь пойдёт о том, что происходило снаружи. Во второй части разберём последствия: как атаки пошли ещё до официальной огласки, почему под ударом оказался больше миллиона хостов и как десятки госсайтов, платформ и даже игровых студий по всему миру в спешке уводили себя в офлайн.
Если хочешь понять, что из этого вышло, открывай часть вторую, скучно не будет)
Опасными их делает место, где они живут, PHP написан на C, крутится под гигантской долей веба и обрабатывает запросы от кого угодно без спроса. В такой среде ошибка с памятью, которой на десктопе грош цена, превращается в пробитие без авторизации на миллионах серверов.
Чистый код сайта тут не спасает. По данным Varonis, выполнение кода показали на актуальных контейнерных образах WordPress, Drupal и Joomla, а уязвимым может оказаться даже приложение уровня Hello World. Ошибка в плагине или слабый логин для атаки не нужны, а срабатывает она раньше кода приложения, так что его логи могут почти ничего не показать.
Механику обеих дыр мы разобрали, на этом техническая часть закончена. Дальше речь пойдёт о том, что происходило снаружи. Во второй части разберём последствия: как атаки пошли ещё до официальной огласки, почему под ударом оказался больше миллиона хостов и как десятки госсайтов, платформ и даже игровых студий по всему миру в спешке уводили себя в офлайн.
Если хочешь понять, что из этого вышло, открывай часть вторую, скучно не будет)
Источники
Объявление PHP о выпусках 8.5.12, 8.4.27, 8.3.36 и 8.2.35: PHP: News Archive - 2026
Адвизори PHP по CVE-2026-109618: RCE via OOM corruption
Адвизори PHP по CVE-2026-109619: RCE via stale global state
Calif, технический разбор PHP2Shell: PHP2Shell by Calif Research
Varonis, разбор цепочки багов: https://www.varonis.com/blog/php2shell
Tenable, FAQ по PHP2Shell: https://www.tenable.com/blog/php2sh...tly-asked-questions-about-critical-php-remote
Rapid7, анализ обеих уязвимостей: Rapid7
CERT-EU, Security Advisory 2026-018: Critical Vulnerabilities in PHP
Дисклеймер: материал носит учебный характер и описывает классы уязвимостей. Операционные детали эксплуатации намеренно опущены. Обновляйте PHP и не запускайте чужие «PoC» из непроверенных источников.
Адвизори PHP по CVE-2026-109618: RCE via OOM corruption
Адвизори PHP по CVE-2026-109619: RCE via stale global state
Calif, технический разбор PHP2Shell: PHP2Shell by Calif Research
Varonis, разбор цепочки багов: https://www.varonis.com/blog/php2shell
Tenable, FAQ по PHP2Shell: https://www.tenable.com/blog/php2sh...tly-asked-questions-about-critical-php-remote
Rapid7, анализ обеих уязвимостей: Rapid7
CERT-EU, Security Advisory 2026-018: Critical Vulnerabilities in PHP
Дисклеймер: материал носит учебный характер и описывает классы уязвимостей. Операционные детали эксплуатации намеренно опущены. Обновляйте PHP и не запускайте чужие «PoC» из непроверенных источников.
AI-выжимка
сгенерировано ИИ
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Продолжить чтение
Следующий разбор
Часть 2. PHP2Shell (CVE-2026-109618 и 109619)
Комментарии
0