
Kit de herramientas de detección y laboratorio reproducible para CVE-2026-87902, un path traversal no autenticado en WordPress. Incluye verificador remoto, analizador de IoC, reglas Sigma y banco de pruebas Docker.
check/ | Controlador remoto, pasivo, sin acceso al servidor |
detect/ | Analizador de IoC + reglas Sigma |
offensive/ | Generador de trazas, para validar la detección sobre registros reales |
docker-compose.yml + provision/ | Banco de pruebas, tres configuraciones |
tests/validate.py | Puerta de calidad — condiciona toda publicación |
docs/ANALYSIS.md | La vulnerabilidad, el parche, el análisis de alcanzabilidad medido |
make up # monta el banco make ioc # corpus de ataque -> registros -> detección
make scan # controla el banco make test # puerta de calidad
Tres instancias de WordPress en 127.0.0.1, que aíslan cada factor del veredicto.
| Puerto | Instancia | Núcleo | Tema activo | Veredicto esperado |
|---|---|---|---|---|
| 8091 | vuln-pre | 6.8.1 — sin corregir | Twenty Twelve, con page-templates/ | AFFECTED_PRECONDITION_MET |
| 8092 | vuln-nopre | 6.8.1 — sin corregir | Twenty Twenty-Four, sin page-* | AFFECTED_CORE_ONLY |
| 8093 | patched | 6.8.10 — corregido | Twenty Twelve, con page-templates/ | NOT_AFFECTED |
8092 es la más instructiva: mismo núcleo vulnerable que 8091, pero falta el
requisito previo del tema. Esto es lo que demuestra que un triaje basado únicamente en
la versión sobreestima la exposición. El tema es la única variable entre 8091 y
8092, el parche la única entre 8091 y 8093: las tres incorporan el mismo tipo de
contenido personalizado (provision/mu-plugins/00-lab-cpt.php) y la misma sonda de
estado de petición (10-lab-debug.php, cabeceras X-Lab-* que exponen is_page(), el
valor de pagename visto por el cargador y la plantilla finalmente incluida).
make up # inicia y aprovisiona — idempotente, reejecutable
make status # versión de cada instancia
make down # detención make clean : elimina también los volúmenes
La imagen Docker oficial hace apuntar /var/log/apache2/access.log a
/dev/stdout: los registros salen por la salida del contenedor, no a un archivo.
docker compose logs --no-log-prefix vuln-pre # acceso + errores
docker compose logs --no-log-prefix vuln-pre > access.log # para análisis
docker compose logs -f --no-log-prefix vuln-pre # en directo
make logs # las tres instancias
En un servidor clásico: /var/log/apache2/access.log, /var/log/nginx/access.log,
o /home/*/logs/ en la mayoría de los proveedores de alojamiento compartido. El
formato debe incluir la cadena de consulta — %r o el formato combined la incluyen,
un LogFormat construido sobre %U la pierde, y sin ella ninguna detección es posible.
check/wp-ghsa-7hp8-check.pyDesde Internet, sin acceso al servidor. Ninguna carga, ningún traversal, ninguna
escritura. Para cada host: detección de WordPress, versión contrastada con cinco
fuentes (meta generator, feed RSS, wp-links-opml.php, readme.html, ?ver= de los
recursos del núcleo), tema activo, y sondeo del directorio page-*.
python3 check/wp-ghsa-7hp8-check.py --hosts-file hosts.txt --csv out.csv --json out.json
| Veredicto | Significado |
|---|---|
AFFECTED_PRECONDITION_MET | Núcleo vulnerable y directorio page-* en el tema activo. Prioritario. |
AFFECTED_THEME_UNKNOWN | Núcleo vulnerable, tema no determinado. |
AFFECTED_CORE_ONLY | Núcleo vulnerable, requisito previo del tema ausente. Corregir de todos modos. |
VERSION_UNKNOWN | WordPress detectado, versión oculta. |
NOT_AFFECTED | Versión ≥ parche de su rama. |
«AFECTADO» significa que el código vulnerable está presente, no que un atacante
pueda ejecutar código. Véase docs/ANALYSIS.md.
--transport browser (por defecto) controla el Google Chrome instalado; las
subpeticiones parten de un fetch() ejecutado dentro de la página y heredan su pila
TLS, su orden de cabeceras HTTP/2 y sus cookies, lo que evita ser filtrado por un CDN
antes de que la URL sea leída. --transport direct solo utiliza la biblioteca estándar.
--scheme http|https evita el repliegue https → http, que de lo contrario deja una
línea 400 con un ClientHello TLS en bruto en el registro del objetivo.
Cada petición está sellada con marca temporal al milisegundo en un registro JSONL:
identificador de sesión, número de petición, fase, URL, estado, tamaño, duración, IP de
salida, marcador. El marcador SECAUDIT/<nonce> se envía en la cabecera
X-Security-Audit y como sufijo del User-Agent — añadido, nunca sustituido, para
permanecer visible en un access log estándar sin romper la firma de navegador.
Personalizable mediante --marker.
Sin oráculo remoto. La opción
--probe-inclusionefectúa una comparación diferencial sobre objetivo inerte (wp-includes/version.php, ya cargado en el bootstrap:require_oncelo convertiría en un no-op integral). Sobre una instalación estándar devuelveNOT_REACHABLEincluso sobre un núcleo vulnerable, con respuestas idénticas al byte entre instancia corregida y no corregida. No es una limitación de la herramienta: WordPress responde 404 antes de consultar la jerarquía de plantillas. Demostración cifrada endocs/ANALYSIS.md, sección 3.
Una regex literal del tipo pagename=.*%2e%2e%2f se elude cambiando las mayúsculas
(%2E), codificando una vez más (%252e), o combinando literal y codificado
(templates/..%2f../). Toda lista de patrones es incompleta por construcción.
Por tanto se parte del código, no de la escritura del atacante:
pagename sufre como máximo dos decodificaciones antes de alcanzar el disco — la
de PHP sobre la cadena de consulta, y luego el urldecode() explícito de
get_page_template().file_exists() debe contener
un componente ... En Linux, el directorio padre se escribe exactamente sobre los
dos bytes 0x2E 0x2E; no existe ninguna otra representación a nivel del sistema
de archivos.Por tanto: decodificar hasta punto fijo y probar en cada nivel. Es un superconjunto estricto de lo que hace WordPress — añadir una capa de codificación solo desplaza la coincidencia de un nivel, que también atravesamos.
python3 detect/wp-ghsa-7hp8-ioc.py access.log
docker compose logs --no-log-prefix vuln-pre | python3 detect/wp-ghsa-7hp8-ioc.py -
| Regla | Severidad | Disparador |
|---|---|---|
GHSA-7hp8-traversal-pagename | CRITICAL | componente .. en pagename, en cualquier nivel de decodificación |
GHSA-7hp8-traversal-param | HIGH | misma primitiva en otro parámetro (temas y extensiones también llaman a locate_template()) |
GHSA-7hp8-traversal-path | HIGH | componente .. en la ruta de URL (nginx deja pasar %2f, Apache no por defecto) |
GHSA-7hp8-overlong-encoding | MEDIUM | sobrecodificación UTF-8 (%c0%ae). Inoperante en Linux, pero nunca legítimo |
GHSA-7hp8-theme-page-dir-probe | LOW | sondeo de un directorio page-* de tema — el reconocimiento |
Reglas Sigma en detect/sigma-wp-ghsa-7hp8.yml.
Sigma no sabe decodificar recursivamente, por lo que enumeran los niveles 0 a 3: es una
aproximación asumida, para el triaje de primer nivel. Volver a pasar las
coincidencias por el analizador para decidir.
WP::parse_request() lee $_POST antes que $_GET. Por tanto
pagename puede llegar en un cuerpo de petición, ausente de todo access log.
Cobertura necesaria a nivel de WAF o ModSecurity, sobre el cuerpo.Ninguna regla sobre access log cubre el primer punto. Es una limitación del soporte, no de la regla — pero debe conocerse antes de anunciar una cobertura completa.
offensive/generate-traces.py reproduce 12 escrituras diferentes de la misma carga
(literal, codificación simple/doble/triple, mayúsculas y minúsculas, combinaciones,
sobrecodificación UTF-8, punto-espacio) más 7 peticiones legítimas que se les
parecen (slug con puntos, wp-includes en un slug, enlace permanente con fecha,
porcentaje codificado).
No obtiene nada: no existe exploit remoto para este vector sobre una instalación estándar. Produce trazas — ese es todo su objeto.
make ioc # genera el corpus, recupera los registros reales, pasa el analizador
Esperado, y verificado por make test sobre registros Apache reales:
12 cargas detectadas de 12 (11 CRITICAL, la sobrecodificación en MEDIUM porque no
es explotable en Linux), 0 alertas sobre las 7 peticiones legítimas, y detección
efectiva en tres niveles de decodificación diferentes.
Objetivo limitado al banco local; cualquier otro exige --i-have-authorization.
register_argc_argv = Off en el php.ini del SAPI web; desinstalar PEAR si no se
usa — es el pivote inclusión → ejecución citado por el aviso.open_basedir limitado a la raíz del sitio: confina toda inclusión local.make test es la condición de publicación: matriz de las 25 ramas del aviso
(trampas de comparación incluidas — 6.8.9 < 6.8.10 numéricamente, pre-versiones,
ramas fuera de matriz), controlador contra las tres instancias, y regla IoC validada
sobre registros reales. La aserción NOT_REACHABLE de la sonda queda fijada
deliberadamente: si se rompe, es el comportamiento el que ha cambiado y el análisis debe
retomarse.
Utilícese únicamente sobre activos de los que usted sea responsable, o bajo mandato
escrito. El banco solo escucha en 127.0.0.1; las instancias vulnerables nunca deben
exponerse. La sonda 10-lab-debug.php divulga rutas del servidor: está reservada al
banco.