
PoC — خطأ في التحقق من الأصل يتيح تسريب ملف تعريف ارتباط SSO الخاص بـ Entra ID PRT في linux-entra-sso (GHSA-g9vc-5j77-f2cm، CVE-2026-87005، CVSS 5.3).
حالة CVE: مطلوبة، في انتظار التعيين. تم نشر هذا الاكتشاف باسم GHSA-g9vc-5j77-f2cm. عند تعيين CVE، يُعاد تسمية هذا المستودع إلى
CVE-YYYY-NNNNN-linux-entra-sso-PoCويُستبدل هذا الشعار برابط CVE.
| الباحث | Dostxodjayev Abdullox (@squeeze440) |
| الإشعار | GHSA-g9vc-5j77-f2cm |
| CVSS 3.1 | 5.3 (متوسط) |
| الضعف | CWE-346, CWE-20 |
يفتقر Platform.SSO_URL في linux-entra-sso إلى فاصل مسار لاحق، لذا يتم تجاوز حارس onBeforeSendHeaders في Firefox/Thunderbird (e.url.startsWith(Platform.SSO_URL)) بواسطة أي نطاق مسجَّل من المهاجم بالشكل login.microsoftonline.com.<attacker-domain>، مما يؤدي إلى قيام الإضافة بجلب وحقن ملف تعريف ارتباط SSO حي لـ Entra ID Primary Refresh Token (PRT) في الطلبات المُرسَلة إلى مضيف المهاجم نفسه.
siemens/linux-entra-sso — إضافة متصفح لنظام Linux للدخول الموحّد (SSO) على Microsoft Entra ID عبر وسيط الهوية المحلي من Microsoft (Intune). إصدار Firefox قابل للاستغلال عمليًا؛ ويشترك Thunderbird في مسار الكود الضعيف نفسه لكنه غير قابل للوصول حاليًا عبر هذا المتجه (انظر التفاصيل)؛ أما Chrome/Chromium فغير متأثر (انظر التفاصيل).
v1.10.0 (commit 676854c، وهو main الحالي اعتبارًا من 2026-08-03). مؤكَّد وجوده في الإصدار الحالي، أي لم يُصلَح بواسطة GHSA-52rj-42vh-2rxc / CVE-2026-42177 (فذلك الإصلاح مسّ فقط محوّل declarativeNetRequest في Chrome).
5.3 (متوسط) — CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
AC:H — يتطلب الاستغلال أن تحمل الإضافة إذن مضيف ممنوحًا يغطي اسم المضيف المُصمَّم من المهاجم. عمليًا، يجب على المهاجم أن يخدع الضحية اجتماعيًا لينقر على رابط "Background SSO (enable)" الخاص بالإضافة أثناء وجوده على صفحة المهاجم — وهو شرط خارج السيطرة المباشرة للمهاجم، بما يتوافق مع منطق التعقيد المستخدم في الإشعار الشقيق GHSA-52rj-42vh-2rxc.UI:R — منح الإذن هو نقرة مستخدم صريحة.C:H — ملف تعريف ارتباط PRT SSO المسروق قابل لإعادة التشغيل مباشرة ضد login.microsoftonline.com لإصدار رموز الوصول/الهوية لأي تطبيق وافق عليه الضحية (اختطاف كامل لجلسة SSO).I:N/A:N — يكشف الخطأ عن بيانات اعتماد لكنه لا يعدّل البيانات أو التوافر بحد ذاته.السبب الجذري — src/platform.js:9:
static SSO_URL = "https://login.microsoftonline.com";
لا يوجد / لاحق.
الفحص الضعيف — platform/firefox/js/platform-firefox.js:55-59:
async #onBeforeSendHeaders(e) {
// filter out requests that are not part of the OAuth2.0 flow
if (!e.url.startsWith(Platform.SSO_URL)) {
return { requestHeaders: e.requestHeaders };
}
...
تُجري String.prototype.startsWith مقارنة بادئة أحرف خام، وليس مقارنة اسم مضيف/أصل (origin). ولأن Platform.SSO_URL لا يحتوي على فاصل لاحق، فإن أي URL يكون مضيفه login.microsoftonline.com.<anything> — وهو اسم DNS صالح نحويًا يتحكم به المهاجم بالكامل — يحقق الفحص أيضًا، على سبيل المثال:
"https://login.microsoftonline.com.attacker.example/phish".startsWith("https://login.microsoftonline.com")
=> true
هذا هو الفحص الدفاعي نفسه الذي وصفه وصف GHSA-52rj-42vh-2rxc بأنه "الفحص الدفاعي القانوني ... الغائب عن محوّل Chrome" — وهو موجود هنا، لكنه قابل للتجاوز بحد ذاته، لذا فإن إصلاح الإشعار (الذي عزّز فقط قاعدة declarativeNetRequest في Chrome) أبقى هذا المسار مفتوحًا. يمتد platform/thunderbird/js/platform-thunderbird.js من PlatformFirefox دون تجاوز update_request_handlers() أو #onBeforeSendHeaders، لذا يرث Thunderbird العيب نفسه.
مسار الإطلاق (update_request_handlers، platform/firefox/js/platform-firefox.js:36-53): يُسجَّل المستمع بـ urls: this.well_known_app_filters، الذي يُملأ من chrome.permissions.getAll().origins (src/platform.js:78-79، update_host_permissions()). أي إذن مضيف منحه المستخدم — بما في ذلك منح موقع واحد عبر رابط "Background SSO (enable)" في النافذة المنبثقة (popup/menu.js:171-260، باستخدام current_filter = "https://" + tab_hostname + "/*" المستمد من التبويب النشط) — يكفي لتسجيل المستمع لذلك المضيف بالتحديد. لا يُشترط إذن شامل https://*/*، بخلاف شرط الاستغلال الموصوف في GHSA-52rj-42vh-2rxc؛ فمنح واحد موجَّه على نطاق المهاجم المشابه يكفي هنا، وهو عتبة أدنى من PoC الإشعار السابق.
المصب — الملف نفسه، الأسطر 60-70:
let prt = await this.#broker.acquirePrtSsoCookie(this.account, e.url);
ssoLog("inject PRT SSO into request headers");
e.requestHeaders.push({ name: prt.cookieName, value: prt.cookieContent });
يُمرَّر e.url (عنوان URL الكامل للمهاجم) مباشرةً كـ ssoUrl إلى الوسيط الأصلي. يوثّق linux-entra-sso.py:27-30 أن خلفية وسيط Microsoft لا تتحقق من ssoUrl من جهة الخادم ("القيمة الصحيحة لا يتم التحقق منها ... بواسطة خلفية التفويض")، لذا يُصدر ملف تعريف ارتباط بغض النظر عن ذلك، ويُحقن في الترويسات على الطلب المتجه فعليًا إلى مضيف المهاجم نفسه — تسريب مباشر، وليس مجرد تعيين ترويسة على طلب موجَّه إلى Microsoft.
Chrome غير متأثر: platform/chrome/js/platform-chrome.js:96-99 (بعد إصلاح GHSA-52rj-42vh-2rxc، commit 8183759) يثبّت باستخدام "|" + Platform.SSO_URL + "/" (مرساة بداية السلسلة + شرطة مائلة لاحقة) ويضيف قائمة سماح صريحة requestDomains: [URL.parse(Platform.SSO_URL).hostname] — وكلاهما يرفض بشكل صحيح مضيف login.microsoftonline.com.attacker.example.
Thunderbird — الكود الضعيف نفسه، غير قابل للوصول حاليًا عبر هذا المتجه: يمتد platform/thunderbird/js/platform-thunderbird.js من PlatformFirefox دون تغيير، لذا يوجد تجاوز startsWith المطابق في ثنائي إصدار Thunderbird. ومع ذلك، يعلن platform/thunderbird/manifest.json فقط عن "host_permissions": ["https://login.microsoftonline.com/*"] ثابتة مع عدم وجود مفتاح optional_host_permissions. تسمح WebExtensions فقط لـ chrome.permissions.request() بمنح أصول مُعلَنة مسبقًا في optional_permissions/optional_host_permissions؛ وبما أن Thunderbird لا يعلن عن أي منها، فإن تدفق "Background SSO (enable)" في النافذة المنبثقة (popup/menu.js:259-261) لا يمكنه الحصول على إذن لنطاق يختاره المهاجم على Thunderbird اليوم. الخطأ كامن هناك، وغير قابل للوصول من المهاجم حاليًا، ويجب مع ذلك إصلاحه للدفاع في العمق / في حال إضافة optional_host_permissions إلى ذلك الملف يومًا ما.
تم تأكيد المسند الضعيف ديناميكيًا مقابل الثابت الحقيقي المُشحَن (مستورد مباشرةً من src/platform.js الخاص بالهدف، وليس منسوخًا يدويًا). السكربت محفوظ في ~/engagements/linux-entra-sso/evidence/prt_bypass_poc.mjs.
$ node prt_bypass_poc.mjs
Real Platform.SSO_URL from src/platform.js: "https://login.microsoftonline.com"
Legit MS URL passes filter (expected true): true
Attacker domain-suffix URL: https://login.microsoftonline.com.attacker.example/phish
Attacker URL passes filter (SHOULD be false, is): true
[CONFIRMED] Platform.SSO_URL lacks a trailing '/', so startsWith()
treats 'login.microsoftonline.com.attacker.example' as an in-scope
SSO endpoint. onBeforeSendHeaders would call:
broker.acquirePrtSsoCookie(this.account, "https://login.microsoftonline.com.attacker.example/phish")
and inject the returned PRT cookie into the request headers sent to
the attacker-controlled host.
باقي السلسلة (إصدار الوسيط لملف تعريف ارتباط لـ ssoUrl غير مُتحقَّق منه، وإطلاق مستمع webRequest في WebExtensions لإذن مضيف واحد ممنوح) مُتتبَّع ساكنًا، وليس مؤكَّدًا ديناميكيًا — إذ يتطلب إعادة إنتاجه من البداية إلى النهاية مضيف Linux مُسجَّلًا حيًا يشغّل microsoft-identity-broker مقابل مستأجر Entra حقيقي، وهو خارج هذه البيئة. الخطوات المُتتبَّعة:
login.microsoftonline.com.attacker.example ويقدّم صفحة هناك.popup/menu.js:259-261 → request_host_permission(["https://login.microsoftonline.com.attacker.example/*"])).chrome.permissions.onAdded → on_permissions_changed() (src/background.js:30-34) → يحدّث update_host_permissions() قيمة well_known_app_filters → يعيد update_request_handlers() تسجيل مستمع webRequest.onBeforeSendHeaders في Firefox بما يشمل نمط المضيف الجديد.main_frame/sub_frame إلى https://login.microsoftonline.com.attacker.example/... المستمع؛ فينجح فحص startsWith (الموضَّح تجاوزه أعلاه).broker.acquirePrtSsoCookie(account, e.url) ملف تعريف ارتباط PRT حقيقي (وفقًا لـ linux-entra-sso.py:27-30، لا تتحقق خلفية الوسيط من ssoUrl).المهاجم الذي يجعل مستخدم linux-entra-sso على Firefox يمنح "Background SSO" لنطاق مشابه واحد يتحكم به، يتلقى ملف تعريف ارتباط PRT SSO الخاص بـ Entra ID للضحية مباشرةً على خادمه. هذا الملف قابل لإعادة التشغيل ضد login.microsoftonline.com للحصول على رموز الوصول/الهوية لأي تطبيق وافق عليه الضحية — اختطاف كامل لجلسة SSO. وخلافًا للإشعار السابق، لا يتطلب ذلك الإذن الشامل https://*/*، بل منح موقع واحد على نطاق المهاجم نفسه فقط. يشحن Thunderbird الفحص المعيب نفسه لكن بيانه لا يملك حاليًا أي وسيلة لمنح إذن نطاق عشوائي في وقت التشغيل، لذا فهو غير قابل للاستغلال عبر هذا المتجه اليوم (انظر التفاصيل).
ثبّت المقارنة على فحص صحيح للأصل/اسم المضيف بدلًا من بادئة سلسلة خام، على غرار الإصلاح المطبَّق بالفعل على محوّل Chrome:
if (!e.url.startsWith(Platform.SSO_URL + "/")) {
return { requestHeaders: e.requestHeaders };
}
أو، بشكل أكثر متانة، قارن new URL(e.url).origin مع new URL(Platform.SSO_URL).origin (يرفض أيضًا حيل userinfo/port). إن تطبيق الإصلاح على مستوى ثابت Platform.SSO_URL (إضافة الشرطة المائلة اللاحقة مرة واحدة في src/platform.js) سيغلق هذا لكلٍّ من فحص Firefox وأي مستهلك آخر للثابت، كما أن اختبار انحدار يؤكد أن startsWith يرفض https://login.microsoftonline.com.attacker.example/... سيغلق فجوة التغطية بالطريقة نفسها التي أغلق بها اختبار الانحدار المقترح testMatchOutcome في إصلاح Chrome الفجوة الخاصة بـ GHSA-52rj-42vh-2rxc.
Dostxodjayev Abdullox (GitHub: squeeze440)
لا يوجد SECURITY.md في المستودع، وتفيد واجهة GitHub API بعدم وجود سياسة أمنية على مستوى المستودع، لكن الإبلاغ الخاص عن الثغرات (Private Vulnerability Reporting) مُفعَّل على siemens/linux-entra-sso (تم التحقق عبر زر "Report a vulnerability" في تبويب Security بالمستودع). أبلغ عبر تلك القناة: https://github.com/siemens/linux-entra-sso/security/advisories/new.