PHP2Shell: como corrigir bem o PHP e reforçá-lo para a próxima falha crítica

Publicado: · 19 min de leitura · Por Oluniyi D. Ajao

php security php2shell cve linux sysadmin hardening
Terminal running php-fpm8.4 -v shows PHP 8.4.26 struck through and the fixed PHP 8.4.27, beside the four fixed versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35

A 21 de outubro de 2026, o projeto PHP lançou versões de segurança para corrigir duas falhas críticas no próprio PHP: CVE-2026-109618 e CVE-2026-109619. Ambas podem permitir que um atacante sem qualquer autenticação execute código num servidor através de pedidos web manipulados. Cinco dias depois, a agência norte-americana de cibersegurança CISA (Cybersecurity and Infrastructure Security Agency) acrescentou as duas ao seu catálogo de vulnerabilidades com exploração confirmada em ataques. As versões corrigidas são PHP 8.5.12, 8.4.27, 8.3.36 e 8.2.35.

A imprensa especializada em segurança batizou o par de «PHP2Shell». Este guia explica em termos simples o que são os dois bugs e, depois, dá a programadores e administradores de sistemas as verificações exatas a fazer: que PHP serve realmente o seu site, se a sua distribuição já fez o backport da correção, como garantir que a atualização produz efeito e o que fazer se não puder atualizar hoje. A seguir, vai além da correção. O PHP2Shell não será a última falha crítica do PHP, e a parte final deste guia trata do reforço de segurança que limita o que um atacante consegue fazer quando a próxima falha chegar antes da sua atualização.

PHP2Shell: How to Patch PHP Properly, Then Harden It
Vídeo em inglês: a correção bem aplicada e, depois, seis formas de reforçar o PHP para a próxima falha.

O que é o PHP2Shell

«PHP2Shell» não é um nome oficial. O projeto PHP, os seus avisos de segurança no GitHub, os registos CVE e a CISA usam apenas os números CVE e títulos descritivos. A alcunha vem da imprensa de segurança e descreve o resultado: um pedido manipulado contra o PHP que acaba numa shell no servidor.

O aviso do projeto PHP descreve assim os dois bugs:

«RCE através de corrupção por falta de memória (OOM), CVE-2026-109618: o tratamento da falta de memória pode interromper o PHP enquanto os dados internos do pedido estão apenas parcialmente inicializados. A limpeza do pedido pode então tratar os dados inválidos como válidos.»

«RCE através de estado global obsoleto, CVE-2026-109619: o estado interno pertencente a um pedido pode sobreviver depois de a memória que o suporta ter sido libertada. Um pedido posterior tratado pelo mesmo worker pode usar esse estado obsoleto antes de a inicialização normal o repor.»

Na prática:

  • A CVE-2026-109618 é atingida através do processamento, pelo PHP, de envios de formulários multipart. Um pedido construído para esgotar a memória do PHP no momento errado deixa dados internos incompletos, e a limpeza que se segue confia neles.
  • A CVE-2026-109619 é um use-after-free entre pedidos. Um worker PHP que serve um pedido pode ficar com um apontador para memória que já foi libertada, e um pedido posterior nesse worker usa-o. Um dos caminhos confirmados é o progresso de upload da sessão (session upload progress), que vem ativo por predefinição e, nas palavras do aviso do GitHub, não precisa de «nenhuma chamada da aplicação a session_start()».

A equipa de segurança do PHP atribui a ambas uma pontuação de 9,2 em 10 (crítica) no CVSS v4.0. O pormenor decisivo é o sítio onde se encontram: no próprio motor de execução do PHP e, no Apache com mod_php, podem ser acionadas antes de correr qualquer código da aplicação. Atualizar o Laravel, o Symfony, o WordPress ou os seus pacotes Composer não corrige o PHP2Shell. É o binário do PHP que tem de mudar.

Quem está exposto

O aviso do projeto PHP é direto: «todas as instalações PHP que servem pedidos web, incluindo PHP-FPM, Apache com mod_php, php-cgi, FrankenPHP e LiteSpeed». Considera o risco mais elevado no Apache com mod_php, «com um exploit demonstrado em que um pedido não autenticado pode desencadear o ataque antes de correr qualquer código da aplicação».

