← Voltar aos artigos

php2shell: como funcionam as duas falhas de RCE do PHP e como se defender

Em 21 de outubro de 2026, o projeto PHP publicou um aviso de segurança com um título raro: "Critical remote code execution vulnerabilities, update now". Eram duas falhas, CVE-2026-109618 e CVE-2026-109619, ambas com nota 9,2 no CVSS v4.0 (o sistema de pontuação de gravidade de vulnerabilidades, de 0 a 10), que permitem execução remota de código (RCE, de remote code execution: o atacante faz o servidor rodar código escolhido por ele) sem autenticação: basta uma requisição HTTP, sem login. A Calif Research, que descobriu o problema em conjunto com a OpenAI e a Wiz, batizou a cadeia de php2shell e confirmou o ataque contra WordPress, Drupal, Joomla, MediaWiki, Magento e outros, em configuração padrão. Como o PHP roda uma fração enorme da web, quase todo mundo que mantém um site ou servidor PHP precisa agir agora.

Este artigo tem dois objetivos. O primeiro é explicar, em nível conceitual, por que essas falhas existem: como um simples estouro de memória vira uma corrupção de estado que o PHP depois usa como se fosse válida. O segundo, o mais urgente, é defensivo: saber se você está exposto, qual versão instalar, o que fazer enquanto não atualiza e como procurar tentativas nos logs. Não há aqui exploit, código de ataque ou passo a passo para reproduzir; o que uso vem das fontes primárias listadas nas Referências, que li e conferi.

Ao final você vai:

  1. entender o que cada CVE descreve e por que a combinação de memória esgotada e estado compartilhado é perigosa;
  2. saber por que mod_php e PHP-FPM se comportam de forma diferente diante dessas falhas;
  3. verificar sua versão e o seu SAPI (a forma como o PHP é ligado ao servidor web);
  4. aplicar o patch e, se não puder, as mitigações parciais que o próprio projeto PHP recomenda;
  5. saber o que procurar nos logs, deixando claro o que é recomendação oficial e o que é heurística minha.

O que você precisa: noções básicas de como o PHP atende requisições web (um servidor recebe a requisição e entrega a um processo PHP) e acesso de leitura à configuração do seu servidor. Não é preciso saber C nem segurança ofensiva. Dois cuidados: este texto é um retrato de 22 de outubro de 2026, e o aviso do projeto PHP e os advisories no GitHub são a fonte que vale se algo mudar depois; e o que está nas mitigações é parcial, não substitui a atualização.

O que são as duas falhas

O aviso oficial do PHP resume cada uma em uma frase. Reproduzo o sentido, em português:

  • CVE-2026-109618, "RCE via OOM corruption".OOM é out of memory, falta de memória. O tratamento de falta de memória pode interromper o PHP enquanto os dados internos da requisição estão apenas parcialmente inicializados. A limpeza da requisição, depois, trata esses dados inválidos como se fossem válidos.
  • CVE-2026-109619, "RCE via stale global state". Estado interno que pertence a uma requisição pode sobreviver depois que a memória que o sustentava foi liberada. Uma requisição seguinte, tratada pelo mesmo processo, pode usar esse estado "velho" antes de a inicialização normal zerá-lo.

Em termos de classe de bug, os advisories as classificam, entre outros, como CWE-416 (use after free, uso de memória depois de liberada), CWE-824 (acesso a ponteiro não inicializado) e CWE-755 (tratamento inadequado de condições excepcionais). Em português corrente: ponteiros que continuam apontando para memória que já não é do PHP, e que o PHP volta a usar.

Segundo a Calif, o ponto mais antigo da cadeia remonta ao PHP 5.4.0 (a página diz "PHP >= 5.4.0"), o que explica por que o problema atinge versões tão variadas. Já a execução de código arbitrário contra qualquer script, no caso do mod_php, é descrita para PHP 7.3 ou mais novo.

Anatomia da cadeia (nível conceitual)

A cadeia tem três ideias encaixadas. Vou descrevê-las no nível de mecanismo; deliberadamente não detalho tamanhos, ordens de alocação nem nada que sirva de receita.

1. Controlar o momento do erro fatal

