WordPress Core <= 7.1.1 LFI non autenticato verso RCE - laboratorio di validazione, PoC, template nuclei (GHSA-7hp8-65ch-5whp)
Materiale di ricerca sulla sicurezza per riprodurre e validare CVE-2026-87902 (GHSA-7hp8-65ch-5whp) in un lab Docker isolato, solo loopback: un local file inclusion non autenticato nella risoluzione dei page-template del core di WordPress, scalabile a remote code execution.
AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)Stato: dimostrato nel lab fornito.
WordPress 7.1.1 (tema Twenty Twelve) consente a un attaccante non autenticato di
includere file PHP al di fuori della directory del tema tramite la variabile di query
pagename, e - con il valore predefinito register_argc_argv = On dell'immagine PHP
ufficiale - di scrivere ed eseguire un file marker tramite PEAR pearcmd.php:
WordPress: 7.1.1
Theme: twentytwelve (page-templates/ present)
PHP: 8.2 (official image, register_argc_argv=On, PEAR present)
Result: LFI oracle fired (<opml version="1.0">); marker probe executed as
uid=33(www-data) and self-deleted
WordPress 7.1.2 con la stessa configurazione blocca completamente l'oracolo (controllo negativo). Questo dimostra l'esecuzione di codice come utente del web server WordPress all'interno del container. Non dimostra accesso root, container escape, movimento laterale o compromissione dell'host.
Requisiti: Docker con Compose v2, Python 3.10+, accesso di rete a Docker Hub e wordpress.org (installazione del tema).
./lab verify
Questo costruisce il lab vulnerabile, richiede che l'oracolo LFI e il marker RCE scattino, poi costruisce il lab patchato e richiede che l'oracolo rimanga silenzioso.
Per usare un'altra porta loopback: LAB_PORT=9499 ./lab verify.
Rimuovi tutto successivamente con ./lab reset.
Controllo vulnerabile:
[+] page-* folder(s) reachable: page-templates (traversal can resolve)
[!!!] LFI CONFIRMED (safe oracle: wp-links-opml.php)
...
[RCE] pearcmd.php reachable and taking argv: /usr/local/lib/php/pearcmd.php
[!!!] RCE CONFIRMED (pearcmd.php config-create file write):
evidence: uid=33(www-data) (command `id` ran as the web user)
cleanup : probe self-deleted on execution: confirmed
RESULT: VULNERABLE CONTROL CONFIRMED
Controllo patchato:
[=] oracles did not fire: target patched (>= 7.1.2), ...
RESULT: PATCHED CONTROL CONFIRMED
Un HTTP 200, un blocco WAF o un traversal parziale senza il marker dell'oracolo non è accettato come prova.
./lab start vulnerable # fresh 7.1.1 lab on 127.0.0.1:9471
./lab test # assert LFI + RCE
./lab start patched # fresh 7.1.2 lab
./lab test # assert oracle absence
./lab status
./lab reset # containers, volumes, state
Esegui gli strumenti manualmente contro il lab in esecuzione:
python3 poc/triage_cve_2026_87902.py --url http://127.0.0.1:9471
python3 poc/poc_cve_2026_87902.py --url http://127.0.0.1:9471 --rce
nuclei -t nuclei/cve-2026-87902.yaml -u http://127.0.0.1:9471 -silent
Per sistemi di tua proprietà o per cui sei esplicitamente autorizzato a testare (ambito bug-bounty, incarico scritto), i driver richiedono l'override esplicito:
python3 poc/poc_cve_2026_87902.py --url https://target.example \
--allow-authorized-non-loopback [--insecure] [--rce]
get_page_template() in wp-includes/template.php costruisce un nome di file template candidato
dalla variabile di query pagename. Il ramo adiacente tre righe
sopra passa lo slug del page-template attraverso validate_file(); il ramo pagename
non lo faceva mai, e inoltre aggiunge un ulteriore candidato decodificato con urldecode():
// wp-includes/template.php, get_page_template(), WordPress <= 7.1.1
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) { // 7.1.2 adds validate_file()
$templates[] = "page-{$pagename_decoded}.php";
}
$templates[] = "page-{$pagename}.php";
}
7.1.2 applica validate_file() al candidato decodificato e aggiunge un
gate di contenimento (_wp_is_template_path_allowed()) che ogni percorso template risolto
deve superare.
locate_template() controlla entrambi)
include una directory di primo livello che inizia con page-. I temi predefiniti legacy
(Twenty Twelve, Twenty Fourteen) e diversi temi wp.org (Neve, Hestia,
Sydney) sono idonei. I temi commerciali analizzati (Divi, Avada, Flatsome,
Woodmart, Betheme, The7, Enfold, Salient, Jupiter/X, Porto, Houzez, XStore,
Kalium, Uncode) non ne includono nessuna - il traversal non può risolversi; vedi
PRO_THEME_PAGE_DIRS nel PoC per la lista basata su mirror..php (aggiunto dal codice).register_argc_argv = On e pearcmd.php di PEAR.L'advisory e il writeup di Patchstack evitano di pubblicare una richiesta funzionante. Questi dettagli sono stati stabiliti empiricamente in questo lab:
page_id è obbligatorio. Un pagename non risolto attiva
is_404, il cui template oscura completamente get_page_template(). Ancora la
query con qualsiasi page_id pubblicato e risolvibile.get_posts() riscrive pagename
tramite sanitize_title_for_query( wp_basename( … ) ): il .. grezzo diventa
-, ma le sequenze %hex sopravvivono; l'urldecode() extra in
get_page_template() rivela il traversal. Funziona solo %252e/%252f.redirect_canonical fa 301 su GET e scarta
il payload; ritorna immediatamente per non-GET/HEAD).../ da page-templates/ su un docroot standard.$_POST nelle query
vars) - la base dei livelli di evasione WAF.$argv, e
il serializer di configurazione di PEAR fa escape di entrambi gli stili di quote - da qui la probe
grezza, senza quote, separata da +.| Path | Purpose |
|---|---|
poc/poc_cve_2026_87902.py | PoC completo: fingerprint versione + WAF, enumerazione tema/cartella page-* (wp.org SVN, scoperta asset-reference, mappa pro-theme analizzata), scoperta anchor, oracolo LFI a 8 varianti, oracolo fallback comparativo a corpo vuoto, catena pearcmd --rce, demo docker --lab |
poc/triage_cve_2026_87902.py | Verdetto per-target o batch con il gate fallito nominato (EXPLOITABLE / NOT EXPLOITABLE / WAF SUSPECTED / INCONCLUSIVE) |
poc/bounty_screen.py | Elabora una lista di asset bug-bounty in-scope: espansione wildcard crt.sh → filtro WP → screen precondizioni → handoff candidati |
nuclei/cve-2026-87902.yaml | Template Nuclei: oracolo LFI sicuro (174 payload: 10 remainder page-*, livelli doppio/triplo codificati, id 1–30) |
Il fingerprinting WAF copre Imperva/Incapsula, Cloudflare, Akamai, AWS
CloudFront/WAF, Sucuri, F5. Otto varianti di oracolo: doppio codificato, spelling alternativo
%25%32%65, parameter pollution, triplo codificato - ciascuna in query
string e body POST. I body multipart e i payload con slash grezzo sono stati testati e
rifiutati (WP non li processa per questo sink).
| Version | Assessment |
|---|---|
| WordPress 7.1.1 | LFI + pearcmd RCE riprodotti end to end |
| WordPress 6.5.3 / 5.9.17 | oracolo LFI riprodotto |
| WordPress 4.7.0–7.1.1 (other branches) | Segnalate come affette; tabella dei rami per GHSA |
| WordPress 7.1.2 | controllo negativo patchato riprodotto |
.
├── .github/workflows/validate.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── docker-compose.yml
├── lab
├── nuclei/
│ └── cve-2026-87902.yaml
└── poc/
├── bounty_screen.py
├── poc_cve_2026_87902.py
└── triage_cve_2026_87902.py
Non committati: .lab-state, __pycache__/, file di output dello screener.
| Claim | Status |
|---|---|
| LFI non autenticato al di fuori della directory del tema | Dimostrato (7.1.1, 6.5.3, 5.9.17) |
RCE tramite pearcmd.php config-create come utente web | Dimostrato (lab 7.1.1) |
| Auto-cancellazione della probe / nessun artefatto persistente | Dimostrato |
| Controllo negativo patchato (7.1.2) | Dimostrato |
| Ogni release intermedia affetta testata | Non testata |
| Varianti di bypass WAF contro WAF commerciali live | Derivate dal lab, non testate su WAF |
| Privilegi root / container escape / compromissione host | Non rivendicati |
page_id e i requisiti di doppia codifica; fonte
dell'oracolo comparativo a corpo vuoto wp-content/index.php, incluso qui come
fallback per siti hardened)Vulnerabilità segnalata da Robert Ressl. Questo repository è analisi e tooling indipendenti di terze parti.
MIT - vedi LICENSE.
Esegui questo repository solo su sistemi di tua proprietà o per cui sei esplicitamente autorizzato a
testare. Il driver del lab rifiuta varianti diverse da vulnerable/patched, i
driver Python rifiutano target non-loopback a meno che
non venga fornito --allow-authorized-non-loopback, e la porta Compose è vincolata
a 127.0.0.1.
Vedi SECURITY.md per la gestione della divulgazione. Questo repository non include target di produzione, dati dei clienti, token di accesso o evidenze da sistemi reali.
Concretamente:
127.0.0.1.wp-links-opml.php, un file core stock che stampa un documento OPML.--rce è opt-in e scrive una probe marker senza quote e auto-cancellante
(<?=id,unlink(__FILE__)?>) tramite la tecnica config-create di pearcmd.php
pubblicamente documentata; la probe si rimuove da sola alla sua singola esecuzione
e il PoC verifica la cancellazione. Nessuna shell, callback o persistenza.start; nessuna
strumentazione del target.