博客 / CVE 解读

CVE-2026-104935:Ivanti 信任的是请求头,而不是 URL

CVE-2026-104935 把一个路由请求头和一次上传竞态串联起来,在 Ivanti Connect Secure 上实现了代码执行。要拿到完整的 root 权限,还需要第三个至今未修复的漏洞。

已完成信息安全方向博士研究生学业已通过校内答辩 · 最终学位论文答辩尚待进行

一台已完全打过补丁的 VPN 设备,却依然沦陷为 root

第一步的全部内容就是这些:向 Ivanti Connect Secure 的公开登录页面 welcome.cgi 发送一个 POST 请求——这个页面原本完全不需要会话——同时附带一个额外的请求头 X-DSCGI-PATH,其值指向另一个通常需要身份认证的 CGI 文件路径。身份认证检查读取的是 URL,而这个请求头才真正决定了请求最终会被发往何处。[1]

Volexity 将此事件背后的攻击者追踪为 UTA0576;在其技术分析于 2026 年 10 月 13 日发布时,该组织早已利用这个请求头技巧,再串联第二个漏洞,在一台已经打上全部可用补丁的 22.8R2.4 设备上,以未经身份认证的方式获得了无特权用户 cgi-server 的代码执行权限。[1] 这两个漏洞被统一记录为 CVE-2026-104935:CVSS 评分 8.6,CWE-290(通过伪造实现的身份认证绕过),影响 25.1.1.2 之前的 Ivanti Connect Secure 25.1.0.0、25.1.3.0 之前的 25.1.2.0,以及截至 22.8R2.4 的所有 22.8R2 版本,并已在 2026 年 10 月 13 日的 25.1.1.2、25.1.3.0、25.1.3.1 和 22.8R2.5 版本中修复。[2] CISA 当天就将其加入了已知被利用漏洞目录,并给联邦机构设定了三天的修复期限,而这一期限早已过去。[2][3]

以 cgi-server 身份实现代码执行,还不是故事的结局。Volexity 还还原出了第三个阶段——至今既没有 CVE 编号,也没有补丁——能够从这一步一直走到 root 权限,整个过程既不留后门,也不留 Webshell。以上内容我都没有亲自操作过;以下分析均基于 Volexity 的技术报告,以及截至 2026 年 10 月 18 日查阅到的 CVE 和 KEV 记录。[1][2][3] 真正值得关注的,并不是一台 VPN 设备被攻破,而是这三个漏洞中有两个,恰恰是代码审计人员本应发现、而不是事件响应人员事后才发现的那类问题。

POST /dana-na/auth/url_default/welcome.cgi HTTP/1.1
Host: vpn.example.com
X-DSCGI-PATH: /home/webserver/htdocs/dana/fb/smb/wu.cgi
Content-Type: application/x-www-form-urlencoded

(welcome.cgi needs no session at all)

// Host and paths are illustrative, not turnkey.
// The URL says "public login page" -> the auth check reads the URL.
// The header says "run wu.cgi instead" -> routing reads the header.
// Nothing checks that the two agree.

登录页面检查的是 URL,而决定运行哪个脚本的却是一个请求头

像这样的每一个分发器,都需要对每个请求回答两个独立的问题:这个调用方是否被允许访问这里,以及实际由哪个文件来处理这个请求。ICS 通过检查 URL 路径(即判断这是否属于公开的、无需认证的端点之一)来回答第一个问题,又通过检查一个名为 X-DSCGI-PATH 的路由请求头(它指明了要调用的 CGI 脚本)来回答第二个问题。[1] 把这拆成两个问题,本身是合理的工程设计,这个设计思路本身并不算粗心。

它出问题的唯一原因,是没有任何机制去检查这两个答案是否一致。分发器想当然地认为:只要请求命中的是公开的登录脚本,那它按定义就是一个公开请求。而攻击者却能够控制一个请求头,悄无声息地覆盖了实际处理请求的脚本。把这个请求头指向 wu.cgi——一个受保护的上传处理器——一个外表看起来像是访问登录页面的请求,实际上就变成了对已认证区域的调用,并享有分发器赋予“已经通过认证关卡”的那份信任。

上传处理器信任的是时钟,而不是内容

被路由到 wu.cgi 只是第一步。第二步才把这种路由变成了代码执行,而这是一个时序漏洞,不是内容漏洞。wu.cgi 会把上传的文件写入 /home/runtime/tmp/<您所选文件名的 MD5 值>,而只有在文件落盘之后,该处理器才会检查这份内容是否真的是一个有效的 ZIP 归档文件;如果不是,就删除这个文件。[1] 只要把一个 Perl 脚本命名为以 .zip 结尾的文件名,在那短暂的窗口期内,一个随时可以被执行的脚本就会出现在您已经提前知道的路径上——因为文件名是您自己选的,而可预测的哈希值把它变成了那个确定的路径。

