Powrót do Bloga

Ochrona serwerów gier przed luką php2shell: 6-godzinny runbook patchowania

Opublikowano 23 października 2026
Ochrona serwerów gier przed luką php2shell: 6-godzinny runbook patchowania Wygenerowano przy użyciu AI

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-fpm moż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

  1. Ł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.
  2. Nigdy nie uruchamiaj pm.max_requests = 0 na 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.
  3. 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.
  4. 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.
  5. 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