Voltar ao Blog

Proteção de Servidores de Jogos contra a Vulnerabilidade php2shell: O Runbook de Patch em 6 Horas

Publicado em 23 de outubro de 2026
Proteção de Servidores de Jogos contra a Vulnerabilidade php2shell: O Runbook de Patch em 6 Horas Gerada com a ajuda de IA

Em resumo

Use este runbook de 6 horas para proteger seu backend de jogos contra php2shell: inventário, patch no runtime e detecção de exploração ativa.

A camada PHP do seu jogo é provavelmente a parte da sua stack que ninguém olha. A loja web que vende cosméticos. O painel administrativo que seu community manager usa. O endpoint de leaderboard legado que você escreveu em 2022 e nunca mais tocou. Essa é a superfície que o php2shell ataca.

Em 21 de outubro de 2026, Calif.io, Wiz, OpenAI e Quarkslab divulgaram duas vulnerabilidades no runtime do PHP, rastreadas como CVE-2026-109618 e CVE-2026-109619, nomeadas coletivamente como php2shell. O CVSS v4.0 as pontua em 9.2. Elas afetam todas as instalações PHP a partir da 5.4, permitem execução remota de código não autenticada, exposição de dados e negação de serviço, e a Cloudflare observou tráfego de ataque ao vivo mutando payloads contra clientes reais antes mesmo da divulgação pública.

Isso não é um problema de plugin do WordPress. É um bug de memory safety no próprio runtime do PHP. Atualizar seu framework, seu CMS ou suas dependências do Composer não resolve nada. Se você opera um backend de jogo com qualquer PHP nele, este runbook é para você.

O que o php2shell Quebra de Verdade

Ambas as CVEs estão na forma como o PHP lida com a memória quando o processamento de uma requisição sai do trilho. Normalmente o PHP aloca memória para uma requisição, usa e libera quando a requisição termina. Os bugs aparecem quando esse caminho de limpeza roda sobre um estado que nunca foi válido em primeiro lugar.

CVE-2026-109618: corrupção por esgotamento de memória

O PHP atinge seu teto de memória no meio da requisição e aborta antes que algumas estruturas internas terminem de inicializar. A rotina de limpeza então trata esses dados semiconstruídos como legítimos. Um atacante que consiga forçar condições de OOM de forma confiável — corpos de requisição grandes, entrada profundamente aninhada, uploads superdimensionados — tem a chance de controlar o que essa memória inacabada contém.

O gatilho prático para backends de jogos é óbvio: qualquer endpoint que aceite um corpo sem limite de tamanho. Submissão de resultados de partida, upload de capturas de tela, metadados de mods, lotes de telemetria.

CVE-2026-109619: estado global obsoleto entre requisições

Um erro fatal interrompe uma requisição, deixando o processo PHP segurando referências à memória que é liberada no fim da requisição. Quando o mesmo processo worker pega a próxima requisição, ele pode seguir essas referências pendentes. Isso é exposição de dados — os dados de sessão de um jogador ou credenciais de banco vazando na resposta de outro jogador — e, sob as condições certas, execução de código.

Este é o bug mais insidioso para servidores de jogos porque os workers do PHP-FPM são de vida longa por design. Um único worker envenenado pode servir centenas de requisições seguintes antes de ser reciclado. Se você roda pm.max_requests = 0 (o padrão, que significa nunca reciclar), um worker corrompido pode persistir durante toda a vida do contêiner.

Passo 1: Inventarie Todo Runtime PHP Que Você Possui

Você não pode corrigir o que não encontrou. A maioria dos estúdios descobre, no meio do incidente, que três dos seus serviços rodam PHP e que um deles está na 8.1 a partir de uma imagem base construída há dois anos. Rode isso antes de fazer qualquer outra coisa.

#!/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

Duas coisas que este script faz de propósito: ele sinaliza branches EOL separadamente e nunca assume que o php -v no host reflete o runtime real do contêiner. Se você usa Kubernetes, troque o docker compose exec por kubectl exec e itere sobre deployments em vez de nomes de serviços.

As versões corrigidas upstream são 8.5.12, 8.4.27, 8.3.36 e 8.2.35. Qualquer coisa na 8.1, 8.0, 7.x ou 5.x está em fim de vida (EOL) e nunca receberá correção. Você está fazendo upgrade, não patch.

Passo 2: Corrija o Runtime, Não a Aplicação

