
Безопасное обнаружение обхода аутентификации 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; другие приводят к падению packet engine и перезапуску устройства. Этот скрипт не делает ни того,
ни другого — он отправляет одну длину, проверенную на то, что она не создаёт сессию и не затрагивает packet engine, и
отвечает на один вопрос для каждой цели: Результат, отличный от
, .
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 virtual server, а не на интерфейс управления. Подверженность
определяется для каждого virtual server, поэтому устройство с несколькими VIP требует проверки каждого из них. Проба несёт
жёсткие рамки безопасности — одна проверенная длина 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
virtual server на одном устройстве — оба отвечают, чего проверка предусловий конфигурации не смогла бы
обеспечить:
$ ./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 в
packet engine — перезапускающие весь NetScaler и отбрасывающие трафик примерно на 45 секунд. Двадцать
байт попадают на чистую ветвь внутренней ошибки, которая не создаёт сессию, и построитель полезной нагрузки
отказывается конструировать любую другую длину, поэтому правка в другом месте не может незаметно расширить пробу.13.1-63.18 без единого сгенерированного кора и без перезапуска packet engine. Сама проверка
с тех пор запускалась против обеих ветвей обслуживания, по обе стороны от исправления, и против
как service-provider, так и IdP-only virtual server, без перезапуска packet engine ни на одном из них.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 | контроль, отправляется только после неисправленного сигнала |
Перенаправления не отслеживаются, и никакой cookie никогда не отправляется обратно — NetScaler отдаёт просроченный NSC_DLGE,
который воспроизводящий клиент перенёс бы в запрос 4, превратив POST в страницу входа.
nsppe записывает SAML POST в /var/log/ns.log на информационном уровне, без необходимости в отладочном переключателе, как
сбой десериализации контекста nFactor, включая строку
nFactor: ctx deserialize aaa_info, base64 decode failed. На этой длине нет строк создания сессии или входа,
что и отличает сканирование от попытки обхода.
Исправление — это одна инструкция. При неудачной десериализации контекста уязвимая сборка переносит
декодированную длину RelayState вперёд как статус запроса; исправленная сборка переносит константную
ошибку 0xe0005, отображаемую как Malformed Assertion. Так что один POST разделяет их:
| Ответ на пробу | Сборка | Вердикт |
|---|---|---|
500 Internal Server Error 43524 | неисправленная | VULNERABLE |
200 Malformed Assertion sent to Netscaler | исправленная | PATCHED |
Исправленная сборка отвечает Malformed Assertion на любой длине, что и делает неисправленный
ответ оракулом, а не совпадением.
VULNERABLE опирается на единственный различающий ответ, поэтому инструмент проверяет, что ответ действительно
зависит от того, что было отправлено. После неисправленного сигнала он повторяет запрос с контрольным
RelayState той же 20-байтовой декодированной длины, отличающимся только 16 байтами после ctx=. Это
содержимое отклоняется защитой перед десериализатором, поэтому каждая протестированная сборка — исправленная и
неисправленная — отвечает на него 200 Malformed Assertion. Вердикт сохраняется только если два ответа различаются.
Если они совпадают, что-то возвращает 500/43524 независимо от ввода, и результат —
INCONCLUSIVE с тегом flat-response, а не ложный VULNERABLE. Удержание контроля на
той же декодированной длине сохраняет его внутри проверенных рамок безопасности и изолирует содержимое как единственную
переменную.
Защита односторонняя. Она не может подтвердить вердикт PATCHED, потому что по-настоящему
исправленное устройство отвечает на пробу и контроль одинаково — это и означает быть исправленным здесь. См.
Ограничения.
Десериализация RelayState выполняется до сопоставления политики SAML, поэтому virtual server, несущий только
samlIdPPolicy, отвечает на пробу, а не только тот, у которого привязан service-provider samlAction. Это
более широкое покрытие, чем может предложить проверка предусловий конфигурации.
Уязвимый ответ — единственный самоподтверждающийся: он несёт конкретный маркер, и
контроль доказывает, что ответ зависит от того, что было отправлено. PATCHED — тоже положительное доказательство —
собственная исправленная ошибка устройства на пути, которого достигла проба — но оно ограничено этим CVE и
этим путём. INCONCLUSIVE и ERROR вообще не доказательства: проба так и не достигла
десериализатора, поэтому состояние исправления неизвестно, и именно поэтому INCONCLUSIVE — отдельный вердикт,
а не отнесён к PATCHED. Сообщать о молчании как о прохождении — значит ошибаться в опасном
направлении. Там, где нужна определённость, подтвердите с помощью show ns version против сборок в
Устранении.
Каждый вердикт несёт короткий тег reason. --brief печатает его третьей колонкой, а --json
несёт его как reason.
| Вердикт | Тег reason | Значение |
|---|---|---|
VULNERABLE | internal-error-43524 | Устройство передало декодированную длину RelayState как свою внутреннюю ошибку, и контроль той же длины доказал, что ответ зависит от того, что было отправлено. Исправление CTX696939 отсутствует — исправьте. |
PATCHED | fixed-error-returned | Устройство вернуло исправленную ошибку на пути, которого достигла проба. Ограничено этим CVE, и см. Ограничения о промежуточных устройствах. |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth вернул 404, поэтому потребитель SAML assertion здесь не обслуживается. На vserver, а не на устройство, и см. оговорку о старых сборках в Ограничениях. |
INCONCLUSIVE | flat-response | Проба и контроль той же длины ответили одинаково, поэтому ответ не зависит от того, что было отправлено. Срабатывание защиты от ложных срабатываний. |
INCONCLUSIVE | generic-internal-error | Конечная точка вернула 43549, свою общую внутреннюю ошибку, которую возвращают и исправленные, и неисправленные сборки. Не дискриминатор — и заметьте, что это есть неисправленный маркер для несвязанного CVE-2026-8452, поэтому выглядит как попадание, но им не является. |
INCONCLUSIVE | unrecognized-reply | Конечная точка ответила чем-то вне распознанного набора. |
INCONCLUSIVE | no-probe-response | Хост ответил на идентификацию, но не на пробу — тайм-аут, сброс или промежуточное устройство, отбрасывающее POST. Повторите. |
ERROR | not-identified | Не идентифицирован как NetScaler или недостижим. |
Все четыре причины INCONCLUSIVE означают одно и то же для принятия решений — неизвестно, не исправлено.
Тег называет, какое условие исправить перед повторным запуском.
| Код | Значение |
|---|---|
0 | Ни одна цель не была VULNERABLE |
1 | Как минимум одна цель VULNERABLE |
2 | Ошибка использования (неверные аргументы / нечитаемый файл целей) |
Выход 0 — не подтверждение безопасности. PATCHED, UNAFFECTED и все четыре причины INCONCLUSIVE
сводятся к нему, и только одна из них — положительный результат. Обёртка, которой нужно отличить
«исправлено» от «не удалось классифицировать», должна читать вердикт — первую колонку --brief или
поле verdict из --json — а не код выхода.
PATCHED. Malformed Assertion — это то, что
устройство возвращает всякий раз, когда проба не достигает десериализатора контекста, и исправленность —
лишь одна из причин, почему это происходит. Промежуточное устройство, которое удаляет, искажает или замыкает
параметр RelayState, поэтому выдаёт PATCHED на уязвимом устройстве, и никакой дополнительный
запрос, который инструмент мог бы отправить, не разделил бы эти два случая. Там, где что-то может стоять перед
устройством, подтвердите с помощью show ns version.UNAFFECTED ограничено текущими сборками. На 14.1-43.55 / 13.1-61.27 и более ранних
бюллетень утверждает, что подверженность вообще не требует SAML action — любой Gateway или AAA virtual
server затронут — поэтому 404 на такой сборке не выводит устройство из области действия. Это положение
взято из бюллетеня, а не измерено.PATCHED ничего не говорит о любой другой
уязвимости NetScaler, включая CVE-2026-19489 из того же бюллетеня.VULNERABLE устанавливает, что исправление
отсутствует на пути, которого достигла проба. Оно не измеряет, как далеко атакующий мог бы зайти с обходом
на вашей конфигурации, и не скажет вам, не сделал ли это уже кто-то — ищите это в
/var/log/ns.log отдельно.Обновитесь до 13.1-63.21 или более поздней, либо 14.1-73.32 или более поздней (FIPS и NDcPP: 13.1-37.277 для 13.1-FIPS и 13.1-NDcPP, 14.1-73.32 FIPS для 14.1-FIPS), согласно CTX696939. Устройства на 12.1 или 13.0 не имеют исправления и не получат его — эти ветви сняты с поддержки и должны быть перенесены на поддерживаемую ветвь.
Три дополнительных замечания:
defaultAuthorizationAction любого vpn sessionAction, привязанного к vserver — она откатывается к
глобальному set vpn parameter -defaultAuthorizationAction. Поставляемое значение по умолчанию — DENY; ALLOW
распространён на практике, потому что это то, к чему администраторы прибегают вместо написания
политик авторизации для каждого пользователя, и именно ALLOW позволяет неаутентифицированной сессии достичь внутренних
ресурсов. Это глобально, а не на vserver. Это ограничивает воздействие; это не исправление.add authentication samlAction и add authentication samlIdPProfile вместе с
add authentication vserver и add vpn vserver — с учётом оговорки о старых сборках выше.CTX696939 также исправляет CVE-2026-19489, и эта поверхность сильно пересекается с CVE-2026-8452 из предыдущего бюллетеня: на сборках с 14.1-43.56 / 13.1-61.28 и далее обе проблемы зависят от настроенного SAML action, поэтому один аудит конфигурации охватывает обе.
Этот код распространяется под лицензией MIT.
Использование этого инструмента для атаки на цели без предварительного взаимного согласия незаконно. Это ответственность конечного пользователя — соблюдать все применимые местные, региональные и федеральные законы. Разработчики не несут ответственности и не отвечают за любое неправомерное использование или ущерб, вызванный этой программой.