O PHP tem a diretiva memory_limit, o teto de memória que um script pode usar (o padrão é 128 MiB). Quando uma requisição passa do teto, o PHP encerra com um erro fatal. Normalmente isso é só uma resposta de erro. O que a pesquisa mostra é que, se o atacante consegue fazer o estouro acontecer num ponto escolhido, ele controla onde o PHP é interrompido.

Para estourar o teto antes de qualquer código da aplicação rodar, o atacante precisa fazer o PHP consumir muita memória ao interpretar o corpo da requisição. Aqui entra um limite que deveria impedir isso: post_max_size (padrão de 8 MiB), o tamanho máximo do corpo de um POST. A Calif explica que o PHP confere o tamanho declarado no cabeçalho Content-Length, e não os bytes realmente consumidos. Requisições chunked (HTTP/1.1, em que o corpo vem em pedaços sem tamanho total anunciado) ou HTTP/2 sem comprimento declarado, além de filtros de descompressão da entrada, criam essa diferença entre o que foi declarado e o que foi lido.

O segundo ingrediente é o formato multipart/form-data (RFC 1867), usado em envios de arquivo. Campos com nomes aninhados (a[b][c]...) viram tabelas hash e strings internas na memória. Segundo a Calif, centenas de partes de arquivo com nome de arquivo vazio e nomes de campo profundamente aninhados expandem-se em muito mais memória do que o corpo ocupa, passando dos 128 MiB sem chegar nem perto dos 8 MiB de post_max_size. Resultado: o corpo "parece pequeno", mas o heap estoura.

2. O padrão: estado compartilhado alterado antes de a inicialização terminar

Se o erro fatal acontecesse num ponto limpo, nada de grave ocorreria. O problema é que o PHP, internamente, trata esse erro com um salto não local (longjmp(), que abandona a pilha de chamadas e volta para um ponto anterior). O advisory do CVE-2026-109618 diz isso explicitamente: o PHP usa longjmp() para sair em caso de falta de memória, e em alguns contextos a memória da requisição já está alcançável antes de estar totalmente inicializada.

O exemplo que a Calif dá é o de uma inserção em tabela hash (a estrutura de dados que o PHP usa para arrays e para quase tudo): o contador de elementos é incrementado antes de a chave ser alocada. Se a alocação da chave falha e o fluxo salta embora, sobra um "balde" (bucket) meio montado que a tabela já conta como vivo. Na hora de desmontar a requisição, a limpeza percorre esse balde como se fosse legítimo.

Esse é o padrão que vale guardar, porque ele se repete em muitos programas em C e em qualquer sistema com tratamento de erro por salto:

Atualizar um estado compartilhado antes de concluir a operação que o torna consistente, e sem um caminho de limpeza quando um erro fatal interrompe o meio do processo.

O CVE-2026-109619 é o outro lado da mesma moeda. Segundo o advisory, o PHP mantém estado global (de erro, de sessão, de objetos e de extensões) que pode apontar para memória de uma requisição já terminada. Se o heap da requisição é liberado sem zerar esse estado, o código que roda antes da inicialização normal da próxima requisição pode usar o ponteiro velho. O advisory descreve duas peças: um produtor, que guarda estado da requisição, e um consumidor, que age sobre esse estado depois; os dois precisam ser atendidos pelo mesmo processo em requisições diferentes, e erros fatais (inclusive falta de memória e Maximum execution time exceeded, o estouro do tempo de execução) podem pular o passo de reset. A Calif dá um exemplo concreto de produtor: o upload progress de sessão, recurso que acompanha o andamento de um upload e é inicializado antes de os módulos do PHP serem ativados; uma falha fatal ali pula o encerramento do módulo de sessão e deixa ponteiros pendurados.

3. Reaproveitar o estado na requisição seguinte, e por que mod_php e PHP-FPM diferem

