针对 CVE-2026-8452 的安全、非破坏性补丁状态检查——这是存在于 Citrix NetScaler ADC / NetScaler Gateway SAML 签名规范化器中的预认证堆溢出漏洞(CTX696604,CVSS 8.8)。在规范化过程中,一个过大的独占规范化 PrefixList 会溢出固定大小的缓冲区,而 NetScaler 在验证携带该列表的签名之前就执行这一操作——因此,无需任何凭据、会话或有效签名即可触达整个路径。由 JPMorgan Chase XOR 团队的 Michael Tucker 报告;根因与利用分析归功于 watchTowr Labs。
该脚本不会利用此漏洞,也不会破坏内存。它针对每个目标回答一个问题:此设备上是否存在修复?——该判断基于行为观察,通过观察补丁而非猜测构建版本来确定。
安全。它专为生产环境和评估用途而设计:
PrefixList 并拒绝 513 字节及以上。该限制精确到字节,在两个受支持分支上完全相同,且不随设备配置或周围 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 角色:
IdP 路由首先被尝试,因为它是两者中更稳健的一个。它对 Issuer 值、AssertionConsumerServiceURL 以及时钟偏移不敏感——即使 IssueInstant 远远超出设备的偏移容限,仍能正确判别,因为规范化先于时间检查和签名检查执行。
路由 1 的
AuthnRequest必须签名。 未签名的请求在已修补和未修补构建上都会返回200 Malformed Assertion sent to Netscaler,这与已修补信号逐字节相同,因此省略签名块的探测会把每台设备都报告为已修补。签名不需要有效,本工具的签名也确实是无效的;它只需存在,因为正是其SignedInfo将PrefixList带入规范化器。
两个受支持分支都在其修复构建处恰好改变行为,且两条路由均如此:
13.1-63.16 和 63.18 是连续版本,因此该变化可归因于补丁本身,而非中间构建之间的漂移。
这些是这个修复首次出现的构建,而该探测正是检测这一转变。它们已不再是应升级到的构建:后续公告已取代它们,因此 13.1-63.18 和 14.1-72.61 在此都会应答 PATCHED,但仍暴露于较新的问题之下。当前修复构建请参阅 修复措施。
因为在原理上它就无法针对此漏洞奏效。13.1-63.16 和 13.1-63.18 是紧邻修复两侧的构建,它们提供逐字节相同的 tmindex.html、base.css 以及 resources.js——该修复不触碰任何 Web 资源。静态资源哈希在各分支之间也存在碰撞,因此基于哈希的方法可能将易受攻击的设备解析为已修补的构建,并报告其为干净设备——这是检测工具最糟糕的失败模式。因此,构建指纹识别被刻意不实现。补丁状态来自探测,或者在你有凭据时来自 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 的设备需要逐一测试。
### 选项
| Flag | 描述 |
| --- | --- |
| `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 是原始证据,因此单个尝试可能
在判定为 INCONCLUSIVE 的目标上显示为 patched:```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 判定,而非按设备判定:** 同一台机器上的另一个 vserver 或 VIP 上可能配置了 SAML。 |
| `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` 都基于*单个*区分性应答,因此该工具会验证应答实际上取决于发送的内容。在得到决定性应答后,它会用一个 35 字节的短 `PrefixList` 重复该请求 — 低于任何大小检查 — 仅当两次应答不同时判定才成立。如果两次应答相同,则说明端点无论收到什么都会以同样方式应答,大小检查从未被触发,结果为 `INCONCLUSIVE`。
这并非假设。配置了 `samlRejectUnsignedAssertion STRICT` 的服务提供商会在*规范化之前*因缺少签名而拒绝探针,并在所有长度下都返回已修补的消息。没有对照时,此类设备会以退出码 0 报告 `PATCHED` — 这一现象曾在真正易受攻击的构建上被观察到。现在它会报告带有原因标签 `flat-response` 的 `INCONCLUSIVE`,运行输出会明确说明大小检查从未被触发。同样的对照还能捕获镜像情况:端点对从未解析的请求返回通用内部错误。
### 可识别的非诊断性应答
NetScaler SAML 端点拥有一大组可能的应答,其中只有两种能够确定补丁状态。该工具可识别其余 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 路由运行中出现:控制项的消息时间戳超出设备的偏移容差。探测携带固定的 IssueInstant,因此通过大小检查的控制项随后会因其时间戳过旧而被拒绝——这是探测本身的属性,而非设备的属性。不属于这 20 种集合的回复会作为独立的第一行打印,而不是作为命名条件。
所有这些响应仍然产生 INCONCLUSIVE,并带有 other-saml-error 标记。识别出某个响应绝不会将其升级为 PATCHED:只有显式的已修补构建应答才能做到这一点,而且每个无法识别的回复同样会落入 INCONCLUSIVE——标记为 unrecognized-reply。这种识别的作用是告诉操作员目标为何无法被分类,而不是对其进行分类。
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 位于设备状态或登录架构第一因素之后是常见模式,请将 nFactor 网关上的 视为“可能可达,请使用 确认”,而不是一种异常情况。升级到 13.1-63.21 或更高版本,或 14.1-73.32 或更高版本(FIPS 和 NDcPP:14.1-73.32 FIPS,或适用于 13.1-FIPS 和 13.1-NDcPP 的 13.1-37.277)。
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
的预认证身份验证绕过。其在 14.1-43.56 / 13.1-61.28 及更高版本构建上的前提条件是配置了 SAML action——
因此,本工具所检查漏洞范围内的设备很可能也在该漏洞的范围内,而且这里的 PATCHED 判定并不是推迟升级的理由。上述构建已清除两份公告中的问题。
12.1 或 13.0 上的设备没有修复方案,也不会获得修复——这些分支已停止维护,应被视为永久易受攻击,并迁移到受支持的版本分支。
还有两点补充说明:
修补 HA 对的两个节点。 未修补的备用节点一旦接管,就是一个完全暴露的设备。
按 SAML 配置而不是按虚拟服务器类型来界定资产范围。 厂商公告中的措辞(Gateway 或 AAA 虚拟服务器)比触发条件更为宽泛。请在运行配置中检查 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,这是 SAML IdP 路径中的一个预认证内存越界读取(overread)漏洞,已在野外遭到积极利用。这两个漏洞共享相同的攻击面,因此相同的配置审计即可覆盖两者。
本代码根据 MIT 许可证 分发。
未经事先双方同意,使用此工具攻击目标属于违法行为。遵守所有适用的地方、州和联邦法律是最终用户的责任。开发者不承担任何责任,也不对本程序造成的任何误用或损害负责。
| 路由 | 请求 | 要求 |
|---|
| 1(首选) | POST /saml/login — 签名后的 AuthnRequest,PrefixList 位于 ds:SignedInfo 中 | 绑定到目标 vserver 的 SAML IdP 策略 |
| 2(后备) | POST /cgi/samlauth — SAMLResponse,PrefixList 位于断言签名中 | 目标 vserver 上的 SAML SP 断言消费者服务 |
| 构建 | 判定 |
|---|
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 |
| Code | Meaning |
|---|
0 | 已修补,或在目标虚拟服务器上不受影响 |
1 | 至少一个目标为 VULNERABLE |
2 | 用法错误(参数错误 / 目标文件不可读) |
3 | 至少一个目标为 INCONCLUSIVE,且没有易受攻击的目标 |
4 | 至少一个目标出错,且没有易受攻击或无法定论的目标 |
INCONCLUSIVEshow ns versionVULNERABLE 判定确认了缺失的大小检查,即补丁状态。它不会衡量攻击者能在你的构建上将内存破坏推进到什么程度。