РАЗБОР Статья

Часть 1. PHP2Shell (CVE-2026-109618 и 109619)

Михаил Гудилин
Михаил Гудилин Script Kiddie · 21 сообщений
Подписаться
79
[ обложка статьи ]
Режим чтения
Обложка статьи PHP2Shell: логотип PHP, терминал и предупреждение об уязвимостях CVE-2026-109618 и CVE-2026-109619


Что вас ждёт в статье:
Краткая сводка
1. Как PHP обрабатывает запрос
2. CVE-2026-109618: поломка при нехватке памяти
3. CVE-2026-109619: use-after-free между запросами
4. В чём разница и почему это класс
Заключение

Источники



Краткая сводка​

Две критические уязвимости в ядре PHP, и обе дают удалённое выполнение кода без авторизации. В этой части разберём только технику: как именно 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)
CVSS9.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-скрипт, а на завершении всё это освобождается, чтобы воркер взялся за следующий запрос чистым. Обе уязвимости живут ровно на границах этого цикла: одна срабатывает, когда инициализация срывается из-за нехватки памяти, вторая — когда очистка отработала не до конца и мусор уехал в следующий запрос.

Схема пути запроса: от браузера и веб-сервера через SAPI (mod_php, PHP-FPM, php-cgi) к ядру PHP на C

Рис. 1. Путь запроса: от SAPI к ядру на C — и два слабых места ручной работы с памятью
Для второй уязвимости важно ещё что один рабочий процесс обслуживает много запросов подряд, потому что так эффективнее и не нужно каждый раз всё поднимать заново. Значит, от запроса к запросу в воркере может что-то оставаться.

Отсюда и два слабых места: что будет, если память кончится прямо посреди обработки, и что от прошлого запроса останется следующему. Логин, чтобы их задеть, не нужен, поскольку вредоносный запрос добивает до уязвимого кода раньше, чем стартует логика сайта.

Опаснее остальных mod_php. Здесь интерпретатор PHP встроен прямо в процесс Apache и живёт в его памяти, а запрос обрабатывает тот же процесс, что принял соединение. Поэтому вредоносный ввод добирается до уязвимого кода на самом раннем этапе, ещё до того, как отработает хоть строчка PHP сайта. PHP-FPM устроен по-другому. Это отдельный пул процессов, и для первой дыры он оказался менее уязвим, потому что цепочка очистки там срабатывает не так охотно. Но «менее» не значит «никак», так что FPM-админам расслабляться тоже рано.

​

2. CVE-2026-109618: поломка при нехватке памяти​

Первая уязвимости срабатывает, когда у PHP заканчивается память прямо посреди обработки запроса.

В этот момент PHP не может спокойно доделать начатое и бросает обработку на полпути. Данные запроса заполнены лишь частично: что-то уже записано, что-то нет, остальное мусор. Дальше включается cleanup и идёт по ним как по валидным, принимая мусор за чистую монету. Так память и портится.

Причём закончить память можно специально. Хитро собранный multipart-запрос (тот самый механизм, которым браузеры загружают файлы) способен выжать её досуха, формально не нарушая настроенных лимитов. Лимиты вроде стоят, а дыру это не закрывает.

Схема CVE-2026-109618: аллокация прерывается при нехватке памяти, а cleanup принимает недозаполненные данные за валидные

Рис. 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. А прервать уборку может фатальная ошибка или таймаут, и тогда протухшее состояние живёт дольше, чем должно.

Схема CVE-2026-109619: 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-проектов это обычная история: в обычной жизни тот самый неудачный сценарий (нехватка памяти ровно в нужный момент или конкретная последовательность запросов) просто не случается, поэтому ошибки с памятью умеют прятаться годами. Пока кто-то не воспроизведёт его нарочно.

​

Заключение​

Обе уязвимости не связаны с дырявым кодом конкретного сайта. Это классика системного программирования: не вовремя кончившаяся память и указатель на уже освобождённую память. Багам этого рода столько же лет, сколько ручному управлению памятью в C.

Опасными их делает место, где они живут, 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» из непроверенных источников.

​
Полезно · 3

Комментарии

0