O terceiro passo é a requisição seguinte usar aquele estado que ficou apontando para memória liberada. Aqui a arquitetura do servidor decide quanto o ataque é fácil.

  • Apache com mod_php. O PHP vive dentro do processo do Apache. Mesmo quando a inicialização da requisição falha, o encerramento da requisição ainda roda, e o processo sobrevive e é reutilizado. Assim, o estado corrompido é consumido, e a requisição seguinte cai em cima do estado velho. É por isso que o projeto PHP considera o risco mais alto no mod_php, com exploração demonstrada em que uma requisição não autenticada dispara o ataque antes de qualquer código da aplicação rodar.
  • PHP-FPM. Nele, uma falha na fase de partida da requisição costuma matar o processo-trabalhador (worker), então o estado corrompido desaparece junto e o primeiro mecanismo não funciona. A Calif descreve, porém, um caminho diferente para o FPM, que acontece depois da partida: uma falha fatal durante a decodificação de sessão ou um unserialize() na aplicação deixa estado do decodificador pendurado, e uma requisição de upload progress seguinte o reaproveita. Segundo o advisory do CVE-2026-109618, o FPM é "plausivelmente suscetível", mas sem exploração completa demonstrada; o do CVE-2026-109619 afirma que o ciclo de vida do upload progress de sessão foi validado contra mod_php e FPM. Resumindo: o FPM é mais difícil de atacar, e não é imune. O aviso lista como em risco todas as instalações que atendem requisições web: FPM, mod_php, php-cgi, FrankenPHP e LiteSpeed.