Volexity 描述了攻击者如何正好针对这个窗口发起并发请求的竞态:通过第一阶段的请求头技巧路由足够多的请求,统统指向那个临时路径,以求在删除操作赢得这场竞态之前,赶上执行那个 Perl 脚本。只要有一个请求恰好在窗口期内到达,就足够了。[1]

Volexity 恢复出的这份载荷并没有止步于 Perl。它进一步串联出了一个 Python 脚本,以 Base64 和 zlib 压缩的形式传递,并通过 exec() 在内存中执行,而不是写成独立的文件。[1] 整个过程的目标始终如一:尽可能少地在磁盘上留下日后能被人发现的痕迹。

// wu.cgi's order of operations, reconstructed from Volexity's writeup.
// This is the sequence, not a working exploit.

1. Attacker uploads "payload.zip" containing a Perl script, not a real ZIP.
2. wu.cgi writes it to /home/runtime/tmp/<MD5("payload.zip")> immediately.
3. wu.cgi then checks: is this actually a valid ZIP archive?
4. Check fails -> wu.cgi deletes the file.

// The gap between step 2 and step 4 is the whole bug.
// The attacker fires repeated requests (via the stage-one header trick)
// at the path from step 2, racing to execute it before step 4 wins.
// One request landing inside that window is enough.

两个阶段已修复,第三个没有,而它恰恰是最值得关注的那个

Ivanti 针对 CVE-2026-104935 发布于 2026 年 10 月 13 日的修复,堵住了第一和第二阶段:25.1.1.2、25.1.3.0、25.1.3.1 和 22.8R2.5,是这条特定利用链不再生效的版本。[2] 如果您在这三条版本线中的任何一条上运行的是更早的版本,这绝不是一个可以“安排到下个迭代再处理”的事项。CVE 记录自身的利用数据已经标注为“活跃”,技术影响为“完全”,而 CISA 给联邦机构设定的三天修复期限也早已过去。[2][3]

补丁说明到第三阶段就戛然而止了。一旦攻击者以无特权用户 cgi-server 的身份获得了代码执行能力,Volexity 发现 UTA0576 会重新指向 dsinvoked——ICS 自身具备 root 权限的维护运行程序——让它以 root 身份执行攻击者选定的代码;具体做法是重定向某个已有任务原本指向的目标,而这一切全部发生在一个已在运行的进程内存之中。[1] 我不会在此逐一讲解 Volexity 所披露的具体库或配置细节。这个机制本身已经公开,但针对它的补丁还没有;而在一个尚未封堵的提权漏洞上,重复提供利用级别的细节,不管别人是否已经公开过,都不是我会在本文中做的事。

不管补丁状态如何,有一点格外重要:这次提权会自我回滚。攻击者一旦确认已获得 root 权限(通过检查 /etc/shadow 是否可读来判断),就会恢复配置项以及内存中库的字节内容,使设备事后看起来毫无异样。Volexity 明确指出,Ivanti 自家的 Integrity Checker Tool 根本捕捉不到这一点,因为 ICT 检查的是磁盘上的内容,而这整个过程从未触碰磁盘。[1] 因此,今天一次干净的 ICT 运行结果,无法说明第三阶段是否已经攻击过某台设备——无论是发生在 10 月 13 日之前,还是现在仍可能发生,因为针对它的修复目前依然不存在。

如果是与 Ivanti 毫无关系的代码,我会怎样找到这类问题

第一和第二阶段并不是 ICS 独有的失误。它们是我在任何反向代理、API 网关或类 CGI 分发器中都会检查的两种模式,适用于任何技术栈。第一个问题是:决定“调用方是否被允许进入”的组件,和决定“由哪个处理器来运行”的组件,读取的是同一个字段吗?如果授权依据的是 URL,而路由依据的却是认证层从未查看过的某个请求头、Cookie 或查询参数,那么同样的漏洞在这里也会存在,只是字段名称不同而已。

第二个问题是:您的服务是否存在这样一种情况——在数据经过校验之前,就已经把它写入了一个可被引用的路径,哪怕只是短暂存在?上传处理器是这类问题的典型发生地,但它可能出现在任何“先持久化、后校验”的地方:一次缓存写入、一个临时文件,或者一条队列记录——工作进程可能在校验器运行之前就把它取走了。下面这两个问题,正是我第一次阅读像 wu.cgi 这样的处理器代码时会真正问自己的问题,而且它们并不需要 Ivanti 的源代码才能应用。

// Two questions I ask reading any dispatcher or upload handler,
// independent of what language or framework it's written in.

// 1. Auth-vs-routing mismatch
//    - What field decides "is this caller allowed in"? (often the URL)
//    - What field decides "which handler actually runs"? (sometimes a
//      header, a cookie, a query parameter)
//    - If those are two different fields, does anything enforce that
//      they agree?

