
PoC مستقل ومختبر قابل لإعادة الإنتاج لـ CVE-2026-97332 — الكشف غير المصادق عنه عن الملفات الخاصة في User Private Files < 2.2.0 على WordPress Multisite.
[!NOTE] هذا المستودع هو إعادة إنتاج مستقلة وإثبات مفهوم (Proof of Concept) لـ CVE-2026-97332.
اكتشاف الثغرة الأصلية: Akshat Parikh (SN1PER). CNA / الاستشارة: WPScan. إثبات المفهوم وإعادة الإنتاج المستقلان: Kolya080808.
الكشف غير المصادق عن الملفات الخاصة لمستخدمين آخرين في إضافة WordPress
User Private Files (معرّف wp.org user-private-files) قبل 2.2.0
على تثبيتات multisite. تخزّن الإضافة المستندات الخاصة لكل مستخدم
داخل شجرة uploads العادية وتحميها بقاعدة RewriteRule واحدة
تُضاف إلى ملف .htaccess الجذري، والتي توجّه طلبات الملفات عبر
فحص وصول بـ PHP. على شبكة multisite لا يتم الوصول أبدًا إلى تلك القاعدة: طلب
GET عادي واحد — بدون ملفات تعريف ارتباط، بدون ترويسات، بدون تسجيل دخول — يُرجع الملف الخاص لمستخدم آخر
كمحتوى ثابت. تحكّم وصول معطوب، CWE-284، CVSS 3.1 5.3
(AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N).
يحتوي هذا المستودع على استغلال Python عامل وخالٍ من التبعيات، ومختبر docker بحاويتين (النسخة المصابة 2.1.9 والنسخة المُرقّعة 2.2.0، وكلتاهما WordPress multisite) لإعادة إنتاج الهجوم من البداية إلى النهاية.
[!CAUTION] هذه المواد مقدَّمة للبحث الأمني المصرّح به والاختبار الدفاعي فقط (انظر إخلاء المسؤولية في الأسفل). مهاجمة أنظمة لا تملكها أو لا تديرها دون إذن كتابي غير قانونية.
تحتفظ الإضافة بالملفات الخاصة كمرفقات عادية في دليل فرعي مسطّح
لـ uploads لكل موقع — wp-content/uploads/upf-docs/ (الموقع الرئيسي) و
wp-content/uploads/sites/N/upf-docs/ (الموقع الفرعي N) — بدون أي
عزل لكل مستخدم. التحكّم في الوصول ليس على الدليل؛ بل يوجد في PHP
(dl-file.php: 403 للمستخدمين المجهولين، ثم فحص المؤلف/قائمة السماح) و
لا يتم الوصول إليه إلا عبر rewrite يُصدر عند تفعيل الإضافة إلى ملف .htaccess
الجذري:
# appended by 2.1.9 (upvf_actdeact.php), AFTER the WordPress block
RewriteRule ^wp-content/uploads/upf-docs/(.*)$ https://site/?file=$1 [QSA,L]
في multisite لا يُفعَّل هذا أبدًا، لسببين مستقلين:
قواعد .htaccess الخاصة بالشبكة تعترض الملفات الموجودة أولًا. يكتب WordPress
multisite مجموعة قواعده الخاصة في ملف .htaccess الجذري:
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
يؤدي الطلب المطابق لملف موجود فعليًا إلى إنهاء إعادة الكتابة عند هذه النقطة مباشرةً — إذ يقدّمه Apache كمحتوى ثابت، ولا يصل mod_rewrite أبدًا إلى كتلة الإضافة في نهاية الملف. (يستخدم WordPress أحادي الموقع شروطًا سالبة !-f/!-d، ولهذا السبب تعمل الإضافة نفسها هناك.) وهذا يكشف بالفعل ملفات الموقع الرئيسي للشبكة.
مسارات المواقع الفرعية لا تطابق النمط إطلاقًا. في Multisite يكون لكل موقع مجلد تحميلات خاص به — wp-content/uploads/sites/N/…، إضافةً إلى بادئة URL /siteN/ في شبكات الأدلّة الفرعية — بينما القاعدة مثبّتة على ^wp-content/uploads/upf-docs/ من عنوان URL الأساسي للموقع الرئيسي. وحتى بعد أن تُزيل إعادة الكتابة الداخلية للشبكة بادئة /siteN/، فإن مقطع sites/N/ لا يزال لا يطابق.
لذلك، بالنسبة إلى أي شبكة Multisite (سواء كانت بأدلّة فرعية أو بنطاقات فرعية)، يستطيع مهاجم غير مُصادَق جلب الملفات الخاصة مباشرةً:
GET /wp-content/uploads/upf-docs/victim.pdf HTTP/1.1 (main site)
GET /site2/wp-content/uploads/sites/2/upf-docs/victim.pdf HTTP/1.1 (subsite)
الاستجابة هي 200 مع جسم الملف — يُقدَّم بواسطة Apache مباشرة من
uploads/، ولا يُنفَّذ فحص الوصول الخاص بالإضافة أبدًا.
لا شيء في عنوان URL سري. تبني الإضافة الأسماء من المعرّف الرقمي للمستخدم المالك ومن الثانية اليونكسية للرفع:
| المولّد (2.1.9) | الاسم على القرص |
|---|---|
واجهة حديثة، inc/functions-file.php:73-77 | usr{ID}_{unixtime}_{original} |
قالب كلاسيكي، inc/classic-user-functions.php | user_{ID}_doc_{unixtime}.{ext} |
معرّفات المستخدمين على شبكة متعددة المواقع هي أعداد صحيحة متسلسلة صغيرة من جدول
wp_users مشترك واحد، لذا يحتاج المهاجم فقط إلى وقت رفع تقريبي (نافذة ±2 دقيقة
تساوي ≈240 طلبًا) وإلى اسم الملف الأصلي أو امتداده فقط. لواحق التصادم (-1، -2)
موجودة لكنها نادرة.
استبدلت النسخة 2.2.0 إعادة كتابة الملف الجذري بـ upf-docs/.htaccess على
مستوى المجلد — RewriteRule ^(.*)$ {home}/?file=$1 [R=302,QSA,L]،
إضافة إلى Require all denied عند غياب mod_rewrite — يُعاد إنشاؤه عند كل
admin_init (إصلاح ذاتي، لكل موقع فرعي). تُفعَّل القاعدة الخاصة بكل مجلد لأي
مسار فعلي تحت upf-docs/ بغض النظر عن بادئة عنوان URL، لذا يصبح طلب GET المباشر
الآن 302 نحو فحص الوصول، وللمستخدمين المجهولين 403. لا تُنقل الملفات؛ تتغير
آلية الاعتراض فقط. هذا التمييز بين 302 والملف الثابت هو أيضًا ما يستخدمه الاستغلال
كاشفًا سلوكيًا.
.htaccess وكان الملف
قابلًا للكتابة عند التنشيط (يحتاج nginx إلى إعادة الكتابة اليدوية من المورّد —
راجع readme الإضافة).exploit.py هو مكتبة Python 3 القياسية الخالصة (http.client)، لا يتبع أي
إعادة توجيه، ولا يرسل أي ملفات تعريف ارتباط:
GET /wp-content/plugins/user-private-files/readme.txt، تحليل Stable tag:.
< 2.2.0 → المتابعة؛ >= 2.2.0 → مربع أحمر NOT VULNERABLE - plugin 2.2.0+ (patched) ورمز خروج 6. إذا تعذّرت قراءة readme.txt، يُعرض فحص سلوكي
لاسم upf-docs/ غير موجود (302 = إعادة كتابة في المكان، 404 = تقديم ثابت)
وتستمر العملية (--skip-check يعطّل الخطوة بأكملها).{base}/wp-content/uploads/upf-docs/{file}
أو، مع --site site2 --site-id 2،
{base}/site2/wp-content/uploads/sites/2/upf-docs/{file}.GET عادي واحد، بلا Cookie، بلا Authorization.200 → * LEAKED مع الأسطر الأولى من الملف كدليل، رمز الخروج 0؛302 → "redirected to the access check - not vulnerable / patched"، رمز الخروج 1؛404 → "file not found (wrong name?)" — جرّب --guess، رمز الخروج 1.يعدّد وضع --guess أنماط الأسماء القابلة للتنبؤ بدلًا من اسم ثابت واحد:
--user-id 2 --orig-name "Invoice 2026.pdf" --around <unixtime> --window 120 يولّد usr2_{t}_Invoice-2026.pdf و
user_2_doc_{t}.pdf لكل ثانية في النافذة ويقرأ كل اسم يستجيب بـ 200.
رموز الخروج: 0 تم كشف الملف · 1 لم يُكشف (404 / 302 / 403) ·
6 غير معرّض (فحص النسخة) · 4 خطأ في الشبكة.
exploit.py Python exploit (stdlib only, zero dependencies)
demo.sh end-to-end demo: leak on 2.1.9, refusal on 2.2.0
README.md this file
logs/ actual runs of demo.sh (vulnerable and patched)
lab/ docker lab: 2 x WordPress multisite + MariaDB
(wp-vuln: port 8080, plugin 2.1.9; wp-control: port 8081, plugin 2.2.0)
cd stand-wp && docker compose up -d # vulnerable 8080 + control 8081
cd ../poc-upf && ./demo.sh # full run, logs into logs/