Artigos
Arthur MairinkArthur Mairink@mairinkdev

PHP2Shell e Node2Shell: por dentro das falhas de outubro

12 min de leitura
CibersegurançaPHPNode.jsSegurança de Memória

O que foi afetado

Em 21 de outubro de 2026, o PHP publicou correções para duas falhas críticas: CVE-2026-109618 e CVE-2026-109619, conhecidas como PHP2Shell. Elas estavam no motor da linguagem. Por isso, atualizar um plugin ou revisar o login da aplicação não bastava: o problema podia ser alcançado antes de o código do site começar a executar.

O número de cerca de 70% apareceu porque essa era a estimativa de sites que usam PHP citada pelos pesquisadores. Não é uma contagem de sites invadidos, nem significa que todas as instalações aceitavam a mesma cadeia de ataque. A versão, a integração com o servidor e o comportamento dos workers mudam a exposição. Um site estático ou uma aplicação que não executa PHP não fica vulnerável só por estar na internet.

No mesmo dia, o Node.js corrigiu a CVE-2026-111497, no decoder Brotli. O problema também podia chegar a execução de código, mas a causa era outra: continuar processando um stream inválido. Vamos acompanhar os dois caminhos, começando pelo ciclo de memória do PHP e depois pela máquina de estados do decoder do Node.

A requisição acaba. O processo pode continuar.

Um worker é um processo que atende requisições. Depois de responder a A, ele pode continuar vivo para atender B. Os dados temporários de A, por outro lado, precisam ser descartados. É uma otimização normal: criar um processo novo para cada acesso seria caro.

Essa diferença exige uma regra: nenhum estado persistente pode continuar tratando memória descartada como se ainda pertencesse à requisição anterior. Um ponteiro é apenas uma referência a uma região de memória; liberar essa região não apaga automaticamente todas as cópias da referência.

  1. Requisição APublica um ponteiro para memória que pertence a A.
  2. Fim de AA memória é liberada. O ponteiro no worker continua lá.
  3. Mesmo workerO processo sobrevive e aceita outra requisição.
  4. Requisição BUm caminho de inicialização usa o ponteiro antes de redefini-lo.
Modelo simplificado de CVE-2026-109619. O ponteiro sobrevive; a memória à qual ele se refere, não.

Na segunda CVE, o advisory descreve justamente um produtor e um consumidor: uma operação deixa estado da requisição acessível pelo worker; outra, em uma requisição posterior no mesmo worker, usa esse estado antes da inicialização normal. Esse acesso depois da liberação é um use-after-free.

Como a falta de memória abria o caminho

Na CVE-2026-109618, o detalhe era o tratamento de falta de memória, ou OOM. O PHP usava longjmp() para abandonar a execução naquele ponto. Se o salto acontecesse enquanto uma estrutura interna estava parcialmente inicializada, a limpeza posterior podia encontrá-la em um estado que não esperava. O advisory oficial explica esse caminho.

Os vetores descritos incluíam parsing de multipart/form-data, o formato usado em uploads. Um corpo de requisição vira estruturas, arrays e metadados em memória. Portanto, bytes recebidos e bytes alocados não são a mesma medida. Muitos cabeçalhos por parte e campos aninhados podiam ampliar esse trabalho; uma requisição sem um tamanho declarado utilizável também complicava a checagem inicial de post_max_size.

No Apache com mod_php, o shutdown podia acontecer mesmo depois de uma falha no startup. É por isso que uma página PHP quase vazia não era uma defesa. A correção mudou o tratamento de OOM: saída controlada em um ponto seguro da VM e término imediato do processo ao atingir um segundo limite. Apenas esconder a mensagem de erro não restaura uma estrutura inválida.

Do erro de memória à execução de código

Uma resposta 500 ou um processo derrubado demonstra uma falha de disponibilidade. Para demonstrar RCE, é preciso ir além: obter uma operação controlada pelo atacante dentro do processo. Não basta enviar uma string com um comando e esperar que o PHP a execute.

Na cadeia analisada pela Wiz, o progresso de upload podia publicar estado de sessão antes de a inicialização normal terminar. Em um dos caminhos de vazamento, PS(id) ficava referenciando memória liberada. Outra requisição podia fazer bytes reaproveitados do heap saírem em um cookie PHPSESSID. Esses bytes ajudavam a reduzir a incerteza sobre endereços; outras estruturas inválidas permitiam avançar para corrupção de memória e RCE.

Reproduzir a cadeia real exigia ajustar o layout das alocações, encontrar o consumidor correto e, nos caminhos entre requisições, atingir o mesmo worker. Build, extensões, limites e distribuição de tráfego importam. Os pesquisadores demonstraram ataques em ambientes com WordPress, Drupal e Joomla; isso não transforma qualquer PoC publicado em um exploit universal.

