
PoC für CVE-2026-87902 — unauthentifizierter Path Traversal in der WordPress-Seitentemplate-Auflösung (lokale PHP-Inklusion, bedingte RCE) mit einem gepinnten verwundbaren Lab
Unauthentifizierte lokale PHP-Dateieinbindung in WordPress Core über einen doppelt kodierten
pagename-Wert — und unter bestimmten Deployment-Bedingungen PHP-Codeausführung
mit den Rechten des Web-Server-Kontos.
| CVE | CVE-2026-87902 |
| Herstellerhinweis | GHSA-7hp8-65ch-5whp |
| Write-up | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Betroffen | WordPress Core 4.7.0 – 7.1.1 (jeder Branch, gemäß den Branch-spezifischen Bereichen des Hinweises); dynamisch reproduziert auf 7.0.2 |
| Behoben | 7.1.2 (7.1-Branch), 7.0.6 (7.0-Branch) und ein Backport für jeden Branch bis hinunter zu 4.7.37 (gemäß dem Hinweis) |
| Schwachstellen | CWE-98 (improper control of filename in include), CWE-22 / CWE-23 (path traversal) |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — 8.1 High |
| CVSS v4.0 (ergänzend) | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — 9.2 Critical |
| Authentifizierung | keine — kein Konto, Cookie, Nonce, Session, Plugin oder ausgehender Request |
| Benutzerinteraktion | keine |
| Autor | Robert Ressl — ressl.ch |
| PoC verifiziert | 2026-09-22 gegen das Lab in diesem Repository — siehe Verifizierte Ergebnisse |
WordPress löst ein Seiten-Template über eine Kette auf, die nie beweist, dass die ausgewählte Datei innerhalb eines Theme-Roots bleibt:
pagename und page_id sind öffentliche Query-Variablen, die WP::parse_request()
aus einem anonymen POST-Body akzeptiert.pagename übersteht die frühe Query-
Bereinigung als escapte %xx-Oktette (wp_basename() kann %2f nicht als
Trennzeichen erkennen, und sanitize_title_with_dashes() behält gültige
Oktette bewusst bei).page_id wählt dann eine echte veröffentlichte Seite aus, während der bösartige
pagename im Query-Objekt verbleibt.get_page_template() ruft später urldecode() auf diesen Wert auf und fügt einen
Kandidaten wie page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
zur Template-Hierarchie hinzu.locate_template() und der finale Template-Loader prüfen nur Existenz,
Lesbarkeit und das .php/.html-Suffix — niemals, ob der kanonische Pfad
noch innerhalb eines erlaubten Theme-Roots liegt — und includen ihn dann.Das ist ein unauthentifiziertes Remote-Local-File-Inclusion-Primitiv in WordPress
Core. Auf der getesteten offiziellen Runtime (wordpress:php8.3-apache, die
PEAR mitbringt und keine php.ini lädt, sodass register_argc_argv On ist), wurde
die Einbindung über PEARs pearcmd.php verkettet: Ein erster anonymer Request lässt
config-create angreiferkontrolliertes PHP nach /tmp schreiben, ein zweiter anonymer
Request bindet diese Datei ein und führt sie als www-data aus.
Die fehlende Pfad-Eingrenzung in Core ist die Schwachstelle. PEAR ist nur eine umgebungsabhängige Route von der Einbindung zur Codeausführung — es ist keine WordPress-Abhängigkeit und in nicht jedem Deployment vorhanden oder nutzbar.
Verifizierte Quellcode-Stellen (WordPress 7.0.2):
| # | Stelle | Rolle |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename und page_id sind öffentliche Query-Vars und werden aus $_POST gelesen |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f ist für wp_basename() kein Trennzeichen |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() behält gültige %xx-Oktette bei, anstatt sie zu entfernen |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — der Traversal wird nach der Bereinigung aktiv |
| 5 | wp-includes/template.php:722-736 | locate_template() konkateniert den Kandidaten unterhalb jedes Theme-Roots und ruft nur file_exists() auf |
| 6 | wp-includes/template-loader.php:116-132 | realpath() normalisiert den Pfad, dann läuft include ohne kanonische Root-Containment-Prüfung |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() kehrt bei Nicht-GET/HEAD früh zurück, sodass ein POST nicht wegkanonisiert wird |
Das eigenständige Einbindungs-Primitiv benötigt:
| # | Voraussetzung | Grund |
|---|---|---|
| 1 | Eine veröffentlichte, anonym erreichbare Seite, ausgewählt über numerische page_id | die Seite muss von der Query zurückgegeben werden, nachdem die Pfadnamen-Suche fehlschlägt |
| 2 | Kein früher auflösbares benutzerdefiniertes Seiten-Template | ein gültig zugewiesenes Template wird vor dem bösartigen Kandidaten geordnet |
| 3 | Ein Top-Level-Verzeichnis im aktiven (Child- oder Parent-)Theme, dessen Name mit page- beginnt, z. B. page-templates/ | WordPress stellt das feste Präfix page- voran, sodass ein ..-Traversal nicht an Position 0 beginnen kann; das Verzeichnis muss nur existieren und durchquerbar sein — es muss nicht beschreibbar sein |
| 4 | Eine ausgewählte lokale .php-Datei, die existiert und für den PHP-Benutzer lesbar ist | der Loader prüft is_file()/is_readable() und erfordert ein .php-Suffix |
| 5 | Keine Dateisystem-Eingrenzung, die diese Datei blockiert | ein open_basedir oder eine MAC-Policy kann die Einbindung verhindern |
Die demonstrierte PEAR-Stufe benötigt zusätzlich eine lesbare pearcmd.php (samt ihrer
Abhängigkeiten), register_argc_argv=On für die Web-SAPI und ein beschreibbares Ausgabe-
verzeichnis. Produktions-php.ini-Dateien setzen register_argc_argv=Off; das getestete
Image lädt keine php.ini, sodass der kompilierte Standard (On) galt. Dies ist eine
wichtige Einschränkung für die Verbreitung der demonstrierten Codeausführungs-Kette.
Die mitgelieferten Themes Twenty Twenty-Three/Four/Five enthalten kein Top-Level-page-*-
Verzeichnis, daher benötigt das Standard-Lab das unten beschriebene Fixture. Benutzerdefinierte Themes können
legitimerweise ein page-templates/-Layout verwenden
(WordPress-Dokumentation).
Der Hinweis dokumentiert, wo diese Bedingungen in ausgelieferter Software auftreten: Die Theme-
Bedingung wird von den Legacy-Themes Twenty Twelve und Twenty Fourteen sowie von
Drittanbieter-Themes wie Neve, Hestia und Sydney erfüllt, während der PEAR-Übergang
für das offizielle PHP-Docker-Image und für Standard-cPanel-Konfigurationen gilt, die
PHP älter als 8.5 ausführen. (Twenty Twelve und Twenty Fourteen liefern beide ein
Top-Level-page-templates/-Verzeichnis; die übrigen Aussagen stammen aus dem
Hinweis.) Dieses Repository misst nicht, wie oft die vollständige Kette
zutrifft.
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + the page-* fixture, installed and ready
python3 cve-2026-87902.py # two anonymous POSTs, prints the proof marker
cve-2026-87902.py benötigt Python 3.6+ (nur Standardbibliothek) und erreicht das Lab unter
http://127.0.0.1:8091 standardmäßig. Erwartete Ausgabe: