Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
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.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-87902 — Python PoC exploiting CVE-2026-87902, an unauthenticated path traversal in WordPress locate_template() leading to LFI and PEAR-based RCE, with safe detection mode. | Kitploit
Tools/GitHubGitHub/crowsec-edtech/cve-2026-87902
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityPenetration TestingRemote Access ToolPayload DevelopmentLabs & Practice
GitHubcrowsec-edtech/cve-2026-87902

CVE-2026-87902

Python PoC exploiting CVE-2026-87902, an unauthenticated path traversal in WordPress locate_template() leading to LFI and PEAR-based RCE, with safe detection mode.

34224 days agoNot yet reviewed
View Repository

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 — PoC: unauthenticated path traversal in WordPress page-template resolution

Exploit file: exploit_locate_template_rce.py — Python 3.6+ (stdlib only).

Single-command pipeline: safe detection (LFI differential, no writes) →, if vulnerable, RCE via the pearcmd.php gadget executing the command of your choice (--command/-c). Use --validate-only to stop before the write stage.

Tests CVE-2026-87902 on WordPress versions without the fix from commit 5fde0bb7 — "Themes: Restrict path traversal in locate_template()" (WordPress 7.1.2, backports down to 4.7.37).

CVECVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High)
AdvisoryGHSA-7hp8-65ch-5whp
AffectedWordPress 4.7.0 – 7.1.1 (all branches)
Fix7.1.2, 7.0.6, 6.9.9 … 4.7.37
Fix commit5fde0bb7b9775523959094bf280cc54bfa78af51 (merge of changeset 63792)
Fileswp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed())
AuthenticationNone — no user, cookie, nonce, or plugin
CreditsDiscovery and disclosure: Robert Ressl

⚠️ For authorized testing only. Use in environments you control (the lab below is isolated) or with the owner's explicit authorization.


1. The vulnerability

In versions without the fix, the chain get_page_template() → locate_template() → template-loader never guarantees that the selected template stays inside the theme.

Pre-fix (wp-includes/template.php):

// get_page_template(): decode LATE, no validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
    $templates[] = "page-{$pagename_decoded}.php";
}

// locate_template(): only tests existence — no path confinement
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
    $located = $wp_stylesheet_path . '/' . $template_name;
    break;
}

Post-fix (commit 5fde0bb7):

if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) { ... }
...
if ( _wp_is_template_path_allowed( $candidate ) ) {   // realpath + confinement
    $located = $candidate;                            // inside theme/parent/theme-compat
    break;
}

Exploitation chain (repository code)

  1. pagename and page_id are public query vars — accepted in the body of an anonymous POST. The POST method makes redirect_canonical() (canonical.php) return early — it only acts on GET/HEAD, so nothing is redirected.
  2. The traversal is double-encoded (templates%252f%252e%252e%252f...) and survives sanitization: WP_Query::get_posts() (l.2191) rewrites the pagename with sanitize_title_for_query( wp_basename( ... ) ) — wp_basename() does not break on %2F (formatting.php l.5768) and the sanitize preserves %XX octets (l.2395).
  3. page_id (l.2252) overwrites the WHERE ("$where =", not ".=") — the real page is loaded, no 404, and the malicious pagename remains in the query object.
  4. get_page_template() does a late urldecode() and builds page-<traversal>.php without validate_file().
  5. Pre-patch locate_template() only calls file_exists() → the include escapes the theme.

Preconditions

#PreconditionReason
1Published page, anonymously accessible, selectable by page_id, and without a custom templateThe request must resolve to a Page; a custom template would come earlier in the hierarchy
2Top-level directory in the active theme (child or parent) starting with page- (e.g. page-templates/)WP prepends the fixed prefix page-; that is how the traversal gets in. It just needs to exist and be traversable (it can be empty). Present in Twenty Twelve/Fourteen, Neve, Hestia, Sydney
3Existing, readable local .php targetThe loader appends .php and checks is_file()/is_readable()
4(RCE only) readable pearcmd.php + register_argc_argv=On + writable directory (e.g. /tmp)PEAR→RCE route: config-create writes the PHP payload to the server

2. What the script does

Stage 1 — safe detection (always runs first)

Three anonymous POSTs against the published page — no disk writes:

RequestpagenameExpected response (vulnerable)
A — baseline—200, normal body (~23 KB)
B — controltraversal → nonexistent file200, normal body (page.php fallback)
C — probetraversal → wp-content/index.php ("Silence is golden")200 with empty body

Signature: C empty + A/B normal ⇒ the include escaped the theme ⇒ VULNERABLE. Detection automatically scans: candidate theme directories (page-templates, page-template) × depths 1–12. The result (theme-dir + depth) feeds the RCE stage.

With --validate-only the script stops here (exit 0 if vulnerable).

Stage 2 — RCE via pearcmd.php (write)

Stage 1  POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
         body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
         → the traversal includes /usr/local/lib/php/pearcmd.php
         → argv comes from the raw query string (register_argc_argv=On)
         → config-create WRITES the payload to /tmp/wp-pear-rce.php (12 serialized copies)

Stage 2  POST /  body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
         → the same traversal includes the generated file → system(<--command>) runs as www-data

Format details (all constraints are handled by the script):

  • POST, not GET: the body carries page_id + pagename; the query string carries exclusively the PEAR argv (?+config-create+<root>+<out>).
  • Payload with no quotes at all: wp_magic_quotes() (load.php l.1290) applies add_magic_quotes($_SERVER) and pearcmd reads argv from $_SERVER['argv'] — any quote would become \' and break the generated file. Every PHP string becomes chr() concatenation: '/tmp' → chr(47).chr(116).chr(109).chr(112).
  • Byte-exact request target (http.client): the raw +, <, > bytes of the query string are the argv — nothing can be re-escaped/re-encoded along the way.
  • config-create requires an absolute root path — the payload is injected as the root itself (/<php>), prefixed with the ___WP_RCE_OK___ marker that delimits the output.
  • The RCE tries depths starting from the detected one (internal order 7, 6, 8, 5, …) and several pearcmd.php paths (official Docker, Debian/Ubuntu, XAMPP) — adjust with --pear-path/--output if needed.

There is no "upload": the detection target is a file that every WordPress ships by default (wp-content/index.php); the RCE payload is written by the server itself via the PEAR gadget, running as www-data.


3. Usage

# 1) Only validate the vulnerability (LFI probe, no writes)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only

# 2) Detection + RCE executing a command (default: 'id')
python exploit_locate_template_rce.py --url http://127.0.0.1:8080
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --command "id"
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "uname -a; cat /etc/passwd"

# Useful combinations
python exploit_locate_template_rce.py -u https://target --page-id 2 --theme-dir page-templates
python exploit_locate_template_rce.py -u http://127.0.0.1:8080 -c "whoami" --depth 7 --verbose

Parameters

Download Tool