
Поведенческий детектор состояния патча для Citrix NetScaler CVE-2026-8452. Отправляет специально сформированные SAML-запросы, чтобы определить, присутствует ли проверка размера PrefixList, без эксплуатации или повреждения памяти.
A safe, non-destructive patch-state check for CVE-2026-8452, the pre-authentication heap overflow
in the Citrix NetScaler ADC / NetScaler Gateway SAML signature canonicalizer
(CTX696604,
CVSS 8.8). An oversized exclusive-canonicalization PrefixList overflows a fixed-size buffer during
canonicalization, which NetScaler performs before validating the signature that carries it — so the
whole path is reachable with no credentials, no session, and no valid signature. Reported by Michael
Tucker of the JPMorgan Chase XOR team; root-cause and exploitation analysis credit to
watchTowr Labs.
This script does not exploit the bug and does not corrupt memory. It answers one question per target: is the fix present on this appliance? — determined behaviourally, by observing the patch rather than guessing the build.
Yes. It is designed for production and assessment use:
PrefixList размером . Этот предел был определён с точностью до байта, одинаков на обеих поддерживаемых ветвях и не зависит от конфигурации устройства
или формы окружающего SAML-сообщения — что подтверждено проверкой обоих маршрутов, которые оборачивают значение в существенно разные объёмы XML, и обнаружением, что они меняют поведение на одном и том же
байте. 575 байт преодолевают предел с запасом в 63 байта, так что вердикт не зависит от того, как настроена цель.PrefixList. Раздувание других полей за его пределы — URL-адресов службы обработчика утверждений, имён издателя, идентификаторов алгоритмов, значений дайджеста и подписи — ничего не меняет на исправленной сборке, поэтому применение исправления не должно приводить к сбоям в рабочей конфигурации SAML.Если вы изменяете пробную нагрузку, не меняйте
PROBE_PREFIXESи не перебирайте длины. 575 байт — несущая конструкция. Другие длиныPrefixListмогут дестабилизировать устройство, по крайней мере в одном случае на сборке, содержащей это исправление, поэтому перебор длин — небезопасный способ исследования этой уязвимости, и меньшее значение не безопаснее.
Исправленные сборки чётко отклоняют слишком большой PrefixList с характерным сообщением. Неисправленные сборки
проходят через парсер и возвращают общую внутреннюю ошибку. Один идентичный запрос — два разных ответа:
575-байтовый PrefixList | Ответ |
|---|---|
| Не исправлено | 500 Internal Server Error 43549 |
| Исправлено | 200 Malformed Assertion sent to Netscaler |
Пробуются два маршрута, сначала IdP, с остановкой, как только один даёт ответ. Каждого из них достаточно по отдельности, а вместе они охватывают обе роли SAML:
| Маршрут | Запрос | Требует |
|---|---|---|
| 1 (первый) | POST /saml/login — подписанный AuthnRequest, PrefixList в ds:SignedInfo | политику SAML IdP, привязанную к целевому vserver |
| 2 (запасной) | POST /cgi/samlauth — SAMLResponse, PrefixList в подписи утверждения | службу обработчика утверждений (assertion consumer service) SAML SP на целевом vserver |
Маршрут IdP идёт первым, потому что он более устойчив из двух. Он нечувствителен к значению Issuer,
к AssertionConsumerServiceURL и к расхождению часов — IssueInstant, значительно выходящий за пределы
допуска устройства по расхождению времени, по-прежнему корректно различает сборки, поскольку канонизация предшествует и проверке времени, и проверке подписи.
AuthnRequestна маршруте 1 должен быть подписан. Неподписанный запрос возвращает200 Malformed Assertion sent to Netscalerна исправленных и неисправленных сборках, что побайтно идентично сигналу исправленной сборки, поэтому пробная нагрузка без блока подписи сообщает о каждом устройстве как об исправленном. Подпись не обязана быть действительной, и у этого инструмента она не действительна; она просто должна присутствовать, потому что еёSignedInfo— это то, что доставляетPrefixListв канонизатор.
Обе поддерживаемые ветви меняют поведение ровно на своей исправленной сборке, на обоих маршрутах:
| Сборка | Вердикт | |
|---|---|---|
13.1-63.16 | последняя уязвимая 13.1 | VULNERABLE |
13.1-63.18 | первая исправленная 13.1 | PATCHED |
14.1-66.59 | уязвимая 14.1 | VULNERABLE |
14.1-72.61 | первая исправленная 14.1 | PATCHED |
13.1-63.16 и 63.18 — последовательные выпуски, поэтому изменение относится к самому исправлению,
а не к дрейфу между промежуточными сборками.
Это сборки, в которых впервые появилось это исправление, и пробная нагрузка обнаруживает именно этот переход.
Они больше не являются сборками, до которых следует обновляться: более поздние бюллетени вытеснили их, поэтому 13.1-63.18 и
14.1-72.61 здесь обе отвечают PATCHED, оставаясь при этом подверженными более новым проблемам. См.
Remediation для актуальных исправленных сборок.
Потому что это не может работать с этой уязвимостью даже в принципе. 13.1-63.16 и 13.1-63.18, сборки
непосредственно по обе стороны от исправления, отдают побайтно идентичные tmindex.html, base.css и
resources.js — исправление не затрагивает ни одного веб-ресурса. Хэши статических ресурсов также совпадают между ветвями, поэтому
подход на основе хэшей может определить уязвимое устройство как исправленную сборку и сообщить о нём как о чистом,
что является наихудшим режимом отказа для инструмента обнаружения. Поэтому определение сборки по отпечатку намеренно
не реализовано. Состояние исправления определяется пробной нагрузкой или show ns version, если у вас есть
учётные данные.
./cve_2026_8452_check.py https://gateway.example.com
./cve_2026_8452_check.py https://gateway.example.com:9443
./cve_2026_8452_check.py -f targets.txt --brief
./cve_2026_8452_check.py -f targets.txt --json > results.json
Направьте инструмент на **виртуальный сервер Gateway или AAA**, а не на интерфейс управления.
Предусловие проверяется для каждого виртуального сервера, поэтому устройство с несколькими VIP-адресами требует проверки каждого из них.
### Параметры
| Флаг | Описание |
| --- | --- |
| `URL` | Один или несколько целевых адресов `https://HOST[:PORT]` |
| `-f, --targets-file FILE` | Читать целевые адреса из файла (по одному в строке; комментарии `#`) |
| `-b, --brief` | Одна выровненная строка на целевой адрес — вердикт, целевой адрес, тег причины — для сканирования множества хостов |
| `--json` | Выводить структурированные результаты в формате JSON |
| `--no-color` | Отключить цветной вывод (также учитывает `NO_COLOR` и не-TTY) |
| `--timeout SECS` | Таймаут на запрос (по умолчанию: 15) |
### Примеры
**Незапатченное устройство,** ответившее на маршруте IdP и подтверждённое контрольной проверкой:```console
$ ./cve_2026_8452_check.py https://gateway.example.com:9443
====================================================================
CVE-2026-8452 - NetScaler SAML PrefixList patch-state check
https://gateway.example.com:9443
====================================================================
>> Identifying the appliance
[ OK ] NetScaler indicators: 5 (CSP contains citrixng://)
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 500 / 43549: no size check present
idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check
[FAIL] Size check absent (via the IDP route)
====================================================================
RESULT: VULNERABLE
====================================================================
https://gateway.example.com:9443 via IDP [size-check-absent]
The size check is absent. This appliance is unpatched for
CVE-2026-8452. Upgrade to 13.1-63.21+ / 14.1-73.32+ (12.1 and
13.0 are EOL and never fixed).
====================================================================
Контрольную строку стоит прочитать внимательно: 35-байтовый запрос проходит проверку размера и затем отклоняется из-за устаревшего IssueInstant, тогда как 575-байтовый зонд до этого не дошёл. Именно на этом порядке держится весь метод — канонизация выполняется до проверки времени, так же как и до проверки подписи.
Исправленное устройство, тот же запрос к сборке с исправлением. Отличается только строка зонда — чрезмерно большой PrefixList отклоняется по имени, а не проваливается во внутреннюю ошибку:```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check [ OK ] Size check present (via the IDP route)
RESULT: PATCHED
https://vpn.example.com via IDP [size-check-present]
**Откат к маршруту SP.** Здесь конечная точка IdP доступна, но ни одна политика IdP не привязана к этому виртуальному серверу, поэтому маршрут 1 воздерживается, а маршрут 2 отвечает. Когда *ни один* маршрут не соответствует политике, вердикт — `INCONCLUSIVE` с тегом `no-policy-match` — но никогда не `PATCHED`, и именно в этом заключается весь смысл существования такого вердикта:```console
>> Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control)
idp /saml/login HTTP 200 "Matching policy not found": parser not reached
sp /cgi/samlauth HTTP 500 / 43549: no size check present
sp /cgi/samlauth 35-byte control: HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
[FAIL] Size check absent (via the SP route)
RESULT: VULNERABLE
Контроль поймал это. Здесь конечная точка ответила пропатченным сообщением при обеих длинах, поэтому проверка размера так и не была задействована, и выглядящий решающим ответ снимается. Это срабатывание защиты от ложных срабатываний, и причина срабатывания указана прямо, а не оставлена для самостоятельного вывода:```console
Probing patch state (64 prefixes / 575 bytes, confirmed against a 35-byte control) idp /saml/login HTTP 200 "Malformed Assertion": size check rejected the probe idp /saml/login 35-byte control: HTTP 200 "Malformed Assertion": size check rejected the probe [WARN] Probe and control answered alike, so the size check was never exercised
RESULT: INCONCLUSIVE
https://sp-strict.example.com via IDP [flat-response]
The 575-byte probe and the 35-byte control got the same answer, so this endpoint replies the same way whatever it is sent and the size check was never exercised. Unknown, not patched.
**Сканирование флота** (`--brief`), одна выровненная строка на цель, заканчивающаяся тегом причины. Код возврата равен `1`, если любая цель `VULNERABLE`:```console
$ ./cve_2026_8452_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE https://gateway.example.com:9443 size-check-absent
VULNERABLE https://gateway.example.com:9444 size-check-absent
PATCHED https://vpn.example.com size-check-present
INCONCLUSIVE https://sp-strict.example.com flat-response
INCONCLUSIVE https://gw-nopolicy.example.com no-policy-match
UNAFFECTED https://mgmt.example.com no-saml-endpoint
ERROR https://offline.example.com not-identified
exit: 1
Машиночитаемый вывод (--json), в котором фиксируется каждый испробованный маршрут. verdict, reason и
detail — основные поля; attempts — это исходные данные, поэтому отдельная попытка может
указывать patched для цели, вердикт которой — INCONCLUSIVE:```console
$ ./cve_2026_8452_check.py https://vpn.example.com --json
[
{
"target": "https://vpn.example.com",
"verdict": "PATCHED",
"reason": "size-check-present",
"route": "idp",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe",
"attempts": [
{
"route": "idp",
"path": "/saml/login",
"state": "patched",
"detail": "HTTP 200 "Malformed Assertion": size check rejected the probe"
},
{
"route": "idp",
"path": "/saml/login",
"state": "control:known-error",
"detail": "35-byte control: HTTP 200 "message timestamp outside the appliance's skew tolerance": a different SAML condition, not the size check"
}
],
"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)"
]
}
]
## Вердикты
Каждый вердикт несёт короткий тег `reason`, называющий условие, стоящее за ним. `--brief` выводит тег третьим столбцом, а `--json` включает его как `reason`.
| Вердикт | Тег причины | Значение |
| --- | --- | --- |
| `VULNERABLE` | `size-check-absent` | Проверка размера отсутствует. Устройство не пропатчено — пропатчьте его. |
| `PATCHED` | `size-check-present` | Проверка размера сработала на пути кода, которого достиг зонд. **В рамках данного CVE:** это не означает, что устройство находится на актуальной сборке. |
| `UNAFFECTED` | `no-saml-endpoint` | Ни одна конечная точка SAML не ответила на этом виртуальном сервере, поэтому уязвимый путь здесь недоступен. **В рамках vserver, а не устройства:** SAML может быть настроен на другом vserver или VIP на той же машине. |
| `INCONCLUSIVE` | `flat-response` | Конечная точка ответила на 575-байтовый зонд и 35-байтовый контрольный запрос одинаково, поэтому проверка размера ни разу не сработала. Выглядящий решающим ответ отозван — это сработала защита от ложных срабатываний. |
| `INCONCLUSIVE` | `no-policy-match` | Конечная точка SAML ответила, но ни одна привязанная политика не совпала с зондом, поэтому ни один маршрут не достиг каноникализатора. |
| `INCONCLUSIVE` | `other-saml-error` | Распознанное, но недиагностическое условие SAML отклонило зонд до проверки размера — иной лимит длины, политика подписи, временная метка. |
| `INCONCLUSIVE` | `unrecognized-reply` | SAML-поверхность ответила чем-то за пределами распознанного набора. |
| `ERROR` | `not-identified` | Не идентифицировано как NetScaler или недоступно. |
Все четыре причины `INCONCLUSIVE` означают для принятия решения одно и то же — **неизвестно, не пропатчено.** Проверьте с помощью `show ns version`. Тег существует, чтобы сообщить оператору, *какое* условие исправить перед повторным запуском: направьте зонд на другой виртуальный сервер или привяжите подходящую политику.
`INCONCLUSIVE` существует как отдельный вердикт со своим кодом возврата, потому что уязвимое устройство может отказаться отвечать на зонд. Если политика SAML, привязанная к виртуальному серверу, не совпадает с запросом зонда, устройство завершает обработку до каноникализатора и не возвращает ничего диагностического. Сканер, который просто не находит совпадения, замолкает на таком хосте, а тишина читается как «пропатчено». Этот инструмент вместо этого сообщает о таком хосте как о неизвестном.
### Каждый вердикт подтверждается контрольным запросом
`PATCHED` и `VULNERABLE` оба опираются на *единственный* различающий ответ, поэтому инструмент проверяет, что ответ действительно зависит от того, что было отправлено. После решающего ответа он повторяет запрос с коротким `PrefixList` длиной 35 байт — ниже любой проверки размера — и вердикт сохраняется только в том случае, если два ответа различаются. Если они совпадают, конечная точка отвечает одинаково на всё, что получает, проверка размера ни разу не сработала, и результат — `INCONCLUSIVE`.
Это не гипотетическая ситуация. Поставщик услуг, настроенный с `samlRejectUnsignedAssertion STRICT`, отклоняет зонд за отсутствующую подпись *до* каноникализации и отвечает пропатченным сообщением при любой длине. Без контрольного запроса такое устройство сообщает `PATCHED` с кодом возврата 0 — это наблюдалось на действительно уязвимой сборке. Теперь оно сообщает `INCONCLUSIVE` с тегом причины `flat-response`, и в выводе прогона прямо сказано, что проверка размера ни разу не сработала. Тот же контрольный запрос ловит зеркальный случай, когда конечная точка возвращает общую внутреннюю ошибку на запросы, которые она не разбирала.
### Распознанные недиагностические ответы
Конечная точка SAML NetScaler имеет большой набор возможных ответов, и только два из них определяют состояние патча. Инструмент распознаёт 20 остальных и называет условие в строке маршрута, а не повторяет тело ответа, например:```text
idp /saml/login HTTP 200 "post body over the appliance's maximum": a different length limit rejected the probe first
sp /cgi/samlauth HTTP 200 "assertion rejected before the size check": a different SAML condition, not the size check
Три из них — ограничения длины — слишком большое тело POST-запроса, слишком большой RelayState, слишком
длинное извлечённое имя пользователя. Они важнее всего, потому что означают, что запрос был отклонён другим
ограничением длины до того, как дошёл до того, которое различает состояние патча. Обычно это означает, что пробу
нужно направить на другой виртуальный сервер, а не что устройство в порядке.
Один из 20 появляется при каждом запуске по маршруту IdP: у контрольного запроса message timestamp outside the appliance's skew tolerance. Проба несёт фиксированный IssueInstant, поэтому контроль, прошедший
проверку размера, затем отклоняется из-за возраста — это свойство пробы, а не устройства. Ответ вне
набора из 20 выводится отдельной первой строкой вместо именованного условия.
Все они по-прежнему дают INCONCLUSIVE, с тегом other-saml-error. Распознавание ответа никогда
не повышает его до PATCHED: только явный ответ о пропатченной сборке делает это, а любой нераспознанный
ответ также переходит в INCONCLUSIVE — как unrecognized-reply. Распознавание существует для того, чтобы сообщить
оператору почему цель не удалось классифицировать, а не для её классификации.
| Code | Meaning |
|---|---|
0 | Пропатчен или не затронут на целевом виртуальном сервере |
1 | Хотя бы одна цель является VULNERABLE |
2 | Ошибка использования (неверные аргументы / нечитаемый файл целей) |
3 | Хотя бы одна цель является INCONCLUSIVE, уязвимых нет |
4 | Хотя бы одна цель завершилась ошибкой, уязвимых или неопределённых нет |
2 — это собственный код выхода argparse для некорректного вызова, поэтому коды вердиктов пропускают его. Обёрточный скрипт
может отличить «это устройство не удалось классифицировать» (3) от «я неправильно вызвал инструмент» (2),
чего не смогла бы сделать схема, перегружающая 2.
При массовом сканировании нескольких целей код выбирается по приоритету, а не по наихудшему статусу:
VULNERABLE > INCONCLUSIVE > ERROR > чисто. Поэтому недостижимый хост никогда не маскирует
уязвимую находку в коде выхода.
PATCHED и код выхода 0 означают, что
проверка размера для CVE-2026-8452 присутствует на пути, которого достигла проба. Они ничего не говорят о любых других
уязвимостях NetScaler, включая те, что были раскрыты после этой ошибки и исправлены в более поздних сборках. Не
воспринимайте код выхода 0 от этого инструмента как подтверждение полной исправности устройства.UNAFFECTED означает «недоступно здесь», а не «это устройство безопасно»./saml/login действует на всё устройство, но на виртуальном сервере без
привязанной политики IdP он отвечает Matching policy not found и завершается до канонизатора.
IdP, чья политика использует правило выражения, которое не удовлетворяет запрос пробы,
попадёт в INCONCLUSIVE, а не даст ответ.INCONCLUSIVE — это не подтверждение полной исправности. Он намеренно отличается от PATCHED
отдельным кодом выхода, чтобы молчание никогда не принималось за успешный результат.INCONCLUSIVE. Когда политика SAML находится
в метке политики, достигаемой через nextFactor, а не привязана напрямую к виртуальному серверу,
непрошеная ассерция не находит подходящей политики и завершается до канонизатора.
Проверено на уязвимой сборке, которая сообщила INCONCLUSIVE. Поскольку SAML за первым фактором
контроля состояния устройства или схемой входа — распространённый паттерн, относитесь к INCONCLUSIVE
на шлюзе nFactor как к «вероятно достижимо, подтвердите с помощью show ns version», а не как к курьёзу.VULNERABLE подтверждает отсутствие проверки размера, что и является
состоянием патча. Он не измеряет, насколько далеко атакующий мог бы продвинуть повреждение в вашей сборке.Обновитесь до 13.1-63.21 или новее, либо до 14.1-73.32 или новее (FIPS и NDcPP: 14.1-73.32 FIPS, или 13.1-37.277 для 13.1-FIPS и 13.1-NDcPP).
Исправление самого CVE-2026-8452 впервые появилось в 13.1-63.18 / 14.1-72.61 согласно
CTX696604,
и именно этот переход обнаруживает данный инструмент. С тех пор эти сборки были заменены
CTX696939
(2026-08-19), который добавляет CVE-2026-19489 и CVE-2026-19490, причём последний — это CVSS 9.3
обход аутентификации до аутентификации (pre-authentication). Его предусловие на сборках, начиная с 14.1-43.56 / 13.1-61.28,
— это настроенное действие SAML — поэтому устройство, попадающее в область действия ошибки, которую проверяет этот инструмент, вероятно,
попадает и в область действия той ошибки, и вердикт PATCHED здесь не является причиной откладывать обновление. Оба
бюллетеня закрываются указанными выше сборками.
Устройства на 12.1 или 13.0 не имеют исправления и не получат его — эти ветви достигли конца жизненного цикла, и их следует считать постоянно уязвимыми и перенести на поддерживаемую ветвь.
Ещё два замечания:
Обновляйте оба узла пары HA. Необновлённый вторичный узел — это полностью открытое устройство в момент переключения на него.
Определяйте область охвата по конфигурации SAML, а не по типу виртуального сервера. Формулировка
в бюллетене вендора (Gateway или AAA virtual server) шире, чем условие срабатывания. Проверяйте текущую
конфигурацию на наличие add authentication samlAction и add authentication samlIdPProfile наряду с
add authentication vserver и add vpn vserver.
Это проверено, а не выведено. На подтверждённо уязвимом устройстве мы удалили все объекты SAML,
привязали вместо них не-SAML фактор аутентификации и оставили виртуальные серверы AAA работать:
конечные точки SAML после этого возвращали 404 на каждый запрос. Они не просто ограничены политиками без
конфигурации SAML — их не существует. Поэтому виртуальный сервер без SAML действительно находится вне области действия
этой ошибки, и UNAFFECTED на такой цели — это настоящий ответ, а не слепое пятно. Оговорка, которая
по-прежнему действует, — это ограничение области выше: оно применяется к каждому виртуальному серверу отдельно, поэтому подтверждайте каждый VIP, а не
делайте выводы об устройстве в целом.
CVE-2026-8452 вышел вместе с пятью «родственниками» в одном бюллетене. Тот, за которым стоит следить рядом с ним, — это CVE-2026-8451, предварительно аутентифицированное чтение памяти за пределами буфера (memory overread) в пути SAML IdP, которое активно эксплуатируется в реальных атаках. Эти две уязвимости имеют общую поверхность атаки, поэтому одна и та же проверка конфигурации покрывает обе.
Этот код распространяется под лицензией MIT.
Использование этого инструмента для атак на цели без предварительного взаимного согласия незаконно. Ответственность за соблюдение всех применимых местных, государственных и федеральных законов лежит на конечном пользователе. Разработчики не несут ответственности и не отвечают за любое неправомерное использование или ущерб, причинённый этой программой.