Os avisos do GitHub detalham a situação por configuração. A execução remota de código completa através do bug de memória foi validada contra mod_php com os limites de memória e de entrada predefinidos. O PHP-FPM é considerado vulnerável com base na análise do código-fonte, e o caminho de sessão do segundo bug foi validado tanto contra mod_php como contra FPM. O FrankenPHP é descrito como provavelmente vulnerável a ataques que não dependem da aplicação; para a interface do LiteSpeed, exploits específicos da aplicação são descritos como prováveis, mas não validados. Se o seu servidor corre PHP para a web de alguma destas formas e não está numa versão corrigida, trate-o como vulnerável.

Versões afetadas, segundo os avisos: todas as versões 8.2 anteriores à 8.2.35, todas as 8.3 anteriores à 8.3.36, todas as 8.4 anteriores à 8.4.27 e todas as 8.5 anteriores à 8.5.12. O PHP 8.1 e versões anteriores já não recebem novas versões do projeto PHP: «Algumas distribuições e fornecedores de suporte LTS disponibilizam correções com backport, por isso contacte o seu fornecedor para mais detalhes, ou atualize para uma versão suportada.» O projeto publicou também backports mínimos do código-fonte para 7.4, 8.0 e 8.1, que são patches e não versões. Note ainda que o PHP 8.2 chega ao fim de vida a 31 de dezembro de 2026, pelo que um servidor que hoje passe para a 8.2.35 precisa, de qualquer forma, de um plano de migração.

Já está a ser explorado?

Sim. A CISA acrescentou as duas CVE ao seu catálogo Known Exploited Vulnerabilities a 26 de outubro de 2026. No mesmo dia, o BleepingComputer noticiou que honeypots geridos por Adel Karimi, responsável de deteção e resposta na Perplexity, registaram uma execução remota de código confirmada contra um servidor Apache com mod_php em PHP 8.4.26, uma versão atrás da correção, e que a Amazon Threat Intelligence tinha detetado atacantes a visar as falhas quatro dias antes da publicação das correções. A Arctic Wolf reportou varrimentos em larga escala pouco depois da divulgação.

Houve código de prova de conceito divulgado antes das correções. A equipa de segurança do PHP disse ao BleepingComputer que nenhum funcionava, mas os ataques confirmados mostram que existem exploits funcionais. Parta do princípio de que scanners automáticos já estão a testar todos os endpoints PHP públicos que conseguem encontrar.

Para programadores: o que verificar

  • Verifique o PHP que serve a web, não o do seu terminal. O php -v mostra a versão do binário de linha de comandos. O seu site pode correr outra build através do FPM, do Apache ou de um painel de alojamento. Coloque no site uma página temporária que mostre PHP_VERSION e PHP_SAPI, abra-a e depois apague-a.
  • Reconstrua os seus contentores. Se faz o deploy do PHP em Docker, a correção está na imagem base. Reconstrua com docker build --pull sobre uma base que traga uma versão corrigida, volte a fazer o deploy e confirme que o contentor em execução indica essa versão.
  • Atualizar a aplicação não resolve isto. Vale a pena atualizar a framework e o CMS, mas as falhas estão no PHP. Se está a ponderar onde alojar o WordPress, a pergunta que aqui importa é quem atualiza e reinicia o PHP, e com que rapidez.
  • Em alojamento partilhado ou gerido, pergunte ao seu fornecedor. Pergunte que versão do PHP o seu site usa e se os workers foram reiniciados depois da atualização. As duas perguntas contam.

Para administradores de sistemas: a correção, passo a passo

1. Encontre todos os PHP que servem tráfego web

É frequente um servidor ter várias builds de PHP lado a lado: pacotes da distribuição, um repositório de terceiros, as builds próprias de um painel de controlo, um contentor. Faça a lista de todas e veja qual delas cada site usa de facto. No PHP-FPM, liste todos os processos master e o binário por trás de cada um:

pgrep -af 'php-fpm: master'
for p in $(pgrep -f 'php-fpm: master'); do sudo readlink /proc/$p/exe; done

Se um caminho terminar em «(deleted)», o pacote foi substituído em disco mas esse processo nunca foi reiniciado, por isso continua a correr o código antigo.

No Apache com mod_php, o módulo faz parte do próprio processo do Apache, pelo que é o pacote PHP instalado que tem de verificar.

2. Leia corretamente a versão

