前言
Java反序列化漏洞发展到今天,单纯的原生无过滤反序列化漏洞已经极少。各类中间件、框架都引入类白名单机制,把危险类拦截在反序列化入口。JEP-200就是Jenkins为了封堵Remoting通道反序列化攻击引入的防护,多年来被视作Jenkins控制器最重要的安全边界之一。
CVE-2026-70426(SECURITY-3911)的特殊之处,不是JEP-200白名单本身失效,而是代码异常分支里的漏校验。正常流程会走类过滤器校验,一旦类加载抛出异常,程序进入catch块直接使用父类resolveClass,跳过全部安全校验。攻击者只要拿到Agent/Connect权限或者控制Agent进程,就可以构造序列化载荷,让控制器加载核心类路径内的恶意类,在Jenkins主节点完成RCE。
很多运维、安全人员会误判这个漏洞,以为是公网直接打Jenkins页面的一键漏洞。真实攻击链路藏在CI/CD架构的控制器与Agent通信通道,属于供应链攻击的典型入口。构建任务在Agent执行,依赖包投毒、Agent账号被接管,都能变成攻击控制器的跳板。控制器一旦失陷,所有凭证仓库、代码仓库密钥、云账号、生产环境部署密钥全部暴露。
本文从第一性原理拆解漏洞,做对抗式审查,从底层代码逻辑、攻击链路、环境复现、检测脚本、临时缓解、长期加固完整落地。附带Mermaid流程图与架构图,可直接插入文章配图。所有检测脚本、验证代码可复制使用。
1 漏洞基础信息
CVE编号:CVE-2026-70426
Jenkins内部安全编号:SECURITY-3911
漏洞类型:不安全反序列化,绕过JEP-200类过滤器,RCE
CVSS:9.0 严重
漏洞组件:Jenkins Remoting通信库
1.1 受影响版本
Remoting库:≤3384.v60d89463d9e0,版本3355.3357.v931d3c992987除外
Jenkins每周版:≤2.575
Jenkins LTS长期支持版:≤2.568.1
修复版本:
Jenkins 2.576
Jenkins LTS 2.568.2
Remoting ≥3386
版本例外说明:3355.3357.v931d3c992987版本已经修复该回退分支问题,不受漏洞影响,升级排查时不要误判。
1.2 攻击前置条件(对抗式审查重点)
这个漏洞不能无授权直接远程攻击,必须满足下面任意一条:
- 攻击者控制Jenkins Agent代理进程,Agent上可以执行自定义代码;
- 攻击者账号拥有Jenkins的
Agent/Connect权限,可以新建Agent节点,建立Agent到控制器的Remoting通信通道。
没有上述权限,载荷无法投递到Remoting通信通道,漏洞无法触发。很多厂商的漏洞通告简化描述,容易让人误解成公网未授权RCE,这是红队与蓝队排查时最容易踩的认知陷阱。
漏洞载荷的限制:只能使用Jenkins核心classpath内的类,插件自带依赖类不能被反序列化。攻击者只能利用JDK原生类、Jenkins core内置类构造gadget链,不能直接加载第三方插件的类。这个边界决定gadget选型范围,也限制攻击载荷的构造思路。
2 Jenkins Remoting与JEP-200底层原理
2.1 Remoting通信架构
Jenkins控制器(Controller)和Agent节点依靠Remoting库完成跨节点通信。Agent执行构建任务,把执行状态、返回对象序列化后通过TCP通道传给控制器;控制器下发任务对象到Agent。整个通道大量使用Java原生序列化传输对象。
flowchart LR A[Jenkins Controller] <-->|Remoting TCP通道 Java序列化对象| B[Jenkins Agent] A --> C[JEP-200类过滤器<br/>反序列化前校验类白名单] C --> D[正常类加载流程] D --> E[业务逻辑执行]控制器在接收Agent发来的序列化对象时,默认启用JEP-200类过滤器。过滤器维护白名单,只有可信类才允许被反序列化,阻断经典Java反序列化gadget。
JEP-200在2018年正式落地,把Jenkins Remoting从黑名单防护切换为白名单防护。在这之前,Jenkins多次出现Remoting通道反序列化RCE。JEP-200上线后,安全业界普遍认为Agent到控制器的序列化攻击面基本关闭。
2.2 正常resolveClass流程
Remoting自定义ObjectInputStreamEx继承原生ObjectInputStream,重写resolveClass方法。
- 读取序列化流中的类名称;
- 在try代码块内尝试使用自定义类加载器加载目标类;
- 加载成功,调用
filter.check(),JEP-200过滤器校验类是否在白名单; - 校验通过,返回Class对象,完成反序列化;
- 校验失败,抛出异常,终止反序列化。
2.3 漏洞:异常catch分支的回退路径无过滤
漏洞根源在两处代码:ObjectInputStreamEx.resolveClass和MultiClassLoaderSerializer$Input.resolveClass。
try块内加载类发生ClassNotFoundException,代码捕获异常,直接调用super.resolveClass。super.resolveClass是父类ObjectInputStream原生方法,完全没有执行JEP-200 filter校验。
flowchart TD S[进入resolveClass] T[try 自定义类加载器加载类] F{加载成功?} OK[执行JEP-200 filter.check<br/>白名单校验] ENDOK[返回Class,继续反序列化] FAIL[捕获ClassNotFoundException] BYPASS[调用super.resolveClass<br/>无JEP-200校验] ENDBYPASS[返回Class,继续反序列化] S --> T T --> F F --成功--> OK --> ENDOK F --失败--> FAIL --> BYPASS --> ENDBYPASS攻击者的目标就是主动制造ClassNotFoundException,迫使代码进入这个未校验的回退分支。
实现思路:构造序列化数据,让第一次类加载失败。例如传入空类加载器、伪造类加载标记,触发异常,走fallback路径。此时控制器使用控制器自身类加载器加载目标类,不再经过JEP-200白名单。
第一性原理视角:安全校验只写在主逻辑,异常分支没有做同等校验。安全编码里最常见的缺陷,防护逻辑没有覆盖全部代码路径。对抗审查的核心思路:不要只看正常业务路径,遍历所有异常捕获分支。
3 攻击完整链路拆解
- 攻击者拿到Agent执行权限或者Agent/Connect权限,创建受控Agent节点,建立Agent到Controller的Remoting TCP通信通道;
- 构造恶意序列化对象,精心设置类加载参数,让第一次自定义类加载抛出ClassNotFoundException;
- 序列化数据通过Remoting通道发送给Jenkins控制器;
- 控制器调用
ObjectInputStreamEx.resolveClass,try加载失败,进入catch; - 执行super.resolveClass,跳过JEP-200校验,加载Jenkins core classpath内的gadget类;
- 反序列化执行
readObject方法,触发gadget链,在控制器主机执行系统命令; - 执行结果沿Remoting通道回传给攻击者。
攻击链路不是Web页面注入,是跨节点通信通道的序列化攻击。这个攻击面在很多企业被低估。很多企业防火墙严格限制公网访问Jenkins 8080端口,但内网Agent节点与控制器50000端口互通。一旦Agent被入侵,就可以横向移动拿下控制器。
供应链场景下,开发人员提交的构建脚本、npm/maven依赖投毒,都能在Agent执行阶段触发载荷,发起对控制器的攻击。攻击者不需要拿到Jenkins账号,只需要污染构建依赖,在构建执行阶段触发攻击。
4 源码缺陷定位与补丁分析
4.1 漏洞原始代码片段
@Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { try { Class<?> c = classLoader.loadClass(desc.getName()); filter.check(desc, c); return c; } catch (ClassNotFoundException e) { return super.resolveClass(desc); } }try块内加载成功,执行filter.check做安全校验。捕获ClassNotFoundException后,直接返回父类resolveClass结果,没有filter校验。MultiClassLoaderSerializer$Input.resolveClass存在完全相同的缺陷。
4.2 官方补丁改动
补丁在catch分支同样增加filter校验逻辑。不管是try内加载成功,还是fallback到super.resolveClass拿到Class对象,都必须执行filter.check()。
@Override protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { Class<?> c; try { c = classLoader.loadClass(desc.getName()); } catch (ClassNotFoundException e) { c = super.resolveClass(desc); } filter.check(desc, c); return c; }把filter校验移出try块,放到所有分支的公共出口。无论走哪条路径,类对象返回前都必须经过JEP-200过滤器。这个改动消除了回退分支的安全缺口。
同时Jenkins新增回归测试用例,专门模拟ClassNotFoundException场景,覆盖fallback路径,防止后续版本迭代再次引入同类漏洞。
5 本地复现环境搭建
警告:仅授权内网测试环境复现,禁止对公网未授权Jenkins实例进行测试,遵守网络安全法规。
5.1 环境清单
- Jenkins版本:2.575(受影响版本)
- Remoting版本:3384.v60d89463d9e0
- JDK版本:JDK11(Jenkins推荐运行环境)
- Agent:JNLP Agent,和控制器建立Remoting通道
- 操作系统:Linux Ubuntu 22.04
5.2 部署步骤
- 下载jenkins.war 2.575版本,启动控制器
java -jar jenkins.war --httpPort=8080- 初始化Jenkins,新建Agent节点,获取JNLP连接密钥;
- 启动Agent,连接控制器50000 JNLP端口;
- 在Agent端部署测试代码,构造序列化载荷,通过Remoting Channel发送至控制器。
5.3 复现关键点
- 必须保证Agent和控制器Remoting通道正常连通;
- 载荷必须触发ClassNotFoundException,进入fallback分支;
- gadget类必须存在于Jenkins core classpath,插件类无法使用;
- 反序列化成功后,命令执行在控制器进程,不是Agent。
6 检测脚本:资产扫描与漏洞检测
下面两个脚本,第一个Python脚本批量扫描内网Jenkins资产,获取版本,判断是否在受影响范围;第二个Java简易检测代码,用于验证Remoting通道通信。
6.1 Python批量资产检测脚本(可直接复制)
#!/usr/bin/env python3 # CVE-2026-70426 Jenkins 版本检测脚本 # 仅检测版本,不做漏洞利用,蓝队资产巡检使用 import requests import argparse import sys from concurrent.futures import ThreadPoolExecutor headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } def check_jenkins(target): try: url = f"{target}/api/json" resp = requests.get(url, headers=headers, timeout=5, verify=False) if resp.status_code == 200: data = resp.json() ver = data.get("version", "") if not ver: print(f"[INFO] {target} 识别为Jenkins,未获取版本号") return # 判断版本范围 vulnerable = False try: # 每周版 <=2.575 ; LTS <=2.568.1 if ver.startswith("2."): if "." not in ver: pass parts = ver.split(".") major = int(parts[1]) if len(parts)==2: # 每周版 if major <= 575: vulnerable=True elif len(parts)==3: # LTS 2.568.1 if major ==568 and int(parts[2]) <=1: vulnerable=True except Exception: pass if vulnerable: print(f"[VULN] {target} | Version:{ver} 存在CVE-2026-70426风险") else: print(f"[SAFE] {target} | Version:{ver} 版本不受影响") except Exception as e: print(f"[ERR] {target} 连接失败 {str(e)[:60]}") def main(): parser = argparse.ArgumentParser() parser.add_argument("-f", "--file", help="目标列表文件,每行一个http://ip:port") parser.add_argument("-t", "--target", help="单个目标 http://ip:8080") parser.add_argument("-w", "--workers", default=8, type=int) args = parser.parse_args() targets = [] if args.file: with open(args.file,"r",encoding="utf-8") as f: for line in f: line = line.strip() if line: targets.append(line) if args.target: targets.append(args.target) if not targets: print("请输入 -t 单个目标或者 -f 目标文件") sys.exit(1) with ThreadPoolExecutor(max_workers=args.workers) as executor: executor.map(check_jenkins, targets) if __name__ == "__main__": main()使用示例:
# 单个目标 python3 jenkins_cve202670426_scan.py -t [http://127.0.0.1:8080](http://127.0.0.1:8080) # 批量扫描 python3 jenkins_cve202670426_scan.py -f targets.txt说明:脚本只做版本指纹识别,不能证明可利用。版本匹配仅代表存在漏洞条件,是否能利用还需要Agent/Connect权限或受控Agent。蓝队风险评估不能只靠版本判定,必须结合权限模型、Agent接入管控综合判断。
6.2 临时缓解措施(无法升级时)
如果业务不能立刻升级Jenkins版本,官方提供临时缓解方案,在Jenkins启动参数添加系统属性,收紧Remoting类过滤。
-Dhudson.remoting.ClassFilter=!*这个配置默认禁止所有类跨Remoting通道反序列化,业务会受影响,需要按需添加业务白名单类。仅作为临时应急,长期方案还是升级。
7 红队攻击面拓展与对抗审查
很多安全人员只盯着Web页面,忽略Agent侧的攻击入口。我们做对抗审查,梳理真实场景的攻击入口。
7.1 攻击入口1:新增Agent节点(Agent/Connect权限)
拥有Agent/Connect权限的用户,可以新建JNLP Agent,拿到Agent连接密钥,在攻击者服务器启动Agent,接入控制器。建立Remoting通道后,发送序列化载荷。
很多企业RBAC配置粗放,普通开发账号被分配Agent/Connect权限,这是高危配置。
7.2 攻击入口2:现有Agent被入侵
Agent服务器被入侵(漏洞、弱口令、恶意构建脚本),攻击者在Agent进程内执行代码,通过已存在Remoting通道发送载荷攻击控制器。
这是供应链攻击最典型路径。构建任务执行npm、maven、pip依赖,恶意依赖包在构建阶段执行代码,接管Agent。
7.3 攻击入口3:云原生动态Agent(K8s Agent)
Kubernetes插件动态创建Agent Pod。如果构建脚本可以控制Pod,攻击者拿到Agent Pod权限,横向攻击控制器。云原生CI/CD场景下,这个攻击面经常被忽略。
7.4 对抗审查要点清单
- 核查用户权限,哪些账号拥有
Agent/Connect权限; - 审计Agent节点接入方式,是否允许外部自建Agent;
- 审计Agent服务器基线,Agent是否最小权限运行;
- 审计构建流水线,是否允许不受控依赖包下载;
- 网络层面:控制器50000端口是否过大范围开放访问;
- 日志审计:Remoting通道通信日志、类加载异常日志。
8 生产环境加固方案
8.1 优先操作:版本升级
升级Jenkins到安全版本:
- 普通版升级 ≥2.576
- LTS长期支持版升级 ≥2.568.2
升级同时Remoting库同步更新至≥3386。升级前备份JENKINS_HOME目录,保存配置与凭证。
8.2 权限最小化
- 严格限制
Agent/Connect权限,只给运维管理员,普通开发账号移除该权限; - Agent运行账号最小权限,Agent进程不能拥有服务器root权限;
- 禁止匿名Agent接入,关闭废弃的Agent协议。
8.3 网络隔离
- Jenkins控制器50000 JNLP端口,仅允许可信Agent网段访问;
- 控制器和Agent之间网络分段,Agent所在网段不能访问控制器管理后台;
- 公网禁止直接暴露Agent JNLP端口。
8.4 日志与监控
开启Remoting通信日志,监控异常类加载、大量ClassNotFoundException报错。
告警规则:短时间内大量Remoting通道抛出ClassNotFoundException,大概率是漏洞探测或攻击尝试。
8.5 流水线安全加固
- 依赖包镜像扫描,构建前做恶意依赖检测;
- 流水线脚本使用SCM审批,不允许用户自由提交未审核脚本;
- 构建容器隔离,Agent使用一次性容器,构建结束销毁,防止持久化入侵。
9 漏洞与同类Jenkins漏洞横向对比
CVE-2024-43044同样依赖Agent/Connect权限,漏洞是文件读取。CVE-2026-70426是RCE,危害等级更高。两者攻击入口一致,都是Remoting通道。
这一组漏洞证明:Agent接入权限本质等同于控制器高风险权限。很多企业认知错误,认为Agent只是执行构建,Agent被攻陷不会影响控制器。CVE-2026-70426直接推翻这个认知边界。
JEP-200作为防护机制,设计思路没问题,但安全校验只覆盖主流程,异常分支遗漏。这是安全编码经典错误。安全校验不能假设程序永远走正常分支,所有异常捕获、降级、回退逻辑,都要复用相同安全校验逻辑。
10 漏洞未来演进预判
同类漏洞还会持续出现在序列化、类加载相关代码。开发人员处理异常时,容易省略安全校验。这类缺陷静态代码扫描很难发现,需要人工做路径遍历审计。
CI/CD平台持续成为攻击热点。攻击者越来越喜欢利用构建流水线作为跳板,供应链攻击会持续增加。Agent与控制器的信任边界,是CI/CD安全最重要的防线。很多企业只加固Web页面,忽略这个内部通信通道。
Jenkins后续安全开发,会强化全路径安全校验、增加更多回归测试覆盖异常分支。但只要Java序列化继续用于跨节点通信,类似风险就不会完全消失。长期安全方案,逐步减少跨节点Java序列化传输,替换为JSON等非序列化通信协议。
结尾互动
- 你的企业Jenkins是否开放了Agent/Connect权限给普通开发账号?
- 你们在CI/CD安全建设里,有没有把Agent当成攻击面重点管控?