
اكتشاف تجاوز مصادقة SAML في Citrix NetScaler CVE-2026-19490 بأمان
فحص آمن وغير مُصادَق للثغرة CVE-2026-19490، وهي تجاوز المصادقة قبل المصادقة في مسار مزوّد خدمة SAML في Citrix NetScaler ADC / NetScaler Gateway
(CTX696939،
نُشر في 2026-08-19). CWE-288، CVSS v4.0 9.3
(AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L). أبلغ عنه Samarth Vashisht من
فريق اختبار الاختراق في JPMorgan Chase.
يقوم الجهاز بفك ترميز base64 للمعامل RelayState في استجابة SAML، وعندما يبدأ النص العادي بـ ctx=،
يمرّر الباقي إلى مُفكِّك تسلسل سياق nFactor. على إصدار غير مُرقَّع، يؤدي فشل فك التسلسل إلى نشر
طول RelayState المُفكَّك كرمز التصرف الداخلي للطلب بدلاً من خطأ،
لذا يختار المهاجم غير المُصادَق الفرع الداخلي الذي يتخذه الجهاز بعد ذلك
بمجرد اختياره لطول RelayState. بعض الفروع تُنشئ جلسة Gateway حقيقية؛
وأخرى تُعطّل محرك الحزم وتُعيد تشغيل الجهاز. هذا النص البرمجي لا يفعل أياً من ذلك
— فهو يرسل الطول الواحد المُتحقَّق منه الذي لا يُنشئ جلسة ويترك محرك الحزم وشأنه، ويجيب
على سؤال واحد لكل هدف: هل هذا الجهاز مُعرَّض للثغرة؟ أي نتيجة غير
VULNERABLE هي ليست بحد ذاتها شهادة سلامة.
# single target
./cve_2026_19490_check.py https://gateway.example.com
# a specific Gateway or AAA virtual server
./cve_2026_19490_check.py https://gateway.example.com:9443
# several targets; the scheme defaults to https://
./cve_2026_19490_check.py gw-a.example.com gw-b.example.com:9443
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_19490_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_19490_check.py -f targets.txt --json > results.json
Python 3.8+، المكتبة القياسية فقط — لا حزم خارجية.
وجّه الأداة إلى خادم Gateway أو AAA الافتراضي، وليس إلى واجهة الإدارة. التعرّض
يكون لكل خادم افتراضي، لذا يحتاج الجهاز الذي يضم عدة VIPs إلى اختبار كل واحد منها. يحمل الفحص
غلافاً أمنياً صارماً — طول RelayState واحد مُتحقَّق منه، دون مسح — وهو ما
يوضحه هل من الآمن تشغيله؟.
| العلم | الوصف |
|---|---|
TARGET | هدف واحد أو أكثر بصيغة [https://]HOST[:PORT]؛ المخطط الافتراضي هو https:// |
-f, --targets-file FILE | قراءة الأهداف من ملف (واحد في كل سطر؛ تعليقات #) |
--timeout SECS | المهلة لكل طلب (الافتراضي: 15) |
--workers N | الأهداف المتزامنة (الافتراضي: 16)؛ يبقى الإخراج بترتيب الإدخال |
-b, --brief | سطر واحد مُحاذى لكل هدف — مثالي لفحص العديد من المضيفين |
--json | إخراج JSON مُهيكل، بما في ذلك كل طلب يُرسَل لكل هدف |
--no-color | تعطيل الإخراج الملوّن (يحترم أيضاً NO_COLOR وغير TTY) |
جهاز مُعرَّض للثغرة (الإخراج الافتراضي المكوّن من سطرين). تظهر العلامة [!] وVULNERABLE
باللون الأحمر على TTY:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443
[!] https://gateway.example.com:9443: VULNERABLE [internal-error-43524]
HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent
جهاز مُرقَّع:
$ ./cve_2026_19490_check.py https://vpn.example.com
[+] https://vpn.example.com: PATCHED [fixed-error-returned]
HTTP 200 "Malformed Assertion": the fixed error was returned on the path this probe reached, so the CTX696939 fix is present (>= 13.1-63.21 / 14.1-73.32)
تفعيل حارس الإيجابيات الكاذبة. أعاد كلٌّ من الفحص و الضابط بنفس الطول إشارة عدم الترقيع، لذا لا تعتمد الاستجابة على ما أُرسل وتُسحَب الإجابة التي تبدو حاسمة:
$ ./cve_2026_19490_check.py https://sp-strict.example.com
[?] https://sp-strict.example.com: INCONCLUSIVE [flat-response]
the probe and the same-length control both answered HTTP 500 / 43524, so the reply does not depend on what was sent and the fix was never exercised; unknown, not patched
مسح مجموعة أصول (--brief). صفّا gateway.example.com هما الخادمان الافتراضيان SP وIdP-only
على الجهاز نفسه — كلاهما يجيب، وهو ما لا تستطيعه أداة فحص الشروط المسبقة للإعداد:
$ ./cve_2026_19490_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 internal-error-43524
VULNERABLE https://gateway.example.com:9444 internal-error-43524
PATCHED https://vpn.example.com fixed-error-returned
INCONCLUSIVE https://sp-strict.example.com flat-response
UNAFFECTED https://lb.example.com no-saml-endpoint
ERROR https://www.example.com not-identified
exit: 1
إخراج قابل للقراءة آلياً (--json). يُضمَّن كل طلب، لذا يمكن إعادة استنتاج النتيجة
من الأدلة بدلاً من الوثوق بها. يُسجَّل الضابط من خلال علاقته بالفحص
لا كحكم خاص به، لأن ضابطاً يبدو كإصدار مُرقَّع هو النتيجة المتوقعة على كل إصدار:
$ ./cve_2026_19490_check.py https://gateway.example.com:9443 --json
[
{
"target": "https://gateway.example.com:9443",
"verdict": "VULNERABLE",
"reason": "internal-error-43524",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error, so the CTX696939 fix is absent",
"netscaler_indicators": [
"CSP contains citrixng://",
"CSP contains com.citrix.nsgclient://",
"CSP contains nsgcepa://",
"CSP report-uri /nscsp_violation/report_uri",
"/vpn/js/rdx/ present (HTTP 404)"
],
"attempts": [
{
"kind": "probe",
"path": "/cgi/samlauth",
"status": 500,
"state": "unpatched",
"detail": "HTTP 500 / 43524: the decoded RelayState length was propagated as the internal error"
},
{
"kind": "control",
"path": "/cgi/samlauth",
"status": 200,
"state": "differs-from-probe",
"detail": "same-length control: HTTP 200 \"Malformed Assertion\": the fixed error was returned"
}
]
}
]
نعم. صُمِّم للاستخدام في الإنتاج والتقييم:
RelayState الخاص بالفحص إلى 20 بايت بالضبط ولا
يرسل النص البرمجي أي طول آخر تحت أي علم. على جهاز غير مُرقَّع، الطول المُفكَّك هو
مُحدِّد الفرع، وتشمل الفروع فروعاً تُنشئ جلسة وأخرى تُسبِّب SIGSEGV
لمحرك الحزم — ما يُعيد تشغيل NetScaler بالكامل ويُسقط حركة المرور لنحو 45 ثانية. عشرون
بايتاً يقع على فرع خطأ داخلي نظيف لا يُنشئ جلسة، ويرفض مُنشئ الحمولة
إنشاء أي طول آخر، لذا لا يمكن لتعديل في مكان آخر أن يوسّع الفحص بصمت.13.1-63.18 غير مُرقَّع دون توليد أي cores ودون إعادة تشغيل محرك الحزم. وقد شُغِّل الفحص نفسه
منذ ذلك الحين ضد فرعي الصيانة، على جانبي الإصلاح، وضد
الخوادم الافتراضية لمزوّد الخدمة وIdP-only على حد سواء، دون إعادة تشغيل محرك الحزم على أي منها.SAMLResponse
موجوداً للوصول إلى فرع RelayState، لكن محتواه غير ذي صلة، لذا فهو أربعة بايتات
من بيانات عشوائية. لا يُصدر الفرع أي cookie ولا يكتب أي إعداد.VULNERABLE أبداً بناءً على استجابة واحدة — انظر
الضابط.إذا عدّلت الفحص، فلا تُغيِّر طول
RelayStateالمُفكَّك ولا تمسح الأطوال. عشرون بايتاً أمر جوهري، والأقصر ليس أكثر أماناً. الأطوال الأخرى غير مُختبرة في أفضل الأحوال وانقطاع للجهاز في أسوأها.
ثلاثة طلبات HTTP لكل هدف، ورابع فقط عندما يُعيد الجهاز إشارة عدم الترقيع:
| # | الطلب | الغرض |
|---|---|---|
| 1 | GET /logon/LogonPoint/tmindex.html | تعريف NetScaler (رموز Content-Security-Policy) |
| 2 | GET /vpn/js/rdx/core/rdx.js | تعريف NetScaler |
| 3 | POST /cgi/samlauth | الفحص |
| 4 | POST /cgi/samlauth | الضابط، يُرسَل فقط بعد إشارة عدم الترقيع |