针对 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 长度,绝不扫描——运行是否安全?对此进行了说明。
| 标志 | 描述 |
|---|---|
TARGET | 一个或多个 [https://]HOST[:PORT] 目标;协议默认 https:// |
-f, --targets-file FILE | 从文件读取目标(每行一个;# 注释) |
--timeout SECS | 每请求超时(默认:15) |
--workers N | 并发目标数(默认:16);输出保持输入顺序 |
-b, --brief | 每个目标一行对齐输出——非常适合扫描大量主机 |
--json | 输出结构化 JSON,包括每个目标发送的每个请求 |
--no-color | 禁用彩色输出(也遵循 NO_COLOR 和非 TTY) |
存在漏洞的设备(默认两行输出)。在 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 请求,仅当设备返回未修补信号时才有第四个:
| # | 请求 | 用途 |
|---|---|---|
| 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 依赖于单个区分性响应,因此工具验证响应确实取决于发送的内容。在未修补信号之后,它用相同 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 携带。