Ochrona serwerów gier przed luką php2shell: 6-godzinny runbook patchowania
W skrócie
Zabezpiecz serwery gier przed luką php2shell w 6 godzin: inwentaryzacja PHP, patchowanie runtime'u, detekcja exploitów i redukcja ryzyka.
Warstwa PHP w twojej grze to prawdopodobnie element stosu, na który nikt nie patrzy. Sklep internetowy sprzedający kosmetyki. Panel administracyjny, z którego korzysta twój community manager. Legacy endpoint leaderboardów napisany w 2022 roku, do którego nigdy więcej nie wróciłeś. To jest powierzchnia ataku php2shell.
21 października 2026 roku firmy Calif.io, Wiz, OpenAI i Quarkslab ujawniły dwie luki w środowisku uruchomieniowym PHP, śledzone jako CVE-2026-109618 i CVE-2026-109619, wspólnie nazwane php2shell. W skali CVSS v4.0 otrzymały ocenę 9.2. Dotykają każdej instalacji PHP od wersji 5.4 wzwyż, umożliwiają nieuwierzytelnione zdalne wykonywanie kodu, ekspozycję danych i denial of service, a Cloudflare zaobserwował na żywo ruch atakowy mutujący payloady przeciwko prawdziwym klientom, zanim jeszcze doszło do publicznego ujawnienia.
To nie jest problem wtyczki do WordPressa. To błąd bezpieczeństwa pamięci w samym środowisku uruchomieniowym PHP. Aktualizacja frameworka, CMS-a czy zależności Composera nic nie daje. Jeśli prowadzisz backend gry zawierający jakikolwiek PHP, ten runbook jest dla ciebie.
Co php2shell faktycznie psuje
Obie luki CVE dotyczą sposobu, w jaki PHP zarządza pamięcią, gdy przetwarzanie żądania idzie nie tak. Normalnie PHP alokuje pamięć dla żądania, używa jej i zwalnia po zakończeniu żądania. Błędy pojawiają się, gdy ścieżka sprzątania działa na stanie, który od początku nigdy nie był prawidłowy.
CVE-2026-109618: uszkodzenie pamięci przy out-of-memory
PHP osiąga swój limit pamięci w trakcie żądania i przerywa działanie, zanim niektóre wewnętrzne struktury zdążą się zainicjalizować. Procedura sprzątania traktuje te częściowo zbudowane dane jako prawidłowe. Atakujący, który potrafi niezawodnie wymusić warunki OOM — duże treści żądań, głęboko zagnieżdżone dane wejściowe, przesadzone uploady — zyskuje szansę na kontrolowanie zawartości tej niedokończonej pamięci.
Praktyczny trigger dla backendów gier jest oczywisty: każdy endpoint przyjmujący treść żądania bez limitu rozmiaru. Przesyłanie wyników meczów, upload zrzutów ekranu, metadane modów, paczki telemetrii.
CVE-2026-109619: nieaktualny stan globalny między żądaniami
Błąd krytyczny przerywa żądanie, pozostawiając proces PHP z referencjami do pamięci, która jest zwalniana na końcu żądania. Gdy ten sam proces roboczy obsłuży następne żądanie, może podążać za tymi zawieszonymi referencjami. To jest ekspozycja danych — dane sesji jednego gracza lub poświadczenia bazy danych wyciekające do odpowiedzi innego gracza — a w odpowiednich warunkach wykonanie kodu.
To bardziej podstępny błąd dla serwerów gier, ponieważ workery PHP-FPM są z założenia długożyjące. Pojedynczy zatruty worker może obsłużyć setki kolejnych żądań, zanim zostanie poddany recyklingowi. Jeśli uruchomisz pm.max_requests = 0 (wartość domyślna, oznaczająca brak recyklingu), uszkodzony worker może przetrwać przez cały czas życia kontenera.
Krok 1: Zinwentaryzuj każde środowisko PHP, które posiadasz
Nie załatasz czegoś, czego nie znalazłeś. Większość studiów odkrywa w trakcie incydentu, że trzy ich usługi działają na PHP, a jedna z nich siedzi na 8.1 z obrazu bazowego zbudowanego dwa lata temu. Uruchom to, zanim zrobisz cokolwiek innego.
#!/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
Dwie rzeczy ten skrypt robi celowo: oznacza gałęzie EOL osobno i nigdy nie zakłada, że php -v na hoście odzwierciedla faktyczne środowisko uruchomieniowe kontenera. Jeśli używasz Kubernetesa, zamień docker compose exec na kubectl exec i iteruj po deploymentach, a nie nazwach usług.
Naprawione wersje upstream to 8.5.12, 8.4.27, 8.3.36 i 8.2.35. Wszystko na 8.1, 8.0, 7.x lub 5.x jest end-of-life i nigdy nie otrzyma poprawki. Czyli aktualizujesz, a nie łatasz.
Krok 2: Załat środowisko uruchomieniowe, nie aplikację
Najczęstszy scenariusz porażki w pierwszej godzinie takiego incydentu to zespół, który aktualizuje kod aplikacji, wdraża ponownie, widzi zielony CI i zakłada, że to koniec. To nie jest koniec.
- PHP-FPM i mod_php: przebuduj obraz bazowy lub podbij wersję pakietu. Debian/Ubuntu:
apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine:apk upgrade php83. Zweryfikuj za pomocąphp -r 'echo PHP_VERSION;'wewnątrz działającego kontenera, nie na hoście budowania. - FrankenPHP / php-cgi: te rozwiązania zawierają środowisko uruchomieniowe, więc potrzebujesz nowej binarki release lub obrazu bazowego, a nie podbicia pakietu.
- Managed hosting: jeśli dostawca kontroluje PHP, otwórz ticket natychmiast i poproś o odniesienie do advisory. Nie akceptuj odpowiedzi „jesteśmy świadomi" jako rozwiązania.
- Obrazy bazowe Docker: pinuj po digest, nie po tagu.
php:8.3-fpmmoże przy następnym pullu wskazać inną kompilację, co jest świetne dla łatania, a fatalne dla powtarzalności. Spinuj digest po zweryfikowaniu wersji.
Po załataniu zabij każdy istniejący worker. Restart kroczący, który pozostawia stare procesy przy życiu na wspólnym sockecie, utrzymuje podatne workery w ruchu. docker compose restart lub kubectl rollout restart — potwierdź, że czas startu procesu się zmienił.
Krok 3: Zmniejsz promień rażenia, czekając
Łatanie zajmuje czas. Masz okno między „wiemy" a „każde środowisko jest przebudowane". Wykorzystaj je.
Ogranicz treści żądań na brzegu
# 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;
}
Limit 2 MB nie powstrzyma zdeterminowanego atakującego — może wywołać OOM wieloma małymi żądaniami — ale eliminuje trywialny pojedynczy trigger i nic cię nie kosztuje.
Agresywnie poddawaj workery recyklingowi
; /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
Oto matematyka kompromisu. Przy 200 żądaniach na sekundę na 24 workerach, każdy worker obsługuje mniej więcej 8,3 req/s. Przy pm.max_requests = 500 każdy worker jest poddawany recyklingowi co ~60 sekund. To ogranicza, jak długo proces uszkodzony przez CVE-2026-109619 może obsługiwać ruch, ale oznacza też 24 spawny procesów na minutę — wymierny narzut na frameworkach z ciężkim cold startem.
Podniesienie memory_limit ze 128M do 256M zmniejsza częstotliwość trafiania w OOM (mniej triggerów CVE-2026-109618), ale podwaja pamięć w najgorszym przypadku: 24 dzieci × 256M = 6 GB zapasu, który musisz teraz mieć na nodze. Jeśli twój node ma 4 GB, obniż pm.max_children, zamiast udawać, że ta pamięć istnieje.
Żadne z tego nie jest poprawką. To kupuje ci godziny. Traktuj to jako triage.
Krok 4: Wykryj, czy już zostałeś trafiony
Ujawnienie nie równa się pierwszej eksploatacji. Ruch atakowy zaobserwowano przed oficjalną datą wydania. Zakładaj, że byłeś sondowany.
# 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
Najpierw baseline. Jeśli twój log PHP-FPM miał już 40 przerwań OOM dziennie z powodu pamięciożernego endpointu eksportu, ta liczba jest szumem. Tego, czego szukasz, to zmiana tempa — skok wyjść z sygnałem 11, nowa ścieżka pojawiająca się na liście 500 lub pojedyncze IP generujące dziesiątki błędów przeciwko jednemu endpointowi.
Jeśli znajdziesz dowody udanej eksploatacji, traktuj to jako pełny breach: rotuj poświadczenia bazy danych, klucze podpisywania sesji, sekrety dostawcy płatności i wszystkie klucze API dostępne z procesu PHP. Playbook na to nie jest specyficzny dla PHP — ta sama dyscyplina, która pozwala studiu projektować backendy gier tak, by przetrwały kompromitacje, ma tu zastosowanie i zaczyna się od założenia, że atakujący miał wykonanie kodu, a nie tylko dostęp do odczytu.
Jeśli twój backend już gubi żądania i nie jesteś pewien, czy to obciążenie, czy atak, kolejność triage'owania awarii z tego protokołu diagnostycznego zamrożeń serwera ma zastosowanie: wyizoluj zawodną warstwę, zanim dotkniesz konfiguracji.
Dlaczego reguły WAF oparte tylko na żądaniach zawiodły
Opis Cloudflare zawiera najbardziej przenośną lekcję inżynieryjną z całego incydentu. Ich początkowe zabezpieczenia były oparte na żądaniach, a atakujący odpowiedzieli mutacją payloadów — sondowaniem, które warianty pasują, a które się prześlizgują, używając sygnału dopasowania/braku dopasowania jako wyroczni.
Same dane żądania nie mogły oddzielić ruchu exploitowego od legalnego, ponieważ duży POST z zagnieżdżonymi tablicami to także to, co wysyła normalny klient gry, gdy przesyła podsumowanie meczu. Sygnałem, który rozstrzygnął niejednoznaczność, była odpowiedź: czy origin zwrócił 500 ze śladem błędu krytycznego PHP? Ta kombinacja — podejrzane żądanie plus odpowiedź diagnostyczna — daje wysoką pewność. Samo żądanie nie daje.
Cloudflare wdrożył to jako „full transaction detection", korelując żądanie i odpowiedź w ramach tej samej transakcji HTTP i blokując odpowiedź, zanim dotrze do żądającego. Jeśli prowadzisz własny edge lub reverse proxy, ten wzorzec warto skopiować:
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
)
Ta lekcja uogólnia się poza to CVE: każdy system detekcji, który widzi tylko ruch przychodzący, zostanie ominięty przez atakującego, który widzi wynik wychodzący. Loguj odpowiedzi. Koreluj je. To różnica między regułą, która działa, a regułą, która staje się zagadką.
Najlepsze praktyki
- Łataj według harmonogramu, nie według ujawnień. Odstęp między „opublikowano CVE" a „exploit w dziczy" mierzy się teraz w godzinach, czasem jest ujemny. Przebudowuj obrazy bazowe PHP co tydzień, nawet gdy nic nie ogłoszono, i trzymaj ścieżkę rollbacku.
- Nigdy nie uruchamiaj
pm.max_requests = 0na produkcji. Nieograniczony czas życia workera zamienia każdy błąd bezpieczeństwa pamięci w trwały przyczółek. 500–1000 to rozsądna domyślna wartość dla API gier. - Ogranicz treść każdego żądania na brzegu. 2 MB dla API JSON, 10 MB dla uploadów i odrzucaj wszystko bez pasującego content type. To zabija całe klasy exploitów opartych na ciśnieniu pamięci, zanim dotrą do PHP.
- Loguj treści odpowiedzi dla błędów 5xx, próbkowane. Nie zbudujesz detekcji pełnej transakcji, jeśli odpowiedzi origin są niewidoczne dla twojego stosu obserwowalności. Próbkuj 10% i trzymaj 7 dni.
- Prowadź inwentaryzację każdego środowiska uruchomieniowego, które obsługujesz. Nie tylko PHP — Node, Python, Go, cokolwiek działa w twojej warstwie webowej. Studia, które zareagowały na php2shell w mniej niż godzinę, miały katalog usług. Te, które zajęły dzień, grepowały Dockerfile'y.
Większa lekcja: twoja warstwa webowa jest powierzchnią ataku
php2shell to przypomnienie, że backendy gier gromadzą śmieci. W pierwszym roku wdrażasz panel administracyjny PHP, bo budowało się go szybko. W trzecim roku nadal tam jest, nadal na 8.1, nadal trzyma poświadczenie bazy danych z prawem zapisu do tabeli graczy — i to on zostaje zhakowany, a nie twoja utwardzona binarka serwera gry.
Rozwiązanie strukturalne to zmniejszenie liczby środowisk uruchomieniowych, które posiadasz. Jeśli jedynym powodem, dla którego prowadzisz warstwę webową PHP, jest obsługa kont graczy, cloud save'ów i leaderboardów, to są dokładnie te możliwości, które horizOn udostępnia jako zarządzane API — uwierzytelnianie użytkowników końcowych z sesjami przypisanymi do konta, Cloud Save z odczytami i zapisami świadomymi rewizji, gdzie klient rozwiązuje konflikty, leaderboardy i konfiguracja zdalna. Żadnej puli PHP-FPM do łatania o 3 nad ranem, bo nie ma żadnej puli PHP-FPM.
To nie argument, że usługi zarządzane są odporne na luki. To argument, że każde środowisko uruchomieniowe, które obsługujesz, musisz inwentaryzować, łatać i monitorować — i że większość studiów ma ich więcej, niż im się wydaje.
Zrób to teraz
Jeśli z tego artykułu weźmiesz jedno działanie, niech to będzie skrypt inwentaryzacji z kroku 1. Uruchom go na każdym środowisku — produkcyjnym, stagingowym, zapomnianym serwerze demo z pitcha do wydawcy. Niemal na pewno znajdziesz środowisko, o którym zapomniałeś, i niemal na pewno będzie na gałęzi EOL.
Potem załat, poddaj workery recyklingowi i zostaw reguły brzegowe na miejscu. Reguły WAF i limity treści to defense in depth, a nie zamiennik poprawki dostawcy — advisory PHP jest źródłem prawdy i to on faktycznie zamyka dziurę.
Jeśli inwentaryzacja wykaże więcej PHP, niż chcesz utrzymywać w przyszłości, horizOn pokrywa warstwę kont, save'ów i leaderboardów jako usługi zarządzane, co jest najszybszym sposobem na zmniejszenie powierzchni, za którą odpowiadasz. Tak czy inaczej: najpierw inwentaryzacja, potem łatanie, i przestań zakładać, że nudna część twojego stosu jest bezpieczną częścią.
Źródło: Cloudflare wdraża zabezpieczenia przeciwko luce php2shell