
تحليل السبب الجذري، ومدقق إصدار سلبي، وإثبات مفهوم مختبري لـ CVE-2026-18322، وهو تصعيد صلاحيات دون مصادقة في إضافة Smart Popup by Supsystic لـ WordPress.
بحث أمني حول CVE-2026-18322، وهي ثغرة تصعيد صلاحيات غير مصادَق عليها في إضافة
ووردبريس Smart Popup by Supsystic (popup-by-supsystic). يحتوي هذا المستودع على
تحليل للسبب الجذري مستخلص من فرق المصدر الأصلي، والتصحيحات المستخرجة، وأداة كشف
(بصمة إصدار سلبية) لتحديد عمليات التثبيت المتأثرة، ومختبر لإثبات الاستغلال.
الثغرة علنية وتم تصحيحها. يُنشر هذا العمل للاستخدام الدفاعي: مساعدة المشغّلين في العثور على المضيفين المتأثرين ومعالجتهم.
| نطاق الإصدار | الحالة |
|---|---|
< 1.13.0 | متأثر |
>= 1.13.0 | مُصحَّح |
الإصدار 1.13.0 (الصادر في 31.07.2026) هو أول إصدار مُصحَّح؛ وهو يصلح الحلقات الثلاث جميعها في السلسلة الموصوفة أدناه. راجع §1 من التحليل.
تتكوّن ثلاث نقاط ضعف مستقلة من إنشاء حساب مسؤول دون مصادقة:
1. تعارض في خريطة الصلاحيات — دمجت havePermissions() خريطتي صلاحيات باستخدام
array_merge(). كلتاهما تستخدم المفتاح النصي PPS_USERLEVELS ('userlevels')،
وarray_merge() تستبدل المفاتيح النصية، لذا استبدلت القائمة الافتراضية القصيرة
للمتحكّم الأساسي قائمة وحدة النوافذ المنبثقة بالكامل — مما أزال بصمت قيد المسؤول من
save وتسع دوال أخرى:
$permissions = $mod->getController()->getPermissions(); // [... 'save' ...]
$permissionsBase = $mod->getController()->getBasePermissions(); // ['getListForTbl','removeGroup','clear']
$permissions = array_merge($permissions, $permissionsBase); // base wins — 'save' is gone
يفشل الفحص مفتوحًا: الإجراء الغائب عن الخريطة لا يُرفض أبدًا.
2. nonce قابل لإعادة الاستخدام يُرسل بالبريد إلى الغرباء — كان save لا يزال
يتطلب pps_nonce، لكن رسالة تأكيد الاشتراك كانت تتضمّن إجراء الـ nonce نفسه. بالنسبة
للمستخدمين غير المسجّلين، يُربط nonce ووردبريس بـ uid=0، لذا فإن الرمز المُنشأ لمشترك
مجهول يتحقق لصالح أي مهاجم مجهول.
3. لا توجد قائمة أدوار مسموحة من جهة الخادم — مرّرت createWpSubscriber() الدور
المُهيّأ مباشرة إلى WP_User::set_role(). أما قائمة الأدوار الآمنة في الإضافة (التي
تستثني administrator) فكانت تُطبَّق فقط عند عرض القائمة المنسدلة في لوحة الإدارة —
وهو تحكّم في الواجهة فقط.
عند التسلسل: اشترك للحصول على nonce → أعد استخدامه ضد popup::save لتعيين
sub_wp_create_user_role=administrator → شغّل تدفق الاشتراك → حساب مسؤول دائم.
الشرح الكامل مع مراجع الملفات والأسطر: docs/ANALYSIS.md.
تحدّد poc/cve_2026_18322_check.py ما إذا كان الموقع يشغّل إصدارًا متأثرًا. وهي لا
تحاول الاستغلال أبدًا: بل تُصدر طلبات HTTP GET عادية وتقرأ الإصدار الذي ينشره
التثبيت عن نفسه.
git clone <this-repo> && cd CVE-2026-18322
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
requests موصى بها لكنها اختيارية — إذ تتراجع الأداة إلى المكتبة القياسية.
# Single target
python3 poc/cve_2026_18322_check.py https://example.com
# With supporting evidence
python3 poc/cve_2026_18322_check.py https://example.com -v
# Many targets, concurrently, exporting results
python3 poc/cve_2026_18322_check.py -f targets.txt -t 20 --json results.json
python3 poc/cve_2026_18322_check.py -f targets.txt --csv results.csv --only-vulnerable
# Through a proxy, ignoring TLS errors (lab use)
python3 poc/cve_2026_18322_check.py https://staging.internal -k --proxy http://127.0.0.1:8080
يمكن تمرير الأهداف كوسائط أو عبر -f (حيث يقرأ - من stdin). ويُفترض أن اسم المضيف
المجرد هو https://.
$ python3 poc/cve_2026_18322_check.py https://example.com -v
[VULNERABLE] https://example.com/ (Smart Popup by Supsystic 1.11.2) < 1.13.0 affected by CVE-2026-18322; upgrade to 1.13.0+
- asset-version: plugin asset enqueued with ?ver=1.11.2 (https://example.com/)
- homepage-reference: references /plugins/popup-by-supsystic/ (https://example.com/)
- readme: plugin readme.txt is publicly readable (https://example.com/wp-content/plugins/popup-by-supsystic/readme.txt)
- readme-stable-tag: Stable tag: 1.11.2 (.../readme.txt)
رموز الخروج: 0 = لا أهداف متأثرة، 1 = هدف متأثر واحد على الأقل، 2 = خطأ في الاستخدام.
إشارتان سلبيتان، كلتاهما طلبات HTTP GET عادية لموارد عامة:
?ver=PPS_VERSION
(classes/frame.php:410,473)، لذا غالبًا ما تكشف الصفحة الرئيسية الإصدار الدقيق.readme.txt — حقل Stable tag: موثوق وله الأولوية على أصل مخزّن مؤقتًا قد
يكون قديمًا؛ ويعمل سجل التغييرات كبديل احتياطي.كما يكتشف فحص الصفحة الرئيسية أدلّة wp-content المُعاد تسميتها (مثل /app/ في
Bedrock) بحيث يتبع البحث عن readme التخطيط الفعلي للموقع.
أداة الكشف لا تحاول الاستغلال. لا يوجد هنا أي استغلال مسلّح لهذه الثغرة: الكود الوحيد الذي يشغّل السلسلة الكاملة مقيّد بشكل صارم بالمختبر المحلي (راجع أدناه).
يقوم الفاحص السلبي ببصمة الإصدار فقط. ولا يكشف HttpClient الخاص به أي فعل HTTP
غير GET — وهو مفروض بالتصميم ومؤكَّد في مجموعة الاختبارات، لذا لا يمكنه إصدار طلب
يغيّر الحالة حتى عن طريق الخطأ. وهو لا يستدعي popup::save أبدًا، ولا يرسل معامل
sub_wp_create_user_role، ولا يرسل نموذج اشتراك، ولا يشغّل رسالة تأكيد بالبريد، ولا
ينشئ أو يعدّل أي مستخدم أو نافذة منبثقة أو إعداد. إنه يقرأ موردين عامين — الصفحة
الرئيسية وreadme.txt — ولا شيء غير ذلك.
لهذا الأمان ثمن، يُذكر بصراحة: الحكم لا يكون أفضل من بيانات الإصدار الوصفية التي ينشرها المضيف. المقايضات وأنماط الفشل: §9.
lab/exploit_full_chain.py هو الجزء الوحيد من الكود هنا الذي ينفّذ الهجوم الفعلي،
وهو ينشئ مسؤول ووردبريس. ويُنشر حتى يمكن إعادة إنتاج التحليل بدلًا من تلقّيه
على الثقة — فادّعاء وجود خلل في التخويل لا يمكن إثباته هو مجرد تأكيد، وليس اكتشافًا.
وقد كُتب من أجل الحزمة القابلة للتخلص في lab/ ولا شيء غيرها:
--lab-confirm إلزامي. يُشغَّل الفحصان في assert_lab_target() قبل أول طلب
HTTP، لذا فإن أي استدعاء مرفوض لا يلمس الهدف إطلاقًا.pps_nonce القابل
لإعادة الاستخدام من رسالة تأكيد الاشتراك عبر واجهة Mailpit في المختبر
(http://localhost:8025/api/v1/…). ولا يوجد مسار برمجي لاسترجاع تلك الرسالة من أي
مكان آخر، لذا لا تملك السلسلة خطوة أولى ضد مضيف خارج المختبر.lab/docker-compose.yml مرتبطة بـ 127.0.0.1.لذلك، كما هو مُوزَّع، لا يمكن توجيهه إلى موقع حقيقي — إذ يتوقف عند
REFUSING TO RUN قبل إصدار أي طلب:
$ python3 exploit_full_chain.py https://example.com --lab-confirm
REFUSING TO RUN: example.com resolves to non-local address(es) 104.20.23.154, ...
This script creates a WordPress administrator and is for the local lab only.
To test a real site, use the non-destructive checker in ../poc/.
هذه الحمايات أساسية وليست شكلية: فالسكربت هو السلسلة الحقيقية، وما يجعله عرضًا
توضيحيًا هو أنه لن يعمل خارج حزمة محلية قابلة للتخلص. الرجاء عدم إزالتها، وراجع
SECURITY.md — فالتغييرات التي تُضعفها، أو استغلال مستقل
مقصود تشغيله خارج المختبر، خارج نطاق هذا المستودع. لمعرفة ما إذا كان موقع حقيقي
متأثرًا، استخدم poc/ — فالفاحص يجيب على السؤال نفسه دون لمس أي شيء.
للحصول على إجابة موثوقة على مضيف تتحكم به:
grep -n "array_merge(\$permissions, \$permissionsBase)" \
wp-content/plugins/popup-by-supsystic/classes/frame.php
أي مخرجات تعني أن المضيف متأثر — فهذا السطر تحديدًا هو ما استبدله الإصدار 1.13.0.
استخدمه فقط ضد الأنظمة التي تملكها أو المصرّح لك صراحةً باختبارها. راجع SECURITY.md.
يحتوي lab/ على حزمة Docker قابلة للتخلص تشغّل الثغرة من البداية إلى النهاية،
بحيث يمكن التحقق من التحليل بدلًا من تلقّيه على الثقة:
cd lab
./setup.sh # WordPress + plugin 1.12.0
../.venv/bin/python exploit_full_chain.py http://localhost:8080 --lab-confirm
./verify.sh # confirm from the database
./teardown.sh
أكثر مخرجاته فائدة هو مقارنة الإصدارات — طلبات متطابقة ضد ثلاثة إصدارات:
./setup.sh --plugin-version 1.11.2 # exploit succeeds
./setup.sh --plugin-version 1.12.0 # exploit succeeds
./setup.sh --plugin-version 1.13.0 # exploit fails at phase 2
المختبر معرّض للثغرة عن قصد وسكربت الاستغلال الخاص به ينشئ مسؤول ووردبريس. كل شيء مرتبط بـ
127.0.0.1، ولن يعمل السكربت ضد أي شيء غير مختبر محلي — راجع استغلال المختبر يعمل في المختبر فقط. إنه ليس ماسحًا ضوئيًا؛ لفحص موقع حقيقي، استخدمpoc/.
sub_wp_create_user_role مضبوط على دور
ذي صلاحيات، وطلبات POST إلى admin-ajax.php تحمل
pl=pps&mod=popup&action=save.الاستعلامات وعمليات البحث في السجلات: §10.
.
├── README.md
├── SECURITY.md Scope, authorized-use policy, disclosure
├── LICENSE MIT
├── requirements.txt
├── docs/
│ └── ANALYSIS.md Full root-cause analysis and fix rationale
├── patches/
│ ├── 01-frame-permission-merge.diff array_merge() → _mergePermissions()
│ ├── 02-subscribe-role-allowlist-and-nonce.diff Role allowlist + dedicated nonce
│ └── 03-v1.12.0-confirmation-page-hardening.diff Unrelated hardening added in 1.12.0
├── poc/
│ └── cve_2026_18322_check.py Passive version fingerprint (GET only)
├── lab/ Disposable Docker lab — INTENTIONALLY VULNERABLE
│ ├── docker-compose.yml MariaDB + WordPress 1.12.0 + Mailpit (loopback only)
│ ├── setup.sh / teardown.sh Bring the lab up / destroy it
│ ├── exploit_full_chain.py Full attack chain — CREATES AN ADMIN; lab-gated
│ └── verify.sh Independent confirmation from the database
├── scripts/
│ └── fetch_versions.sh Export plugin versions from SVN and regenerate diffs
└── tests/
└── test_check.py Offline unit tests for the passive checker
pip install -r requirements.txt pytest
python3 -m pytest tests/ -v
المجموعة تعمل دون اتصال بالكامل — إذ تستخدم بيانات اختبار مسجّلة، ولا تستهدف أنظمة حية أبدًا. وهي تغطي فرز الإصدارات عبر حدّ 1.13.0، وتطبيع الأهداف، وقواعد الأدلة التي تحدد كل حالة.
تُؤكَّد خصائص الأمان، لا تُوثَّق فحسب: أن الفحص الكامل لا يصدر طلبات تغيّر الحالة، وأن
HttpClient لا يكشف أي فعل HTTP غير GET.
تم تثبيت تراجعين اكتُشفا أثناء التطوير: ملف readme.txt بأسطر CRLF يُفشل التحليل
المُرسَّخ على الأسطر، وصفحة خطأ HTTP تُحتسب خطأً كدليل على وجود الإضافة.
لإعادة إنتاج التحليل من المصادر الأصلية:
./scripts/fetch_versions.sh # exports 1.11.2 / 1.12.0 / 1.13.0 and regenerates patches/
MIT — راجع LICENSE. الإضافة التي جرى تحليلها مرخّصة بموجب GPLv2-or-later
ولا يُعاد توزيعها هنا؛ إذ يجلبها scripts/fetch_versions.sh من مستودع SVN الرسمي
لووردبريس.
| الحالة | المعنى |
|---|
VULNERABLE | تم كشف الإضافة عند < 1.13.0 |
NOT_VULNERABLE | تم كشف الإضافة عند >= 1.13.0 |
PLUGIN_DETECTED_VERSION_UNKNOWN | الإضافة موجودة، لكن الإصدار غير قابل للتحديد — تحقق يدويًا |
PLUGIN_NOT_DETECTED | لا دليل على وجود الإضافة (وليس دليلًا على غيابها) |
ERROR | الهدف غير قابل للوصول أو مشوّه |