CVE-2026-88771 复现
0x01 漏洞信息
- CVE 编号:CVE-2026-88771
- 漏洞类型:命令注入。由于输入校验不当,允许未认证攻击者执行任意命令,默认配置即可触发
- 漏洞位点:
ns_monuploadd_err.pl,一个 Perl 运维脚本,而非nsppe二进制
0x02 触发方式
向 /nf/auth/doAuthentication.do 发一个未认证的 POST 登录请求,login 字段携带伪造的 pitboss 崩溃记录(URL 全编码):
1 | POST /nf/auth/doAuthentication.do HTTP/1.1 |
<@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 | my $WR_PPE_COREFILE_NAME = `grep -E -i "pitboss.*PPE.*missed too many heartbeats|pitboss.*PPE.*unexpectedly died" @NR_FILES | tail -1 | |
提取链路:
- 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 | pitboss: NSPPE-00 (12345) unexpectedly died |
构造 payload
如果这个任意字符串是 core 崩溃后日志的格式呢?导致以下日志被写入:
1 | <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" |
伪造的日志经过同一条管道处理(三行均能被 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 | 审计脚本层(形状扫描:grep 反引号 / 两段式插值) |
0x05 类似漏洞的挖掘思路
辅助脚本层审计:
特权消费者(root 脚本)+ 共享存储(日志/文件)+ 未认证生产者(表单/UA)= 二阶注入流