CVE-2022-22965漏洞分析复现
CVE-2022-22965漏洞分析复现
注:本文参考极盾科技相关文章,有非常多引用,但是由于太多了影响阅读因此只在标题做了标明,因此本文仅供自行学习,若有需要,可以参考原链接:
https://www.aqniu.com/industry/82365.html
先来一张java现状,lol:
0x01 漏洞基本情况
基本概况:
After Spring Cloud, on March 29, another heavyweight vulnerability of Spring broke out on the Internet: Spring Core RCE
On March 31 Spring released new versions which fixes the vulnerability. See section Patching.
On March 31 a CVE-number was finally assigned to the vulnerability with a CVSS score 9.8 (CRITICAL)
环境需求:
- JDK9及其以上版本;
- 使⽤了Spring-beans包;
- 使⽤了Spring参数绑定;
- Spring参数绑定使⽤的是⾮基本参数类型,例如⼀般的POJO即可;
简单的说,就是java+maven+tomcat三件套,不做赘述。
0x02 背景知识(部分引用):
一、springMVC参数绑定基础
为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。之后,Controller方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest中获取请求数据以及类型转换。下面是一个简单的示例:
1 | import org.springframework.stereotype.Controller; |
1 | public class User { |
1 | public class Department { |
这里这个程序是什么呢?就是说当我们请求了/addUser?name=test&department.name=SEC
的时候,虽然我们没有给controller中的user赋值,但是我们上文中的这个test和SEC会直接赋值到user对象上,然后作为参数传入,这就是所谓的参数绑定。
那么,我们可以看到user对象里还嵌套了一层department对象,在这种情况下依然能够赋值成功,就说明SpringMVC的参数绑定是支持多层嵌套的,换言之,Spring会构造一个调用链,以实现这样的多层赋值:
1 | User.getDepartment() |
也就是,在为user中department赋值的调用链如上,如果调用链的层数更多,当然也是同理的!
那么,我们提到的这个参数绑定是由谁实现的呢?SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)
。
二、Bean实现对属性的读写
1、BeanWrapper
在Spring中,BeanWrapper
接口是对Bean的包装,其中定义了大量可以非常方便的方法对Bean的属性进行访问和设置。BeanWrapperImpl
类是BeanWrapper
接口的默认实现类,BeanWrapperImpl.wrappedObject
属性就是被包装的Bean对象。
1 | import org.springframework.beans.BeanWrapper; |
从上面的案例中可以看到,调用BeanWrapper
类的getPropertyValue()
函数可以实现对象属性的读操作,而setPropertyValue()
函数可以实现对象属性的写操作。
那么我们进一步深究,这些函数的具体实现是通过什么呢?其下层实现是通过PropertyDescriptor
类完成的。
2、PropertyDescriptor
PropertyDescriptor
是JDK自带的java.beans
包下的类,意为属性描述器,用于获取符合Java Bean规范的对象属性和get/set方法。下面是一个简单的例子:
1 | import java.beans.BeanInfo; |
可以看到,对于上述方法的调用果然要比BeanWrapper要复杂的多,换言之就是封装程度低,也就是处于更加底层的位置了。
通过这一部分,我们学到Spring中Bean对象由BeanWrapper包装,然后其下层的实现是由PropertyDescriptor作为储存每一个属性的getter和setter的集合。
三、Tomcat日志
Valve
是tomcat中用于处理请求和响应的一种结构,tomcat通过组合多个Valve
的Pipeline
(管道队列),来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve
的作用是,用来记录访问日志(access_log
)。Tomcat的server.xml
中默认配置了AccessLogValve
,因此所有部署在Tomcat中的Web应用均会执行该Valve
,内容如下:
1 | <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" |
其中属性意义如下:
- directory:access_log文件输出目录。
- prefix:access_log文件名前缀。
- pattern:access_log文件内容格式。
- suffix:access_log文件名后缀。
- fileDateFormat:access_log文件名日期后缀,默认为
.yyyy-MM-dd
。
那么通过这些背景知识,我们大致也就可以猜到,这个漏洞就是通过上述的mvc属性绑定,更改了AccessLogValue
,以达到在服务器端webshell写入。
0x03 复现过程
复现吧!
一、实验环境
操作系统:Win10或Linux系统都可
JDK版本:Java(TM) SE Runtime Environment (build 15.0.2+7-27)
tomcat版本:apache-tomcat-9.0.60(注意要在9.0.62以前!)
springboot:2.6.3
二、代码编写
写一个user实体类,随便给个属性,设置上get和set方法,然后提供一个添加用户的接口(UserController),最后提供一个Springboot的启动类(ApplicationMain),实现起来比较简单,如下:
1 | public class User { |
1 | import com.lu.pojo.User; |
1 | import org.springframework.boot.SpringApplication; |
三、测试步骤
- 使用命令:
mvn clean package
打包为**.WAR**文件; - 放在tomcat的webapps目录下,tomcat中bin目录下start开启tomcat;
- 访问http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser,显示ok,说明部署成功。
- 执行poc脚本(poc见下一部分),按照poc显示的位置进行命令执行:
1 | python poc.py --url http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser |
0x04 POC拆解:
一、poc源码:
1 | import requests |
二、data段内容分析
1.调用链分析
显而易见,这个poc的核心就在于发送数据包中的内容,也就是data段的内容,我们将其进行url解码,看看他做了怎样的参数传递:
1 | //这里添加了缩进,没有按照原来的空格格式,方便理解 |
可以看到,每个参数就是由class.module.classLoader.resources.context.parent.pipeline.first.xxx这样的调用链进行赋值的,我们虽然能看到这样的调用关系,但我们依然不知道最后的调用链怎么样的,也就是说,我们能看到有一个属性叫pattern,但我们只知道他的名字,不知道他究竟是谁。
因此,我们选择背景知识第一部分提到的SpringMVC实现参数绑定的方法:WebDataBinder.doBind(MutablePropertyValues)
方法设置断点!
从此处开始,就开始进行参数的绑定工作,那么,spring的递归绑定,也就是上文中提到的可嵌套绑定又是如何实现的呢?查询发现,是通过AbstractNestablePropertyAccessor
类,提供对嵌套属性的支持,对于这个类的讲解,具体可以参看:https://www.bbsmax.com/A/WpdKV4ZA5V/。
getPropertyAccessorForPropertyPath 根据属性(propertyPath)获取所在 bean 的包装对象 beanWrapper。如果是类似 director.info.name 的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法 getNestedPropertyAccessor 完成。
1 | //引用自https://www.bbsmax.com/A/WpdKV4ZA5V/ |
这里提到getPropertyAccessorForPropertyPath方法用于递归获取beanWrapper,然后交给getNestedPropertyAccessor 进行包装。也就是说,我们只需要在这个负责包装的函数处设置断点,步入后进行调试,一直执行到BeanWrapperImpl类中,BeanPropertyHandler.getValue()
中就可以查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。
实际上,前8轮的递归结果都无关紧要,我们想知道的是最后一轮的情况,但是为了看的更清楚,我们附上完整的调用链条:)
1 | User.getClass() |
这样我们就看到,最后一层调用pattern
参数最终对应的就是AccessLogValve.setPattern()
方法,也就是我们在tomcat前置知识中提到写入日志的部分,我们最终利用参数绑定更改了tomcat的日志写入的内容,最终导致了webshell的写入。
2.参数刨析(大部分引用)
pattern参数:
1 | class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i |
很明显,这是一个webshell,这里使用了形如%{xxxx}i
的东西实际是AccessLog的一个特性,为了便于写入日志,用这样的结构来引用请求和响应header中的内容,这就解释了刚刚poc中header部分的内容:
1 | headers = { |
进行替换后的jsp脚本:
1 | <% |
那我们为什么要采取这种方法呢?脚本作者是这样解释的:
# This may seem strange, but this seems to be needed to bypass some check that looks for “Runtime” in the log_pattern
大概是一种安全措施吧。
suffix参数:
- 参数值:
.jsp
该参数为文件后缀。
directory参数:
- 参数值:
webapps/ROOT
该参数为access_log的文件的输出目录,ROOT为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/文件名
访问(仅仅是为了方便,其他目录也可以)。
prefix参数:
- 参数值:
tomcatwar
即access_log的文件名。
fileDateFormat参数
- 参数值:空
access_log的文件名不包含日期。
为什么要单独设置这个函数呢?
#Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times
#If re-running the exploit, this will create an artifact of {old_file_name}_.jsp
大概意思是说,这样为空的设置可以让脚本重复运行,但是亲测无效:
只是简单的在一个文件里写了两遍。
三、总结
通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve
的属性,让Tomcat在webapps/ROOT
目录输出定制的“访问日志”tomcatwar.jsp
,该“访问日志”实际上为一个JSP webshell。
0x05 环境分析
漏洞是一个spring+tomcat的漏洞,看似危害比较大,但是我认为还是有较为苛刻的环境限制的:
一、Tomat+War的限制
上文中存在一个调用org.apache.catalina.loader.ParallelWebappClassLoader.getResources(),只有通过这个调用关系,才能顺利实现我们预判的调用链条,二这里出现的ParallelWebappClassLoader类是只有在Web应用以war包部署到Tomcat中时才会使用到的。
如果我们更换为其他的,如jar包的部署方式,在第三次递归中,classLoader
嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader
,查看其源码,没有getResources()
方法,也就正常的进行我们预判的调用链。
但是,这只是限制了本poc的运行环境,并不是说这个漏洞就不存在了,这个漏洞依然存在,只是需要研究出一个合理的,从Web应用到Web服务中间件的class.module.classLoader....
的调用链,理论上如Jetty、Weblogic、Glassfish等其他web应用也是可被利用的。而且这里给出的利用方法是写入日志,还有可能写入配置文件,当然,使用更先进的技术如内存马,也是有可能的!
因此,并不是说使用其他web应用我们就不需要防范了。
二、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...
这种嵌套参数走不通,即形如下方的调用链:
1 | Foo.getClass() |
这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class
中增加了module
属性和对应的getModule()
方法,自然就能通过如下调用链绕过判断:
1 | Foo.getClass() |
这就是为什么本漏洞利用条件之二,JDK>=1.9。
三、补丁分析(引用)
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()
的链路就走不通了。
2. Tomcat 9.0.62补丁
通过对比Tomcat 9.0.61和9.0.62的版本,可以看到在4月1日有一项名为“Security hardening. Deprecate getResources() and always return null.”提交。
进入该提交,可以看到对getResources()
方法的返回值做了修改,直接返回null
。WebappClassLoaderBase
即ParallelWebappClassLoader
的父类,在章节3.2.1 ``关键点一:Web应用部署方式
中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
的链路就走不通了。
这里我做了一个实验,提高了tomcat的版本,结果果然是做不通了,证实了作者的观点。
0x06 参考文献
主要参考:https://www.aqniu.com/industry/82365.html
poc脚本参考:
https://github.com/reznok/Spring4Shell-POC
https://github.com/TheGejr/SpringShell
https://github.com/BobTheShoplifter/Spring4Shell-POC
漏洞信息:
https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
https://nvd.nist.gov/vuln/detail/CVE-2022-22965
基本情况分析参考:
https://unit42.paloaltonetworks.jp/cve-2022-22965-springshell/
其他知识参考:
- Tomcat access_log配置参考文档:https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging
- Spring 5.3.17和5.3.18版本比较:https://github.com/spring-projects/spring-framework/compare/v5.3.17…v5.3.18
- Spring 5.3.18补丁提交内容:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
- Tomcat 9.0.61和9.0.62版本比较:https://github.com/apache/tomcat/compare/9.0.61…9.0.62
- Tomcat 9.0.62补丁提交内容:https://github.com/apache/tomcat/commit/8a904f6065080409a1e00606cd7bceec6ad8918c
- LaunchedURLClassLoader源码:https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java