欢迎回到这间屋子:CitrixNetScaler预认证RCE(疑似CVE-2026-8452)

admin 2026-08-18 05:17:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: watchTowrLabs披露CitrixNetScaler预认证RCE漏洞(疑似CVE-2026-8452)。漏洞位于SAML签名规范化过程,攻击者通过超大PrefixList触发固定缓冲区堆溢出,在splitPktInner中获得目标与来源均受控的memcpy原语,实现write-what-where并劫持nsppe控制流提权至root。建议企业尽快排查并升级受影响版本。 综合评分: 89 文章分类: 漏洞分析,二进制安全,渗透测试,WEB安全,代码审计


cover_image

欢迎回到这间屋子:Citrix NetScaler 预认证 RCE(疑似 CVE-2026-8452)

幻泉之洲

2026年8月17日 10:04 北京

在小说阅读器读本章

去阅读

watchTowr Labs 披露了一个影响 Citrix NetScaler ADC/Gateway 的预认证远程代码执行漏洞。漏洞藏在 SAML 签名的规范化过程中——攻击者发送超大 PrefixList,堆溢出后获得 write-what-where 原语,最终劫持 nxppe 的控制流。文章详细拆解了从崩溃到 Rip 控制、绕过 pitboss 监控、最后提权到 root 的完整利用链。

突然之间,你坐在一个房间里。环顾四周——你被新公司的其他新人包围着。没错,周一早上,入职培训。

流程你都熟——典型的企业腔调:”请不要做坏事,否则我们可能不得不解雇你。”不过你心里清楚接下来是什么。那是你每次入职最喜欢的环节。

来了!密码策要求。你正了正领带,因为你敢打赌,这家机构的 SSLVPN 凭据不会成为突破口。这次不会。绝对不会。你对自己发过誓,这次一定要用特殊符号。

等等——他们刚刚说的是 NetScaler?

世界在你周围凝固。你怎么又回到了这片地狱?你慌了一下——跟每天晚上折磨你的噩梦相比,特殊符号算什么?

你意识到一个事实:没人在乎你的密码有没有特殊符号。你早就已经输了。

欢迎回到 watchTowr Labs 的博客文章。

Citrix NetScaler 是什么,为什么他们的网关是第一个 C 项目?

Citrix NetScaler(正式改过名,又改回来了,只有企业网络供应商能玩出这种操作)是一系列应用交付控制器和 VPN 网关设备,几乎遍布全球每大型企业网络。NetScaler 处理负载均衡、SSL 卸载、认证和远程访问——而 NetScaler Gateway 则充当数千个组织远程访问基础设施的前门。

距离上一篇公开记录的 NetScaler RCE 分析(bishopfox.com/blog/analysis-exploitation-cve-2023-3519)已经过去三年了。

ChatGPT 告诉我们,今天我们要改变这个现状。

背景设定

为了支撑今天的分析,我们分析并利用了一台配置为使用 SAML 的 NetScaler 13.1 设备。

注意:我们刻意省略了利用开发过程中使用的具体 build 号。你不需要它。

基于我们的测试,今天讨论的漏洞在 NetScaler 设备配置为 SAML 服务提供方(SP)或身份提供方(IdP)时即可触达。Citrix 列出了以下受影响版本:

  • NetScaler ADC 和 NetScaler Gateway 14.1 在 14.1-72.61 之前
  • NetScaler ADC 和 NetScaler Gateway 13.1 在 13.1-63.18 之前

我们今天分析的是什么?

事情从这里开始变得有点混乱、恼人,或者神秘——你选哪个词取决于你对誓言的忠诚度。

虽然我们很想告诉你我们肯定在分析 CVE-2026-8452,但 Citrix 一贯的操作(在我们看来)让我们无法确认。不过我们相信这就是 CVE-2026-8452,因为它的描述是”内存溢出”漏洞。

Citrix 没有将各个 CVE 与公告中致谢的研究人员一一对应,但被致谢的研究人员之一包括来自 JPMorgan Chase XOR 团队的 Michael Tucker(其他人包括我们自己,我们可以排除自己的漏洞)。

为我们的毫无根据的理论加把火:这个漏洞足够有趣、足够复杂,完全有可能是 Mythos 辅助研究的产物——JPMorgan 非常公开地拥有这个模型的使用权。

我们有证据吗?

完全没有。

猜测有趣吗?

永远有趣。

我们有更好的事情要做吗?

反正,我们继续。

怎么开始的

按照我们通常的分析流程,尤其是面对塞进单次修复里的多个已修补漏洞时,我们不会从特定目标入手。相反,我们问:”什么变了?”

具体来说,nsppe 二进制——NetScaler 的包处理引擎——显示出了大量变化,还附赠了一堆被剥离的符号。

面对让 Diaphora 去 diff 39,637 个函数的前景,我们尖叫了一声——没人开心。

经过魔法、痛苦的劳动,以及一点点对”Secure by Designers”的不信任,我们最终有 174 个被修改的函数需要审查。

