Понимание PHP2Shell: всё, что вам нужно знать

Источник: Akamai•

Понимание PHP2Shell: всё, что вам нужно знать

Узнайте, как критические уязвимости PHP2Shell (CVE-2026-109618 и CVE-2026-109619) позволяют выполнять удаленный код в PHP, а также ознакомьтесь с основными мерами защиты.

Основные выводы

20 октября 2026 года разработчики PHP объявили, что выпустят патч 21 октября в 14:00 UTC для устранения двух критических уязвимостей.

Эти уязвимости получили название PHP2Shell, поскольку позволяют злоумышленнику добиться выполнения удаленного кода (RCE).

Детали планировалось раскрыть 22 октября, но информация об уязвимостях, включая их идентификаторы CVE (CVE-2026-109619 и CVE-2026-109618), была опубликована раньше времени.

CVE-2026-109619 — это уязвимость типа «использование после освобождения» (use-after-free, UAF) в функции отслеживания прогресса загрузки ядра PHP.

CVE-2026-109618 — это уязвимость повреждения памяти, возникающая из-за состояния исчерпания памяти (OOM).

В этой статье подробно описываются обе эти уязвимости.

Предпосылки для атаки

  • Целевая система работает на PHP 5 или более поздней версии с параметром session.upload_progress.enabled = On (по умолчанию)

Целевая система работает на PHP 5 или более поздней версии с параметром session.upload_progress.enabled = On (по умолчанию)

  • Для любой конечной точки, принимающей загрузку файлов (даже той, которая их игнорирует): уровень сессий PHP обрабатывает прогресс загрузки до того, как выполнится пользовательский код

Для любой конечной точки, принимающей загрузку файлов (даже той, которая их игнорирует): уровень сессий PHP обрабатывает прогресс загрузки до того, как выполнится пользовательский код

  • Злоумышленникам требуется специфичное для сборки смещение между zval_ptr_dtor и zend_call_function (полученное во время выполнения или вычисленное на основе точного бинарного файла PHP / дистрибутива цели)

Злоумышленникам требуется специфичное для сборки смещение между zval_ptr_dtor и zend_call_function (полученное во время выполнения или вычисленное на основе точного бинарного файла PHP / дистрибутива цели)

Детали уязвимости CVE-2026-109619

Эксплуатация уязвимости состоит из 5 шагов:

  • Доступ к функции отслеживания прогресса загрузки

Доступ к функции отслеживания прогресса загрузки

  • Создание условия освобождения памяти

Создание условия освобождения памяти

  • Инициация освобождения памяти

Инициация освобождения памяти

  • Кучное распыление (Heap spray)

Кучное распыление (Heap spray)

  • Инициация эксплойта

Инициация эксплойта

Шаг 1: Доступ к функции отслеживания прогресса загрузки

В PHP есть функция под названием «upload_progress», доступная в стандартных сборках. Она обеспечивает обратную связь в реальном времени о том, какая часть файла успешно загружена на сервер.

Когда пользователь инициирует загрузку данных multipart/form-data (описанную в RFC 1867) и включает POST-параметр с именем «PHP_SESSION_UPLOAD_PROGRESS», PHP перехватывает его еще до начала выполнения целевого скрипта.

PHP немедленно создает специальный ключ внутри массива $_SESSION (встроенного суперглобального ассоциативного массива, используемого для хранения и доступа к пользовательским данным на разных страницах за один визит на сайт). По мере того как файл загружается чанк за чанком, PHP непрерывно обновляет этот массив сессии такими метриками, как content_length (общий размер), bytes_processed и имена файлов.

Под капотом, когда HTTP-парсер PHP сталкивается с файловым полем, он инициирует внутреннее событие FILE_START. Чтобы эффективно отслеживать прогресс, PHP создает указатель zval (т. е. указатель памяти C, используемый внутри ядра PHP), напрямую связанный с этим массивом сессии в памяти, что позволяет движку на уровне C быстро записывать поступающие счетчики байтов.

Давайте взглянем на структуру «php_session_rfc1867_progress» из файла php_session.h в исходном коде ядра PHP:

Оказывается, при обработке многочастной загрузки PHP кэширует два сырых указателя zval *:

zval *post_bytes_processed;

zval *current_file_bytes_processed;

Таким образом, когда злоумышленник отправляет POST-запрос с:

  • Cookie/полем PHPSESSID — задает идентификатор сессии

Cookie/полем PHPSESSID — задает идентификатор сессии

  • Полем PHP_SESSION_UPLOAD_PROGRESS — активирует отслеживание прогресса

Полем PHP_SESSION_UPLOAD_PROGRESS — активирует отслеживание прогресса

  • Файловым полем — инициирует событие FILE_START, которое кэширует оба указателя