O código executa inicialmente com as permissões do worker, não recebe privilégio de root por definição. Ainda assim, credenciais de banco, arquivos graváveis e tokens disponíveis à aplicação podem tornar esse acesso suficiente para um incidente grave.

Uma reprodução local que mostra o defeito

Para observar o erro sem depender de endereços de uma build específica do PHP, preparei um programa pequeno em C. Ele modela apenas o tempo de vida da referência. Não é código interno do PHP, não envia requisições e não comprova que um servidor está vulnerável.

request-lifetime.c — trecho do modelo
c
// Simplified C model: worker state outlives a request.char *request_memory = malloc(32);strcpy(request_memory, "error from request A");worker.last_error = request_memory; // Request A ends, but the worker keeps the pointer.free(request_memory); // Request B consumes freed memory.puts(worker.last_error); // In this model, the fix is to clear the reference BEFORE free().// The real PHP patches cover more paths and change OOM handling.

Baixe o arquivo C completo, que inclui os dois caminhos, e compile com Clang e AddressSanitizer:

terminal
bash
clang -g -O0 -fsanitize=address -fno-omit-frame-pointer \  request-lifetime.c -o request-lifetime ./request-lifetime# Expected: AddressSanitizer reports heap-use-after-free (nonzero exit). ./request-lifetime --fixed# Expected: No stale state: request B has no error to consume.# Exit status: 0.

Na primeira execução, o sanitizer identifica a leitura após free(). Com --fixed, o modelo limpa a referência persistente antes de liberar a alocação e não tenta lê-la depois. Limpar só a variável local não resolveria: a referência usada por B está no worker.

Esse contraste permite verificar o mecanismo. Uma reprodução das CVEs no PHP requer a versão, SAPI, configuração e sequência documentadas no advisory; usar o servidor embutido do CLI como substituto de mod_php pode mudar justamente o ciclo que se quer investigar.

Node2Shell: quando um erro vira um pedido por mais dados

No mesmo 21 de outubro, o Node.js corrigiu a CVE-2026-111497, conhecida como Node2Shell. A origem é o decoder nativo da biblioteca Brotli, usada para descomprimir dados. O JavaScript pode estar correto e ainda alcançar código C vulnerável por uma API como node:zlib.

Um decoder de streaming recebe pedaços de entrada e guarda estado entre chamadas. Precisa distinguir três resultados: terminou, faltam dados ou o formato é inválido. Nesse bug, um block switch inválido era confundido com falta temporária de entrada. O decoder seguia acumulando bytes em um buffer interno de oito bytes e podia escrever além dele. É um out-of-bounds write, diferente do ponteiro pendurado no PHP.

O patch do Brotli passa a devolver um erro de formato específico e propaga esse resultado pelos chamadores. A diferença é uma condição de continuidade: uma entrada incompleta pode melhorar com mais bytes; uma transição impossível já precisa encerrar o processamento.

decoder-state — modelo conceitual
typescript
// Conceptual state machine, not the Brotli implementation.const result = decodeNextBlock();if (result === "INVALID_BLOCK_SWITCH") {  return "FORMAT_ERROR"; // Stop: more bytes cannot repair this state.}if (result === "INCOMPLETE_INPUT") {  return "NEEDS_MORE_INPUT"; // A valid state may resume with more bytes.}

Para observar essa diferença, baixe este segundo modelo em C. Ele simula o erro de status e a escrita fora do buffer. Não contém um stream Brotli malicioso nem reproduz a CVE no Node:

terminal
bash
clang -g -O0 -fsanitize=address -fno-omit-frame-pointer \  decoder-state.c -o decoder-state./decoder-state# Expected: AddressSanitizer reports heap-buffer-overflow../decoder-state --fixed# Expected: Invalid format rejected before buffering more input.

Na execução vulnerável, o AddressSanitizer acusa heap-buffer-overflow; com --fixed, o estado inválido é rejeitado antes de acumular bytes. A análise técnica da Akamai detalha o decoder real. Nos dois casos, detectar a corrupção é uma etapa; transformar a primitiva em RCE exige lidar com o layout de memória e as proteções da build.

Como os bytes do atacante chegam ao decoder do Node

O pré-requisito é a aplicação descomprimir Brotli controlado por alguém de fora. Um serviço receber Accept-Encoding: br não prova isso: esse cabeçalho pede uma resposta comprimida. Comprimir a própria resposta e descomprimir entrada hostil são operações diferentes.

Considere um importador que busca um documento em uma URL fornecida pelo usuário. O servidor remoto devolve Content-Encoding: br; o cliente HTTP descomprime a resposta antes de o código de negócio ler o documento. O parser nativo pode ser atingido nessa etapa. Validar o JSON depois não impede um erro que já aconteceu na descompressão.

Esse caminho de retorno é o motivo de um WAF na entrada não cobrir toda a exposição. O boletim da Vercel separa ataques que chegam pela borda de respostas que uma função busca em outro servidor. Mesmo uma aplicação sem endpoint de upload pode ter esse segundo caminho.