As distribuições fazem muitas vezes o backport das correções de segurança sem mudar o número de versão upstream, uma das razões por que a distribuição que escolhe importa (veja o nosso guia sobre como escolher o sistema operativo do seu VPS). O Ubuntu 24.04, por exemplo, corrigiu as duas CVE no pacote 8.3.6-0ubuntu0.24.04.13: continua a ser «8.3.6», mas está corrigido. Assim, uma versão abaixo da 8.3.36 num pacote da distribuição não significa, por si só, que esteja vulnerável. Quem esclarece é o tracker de segurança do seu fornecedor ou o changelog do pacote:

# Debian / Ubuntu
zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php*-fpm/changelog.Debian.gz

# Pacotes de distribuições da família RHEL
rpm -q --changelog php-fpm | grep -cE 'CVE-2026-10961[89]'

Um resultado acima de zero significa que o pacote inclui as correções. As builds que acompanham a numeração upstream, como as de repositórios de terceiros, as de painéis de controlo e as imagens Docker oficiais, muitas vezes não indicam números de CVE, por isso nelas um zero não quer dizer nada: o teste é a própria versão. Os trackers de segurança públicos do Debian e do Ubuntu indicam a versão corrigida do pacote para cada versão da distribuição.

3. Atualize e depois reinicie todos os workers

Instalar o pacote não altera os processos que já estão a correr. Os workers do PHP-FPM e os processos filhos do Apache mantêm o código antigo em memória até serem reiniciados. Depois da atualização, reinicie o serviço por completo:

systemctl restart php8.3-fpm     # ou php-fpm, conforme a sua distribuição
systemctl restart apache2        # ou httpd, se usar mod_php

Confirme depois que a atualização produziu efeito. A hora de arranque do serviço tem de ser posterior à hora de instalação do pacote:

systemctl show php8.3-fpm -p ActiveEnterTimestamp
rpm -q --last php-fpm                                         # família RHEL
zgrep -h " upgrade .*php" /var/log/dpkg.log* | sort | tail     # Debian / Ubuntu

4. Trate do PHP em fim de vida

Os servidores com PHP 8.1 ou anterior têm três opções: um backport do fornecedor (algumas distribuições e fornecedores de suporte de longa duração disponibilizam-no), os patches mínimos de código-fonte do projeto PHP para 7.4, 8.0 e 8.1, se compilar o PHP por conta própria, ou a migração para um ramo suportado. Das três, a migração é a única que também cobre a próxima falha crítica.

5. Se não puder atualizar hoje

O projeto PHP é claro quanto à inexistência de uma solução de contorno completa: «Não há mitigações completas, atualizar é a única opção segura.» Indica, ainda assim, medidas que fecham caminhos de ataque específicos enquanto agenda a atualização. Defina-as na configuração do pool do FPM ou do Apache, nunca com ini_set() nem com .user.ini, e reinicie depois todos os workers:

  • Para a CVE-2026-109619: session.upload_progress.enabled=0, que bloqueia os caminhos de sessão confirmados. No PHP 8.5, também fatal_error_backtraces=0.
  • Para a CVE-2026-109618: memory_limit=-1, imposto ao nível administrativo. Isto retira o limite de memória do próprio PHP, por isso combine-o com um limite de memória ao nível do contentor ou do cgroup. O aviso do GitHub desaconselha RLIMIT_AS ou RLIMIT_DATA, que acionam o caminho vulnerável. O projeto PHP recomenda também limites baixos para o tamanho dos pedidos: post_max_size à volta de 8M onde não recebe uploads, 32M onde recebe.

Num ficheiro de pool do PHP-FPM:

php_admin_value[session.upload_progress.enabled] = 0
php_admin_value[memory_limit] = -1

Na configuração do Apache, para mod_php:

php_admin_flag session.upload_progress.enabled off
php_admin_value memory_limit -1

6. Parta do princípio de que um servidor exposto pode ter sido sondado

Se um servidor exposto à internet correu um PHP vulnerável a partir de meados de outubro, a correção fecha a porta mas não diz nada sobre quem entrou antes. A entrada no catálogo da CISA remete as agências federais dos EUA para uma triagem forense, e essa é uma fasquia sensata para toda a gente: reveja os registos web e os registos de erros do PHP à procura de pedidos POST multipart invulgares e de falhas de workers, procure ficheiros e processos do utilizador web que não tenha criado e mude as credenciais que o utilizador web conseguia ler.

Além da correção: reforce o PHP para a próxima falha