在这些变更里,我们发现了 SAML 认证功能中的一堆变化。其中有项变更特别有意思:一个未认证、可远程触达的内存破坏。

这个特定变化引起了我们的注意——63.18 补丁在这些拷贝周围添加了显式的大小检查,这恰好是我们最终定位漏洞的入口。

在签名规范化过程中,早期版本的 NetScaler 会把攻击者可控的数据——来自 SAML 消息里的 ds:SignedInfo 元素——复制到一个固定大小的全局缓冲区,并且没有检查它是否真的放得下。

发送一个超大的 SignedInfo 元素,拷贝就会越过缓冲区末尾,破坏相邻的包引擎状态,最终让 nsppe(根包引擎)崩溃。

例如:

[..SNIP..]   v73 = (int)v191 + 46LL;            if ( v73 >= 0x1001 )            {              if ( (*((_BYTE *)off_2E26480 + 12682) & 1) != 0 || (*((_BYTE *)off_2E26480 + 655736) & 8) == 0 )                goto LABEL_92;              sub_142E640((unsigned int)v195, 25, 603, 3, 0, 0, 0);              v48 = sub_208CB90(                      LODWORD(v195[0]) + 56,                      1156,                      (unsigned int)”SAML signature validation failed: SignedInfo size (%u bytes) is too large (inline ns)”,                      v73,                      v93,                      v94,                      v164);              v50 = v195[0];              *(_WORD *)(v195[0] + 12LL) = v48;              v51 = v48;              if ( v48

当有人向 NetScaler 发送一条经过签名的 SAML 消息时,NetScaler(毕竟是一台安全设备)需要核验签名是否真实。但它并不签名整条消息。相反,它只签名内部的一个小模块,叫做 SignedInfo。

把 SignedInfo 想象成一张小”收据”,上面写着:这是我要签名的东西,这是签名方法。实际的签名就是在收据上计算的。所以 SignedInfo 才是真正重要的部分;核验了它,就等于核验了签名。

一条签名消息看起来像这样:

     …    …

不过你首先得知道一件事:”规范化”(canonicalization)。

同一个 XML 可以写成许多略有不同的形式——多余的空格、属性顺序不同、换行符等等——但表达的仍旧是同一个意思。这对签名是个问题,因为签名是对准确字节的数学运算:改动一个空格,逻辑上数学就崩了,尽管没有重要内容变化。

所以在哈希 SignedInfo 之前,双方会先把它重写成一个标准的、整洁的形式。这个清理步骤就叫规范化(通常缩写为 c14n)。发送方和 NetScaler 执行相同的清理,因此他们最终哈希到同样的字节。

这个清理步骤就是 bug 所在。为了整理 SignedInfo,NetScaler 必须在一个内存缓冲区中构建清理后的文本,而那个缓冲区大小是固定的。

SignedInfo 里面有什么?

SignedInfo 始终有三个部分:

     …      …      …

  • CanonicalizationMethod 指定使用哪条清理规则。它有一个 Algorithm 属性(命名方法的 URL)。当使用某特定方法(”独占”清理)时,它可以包含一个子元素:PrefixList——就是发送方选择的一串单词。
  • SignatureMethod 说明使用的签名算法(例如 RSA with SHA-256)。只有一个 Algorithm 属性。
  • Reference 指向被签名的真实对象(在 SAML 中通常是 assertion)。它有一个 URI 属性和几个子元素:一组 Transforms(更多处理步骤,也可以包含 PrefixList)、一个 DigestMethod(用哪个哈希)和一个 DigestValue(哈希本身)。

请记住,规范化必须把文本复制到固定大小的缓冲区。所以如果你在问”SignedInfo 的哪部分攻击者能做得特别大?”,答案是……

所以我们……就……炸了……

我们一边乱试一边 fuzz 所有能找到的属性,发现它们大多要么有自己的检查,要么被限制在很小的常数值集合里。

但有个属性表现不大一样——PrefixList。

毫不意外(毕竟是 NetScaler),事情开始变得有趣。

逆向了一下,发现 PrefixList 几乎可以容纳任意字符串。唯一的限制是每个空格分隔的值必须唯一。

所以我们不再使用 AAAA AAAA AAAA AAAA,而是用 N0000 N0001 N0002 N0003 这样的唯一标记:

试着构造这样的东西:

[..SNIP..] prefix_list_overflow = ” “.join(f”N{i}” for i in range(2000)) # -> “N0 N1 N2 N3 … N1999″ inclusive_ns = f'” PrefixList=”{prefix_list_overflow }”/>’ [..SNIP..]

解码后:

Boom——又到了 NetScaler”计划外的预期功能”时间:

Program received signal SIGBUS, Bus error. 0x000000001c6dea0 in ??()

理解”计划外的预期功能”

由于缺少符号,一开始我们根本不知道崩在哪里。恢复了几个符号并加载进 GDB 会话之后,事情逐渐变得清晰起来。

再次触发崩溃给了我们:

Program received signal SIGBUS, Bus error. 0x000000001c6dea0 in ns_memcpy_avx ()

查看崩溃指令,$rax 寄存器即将被解引用,但因为它的值无效,我们得到的是 SIGBUS 错误:

(gdb) x/i $rip => 0x1c6dea0 :   vmovdqu YMMWORD PTR [rax], ymm0 (gdb) p/x $rax $1 = 0x4d4d4d4d4d4d43d3

说一下 SIGBUS,因为我们一开始被它搞懵了。在 FreeBSD 上,这类无效内存访问可能产生 SIGBUS 而不是你在 Linux 上期望的 SIGSEGV。

但重点在这里:那些 0x4d 字节是我们自己的。我们刻意在溢出部分填满 M 字符(0x4d)作为显著标记。在 memcpy 用到的指针里看到 0x4d4d4d4d4d4d4d4d,说明我们的数据已经侵入了分配器元数据。

你可能也注意到低字节是 0x43d3 而不是 0x4d4d。那不是损坏,而是某些算术操作的结局,我们稍后会讲。

从初始崩溃状态和 backtrace 看,崩溃并非发生在溢出点本身。相反,它发生在稍后,当 NetScaler 从 freelist 检索一个 chunk,而那个 chunk 里包含我们已经破坏的元数据:

(gdb) bt

0  ns_memcpy_avx ()

1  0x00000000015d128b in splitPktInner ()

2  0x00000000015d2a40 in splitPkt ()

3  …              in nstcp_send_nsb ()

4  …              in the aaad send path

因为我们在规范化期间砸坏了元数据,程序安然继续运行。直到后来同一块 chunk 从 freelist 被取回,NetScaler 才尝试使用被破坏的元数据并踉跄跌倒。

换句话讲,ns_memcpy_avx 里的崩溃离实际溢出位置远得很。盯着崩溃本身看,没多少收获。

于是开始了一场打地鼠游戏。我们在 backtrace 沿路的函数上设置断点,逐步往回排查。最终找到了 splitPktInner。反编译函数、设断点、再次触发溢出之后,我们发现了有用的东西。

我们控制了传入 rdi 的值:

Program received signal SIGBUS, Bus error. 0x00000000015d128b in splitPktInner () (gdb) p/x $rdi $1 = 0x4d4d4d4d4d4d43d3     # dest = poisoned nsb+0x50, mangled by an internal subtraction

splitPktInner 函数在 0x15D128B 左右调用了 memcpy。在调用前检查参数揭示了一些有趣的东西:

// a3 是我们通过溢出正在破坏其 header 的新分配 chunk // a1 是指向保存规范化数据的 chunk 的指针 memcpy(*(a3 + 0x50),           // DEST  = poisoned data pointer *(a1 + 0x50),           // SRC   = our canon *(a1 + 0xE0) – *(a1 + 0x50));  // LEN = packet length

那 0x43d3 从哪来的?

我们之前说 0x43d3 是数学而不是破坏,现在就来算这笔数学。我们在 a3+0x50 种下了一串 M 字节,但 splitPktInner 并不直接使用原始值。它在交给 memcpy 之前会先按包长度调整它:

dest = *(a3+0x50) – pktlen

我们的包是 0x97A(2426)字节,所以:

0x4D4D4D4D4D4D4D4D      our planted M tag at a3+0x50 (“MMMMMMMM”)

–         0x0000097A      minus the packet length

 0x4D4D4D4D4D4D43D3      the dest ns_memcpy_avx faulted on

所以目标不仅仅是”接近我们数据的某个地方”。它是一个我们完全控制的值,减去一个我们也控制的长度。如果我们想让拷贝落在某个地址 X 上,就调整溢出长度,让减法正好把它放到 X 上。在玩任何花样之前,我们就已经拥有了一个目标地址完全受控的 memcpy。

好,但来源是什么?

目标是好,但 memcpy 需要复制点什么。有一阵子我们以为来源是一些我们无法控制的内部帧头。我们错了,这浪费了不少时间。来源是 *(a1+0x50),而 a1 正是保存我们规范化 PrefixList 的 chunk。所以来源也是我们的。

我们在 splitPktInner 断点处检查了 a1+0x50,在拷贝执行之前确认了这一点:


参考资料

[1] https://labs.watchtowr.com/youre-back-in-the-room-citrix-netscaler-pre-auth-rce-cve-2026-8452/


免责声明:

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

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

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

本文转载自:幻泉之洲 《欢迎回到这间屋子:Citrix NetScaler 预认证 RCE(疑似 CVE-2026-8452)》

某讯-安全技术实习生面经 网络安全文章

某讯-安全技术实习生面经

文章总结: 文档标题为某讯安全技术实习生面经,但正文内容缺失,仅包含标题和作者信息,无具体面经细节。 综合评分: 0 文章分类: 安全意识某讯-安全技术实习生面
评论:0   参与:  0