Файловым полем — инициирует событие FILE_START, которое кэширует оба указателя

Когда пользователь отправляет POST-запрос, запускающий отслеживание прогресса, появляются два указателя zval, которые указывают на активные бакеты хэш-таблицы внутри декодированного дерева прогресса.

Шаг 2: Создание условия освобождения памяти

Злоумышленник приостанавливает TCP-поток более чем на 1 секунду (значение по умолчанию session.upload_progress.min_freq). Это необходимо для обхода ограничения частоты обновлений прогресса в PHP.

Шаг 3: Инициация освобождения памяти

В этот момент, перед вторым вызовом update(), память выглядит следующим образом:

progress->post_bytes_processed → [ бакет в Дереве-A ]

progress->current_file_bytes_processed → [ бакет в Дереве-A ]

Дерево-A — это активные декодированные данные сессии, хранящиеся в $_SESSION под ключом "upload_progress_x".

При возобновлении передачи данных срабатывает второе событие FILE_DATA. Поскольку прошло достаточно времени, запускается второй php_session_rfc1867_update(), что приводит к следующей последовательности действий:

  • flush() записывает Дерево-A на диск и сбрасывает статус. После flush устанавливает session_status = none

flush() записывает Дерево-A на диск и сбрасывает статус

  • После flush устанавливает session_status = none

После flush устанавливает session_status = none

  • php_session_initialize() повторно читает файл сессии. Поскольку статус равен none, PHP рассматривает это как открытие новой сессии. Он читает файл с диска и десериализует его в совершенно новое дерево — назовем его Дерево-B. Дерево-B представляет собой абсолютно новое выделение памяти по другому адресу. Оно содержит те же данные, что и Дерево-A, но является их копией.

php_session_initialize() повторно читает файл сессии

  • Поскольку статус равен none, PHP рассматривает это как открытие новой сессии. Он читает файл с диска и десериализует его в совершенно новое дерево — назовем его Дерево-B. Дерево-B представляет собой абсолютно новое выделение памяти по другому адресу. Оно содержит те же данные, что и Дерево-A, но является их копией.

Поскольку статус равен none, PHP рассматривает это как открытие новой сессии. Он читает файл с диска и десериализует его в совершенно новое дерево — назовем его Дерево-B. Дерево-B представляет собой абсолютно новое выделение памяти по другому адресу. Оно содержит те же данные, что и Дерево-A, но является их копией.

  • php_session_track_init() уничтожает Дерево-A. Теперь у PHP есть два дерева с одинаковым ключом сессии: Дерево-A (старое, все еще находится в $_SESSION) Дерево-B (новое, только что десериализованное)

php_session_track_init() уничтожает Дерево-A

  • Теперь у PHP есть два дерева с одинаковым ключом сессии: Дерево-A (старое, все еще находится в $_SESSION) Дерево-B (новое, только что десериализованное)

Теперь у PHP есть два дерева с одинаковым ключом сессии:

  • Дерево-A (старое, все еще находится в $_SESSION)

Дерево-A (старое, все еще находится в $_SESSION)

  • Дерево-B (новое, только что десериализованное)

Дерево-B (новое, только что десериализованное)

  • zend_hash_update() помещает Дерево-B в $_SESSION. Функция pDestructor (zval_ptr_dtor) хэш-таблицы срабатывает для старого значения — Дерева-A. Это освобождает Дерево-A и все его бакеты. Теперь указатели висят в воздухе (dangling pointers): progress->post_bytes_processed и progress->current_file_bytes_processed освобождены. Дерево-B существует по новому адресу, но никто не обновляет указатели.

zend_hash_update() помещает Дерево-B в $_SESSION. Функция pDestructor (zval_ptr_dtor) хэш-таблицы срабатывает для старого значения — Дерева-A. Это освобождает Дерево-A и все его бакеты.

  • Теперь указатели висят в воздухе (dangling pointers): progress->post_bytes_processed и progress->current_file_bytes_processed освобождены.

Теперь указатели висят в воздухе (dangling pointers): progress->post_bytes_processed и progress->current_file_bytes_processed освобождены.

  • Дерево-B существует по новому адресу, но никто не обновляет указатели.

Tree-B существует как новый адрес, но никто не обновляет указатели.

По сути, PHP кэширует сырые указатели zval * в хэш-таблице, а затем уничтожает и заменяет эту хэш-таблицу, не обновляя кэшированные указатели. Это приводит к уязвимости Use-After-Free (UAF), которая лежит в основе ошибки CVE-2026-109619.

Шаг 4: Распыление кучи (Heap spray)

Сразу после освобождения памяти, в том же теле HTTP POST-запроса вместе с загрузкой, злоумышленник размещает множество (не одно, чтобы обеспечить насыщение) полей формы, названных t[]:

--boundary

Content-Disposition: form-data; name="t[]"