// 2. Persist-before-validate
//    - Where does a handler write attacker-influenced content to a
//      referenceable path (disk, cache, queue) before fully validating it?
//    - Is there a window, however small, where something worse can
//      reach what was just written before cleanup or validation runs?

这是不是您的紧急事件?如果您在用 Connect Secure,答案是肯定的

坦率地说清楚这件事影响的是谁。如果您没有在使用 Ivanti Connect Secure,可以直接跳过,这与您无关。如果您在使用,这里不会有那种有时出现在低危发现被夸大成紧急事件时的“软着陆”。CVE-2026-104935 是未认证即可利用的,其自身的利用数据已经标注为“活跃”,而 Volexity 的取证还原发现,它曾攻陷过一台已经应用了所有可用补丁的设备。[1][2] 对那位客户来说,“我们已经打过补丁”这句话是真的,但并没有能救得了他们。

有三件事决定了您该有多担心。第一,是这台设备当下真正运行的版本,而不是您变更日志上记录的“应该已经升级到”的版本:25.1.1.1 及更早、25.1.2.2 及更早,以及 22.8R2.4 及更早,这些版本对第一和第二阶段仍然完全开放。[2] 第二,CVE-2026-104935 的补丁究竟是您风险评估的终点,还是起点。它只堵上了 UTA0576 当初闯入的那扇门,而不是 Volexity 发现他们进入后实际使用过的那扇。第三,一次干净的 Integrity Checker Tool 结果,是否正被当作“未遭入侵”的证明——而 Volexity 自己的报告已经明确指出,对这条利用链而言,情况并非如此。[1]

8.6 这个评分对第一、第二阶段是如实的,但它没有说明第三阶段的任何情况,因为第三阶段目前根本还没有评分;而一个把三个阶段串联起来使用的攻击者,并不会因为 CVSS 评分到此为止,就不再关心第三阶段。

在宣布事件了结之前,应当确认哪些事项

显然,第一步是打补丁。但事后打补丁回答的问题是“我们从现在起是否安全”,而不是“我们是否已经中招”;而这条利用链的利用窗口,并不要求您处于任何当时就能察觉到的“未打补丁”状态。[1]

Volexity 将攻击者的基础设施回溯到了 2026 年 9 月 30 日,并观测到实际利用活动始于 10 月 3 日。[1] 如果这台设备在 2026 年 9 月下旬以来的任何时间点,曾以存在漏洞的版本暴露在互联网上,请将其视为可能已被攻陷,在宣布事件了结之前先进行排查。下面这份清单,就是我会遵循的工作顺序。

Before you call this closed:
  [ ] Confirm the build actually RUNNING on the appliance, not the one
      your change log says. 25.1.1.2 / 25.1.3.0 / 25.1.3.1 / 22.8R2.5 or
      later closes CVE-2026-104935 (stages one and two only).
  [ ] Pull logs for requests to welcome.cgi where X-DSCGI-PATH does not
      match the URL path. That mismatch is this chain's fingerprint.
  [ ] Do not close the incident on a clean Integrity Checker Tool run
      alone. ICT checks on-disk state; stage three never touches disk.
  [ ] If the appliance was internet-facing on a vulnerable build any time
      from late September 2026 on, treat it as possibly compromised, not
      just unpatched.
  [ ] Track Ivanti's advisory for the still-missing privilege-escalation
      fix and apply it the day it ships. There is no CVE for it yet.

CVE 编号只是最容易的部分

CVE-2026-104935 这个编号本身很快就会淡出新闻周期,但它背后的那两类漏洞不会,因为它们从来就不是 Ivanti 独有的问题。一个授权检查和一个路由决策,悄悄信任着同一个请求中不同的字段;一条先持久化、后校验的写入路径——这两种模式,我在从未接触过任何 VPN 设备的 Web 和 API 代码库中也同样见过,而且往往就藏在那些没有人多看一眼的地方,因为显而易见的检查(这个端点是否需要认证、这种上传类型是否经过校验)早已通过。

这正是安全代码审计所要追踪的那道边界:不是看请求有没有到达某个函数,而是看这条路径上的每一道关卡,是否都在读取关于这个请求的同一套事实。如果您希望在您的认证与路由层变成别人笔下的零日分析报告之前,让人带着这个问题去审查它,这正是我所做的工作。告诉我您正在运行什么,我们一起确定范围。

参考来源

需要在您自己的产品中检查这个问题吗?

安全代码审计

想学会自己发现这类漏洞吗?

安全代码审计课程(一对一)
全部文章

08联系

即将上线处理用户数据的产品?

给我发一份简要需求。我会在三天内回复问题或方案大纲,价格在开始前以书面形式确定。