
Kit de ferramentas de detecção e laboratório reproduzível para CVE-2026-87902, uma travessia de caminho não autenticada no WordPress. Inclui verificador remoto, analisador de IoC, regras Sigma e bancada de testes Docker.
check/ | Controlador remoto, passivo, sem acesso ao servidor |
detect/ | Analisador de IoC + regras Sigma |
offensive/ | Gerador de rastros, para validar a detecção em logs reais |
docker-compose.yml + provision/ | Bancada de testes, três configurações |
tests/validate.py | Portão de qualidade — condiciona qualquer publicação |
docs/ANALYSIS.md | A vulnerabilidade, a correção, a análise de alcançabilidade medida |
make up # monta a bancada make ioc # corpus de ataque -> logs -> detecção
make scan # verifica a bancada make test # portão de qualidade
Três instâncias WordPress em 127.0.0.1, que isolam cada fator do veredito.
| Porta | Instância | Núcleo | Tema ativo | Veredito esperado |
|---|---|---|---|---|
| 8091 | vuln-pre | 6.8.1 — não corrigido | Twenty Twelve, com page-templates/ | AFFECTED_PRECONDITION_MET |
| 8092 | vuln-nopre | 6.8.1 — não corrigido | Twenty Twenty-Four, sem page-* | AFFECTED_CORE_ONLY |
| 8093 | patched | 6.8.10 — corrigido | Twenty Twelve, com page-templates/ | NOT_AFFECTED |
A 8092 é a mais instrutiva: mesmo núcleo vulnerável que a 8091, mas o pré-requisito
de tema está ausente. É o que mostra que uma triagem baseada apenas na versão superestima
a exposição. O tema é a única variável entre 8091 e 8092, a correção a única
entre 8091 e 8093: as três incluem o mesmo tipo de conteúdo personalizado
(provision/mu-plugins/00-lab-cpt.php) e a mesma sonda de estado de requisição
(10-lab-debug.php, cabeçalhos X-Lab-* que expõem is_page(), o valor de
pagename visto pelo carregador e o template finalmente incluído).
make up # inicia e provisiona — idempotente, reexecutável
make status # versão de cada instância
make down # parada make clean : também remove os volumes
A imagem Docker oficial aponta /var/log/apache2/access.log para
/dev/stdout: os logs saem na saída do contêiner, não em um arquivo.
docker compose logs --no-log-prefix vuln-pre # acesso + erros
docker compose logs --no-log-prefix vuln-pre > access.log # para análise
docker compose logs -f --no-log-prefix vuln-pre # ao vivo
make logs # as três instâncias
Em um servidor clássico: /var/log/apache2/access.log, /var/log/nginx/access.log,
ou /home/*/logs/ na maioria dos provedores de hospedagem compartilhada. O formato deve
incluir a string de consulta — %r ou o formato combined a incluem, um LogFormat
construído sobre %U a perde, e sem ela nenhuma detecção é possível.
check/wp-ghsa-7hp8-check.pyDa Internet, sem acesso ao servidor. Nenhuma carga, nenhum traversal, nenhuma
escrita. Para cada host: detecção do WordPress, versão cruzada em cinco fontes
(meta generator, feed RSS, wp-links-opml.php, readme.html, ?ver= dos
recursos do núcleo), tema ativo, e sondagem do diretório page-*.
python3 check/wp-ghsa-7hp8-check.py --hosts-file hosts.txt --csv out.csv --json out.json
| Veredito | Significado |
|---|---|
AFFECTED_PRECONDITION_MET | Núcleo vulnerável e diretório page-* no tema ativo. Prioritário. |
AFFECTED_THEME_UNKNOWN | Núcleo vulnerável, tema não determinado. |
AFFECTED_CORE_ONLY | Núcleo vulnerável, pré-requisito de tema ausente. Corrigir mesmo assim. |
VERSION_UNKNOWN | WordPress detectado, versão oculta. |
NOT_AFFECTED | Versão ≥ correção de seu branch. |
"AFETADO" significa que o código vulnerável está presente, não que um atacante pode
executar código. Ver docs/ANALYSIS.md.
--transport browser (padrão) controla o Google Chrome instalado; as sub-requisições
partem de um fetch() executado dentro da página e herdam sua pilha TLS, sua ordem
de cabeçalhos HTTP/2 e seus cookies, o que evita ser filtrado por um CDN antes que
a URL seja lida. --transport direct usa apenas a biblioteca padrão.
--scheme http|https evita o fallback https → http, que deixa senão uma linha 400
com um ClientHello TLS bruto no log do alvo.
Cada requisição é registrada com timestamp em milissegundos em um log JSONL: identificador
de sessão, número da requisição, fase, URL, status, tamanho, duração, IP de saída, marcador.
O marcador SECAUDIT/<nonce> vai no cabeçalho X-Security-Audit e como sufixo
do User-Agent — adicionado, nunca substituído, para permanecer visível em um access log
padrão sem quebrar a assinatura do navegador. Personalizável via --marker.
Sem oráculo remoto. A opção
--probe-inclusionrealiza uma comparação diferencial em alvo inerte (wp-includes/version.php, já carregado no bootstrap:require_onceo tornaria um no-op integral). Em uma instalação padrão ela retornaNOT_REACHABLEinclusive em um núcleo vulnerável, com respostas idênticas byte a byte entre instância corrigida e não corrigida. Não é uma limitação da ferramenta: o WordPress responde 404 antes de consultar a hierarquia de templates. Demonstração numérica emdocs/ANALYSIS.md, seção 3.
Uma regex literal do tipo pagename=.*%2e%2e%2f é contornada alterando a caixa
(%2E), codificando mais uma vez (%252e), ou misturando literal e codificado
(templates/..%2f../). Qualquer lista de padrões é incompleta por construção.
Parte-se, portanto, do código, não da escrita do atacante:
pagename sofre no máximo duas decodificações antes de atingir o disco — a do
PHP na string de consulta, depois o urldecode() explícito de get_page_template().file_exists() deve conter
um componente ... No Linux, o diretório pai é escrito exatamente nos dois
bytes 0x2E 0x2E; não existe nenhuma outra representação no nível do
sistema de arquivos.Portanto: decodificar até o ponto fixo e testar em cada nível. É um superconjunto estrito do que o WordPress faz — adicionar uma camada de codificação apenas desloca a correspondência de um nível, que também atravessamos.
python3 detect/wp-ghsa-7hp8-ioc.py access.log
docker compose logs --no-log-prefix vuln-pre | python3 detect/wp-ghsa-7hp8-ioc.py -
| Regra | Severidade | Gatilho |
|---|---|---|
GHSA-7hp8-traversal-pagename | CRITICAL | componente .. em pagename, em qualquer nível de decodificação |
GHSA-7hp8-traversal-param | HIGH | mesma primitiva em outro parâmetro (temas e extensões também chamam locate_template()) |
GHSA-7hp8-traversal-path | HIGH | componente .. no caminho da URL (nginx deixa passar %2f, o Apache não por padrão) |
GHSA-7hp8-overlong-encoding | MEDIUM | sobrecodificação UTF-8 (%c0%ae). Inoperante no Linux, mas nunca legítimo |
GHSA-7hp8-theme-page-dir-probe | LOW | sondagem de um diretório page-* de tema — o reconhecimento |
Regras Sigma em detect/sigma-wp-ghsa-7hp8.yml.
Como o Sigma não sabe decodificar recursivamente, elas enumeram os níveis 0 a 3: é
uma aproximação assumida, para a triagem de primeiro nível. Reprocessar as
correspondências no analisador para decidir.
WP::parse_request() lê $_POST antes de $_GET. pagename pode, portanto,
chegar em um corpo de requisição, ausente de qualquer access log. Cobertura necessária
no nível de WAF ou ModSecurity, sobre o corpo.Nenhuma regra sobre access log cobre o primeiro ponto. É uma limitação do suporte, não da regra — mas deve ser conhecida antes de anunciar cobertura completa.
offensive/generate-traces.py reproduz 12 escritas diferentes da mesma carga
(literal, codificação simples/dupla/tripla, caixa alta e baixa, misturas,
sobrecodificação UTF-8, ponto-espaço) mais 7 requisições legítimas que
se parecem com elas (slug com pontos, wp-includes em um slug, permalink com data,
porcento codificado).
Ele não obtém nada: não existe exploit remoto para este vetor em uma instalação padrão. Ele produz rastros — é todo o seu propósito.
make ioc # gera o corpus, obtém os logs reais, passa o analisador
Esperado, e verificado por make test em logs Apache reais:
12 cargas detectadas de 12 (11 CRITICAL, a sobrecodificação em MEDIUM porque
não é explorável no Linux), 0 alerta nas 7 requisições legítimas, e
detecção efetiva em três níveis de decodificação diferentes.
Alvo limitado à bancada local; qualquer outro exige --i-have-authorization.
register_argc_argv = Off no php.ini do SAPI web; desinstalar o PEAR se
não utilizado — é o pivô inclusão → execução citado no aviso.open_basedir limitado à raiz do site: confina qualquer inclusão local.make test é a condição de publicação: matriz dos 25 branches do aviso
(incluindo armadilhas de comparação — 6.8.9 < 6.8.10 numericamente, pré-versões,
branches fora da matriz), controlador contra as três instâncias, e regra IoC validada
em logs reais. A asserção NOT_REACHABLE da sonda é fixada ali voluntariamente:
se ela quebrar, é o comportamento que mudou e a análise deve ser refeita.
Usar apenas em ativos pelos quais você é responsável, ou sob mandato escrito.
A bancada escuta apenas em 127.0.0.1; as instâncias vulneráveis nunca devem ser
expostas. A sonda 10-lab-debug.php divulga caminhos do servidor: é reservada
à bancada.