<288 байт поддельной структуры>

--boundary

Content-Disposition: form-data; name="t[]"

<288 байт поддельной структуры>

... (×149)

Многочастный парсер PHP (rfc1867.c) обрабатывает их как события FORMDATA. Каждое значение становится zend_string в массиве PHP $_POST['t']. Имя t[] указывает PHP добавлять элементы в массив, поэтому все 149 попадают в $_POST['t'][0] — $_POST['t'][148].

Каждый тег содержит поддельную структуру внутренней функции PHP с зашитой командой system("<command>"). Эти выделения памяти имеют размер, позволяющий попасть в тот самый слот памяти, который только что был освобожден. Затем распределитель памяти может выдать тот же самый слот с только что освобожденными данными, и полезная нагрузка злоумышленника может оказаться внутри, в результате чего повисший (dangling) указатель будет указывать на нее.

Шаг 5: Запуск эксплойта

Злоумышленник отправляет второй файл в том же POST-запросе. PHP производит запись через повисшие указатели в то, что он считает структурой прогресса:

Но теперь это поддельная функция злоумышленника. Когда движок PHP обращается к этим поврежденным данным, он следует по поддельным указателям и вызывает system ("команда злоумышленника").

Детали уязвимости CVE-2026-109618

Проблема в этой уязвимости заключается в том, что исчерпание памяти (OOM) заставляет PHP прерывать выполнение неожиданным образом, из-за чего обычная очистка может быть пропущена, оставляя неинициализированную память уязвимой для злонамеренной перезаписи и выполнения.

Злоумышленник отправляет специально сформированный запрос, предназначенный для быстрого исчерпания памяти до достижения настроенного в PHP лимита memory_limit. Этого можно добиться с помощью массовой загрузки файлов.

Затем, когда ядро PHP обнаруживает условие OOM, оно должно остановить выполнение. Вместо нормального возврата из функций и пошаговой очистки переменных оно использует longjmp() (лежащий в основе механизм уровня C) для мгновенного развертывания стека и прямого перехода к механизму завершения запроса (shutdown).

Поскольку longjmp() резко пропускает локальный код очистки и инициализации, определенные внутренние структуры остаются в нестабильном, неинициализированном или повисшем состоянии.

Злоумышленник может воспользоваться этим, и до возникновения OOM он может манипулировать кучей (например, используя такие методы, как отправка большого количества полей формы “t[]”, как мы видели ранее).

Это заполняет сегменты памяти поддельными C-структурами, содержащими вредоносные указатели выполнения.

Наконец, во время фазы php_request_shutdown PHP систематически обходит активные ресурсы, чтобы освободить их. Когда движок сталкивается с неинициализированной памятью, оставленной прерыванием longjmp(), он слепо читает поддельную структуру злоумышленника. Если движок выполняет ветвление по указателю функции (например, деструктору) внутри этой поврежденной памяти, выполнение перенаправляется на полезную нагрузку злоумышленника, что приводит к RCE (удаленному выполнению кода).

Митигация с помощью Akamai Guardicore Segmentation

Клиенты Akamai ExAR уже получили отчеты об инцидентах, содержащие информацию об их затронутых активах, включая уязвимые версии PHP, используемую интеграцию с веб-сервером, а также рекомендуемые шаги по митигации и сегментации.

Для обнаружения и сегментации уязвимых приложений можно использовать следующие поисковые запросы Akamai Guardicore Segmentation Insight:

Ресурсы Linux

Ресурсы Windows

Итог

Две критические уязвимости, известные как PHP2Shell (CVE-2026-109618 и CVE-2026-109619), позволяют злоумышленникам добиться RCE на серверах под управлением PHP 5 и выше. Обязательно немедленно установите патч для PHP.

Оставайтесь с нами

Группа анализа угроз Akamai Security Intelligence Group продолжит мониторинг, публикацию отчетов и создание средств митигации для подобных угроз как для наших клиентов, так и для сообщества кибербезопасности в целом. Чтобы быть в курсе последних новостей от Akamai Security Intelligence Group, посетите нашу домашнюю страницу исследований и подписывайтесь на нас в социальных сетях.

Об авторе

Akamai Security Intelligence Group

Akamai SIG — это глобальная команда исследователей мирового уровня, инженеров, стратегов и специалистов по обработке данных с широким спектром опыта и дисциплин в области безопасности. Нашими источниками данных являются огромная облачная инфраструктура Akamai, открытые источники, сотрудничество с третьими сторонами и данные разведки из даркнета. Мы также разработали собственные алгоритмы и инструменты, которые помогают нам проводить исследования и поддерживать решения безопасности Akamai в актуальном состоянии.

О чём эта статья

Ещё в разделе «Облака и инфраструктура»

Все →

Ещё от Akamai