O modo de falha mais comum na primeira hora de um incidente assim é um time que atualiza o código da aplicação, faz redeploy, vê o CI verde e acha que terminou. Não terminou.

  • PHP-FPM e mod_php: reconstrua a imagem base ou faça um bump do pacote. Debian/Ubuntu: apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common. Alpine: apk upgrade php83. Verifique com php -r 'echo PHP_VERSION;' dentro do contêiner em execução, não no host de build.
  • FrankenPHP / php-cgi: eles empacotam o runtime, então você precisa de um binário de release novo ou imagem base nova, não de um bump de pacote.
  • Hospedagem gerenciada: se um provedor controla o PHP, abra um ticket imediatamente e peça a referência do advisory. Não aceite "estamos cientes" como resolução.
  • Imagens base Docker: fixe por digest, não por tag. php:8.3-fpm pode resolver para um build diferente no próximo pull, o que é ótimo para patches e péssimo para reprodutibilidade. Fixe o digest depois de verificar a versão.

Depois de aplicar o patch, mate todos os workers existentes. Um restart em rolling que deixa processos antigos vivos em um socket compartilhado mantém workers vulneráveis servindo tráfego. docker compose restart ou kubectl rollout restart — confirme que o horário de início do processo mudou.

Passo 3: Reduza o Raio de Impacto Enquanto Espera

Aplicar o patch leva tempo. Existe uma janela entre "sabemos" e "todo ambiente foi reconstruído". Use-a.

Limite os corpos das requisições na borda

# 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;
}

Um limite de 2 MB não impede um atacante determinado — ele pode disparar OOM com muitas requisições pequenas — mas elimina o gatilho trivial de requisição única e não custa nada.

Recicle os workers agressivamente

; /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

Aqui está a matemática do trade-off. A 200 requisições por segundo em 24 workers, cada worker lida com aproximadamente 8,3 req/s. Com pm.max_requests = 500, cada worker é reciclado a cada ~60 segundos. Isso limita por quanto tempo um processo corrompido pela CVE-2026-109619 pode servir tráfego, mas também significa 24 spawns de processo por minuto — overhead mensurável em frameworks com cold start pesado.

Aumentar o memory_limit de 128M para 256M reduz a frequência com que você atinge OOM (menos gatilhos da CVE-2026-109618), mas dobra o pior caso de memória: 24 children × 256M = 6 GB de folga que você agora precisa no nó. Se o seu nó tem 4 GB, diminua o pm.max_children em vez de fingir que a memória existe.

Nada disso é uma correção. Isso te compra horas. Trate como triagem.

Passo 4: Detecte se Você Já Foi Atacado

Divulgação não é o mesmo que primeira exploração. Tráfego de ataque foi observado antes da data oficial de divulgação. Presuma que você foi sondado.

# 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

Primeiro, estabeleça a baseline. Se o seu log do PHP-FPM já tinha 40 aborts por OOM por dia por causa de um endpoint de exportação guloso por memória, esse número é ruído. O que você procura é uma mudança na taxa — um pico em saídas por signal 11, um novo caminho aparecendo na lista de 500s, ou um único IP gerando dezenas de falhas contra um endpoint.

Se você encontrar evidências de exploração bem-sucedida, trate como uma violação completa: rotacione credenciais de banco de dados, chaves de assinatura de sessão, segredos de provedor de pagamento e quaisquer chaves de API acessíveis a partir do processo PHP. O playbook para isso não é específico de PHP — a mesma disciplina que permite a um estúdio arquitetar backends de jogos para sobreviver a comprometimentos se aplica aqui, e começa presumindo que o atacante tinha execução de código, não apenas acesso de leitura.

Se o seu backend já está derrubando requisições e você não sabe se é carga ou ataque, a ordem de triagem de crash neste protocolo de diagnóstico de congelamento de servidor se aplica: isole a camada com falha antes de mexer na configuração.

Por Que Regras de WAF Baseadas Apenas em Requisições Falharam

O relatório da Cloudflare contém a lição de engenharia mais transferível de todo o incidente. As proteções iniciais eram baseadas em requisições, e os atacantes responderam mutando payloads — sondando quais variantes batiam e quais passavam, usando o sinal de match/no-match como um oráculo.