A correção resolve esta falha. Não resolve a próxima, que pode ser explorada antes de existir qualquer correção. As medidas abaixo não impedem que uma falha do PHP seja acionada. Decidem o que um atacante obtém quando isso acontece: os ficheiros de um único site, ou o servidor inteiro, as suas credenciais e tudo aquilo a que consegue chegar.

Dê a cada site o seu próprio utilizador PHP sem privilégios

Corra cada site no seu próprio pool do PHP-FPM, com o seu próprio utilizador de sistema, e nunca como root. A execução de código «como o utilizador do worker web», nas palavras do aviso do GitHub, passa então a ser execução como o utilizador de um único site, e não como o utilizador que consegue ler todos os sites do servidor:

[site1]
user = site1
group = site1
listen = /run/php/site1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = ondemand
pm.max_children = 10

Na família RHEL, use /run/php-fpm/site1.sock e listen.owner = nginx (ou apache). Feche o diretório de cada site aos restantes (modo 750), caso contrário os utilizadores de cada site continuam a conseguir ler os ficheiros uns dos outros.

Torne o código só de leitura e nunca execute ficheiros carregados

O utilizador PHP deve conseguir ler a aplicação, mas não alterá-la. Faça com que o código pertença a um utilizador de deploy separado e dê ao utilizador PHP permissão de escrita apenas nos diretórios que precisam dela, como os de uploads e de cache. Um worker que não consegue escrever na árvore de código não consegue reescrever a sua aplicação. Pode, ainda assim, deixar ficheiros nos diretórios com permissão de escrita, alterar a base de dados ou ficar residente num worker em execução, por isso trate todos os caminhos com permissão de escrita como não fiáveis, nunca inclua PHP a partir deles e reinicie os workers depois de um incidente. Depois, impeça a execução de PHP a partir desses diretórios. No nginx, coloque isto acima do seu bloco location principal do PHP:

location ~* ^/(?:uploads|wp-content/uploads)/.*\.(?:php\d*|phtml|phar)(?:/|$) {
    deny all;
}

No mod_php, coloque php_admin_flag engine off num bloco <Directory> para o diretório de uploads.

Reduza o que o servidor expõe

  • Remova as versões e extensões do PHP que nenhum site usa. Cada build instalada é mais uma para corrigir.
  • Defina expose_php = Off para que as respostas não anunciem aos scanners a sua versão exata do PHP.
  • Apague todas as páginas phpinfo().
  • Limite o tamanho dos pedidos antes de chegarem ao PHP, no servidor web ou no proxy (client_max_body_size no nginx). A recomendação do próprio projeto PHP para a CVE-2026-109618 inclui limites ao tamanho descodificado, ao número de campos e ao número de partes antes de os pedidos chegarem ao PHP. Limitar campos e partes exige uma regra num proxy ou numa WAF; se não recebe uploads por formulário, bloqueie aí os pedidos multipart.

Restrinja aquilo a que o PHP consegue chegar

Grande parte do que um atacante faz depois de entrar precisa de uma ligação para fora: descarregar um payload, abrir uma reverse shell, enviar dados roubados. A maioria dos workers web só precisa de chegar a uma base de dados, a uma cache e a algumas APIs conhecidas. O systemd consegue impor isso ao serviço PHP-FPM e, ao mesmo tempo, pôr os diretórios do sistema só de leitura para ele. Execute systemctl edit php8.3-fpm (ou php-fpm) e acrescente:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
IPAddressDeny=any
IPAddressAllow=localhost 10.0.0.0/8

Ajuste o IPAddressAllow ao que o PHP precisa realmente. Estas regras só olham para endereços IP: o DNS falha a menos que o seu resolver seja local (127.0.0.53 no Ubuntu) ou esteja na lista, as APIs externas precisam de ter os endereços na lista ou de um proxy de saída, e um FPM em TCP atrás de um proxy remoto precisa desse proxy na lista. O NoNewPrivileges também bloqueia o utilitário setgid que o Postfix usa para o mail(), por isso envie o correio por SMTP para um relay autorizado. Aplique isto apenas ao PHP-FPM, nunca ao apache2, ao httpd ou ao nginx. Teste em staging, reinicie e confirme que o PHP não consegue chegar a um endereço externo e que o site continua a funcionar.

Mantenha os segredos reduzidos e separados