Ao investigar, eu rastrearia o fluxo até a chamada que efetivamente decodifica: uploads, clientes HTTP, proxies de imagem, importadores e dependências nativas. O critério é a entrada alcançar a implementação vulnerável. Nem o nome do framework nem a mera presença de um header respondem a essa pergunta.

Tentativa, exploração e escala são medidas diferentes

Em 26 de outubro, as duas CVEs do PHP entraram no catálogo KEV da CISA, conforme o alerta do Centro Canadense de Cibersegurança. Portanto, seria errado apresentar o caso como uma possibilidade exclusivamente teórica.

Ao mesmo tempo, a atualização da Wiz de 27 de outubro não encontrou cadeias completas bem-sucedidas na própria telemetria e discutiu evidências públicas de exploração em honeypots favoráveis. A equipe também identificou ferramentas chamadas PHP2Shell que dependiam de uma falha separada de inclusão de arquivos, ou LFI. O nome do repositório não valida o mecanismo.

Isso não é uma contradição. Um catálogo de exploração conhecida, uma rede de sensores e uma demonstração de laboratório respondem a perguntas diferentes. A interpretação útil é conservar as condições de cada evidência: qual configuração, qual resultado observável e qual população foi medida.

O que precisava ser atualizado

Estas foram as primeiras versões upstream do PHP com as correções de 21 de outubro, segundo o comunicado do PHP. Não são uma recomendação para instalar uma versão antiga quando existe uma atualização de segurança posterior.

Primeiras versões upstream com as duas correções de 21 de outubro
LinhaPrimeira corrigida
8.28.2.35
8.38.3.36
8.48.4.27
8.58.5.12

No Node.js, as primeiras versões corrigidas em 21 de outubro foram:

Primeiras versões corrigidas do Node.js
Node.jsPrimeira corrigida
22.x22.23.4
24.x24.22.1
26.x26.12.1

A release de segurança também corrigiu problemas de DNS, TLS e outros componentes. Atualize o runtime inteiro em uma linha suportada; npm update não substitui o executável Node. Confira a versão dentro do processo ou contêiner que atende a aplicação, por exemplo com node -p 'process.version'.

Pacotes de distribuições podem receber backports sem mudar para esses mesmos números. Nesse caso, confirme o pacote corrigido no boletim do fornecedor. Para instalações antigas, verifique suporte e migração; um número de versão sozinho não substitui essa confirmação.

runtime-inventory.sh
bash
# Run INSIDE each relevant host/container, not just on your laptop.php -r 'printf("PHP=%s SAPI=%s\n", PHP_VERSION, PHP_SAPI);'php --ini # Linux FPM deployment: use the binary name installed by your distro.php-fpm -v # These commands inspect CLI/FPM binaries. They do not prove which# build is serving HTTP or that old workers have been replaced.# Check the running service, image digest and vendor package advisory.

Depois de instalar o pacote ou reconstruir a imagem, substitua os workers antigos. Ver o PHP atualizado no terminal não prova que o processo atendendo HTTP usa o mesmo binário. Em uma aplicação com várias réplicas, confira todas elas.

Há ainda um aviso de 23 de outubro sobre progresso de upload de sessão, associado a CVE-2026-115560. Ele descreve uma combinação incomum de serializador configurado e trocado em runtime. O PHP sugeriu manter session.upload_progress.enabled=0 até a próxima correção para cobrir esse vetor. Não confunda essa atualização com a ausência dos patches das duas CVEs originais.

Como investigar sem confundir contenção com correção

A Cloudflare distribuiu regras de WAF para interceptar tráfego relacionado às falhas. Esse controle ajuda na borda, mas não troca o runtime do servidor. Para avaliar a cobertura, confira se há acesso direto à origem ou outras rotas que não passam pelo filtro.

Eu procuraria uma sequência correlacionada, não um único erro: requisições multipart incomuns, falhas de memória, reinícios de workers, cookies de sessão anômalos e atividade posterior incompatível com o processo. Reúna proxy, runtime e host pela janela de tempo. Tráfego do mesmo IP pode atingir workers diferentes; requisições de IPs diferentes podem acabar no mesmo processo.

Também separaria detecção de RCE da investigação de vazamento. Um atacante que leu memória não precisa deixar um web shell. A ausência de arquivo suspeito não prova ausência de exposição. Se os indícios sustentarem comprometimento, preserve evidências, reimplante uma imagem confiável e revogue os segredos acessíveis ao processo afetado.

O aprendizado de engenharia é específico: caminhos de erro também precisam manter as regras de ownership e inicialização. Nos testes, isso significa interromper operações em pontos diferentes e verificar o próximo uso do estado, não apenas conferir que a requisição que falhou devolveu um erro.

Fontes e data da análise

Continue estudando o tema

Veja outros estudos sobre decisões de arquitetura, segurança de aplicações, inteligência artificial e contribuições open source.