Duas observações de contexto. A primeira: a ideia de que o memory_limit poderia ser um problema de segurança não é nova; a própria Calif cita um alerta de Stefan Esser de 2004 e um bug de 2020 (#78876) sobre variáveis longas em multipart. O que mudou foi a descoberta de caminhos que transformam a falha em execução de código. A segunda: o que importa para você não é a engenharia da exploração, e sim que a cadeia depende de poder provocar erro fatal na fase de leitura da requisição, e é exatamente isso que as mitigações e a correção atacam.

Nota 1

O patch do CVE-2026-109618 troca o longjmp() por outra estratégia. Segundo o advisory, a falta de memória passa a ser tratada com um "OOM suave" adiado até o próximo ponto seguro da máquina virtual do PHP e, num limite mais alto, com uma saída imediata via _exit(). A correção, portanto, não "conserta o contador": remove o mecanismo que deixava o estado pela metade.

Estou exposto?

Pela fonte oficial, qualquer PHP que atenda requisições web abaixo das versões corrigidas está afetado. Os advisories dizem "abaixo de 8.2.35, 8.3.36, 8.4.27 e 8.5.12". Isso responde em duas etapas.

Passo 1: qual a versão e qual o SAPI. SAPI (Server API) é a interface pela qual o PHP conversa com o servidor: apache2handler (o mod_php), fpm-fcgi (PHP-FPM), cgi-fcgi, litespeed, frankenphp. Para ver a versão do PHP de linha de comando:

php -v

Atenção: o PHP de linha de comando pode não ser o mesmo que atende o site. O jeito mais confiável é criar, temporariamente e protegido, um arquivo que imprima PHP_VERSION e PHP_SAPI, ou consultar o pacote instalado no sistema:

php-fpm -v
apache2 -M | grep php

Passo 2: compare com as versões corrigidas.

RamoCorrigido em
8.58.5.12
8.48.4.27
8.38.3.36
8.28.2.35
8.1 e anterioressem correção da equipe do PHP (veja abaixo)

Pontos que costumam enganar:

  • Distribuições Linux (Debian, Ubuntu, RHEL e derivadas) costumam aplicar o patch sem mudar o número de versão do PHP. Um 8.3.30 de um pacote pode já estar corrigido; confirme no changelog do pacote ou no aviso de segurança do fornecedor. O PHP informa que as distribuições receberam os patches com antecedência.
  • PHP 8.1 e anteriores não recebem mais atualizações do projeto. O aviso diz para procurar o fornecedor (alguns têm backports) ou migrar para uma versão suportada; para quem compila do código-fonte, o projeto publicou backports mínimos para 7.4, 8.0 e 8.1 em ramos do repositório php-src (nomeados PHP-7.4-CVE-2026-109618-109619, PHP-8.0-... e PHP-8.1-...).
  • O PHP 8.2 chega ao fim da vida em 31 de dezembro de 2026, segundo o anúncio do 8.2.35. Atualize para esse ramo apenas como ponte.
  • Quem usa o repositório voluntário rpms.remirepo.net foi avisado de que ele não teria atualização no dia; a recomendação é usar pacotes da distribuição ou compilar.

O patch

A solução completa é atualizar para PHP 8.5.12, 8.4.27, 8.3.36 ou 8.2.35 (ou o pacote equivalente corrigido do seu fornecedor). Depois de instalar:

  1. reinicie todos os processos (systemctl restart php-fpm ou o nome do seu serviço; systemctl restart apache2 para o mod_php). Processos antigos continuam com o código vulnerável na memória;
  2. confirme a nova versão pelo SAPI que atende o site, não só pela linha de comando;
  3. em contêineres, reconstrua a imagem a partir de uma base corrigida e reimplante; atualizar o host não muda a imagem.

Se você não pode atualizar agora: mitigações

O aviso é enfático: as medidas abaixo reduzem a exposição a caminhos específicos, mas não tornam a instalação segura. O advisory do CVE-2026-109619 diz literalmente que não existe mitigação genérica completa. Use-as só como ponte até o patch.

CVE-2026-109618 (corrupção por falta de memória):

  • Definir memory_limit=-1, de preferência de um jeito que a aplicação não consiga sobrescrever (php_admin_value no pool do FPM ou na configuração do Apache), e conferir que nenhum pool, diretório ou aplicação recoloca um limite finito. Isso remove a falha controlável do limite de memória do PHP, mas uma falha de alocação "de verdade" ainda pode chegar ao caminho vulnerável.
  • Manter post_max_size baixo: 8M para aplicações sem upload, 32M se precisam de upload, com upload_max_filesize abaixo disso. Considerar impor limites no servidor web ou proxy sobre o tamanho decodificado do corpo, número de campos, partes multipart, cabeçalhos e profundidade de aninhamento, inclusive para requisições sem Content-Length utilizável.
  • Usar um limite de contêiner/cgroup como fronteira de disponibilidade, de preferência com política que mate só o worker afetado. Não usar RLIMIT_AS nem RLIMIT_DATA: eles podem provocar justamente a falha de alocação vulnerável.

CVE-2026-109619 (estado global velho):

  • session.upload_progress.enabled=0 e reiniciar todos os workers, num lugar aplicado antes da leitura da requisição: php.ini, pool do FPM ou configuração do servidor. Não confie em ini_set() nem em .user.ini do FPM. Isso bloqueia os caminhos de sessão confirmados, não outras variantes.
  • No PHP 8.5, fatal_error_backtraces=0, também com reinício. Bloqueia só o caminho confirmado do backtrace de erro fatal.
  • Remover configurações inválidas ou obsoletas aplicadas por requisição que emitam diagnósticos antes da inicialização. Esconder o aviso com error_reporting, display_errors ou @ não resolve.
  • Corrigir erros fatais e timeouts de execução que um atacante consegue provocar (em callbacks, processamento de upload ou importação, limpeza).

O aviso sobre memory_limit=-1. Tirar o teto de memória para fechar um caminho de ataque abre outro: um script, ou uma requisição maliciosa, passa a poder consumir toda a RAM do servidor, e o resultado é exaustão de memória e queda do serviço. A Calif avisa que a mudança "aumenta o risco de exaustão de memória" e não substitui o patch. Por isso o aviso do PHP combina a medida com um limite externo (cgroup/contêiner) que mate só o processo culpado. Se o seu servidor não tem isso, é um risco real a pesar.

Resumo do que a Calif sugere por SAPI: no PHP-FPM, session.upload_progress.enabled=0 mais memory_limit=-1; no mod_php, memory_limit=-1. Os advisories e o aviso do PHP acrescentam as demais camadas acima.

Como conferir a configuração efetiva

Cuidado: php -i lê o php.ini da linha de comando, que pode diferir do FPM/Apache. Prefira o binário do SAPI e os arquivos do pool:

php -i | grep -E 'memory_limit|post_max_size|upload_progress.enabled'
php-fpm -tt 2>&1 | grep -E 'memory_limit|upload_progress|post_max_size'
grep -rE 'memory_limit|upload_progress|php_(admin_)?value' /etc/php* /etc/apache2 2>/dev/null

Se memory_limit estiver finito num pool ou diretório, a mitigação não vale ali.

Como detectar tentativas

Um esclarecimento importante: nem a Calif, nem os advisories, nem o aviso do PHP publicam assinaturas de ataque ou indicadores de comprometimento. Tudo o que segue nesta seção é heurística minha, derivada do mecanismo descrito acima, para você ter por onde começar. Não é um conjunto de regras validado, vai gerar falso positivo e a ausência de sinais não prova que não houve ataque.

O que procurar, e por quê:

  1. Erros fatais de memória esgotada. O ataque depende de provocar falta de memória. No log de erros do PHP, picos de Allowed memory size of ... bytes exhausted (a mensagem padrão do PHP) vindos de poucos IPs, sobretudo em URLs que não aceitam upload.
  2. Processos que caem. No FPM, linhas como child ... exited on signal 11 (SIGSEGV) no log do php-fpm; no Apache, child pid ... exit signal Segmentation fault no error.log. Falhas de segmentação em sequência, num servidor que antes não as tinha, merecem investigação (e podem indicar também tentativas fracassadas).
  3. Requisições multipart suspeitas.POST com Content-Type: multipart/form-data em rotas que normalmente não recebem arquivo, corpos incomumente grandes em relação ao que a aplicação espera, ou com transferência chunked (Transfer-Encoding: chunked) sem Content-Length. Por padrão os logs de acesso não registram isso, então vale habilitar temporariamente os campos no formato de log:
# nginx: acrescente ao log_format
log_format php2shell '$remote_addr [$time_local] "$request" $status $request_length '
                     'te="$http_transfer_encoding" ct="$content_type"';
# Apache: LogFormat com o tipo e a codificação de transferência
LogFormat "%h %t \"%r\" %>s %I te=\"%{Transfer-Encoding}i\" ct=\"%{Content-Type}i\"" php2shell
  1. Padrão de dois tempos. Como o ataque usa uma requisição para deixar estado corrompido e outra para consumi-lo, procure, num mesmo IP e janela curta, uma requisição multipart anormal seguida de outra de upload progress ou de erro fatal. Nos logs, uma sequência de POST multipart e uma queda de worker logo depois.
  2. Sinais pós-comprometimento. Se houver suspeita, o que vale mais que o log de entrada é a máquina: arquivos .php novos ou alterados em diretórios de upload e de aplicação, processos filhos incomuns do usuário do servidor web (www-data, apache, nginx), conexões de saída novas. Compare com o último deploy conhecido.

Exemplos de buscas (adapte caminhos e formato ao seu ambiente):

grep -c 'Allowed memory size' /var/log/php*/error.log
grep -E 'exited on signal 11|Segmentation fault' /var/log/php*-fpm.log /var/log/apache2/error.log
awk '$0 ~ /te="chunked"/ && $0 ~ /multipart/' /var/log/nginx/php2shell.log

Se encontrar algo, trate como incidente: isole, preserve os logs, atualize e troque credenciais acessíveis ao processo PHP (senhas de banco, chaves de API, wp-config.php e equivalentes), porque RCE roda com os privilégios do worker.

Glossário

TermoO que é
RCERemote code execution: execução remota de código. O atacante faz o servidor executar código escolhido por ele, a distância.
Pré-autenticaçãoA falha pode ser explorada sem login; basta enviar uma requisição ao servidor.
CVECommon Vulnerabilities and Exposures: identificador público e único de uma vulnerabilidade, no formato CVE-ano-número.
CVSSCommon Vulnerability Scoring System: pontuação de gravidade de 0 a 10. As duas falhas têm 9,2 na versão 4.0.
CWECommon Weakness Enumeration: catálogo de tipos de fraqueza de software (por exemplo, CWE-416, uso após liberação).
SAPIServer API: a camada pela qual o PHP se liga ao servidor web (mod_php, PHP-FPM, CGI, FrankenPHP, LiteSpeed).
mod_phpMódulo do Apache que embute o PHP no próprio processo do servidor; o worker é reutilizado entre requisições.
PHP-FPMFastCGI Process Manager: gerenciador de processos PHP separado do servidor web, usado com nginx, Apache e outros.
WorkerProcesso (ou thread) que atende requisições; vive e é reutilizado por várias delas.
OOMOut of memory: falta de memória.
memory_limitDiretiva do PHP que limita a memória por script (padrão 128 MiB). -1 remove o limite.
post_max_sizeDiretiva que limita o tamanho do corpo de um POST (padrão 8 MiB).
Erro fatalErro do PHP que encerra a execução da requisição na hora.
longjmp()Função de C que abandona as chamadas em andamento e salta para um ponto anterior; o PHP a usava para sair em caso de falta de memória.
Tabela hashEstrutura de dados que associa chaves a valores; base dos arrays do PHP.
Use after freeUso de memória depois de ela ter sido liberada; o ponteiro continua existindo, mas o conteúdo já não é do programa.
Estado globalDados compartilhados que vivem fora de uma função e podem sobreviver entre etapas ou requisições.
multipart/form-dataFormato de corpo HTTP usado para envio de arquivos e formulários, dividido em partes (RFC 1867).
ChunkedCodificação de transferência HTTP/1.1 em que o corpo chega em pedaços, sem tamanho total declarado.
Upload progress de sessãoRecurso do PHP (session.upload_progress.*) que guarda na sessão o andamento de um upload em curso.
cgroupMecanismo do kernel Linux para limitar recursos (como memória) de um grupo de processos; base dos limites de contêiner.
BackportAplicar uma correção feita numa versão nova em uma versão mais antiga.

Comandos

ComandoO que faz
php -vMostra a versão do PHP de linha de comando (que pode diferir da do servidor web).
php-fpm -vMostra a versão do PHP-FPM instalado.
apache2 -M | grep phpLista os módulos do Apache e filtra o mod_php. Em algumas distribuições o comando é apachectl -M ou httpd -M.
php -i | grep -E '...'Mostra a configuração efetiva do PHP da linha de comando e filtra as diretivas relevantes.
php-fpm -ttTesta a configuração do FPM e imprime a configuração efetiva, inclusive dos pools.
grep -rE '...' /etc/php* /etc/apache2Procura diretivas (memory_limit, upload_progress, php_admin_value) nos arquivos de configuração.
systemctl restart php-fpm / apache2Reinicia o serviço para que a nova versão ou configuração valha. O nome da unidade varia (php8.3-fpm, httpd).
grep -c 'Allowed memory size' ...Conta ocorrências de erro de memória esgotada no log do PHP.
grep -E 'exited on signal 11|Segmentation fault' ...Procura quedas de worker por falha de segmentação nos logs do FPM e do Apache.
awk '$0 ~ /te="chunked"/ && $0 ~ /multipart/' ...Filtra do log de acesso as requisições multipart enviadas em chunked.

Conclusão

As falhas do php2shell não são "um bug de upload". São o encontro de três coisas: um limite (post_max_size) que mede o corpo declarado e não a memória consumida, um tratamento de falta de memória que abandona o meio de uma inicialização, e estado global que sobrevive à memória que o sustentava. Sozinha, cada uma parece inofensiva. Juntas, com um worker que sobrevive (mod_php) ou uma requisição seguinte que reutiliza o estado, viram execução remota de código sem login. O padrão geral é útil fora do PHP: nunca publique estado compartilhado antes de concluir a inicialização, e garanta limpeza em todo caminho de erro, inclusive o fatal.

Na prática, o que fazer hoje: confira versão e SAPI, atualize para 8.5.12, 8.4.27, 8.3.36 ou 8.2.35 (ou o pacote corrigido da sua distribuição), reinicie os processos, use as mitigações só como ponte e olhe os logs em busca de memória esgotada, quedas de worker e multipart incomum. Se está em PHP 8.1 ou anterior, a atualização é migrar ou conseguir um backport do fornecedor. Isto vale também para quem hospeda WordPress, Drupal, Joomla e afins: a falha está no PHP, não na aplicação, e atualizar o CMS não resolve.

Para ir além

  • Leia o texto completo da Calif (abaixo) para a cronologia da descoberta e os detalhes técnicos dos caminhos de FPM e mod_php. Evite reproduzir qualquer coisa em ambiente que não seja seu e isolado.

Referências

Comentários

Os comentários usam o Giscus, que guarda as discussões no GitHub. Ao carregar, seu navegador se conecta ao giscus.app e ao GitHub, que recebem o seu endereço IP. Saiba mais na política de privacidade.