Защита игровых серверов от уязвимости php2shell: план патчинга на 6 часов
Коротко о главном
Защитите игровые серверы от php2shell за 6 часов: инвентаризация PHP-рантаймов, срочный патч, WAF-правила и поиск следов успешной эксплуатации.
PHP-часть вашего игрового бэкенда — вероятно, та часть стека, на которую никто не смотрит. Веб-магазин, продающий косметику. Админка, которой пользуется комьюнити-менеджер. Легаси-эндпоинт лидерборда, написанный в 2022 году и с тех пор не тронутый. Именно эту поверхность и атакует php2shell.
21 октября 2026 года Calif.io, Wiz, OpenAI и Quarkslab раскрыли две уязвимости PHP-рантайма — CVE-2026-109618 и CVE-2026-109619, совместно названные php2shell. По CVSS v4.0 они получили 9.2 балла. Затронуты все установки PHP начиная с 5.4; уязвимости позволяют неаутентифицированное удалённое выполнение кода, утечку данных и отказ в обслуживании. Cloudflare наблюдала боевой атакующий трафик с мутацией payload'ов против реальных клиентов ещё до публичного раскрытия.
Это не проблема плагина WordPress. Это memory-safety баг в самом PHP-рантайме. Обновление фреймворка, CMS или Composer-зависимостей ничего не даёт. Если вы эксплуатируете игровой бэкенд, в котором есть PHP, — этот план действий для вас.
Что именно ломает php2shell
Обе CVE находятся в том, как PHP управляет памятью, когда обработка запроса идёт не по плану. Обычно PHP выделяет память под запрос, использует её и освобождает, когда запрос завершается. Баги проявляются, когда путь очистки выполняется над состоянием, которое изначально никогда не было валидным.
CVE-2026-109618: повреждение памяти при OOM
PHP упирается в потолок памяти на середине запроса и аварийно завершается до того, как некоторые внутренние структуры закончат инициализацию. Затем подпрограмма очистки обрабатывает эти полуготовые данные как легитимные. Атакующий, способный надёжно вызывать состояние OOM — большие тела запросов, глубоко вложенный ввод, слишком большие загрузки, — получает шанс повлиять на содержимое этой незавершённой памяти.
Практический триггер для игровых бэкендов очевиден: любой эндпоинт, принимающий body без ограничения размера. Отправка результатов матча, загрузка скриншотов, метаданные модов, пачки телеметрии.
CVE-2026-109619: устаревшее глобальное состояние между запросами
Фатальная ошибка прерывает запрос, и процесс PHP продолжает держать ссылки на память, которая освобождается в конце запроса. Когда тот же самый воркер-процесс берёт следующий запрос, он может перейти по этим висячим ссылкам. Это утечка данных — данные сессии одного игрока или учётные данные БД протекают в ответ другому игроку, — а при определённых условиях и выполнение кода.
Это более коварный баг для игровых серверов, потому что PHP-FPM воркеры по своей конструкции живут долго. Один отравленный воркер может обслужить сотни последующих запросов, прежде чем его перезапустят. Если у вас pm.max_requests = 0 (значение по умолчанию, то есть без перезапуска), повреждённый воркер может существовать всё время жизни контейнера.
Шаг 1: Инвентаризация всех PHP-рантаймов, которыми вы владеете
Нельзя пропатчить то, что не нашёл. Большинство студий в разгар инцидента обнаруживают, что три их сервиса работают на PHP, и один из них — на 8.1 из базового образа двухлетней давности. Запустите это, прежде чем делать что-либо ещё.
#!/usr/bin/env bash
# php2shell exposure inventory (CVE-2026-109618 / CVE-2026-109619)
set -euo pipefail
declare -A FIXED=( [8.5]="8.5.12" [8.4]="8.4.27" [8.3]="8.3.36" [8.2]="8.2.35" )
SERVICES=("game-api" "web-store" "admin-panel" "forums")
for svc in "${SERVICES[@]}"; do
ver=$(docker compose exec -T "$svc" php -r 'echo PHP_VERSION;' 2>/dev/null | tr -d '[:space:]') || {
echo "SKIP $svc (no php binary)"; continue; }
branch="${ver%.*}" # "8.3.12" -> "8.3"
min="${FIXED[$branch]:-}"
if [[ -z "$min" ]]; then
echo "EOL $svc on $ver -> no upstream patch, upgrade required"
elif php -r 'exit(version_compare($argv[1], $argv[2], ">=") ? 0 : 1);' "$ver" "$min"; then
echo "OK $svc on $ver"
else
echo "VULN $svc on $ver (fixed in $min)"
fi
done
Этот скрипт намеренно делает две вещи: отдельно помечает EOL-ветки и никогда не предполагает, что php -v на хосте отражает реальный рантайм контейнера. Если вы на Kubernetes, замените docker compose exec на kubectl exec и обойдите deployment'ы, а не сервисы.
Исправленные версии из апстрима — 8.5.12, 8.4.27, 8.3.36 и 8.2.35. Всё, что на 8.1, 8.0, 7.x или 5.x, — end-of-life и никогда не получит исправление. Это апгрейд, а не патч.
Шаг 2: Патчите рантайм, а не приложение
Самая частая ошибка в первый час такого инцидента — команда, которая обновляет код приложения, передеплоивает, видит зелёный CI и считает, что всё готово. Это не так.
- PHP-FPM и mod_php: пересоберите базовый образ или обновите пакет. Debian/Ubuntu:
apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine:apk upgrade php83. Проверяйте версию командойphp -r 'echo PHP_VERSION;'внутри запущенного контейнера, а не на сборочном хосте. - FrankenPHP / php-cgi: здесь рантайм идёт в комплекте, так что нужен новый релизный бинарник или базовый образ, а не обновление пакета.
- Managed hosting: если PHP контролирует провайдер, немедленно откройте тикет и запросите ссылку на advisory. Не принимайте «мы в курсе» в качестве решения.
- Docker base images: пините по digest, а не по тегу.
php:8.3-fpmпри следующем pull может разрешиться в другую сборку — это отлично для патчинга и ужасно для воспроизводимости. Запините digest после проверки версии.
После патча убейте все существующие воркеры. Rolling restart, оставляющий старые процессы живыми на общем сокете, продолжает обслуживать трафик уязвимыми воркерами. docker compose restart или kubectl rollout restart — и убедитесь, что время запуска процессов изменилось.
Шаг 3: Сократите радиус поражения, пока ждёте
Патчинг требует времени. У вас есть окно между «мы знаем» и «все окружения пересобраны». Используйте его.
Ограничьте тела запросов на периметре
# Shrink the php2shell trigger surface
client_max_body_size 2m; # game APIs rarely need more than this
client_body_timeout 10s;
client_header_timeout 10s;
large_client_header_buffers 4 8k;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php-fpm:9000;
fastcgi_read_timeout 15s;
fastcgi_buffering on;
}
Ограничение в 2 МБ не остановит решительного атакующего — он может вызвать OOM множеством маленьких запросов, — но оно убирает тривиальный триггер одним запросом и ничего вам не стоит.
Агрессивно перезапускайте воркеров
; /usr/local/etc/php-fpm.d/zz-php2shell.conf
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 20
pm.max_requests = 500
pm.max_children = 24
request_terminate_timeout = 20s
Вот математика компромисса. При 200 запросах в секунду на 24 воркерах каждый воркер обрабатывает примерно 8,3 req/s. С pm.max_requests = 500 каждый воркер перезапускается примерно каждые 60 секунд. Это ограничивает время, в течение которого процесс, повреждённый CVE-2026-109619, может обслуживать трафик, но также означает 24 порождения процессов в минуту — измеримые накладные расходы на фреймворках с тяжёлым cold start'ом.
Поднятие memory_limit со 128M до 256M уменьшает частоту OOM (меньше триггеров CVE-2026-109618), но удваивает худший случай по памяти: 24 потомка × 256M = 6 ГБ запаса, который теперь нужен на ноде. Если у ноды 4 ГБ, лучше снизить pm.max_children, а не делать вид, что память есть.
Ничто из этого не является фиксом. Это покупает вам часы. Относитесь к этому как к триажу.
Шаг 4: Определите, были ли вы уже атакованы
Раскрытие не означает, что эксплойта не было раньше. Атакующий трафик наблюдался до официальной даты публикации. Считайте, что вас уже сканировали.
# 1. OOM aborts — strongest single signal for CVE-2026-109618
grep -c "Allowed memory size" /var/log/php-fpm/error.log
# 2. Worker crashes — signal for both CVEs
journalctl -u php8.3-fpm --since "48 hours ago" \
| grep -Ei "segfault|SIGSEGV|exited on signal 11"
# 3. Which paths are generating 500s
awk '$9 == 500 {print $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
# 4. Same source IP hitting the same 500-ing path repeatedly
awk '$9 == 500 {print $1, $7}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20
Сначала снимите бейзлайн. Если в вашем PHP-FPM логе уже было 40 OOM-аварий в день из-за прожорливого до памяти экспортного эндпоинта, эта цифра — шум. Ищите изменение частоты: всплеск выходов по signal 11, новый путь, появившийся в списке 500-х, или один IP, генерирующий десятки ошибок на одном эндпоинте.
Если вы найдёте доказательства успешной эксплуатации, обращайтесь с этим как с полным взломом: ротируйте учётные данные БД, ключи подписи сессий, секреты платёжного провайдера и все API-ключи, доступные из PHP-процесса. Этот плейбук не специфичен для PHP — та же дисциплина, которая позволяет студии проектировать игровые бэкенды, способные пережить компрометацию, применима и здесь; она начинается с допущения, что у атакующего было выполнение кода, а не только доступ на чтение.
Если ваш бэкенд уже роняет запросы и вы не уверены, нагрузка это или атака, применим порядок триажа сбоев из этого протокола диагностики зависаний сервера: изолируйте отказывающий тир, прежде чем трогать конфигурацию.
Почему WAF-правила, основанные только на запросах, провалились
В разборе Cloudflare есть самый переносимый инженерный урок всего инцидента. Их первичная защита была основана на запросах, и атакующие ответили мутацией payload'ов — проверяя, какие варианты срабатывают, а какие проходят, и используя сигнал совпадения/несовпадения как оракул.
Одних данных запроса было недостаточно, чтобы отделить эксплойт-трафик от легитимного, потому что большой POST с вложенными массивами — это также то, что обычный игровой клиент отправляет, передавая сводку матча. Сигналом, снявшим неоднозначность, был ответ: вернул ли origin 500 с трейсом фатальной ошибки PHP? Эта комбинация — подозрительный запрос плюс диагностический ответ — даёт высокую уверенность. Запрос сам по себе — нет.
Cloudflare выкатил это как «full transaction detection», коррелируя запрос и ответ внутри одной HTTP-транзакции и блокируя ответ до того, как он достигнет запрашивающей стороны. Если вы управляете собственным edge или reverse proxy, этот паттерн стоит скопировать:
def is_probable_php2shell(resp) -> bool:
"""Correlate request shape with origin response, not request alone."""
body = resp.body[:4096]
return (
resp.status == 500
and b"Fatal error" in body
and resp.origin_latency_ms < 50 # exploit aborts fast
and b"Allowed memory size" in body # OOM path = 109618 signature
)
Урок обобщается за пределами этой CVE: любая система детектирования, которая видит только входящий трафик, будет обойдена атакующим, способным видеть исходящий результат. Логируйте ответы. Коррелируйте их. Это разница между правилом, которое работает, и правилом, которое превращается в головоломку.
Лучшие практики
- Патчите по расписанию, а не по факту раскрытия. Разрыв между «CVE опубликована» и «эксплойт в дикой природе» теперь измеряется часами, иногда он отрицательный. Пересобирайте базовые PHP-образы еженедельно, даже если ничего не анонсировано, и держите путь отката.
- Никогда не запускайте
pm.max_requests = 0в проде. Безграничное время жизни воркера превращает каждый memory-safety баг в постоянную точку опоры. 500–1000 — разумный дефолт для игровых API. - Ограничивайте каждое тело запроса на периметре. 2 МБ для JSON API, 10 МБ для загрузок, и отклоняйте всё, у чего нет подходящего content type. Это убивает целые классы эксплойтов, основанных на давлении на память, до того как они достигнут PHP.
- Логируйте тела ответов для ошибок 5xx, с сэмплированием. Вы не построите full-transaction детекцию, если ответы origin невидимы для вашего observability-стека. Сэмплируйте 10% и храните 7 дней.
- Ведите инвентаризацию каждого рантайма, которым управляете. Не только PHP — Node, Python, Go, всё, что работает в вашем веб-тире. Студии, отреагировавшие на php2shell меньше чем за час, имели каталог сервисов. Те, кому понадобился день, грепали Dockerfile'ы.
Более крупный урок: ваш веб-тир — это поверхность атаки
php2shell — напоминание о том, что игровые бэкенды обрастают хламом. В первый год вы выпускаете PHP-админку, потому что её быстро написать. На третий год она всё ещё там, всё ещё на 8.1, всё ещё хранит учётку БД с правом записи в таблицу игроков — и взламывают именно её, а не ваш укреплённый бинарник игрового сервера.
Структурное решение — сократить количество рантаймов, которыми вы владеете. Если единственная причина держать PHP-веб-тир — обработка аккаунтов игроков, облачных сохранений и лидербордов, то именно эти возможности horizOn предоставляет как managed API: аутентификация конечных пользователей с привязанными к аккаунту сессиями, Cloud Save с ревизионно-зависимыми чтениями и записями, где конфликты разрешает клиент, лидерборды и удалённая конфигурация. Никакого PHP-FPM пула, который надо патчить в 3 часа ночи, потому что PHP-FPM пула нет.
Это не довод, что managed-сервисы неуязвимы для уязвимостей. Это довод, что каждый рантайм, которым вы управляете, нужно инвентаризировать, патчить и мониторить, — и что у большинства студий их больше, чем они думают.
Сделайте это сейчас
Если из этой статьи взять одно действие, пусть это будет скрипт инвентаризации из шага 1. Прогоните его на всех окружениях — проде, стейджинге, забытом демо-сервере с той презентации издателю. Почти наверняка вы найдёте рантайм, о котором забыли, и почти наверняка он окажется на EOL-ветке.
Затем пропатчите, перезапустите воркеров и оставьте edge-правила на месте. WAF-правила и лимиты тела — это defense in depth, а не замена вендорскому фиксу: PHP-advisory — источник истины, и именно он реально закрывает дыру.
Если инвентаризация выявит больше PHP, чем вы хотите обслуживать дальше, horizOn покрывает тир аккаунтов, сохранений и лидербордов как managed-сервисы — это самый быстрый способ сократить поверхность, за которую вы отвечаете при патчинге. В любом случае: сначала инвентаризация, потом патч, и перестаньте считать, что скучная часть стека — безопасная часть.
Источник: Cloudflare разворачивает защиту от уязвимости php2shell