针对 CVE-2026-19490 的安全、未认证漏洞检查,该漏洞是 Citrix NetScaler ADC / NetScaler Gateway SAML 服务提供者路径中的预认证认证绕过(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)。由 JPMorgan Chase 渗透测试团队的 Samarth Vashisht 报告。
该设备对 SAML 响应的 RelayState 参数进行 base64 解码,当明文以 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 虚拟服务器,而不是管理接口。暴露是按虚拟服务器计算的,因此具有多个 VIP 的设备需要逐一测试。探测带有硬性安全边界——一个经过验证的 RelayState 长度,绝不扫描——运行是否安全?对此进行了说明。
存在漏洞的设备(默认两行输出)。在 TTY 上 [!] 标记和 VULNERABLE 显示为红色:
$ ./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 虚拟服务器——两者都有响应,这是配置前置条件检查无法做到的:
$ ./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 秒。20 字节落在干净的内部错误分支上,不会创建会话,并且载荷构建器拒绝构造任何其他长度,因此其他地方的编辑无法悄悄扩大探测范围。13.1-63.18 连续运行了 10 次,零核心转储生成,无数据包引擎重启。此后该检查已针对两个维护分支、修复的两侧,以及服务提供者和仅 IdP 虚拟服务器运行,均无数据包引擎重启。SAMLResponse 参数必须存在才能到达 RelayState 分支,但其内容无关紧要,因此它是四个字节的垃圾数据。该分支不发放 cookie,也不写入配置。VULNERABLE 绝不会仅凭单个响应报告——参见对照。如果你修改探测,不要更改解码后的
RelayState长度,也不要扫描长度。 20 字节是关键,更短并不更安全。其他长度最好情况是未测试,最坏情况是设备中断。
每个目标三个 HTTP 请求,仅当设备返回未修补信号时才有第四个:
不跟随重定向,也从不回传 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 依赖于单个区分性响应,因此工具验证响应确实取决于发送的内容。在未修补信号之后,它用相同 20 字节解码长度的对照 RelayState 重复请求,仅在 ctx= 之后的 16 字节上不同。该内容会被反序列化器之前的守卫拒绝,因此所有测试的版本——已修补和未修补——都回答 200 Malformed Assertion。只有当两个答案不同时判定才成立。如果它们匹配,则某些东西无论输入如何都返回 500/43524,结果是标记为 flat-response 的 INCONCLUSIVE,而不是错误的 VULNERABLE。将对照保持在同一解码长度使其处于验证过的安全边界内,并将内容隔离为唯一变量。
该守卫是单向的。 它无法验证 PATCHED 判定,因为真正已修补的设备对探测和对照的回答完全相同——这就是此处已修补的含义。参见局限性。
RelayState 反序列化在 SAML 策略匹配之前运行,因此仅携带 samlIdPPolicy 的虚拟服务器也会回答探测,而不仅仅是绑定了服务提供者 samlAction 的虚拟服务器。这比配置前置条件检查能提供的覆盖范围更广。
存在漏洞的答案是唯一自我印证的:它带有特定标记,并且对照证明回复取决于发送的内容。PATCHED 也是正面证据——设备自身在探测所到达路径上的固定错误——但它仅限于此 CVE 和该路径。INCONCLUSIVE 和 ERROR 根本不是证据:探测从未到达反序列化器,因此补丁状态未知,这就是为什么 INCONCLUSIVE 是单独的判定而不是并入 PATCHED。将沉默报告为通过会在危险的方向上出错。在需要确定性的地方,请对照修复中的版本用 show ns version 确认。
每个判定都带有一个简短的 reason 标签。--brief 将其打印为第三列,--json 将其作为 reason 携带。
所有四个 INCONCLUSIVE 原因对决策而言含义相同——未知,未修补。 标签指明了重新运行前需要修复的条件。
| 代码 | 含义 |
|---|---|
0 | 没有目标为 VULNERABLE |
1 | 至少一个目标为 VULNERABLE |
2 | 用法错误(参数错误 / 目标文件不可读) |
退出 0 不是健康证明。 PATCHED、UNAFFECTED 和所有四个 INCONCLUSIVE 原因都归入其中,而其中只有一个是正面结果。需要区分“已修补”和“无法分类”的包装器必须读取判定——--brief 的第一列,或 --json 的 verdict 字段——而不是退出代码。
PATCHED 判定失真。 Malformed Assertion 是设备在探测未能到达上下文反序列化器时返回的内容,而已修补只是发生这种情况的原因之一。因此,剥离、篡改或短路 RelayState 参数的中间设备会在存在漏洞的设备上产生 PATCHED,而工具能发送的任何额外请求都无法区分两者。在可能有东西位于设备前方的地方,请用 show ns version 确认。UNAFFECTED 仅限于当前版本。 在 14.1-43.55 / 13.1-61.27 及更早版本上,公告指出暴露根本不需要 SAML 操作——任何 Gateway 或 AAA 虚拟服务器都受影响——因此在此类版本上 404 并不能使设备脱离范围。该分支取自公告而非测量。PATCHED 对任何其他 NetScaler 漏洞一无所知,包括同一公告中的 CVE-2026-19489。VULNERABLE 确定修复在探测所到达的路径上缺失。它不衡量攻击者在你的配置上能将绕过推进多远,也不会告诉你是否已经有人利用了——请单独在 /var/log/ns.log 中搜寻。升级到 13.1-63.21 或更高版本,或 14.1-73.32 或更高版本(FIPS 和 NDcPP:13.1-FIPS 和 13.1-NDcPP 为 13.1-37.277,14.1-FIPS 为 14.1-73.32 FIPS),依据 CTX696939。12.1 或 13.0 上的设备没有修复,也不会获得修复——这些分支已终止支持,应迁移到受支持的分支。
另外三点说明:
vpn sessionAction 的 defaultAuthorizationAction——它会回退到全局 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 操作为前提,因此一次配置审计就能界定两者。
此代码在 MIT 许可证下分发。
未经事先相互同意,使用此工具攻击目标是非法的。最终用户有责任遵守所有适用的地方、州和联邦法律。开发者不承担任何责任,也不对由此程序造成的任何误用或损害负责。
| 标志 | 描述 |
|---|
TARGET | 一个或多个 [https://]HOST[:PORT] 目标;协议默认 https:// |
-f, --targets-file FILE | 从文件读取目标(每行一个;# 注释) |
--timeout SECS | 每请求超时(默认:15) |
--workers N | 并发目标数(默认:16);输出保持输入顺序 |
-b, --brief | 每个目标一行对齐输出——非常适合扫描大量主机 |
--json | 输出结构化 JSON,包括每个目标发送的每个请求 |
--no-color | 禁用彩色输出(也遵循 NO_COLOR 和非 TTY) |
| # | 请求 | 用途 |
|---|
| 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 | 对照,仅在未修补信号后发送 |
| 判定 | 原因标签 | 含义 |
|---|
VULNERABLE | internal-error-43524 | 设备将解码后的 RelayState 长度作为其内部错误传播,并且同长度对照证明回复取决于发送的内容。CTX696939 修复缺失——请修补。 |
PATCHED | fixed-error-returned | 设备在探测所到达的路径上返回了固定错误。仅限于此 CVE,关于中间设备参见局限性。 |
UNAFFECTED | no-saml-endpoint | /cgi/samlauth 返回 404,因此此处不提供 SAML 断言消费者。按 vserver 而非按设备,参见局限性中的旧版本注意事项。 |
INCONCLUSIVE | flat-response | 探测和同长度对照回答相同,因此回复不取决于发送的内容。误报防护触发。 |
INCONCLUSIVE | generic-internal-error | 端点返回 43549,其通用内部错误,已修补和未修补版本都会返回。不是区分标志——并且注意它是无关的 CVE-2026-8452 的未修补标记,所以它看起来像命中但并非命中。 |
INCONCLUSIVE | unrecognized-reply | 端点回答了识别集合之外的内容。 |
INCONCLUSIVE | no-probe-response | 主机回答了识别但未回答探测——超时、重置或中间盒丢弃了 POST。请重试。 |
ERROR | not-identified | 未被识别为 NetScaler,或不可达。 |