Parta do princípio de que tudo o que o utilizador PHP consegue ler vai ser lido. Dê a cada site uma conta de base de dados apenas com os privilégios de que a aplicação precisa, guarde os ficheiros de configuração com credenciais fora da raiz web e legíveis apenas pelo utilizador desse site, e não deixe num servidor web chaves de que ele não precisa, como credenciais de backup ou tokens de fornecedores cloud. Uma intrusão passa então a custar os dados de uma base de dados, e não toda a sua infraestrutura.

Encurte o tempo até à correção

O PHP2Shell passou da correção a ataques confirmados em poucos dias. Subscreva os anúncios do projeto PHP e a lista de segurança da sua distribuição. Ative as atualizações de segurança automáticas (unattended-upgrades no Debian e no Ubuntu, dnf-automatic com apply_updates = yes na família RHEL). Confirme que cobrem o repositório de onde vem o seu PHP: no Debian e no Ubuntu, os repositórios PHP de terceiros não estão incluídos por predefinição. Combine-as com uma verificação dos serviços que ainda correm código antigo: needrestart no Debian e no Ubuntu, dnf needs-restarting -s na família RHEL. Uma atualização instalada mas ainda sem reinício deixa o servidor a correr o código vulnerável.

Saiba detetar o ataque e recuperar dele

  • Envie os registos web, do PHP e do sistema para fora do servidor, para que um atacante que lá tenha entrado não os consiga apagar.
  • Vigie as alterações à árvore de código com uma ferramenta de integridade de ficheiros como o AIDE. Numa árvore de código só de leitura, qualquer alteração fora de um deploy é um sinal.
  • Crie alertas para quando o utilizador web arranca processos que normalmente não arranca, como uma shell interativa ou uma ferramenta de download. Defina primeiro uma referência: o mail() do PHP arranca o sh para chamar o sendmail.
  • Guarde os backups num local onde o servidor web não consiga escrever nem apagar, e teste um restauro. Quando já não se pode confiar num servidor, reconstruí-lo a partir de uma imagem limpa e de um backup em bom estado é mais rápido do que limpá-lo.

Não confunda com a CVE-2026-115560

A 23 de outubro, o projeto PHP publicou um aviso informativo separado sobre um problema menos provável no progresso de upload da sessão, a CVE-2026-115560, que não faz parte do PHP2Shell e não foi corrigida nas versões de 21 de outubro. O conselho é a mesma definição: «Sugerimos que defina session.upload_progress.enabled=0 até à próxima versão regular de correção de bugs, para se proteger totalmente de um vetor de RCE improvável.»

A lista de verificação

Para o PHP2Shell, desde já:

  1. Liste todas as builds de PHP no servidor e qual delas serve cada site.
  2. Confirme que cada uma está na 8.5.12, 8.4.27, 8.3.36, 8.2.35 ou numa build do fornecedor que inclua as correções.
  3. Reinicie todos os pools do FPM e todas as instâncias do Apache, e compare a hora de arranque com a hora de instalação.
  4. Reconstrua e volte a fazer o deploy dos contentores a partir de imagens base corrigidas.
  5. Planeie já a saída do PHP 8.1 e anteriores, e da 8.2 antes de 31 de dezembro de 2026.
  6. Defina session.upload_progress.enabled=0 se não usar o progresso de upload.
  7. Reveja os registos de qualquer servidor que tenha estado exposto a partir de meados de outubro.

Para a próxima:

  1. Um pool do PHP-FPM e um utilizador sem privilégios por site, nunca root.
  2. Código pertencente a um utilizador de deploy, só de leitura para o PHP; nenhuma execução de PHP nos diretórios de uploads.
  3. Builds e extensões do PHP sem uso removidas, expose_php = Off, tamanho dos pedidos limitado antes do PHP.
  4. Ligações de saída do PHP limitadas ao que a aplicação precisa.
  5. Contas de base de dados com o mínimo de privilégios, e nenhuma chave esquecida no servidor web.
  6. Atualizações de segurança automáticas e uma verificação de reinícios.
  7. Registos fora do servidor, alertas de integridade de ficheiros e backups testados em que o servidor web não consegue mexer.

Num servidor autogerido, cada um destes passos fica a seu cargo. Os Servidores Cloud da AFRICLOUD incluem acesso root total, por isso a versão do PHP, a sua atualização e o seu reinício ficam sob o seu controlo.

Fontes

Artigos relacionados

Implementar Agora