Somente os dados da requisição não conseguiam separar tráfego de exploit de tráfego legítimo, porque um POST grande com arrays aninhados é também o que um cliente de jogo normal envia ao submeter um resumo de partida. O sinal que resolveu a ambiguidade foi a resposta: a origem retornou um 500 com trace de erro fatal do PHP? Essa combinação — requisição suspeita mais resposta diagnóstica — tem alta confiança. A requisição sozinha não tem.

A Cloudflare implementou isso como "detecção de transação completa", correlacionando requisição e resposta na mesma transação HTTP e bloqueando a resposta antes que chegue a quem requisitou. Se você opera sua própria borda ou reverse proxy, vale a pena copiar o padrão:

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
    )

A lição vai além desta CVE: qualquer sistema de detecção que só enxerga tráfego de entrada será contornado por um atacante que consegue ver o resultado de saída. Registre respostas. Correlacione-as. É a diferença entre uma regra que se sustenta e uma regra que vira um quebra-cabeça.

Melhores Práticas

  1. Faça patch em um cronograma, não na divulgação. A distância entre "CVE publicada" e "exploit à solta" agora é medida em horas, às vezes negativa. Reconstrua as imagens base do PHP semanalmente mesmo quando nada é anunciado e mantenha um caminho de rollback.
  2. Nunca rode pm.max_requests = 0 em produção. A vida útil ilimitada do worker transforma qualquer bug de memory safety em uma porta de entrada persistente. 500–1000 é um padrão sensato para APIs de jogos.
  3. Limite todo corpo de requisição na borda. 2 MB para APIs JSON, 10 MB para uploads, e rejeite qualquer coisa sem um content type correspondente. Isso elimina classes inteiras de exploits de pressão de memória antes que cheguem ao PHP.
  4. Registre corpos de resposta para erros 5xx, com amostragem. Você não consegue construir detecção de transação completa se as respostas da origem são invisíveis para sua stack de observabilidade. Faça amostragem de 10% e mantenha por 7 dias.
  5. Mantenha um inventário de todos os runtimes que você opera. Não só PHP — Node, Python, Go, o que quer que rode sua camada web. Os estúdios que responderam ao php2shell em menos de uma hora tinham um catálogo de serviços. Os que levaram um dia estavam fazendo grep em Dockerfiles.

A Lição Maior: Sua Camada Web É a Superfície de Ataque

php2shell é um lembrete de que backends de jogos acumulam lixo técnico. Você publica um painel administrativo em PHP no primeiro ano porque era rápido de construir. No terceiro ano, ele ainda está lá, ainda na 8.1, ainda segurando uma credencial de banco com acesso de escrita à tabela de jogadores — e é ele que é comprometido, não o binário endurecido do seu servidor de jogo.

A correção estrutural é reduzir a quantidade de runtime que você opera. Se a única razão para você operar uma camada web em PHP é lidar com contas de jogadores, cloud saves e leaderboards, essas são exatamente as capacidades que o horizOn expõe como APIs gerenciadas — autenticação de usuário final com sessões vinculadas à conta, Cloud Save com leituras e escritas cientes de revisão, onde o cliente resolve conflitos, leaderboards e configuração remota. Sem pool de PHP-FPM para corrigir às 3 da manhã porque não existe pool de PHP-FPM.

Isso não é um argumento de que serviços gerenciados são imunes a vulnerabilidades. É um argumento de que todo runtime que você opera é um runtime que você precisa inventariar, corrigir e monitorar — e que a maioria dos estúdios tem mais deles do que imagina.

Faça Isso Agora

Se você tirar uma única ação deste artigo, que seja o script de inventário do Passo 1. Rode-o contra todos os ambientes — produção, staging, o servidor de demonstração esquecido daquele pitch para publisher. Você quase certamente vai encontrar um runtime que esqueceu, e ele quase certamente estará em uma branch EOL.

Depois, aplique o patch, recicle os workers e mantenha as regras de borda no lugar. As regras de WAF e os limites de corpo são defesa em profundidade, não um substituto para a correção do fornecedor — o advisory do PHP é a fonte da verdade e é ele que realmente fecha o buraco.

Se o inventário revelar mais PHP do que você quer manter daqui pra frente, o horizOn cobre a camada de contas, saves e leaderboards como serviços gerenciados, que é o caminho mais rápido para reduzir a superfície pela qual você é responsável por aplicar patches. De qualquer forma: inventário primeiro, patch segundo, e pare de achar que a parte chata da sua stack é a parte segura.


Fonte: Cloudflare implanta proteções contra a vulnerabilidade php2shell