Spring4Shell简析(CVE-2022-22965)
简介
继去年年底爆出的Log4shell漏洞在安全圈造成巨大影响的阴影下,今年3月底的Spring4shell(也有叫Spring Beans RCE的)消息刚刚放出,就在坊间流传开来。但由于前一次的某事件,这回漏洞详情来得很平静,也没有听说过有大面积的利用事件产生。这无疑给该漏洞带来很多神秘色彩。
目前网上已有不少的分析文章,此篇仅作为自己学习和理解的记录。看了一圈,发现Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考写的比较好,本文主要是基于这篇文章的复现和个人理解。
这个漏洞基于CVE-2010-1622,是该漏洞的补丁绕过,该漏洞即Spring的参数绑定会导致ClassLoader的后续属性的赋值,最终能够导致RCE。而CVE-2022-22965的利用方式参考了Struts2 S2-020在Tomcat 8下的命令执行分析的方法
漏洞存在条件
- JDK 9+
- 直接或者间接地使⽤了Spring-beans包(Spring boot等框架都使用了)
- Controller通过参数绑定传参,参数类型为非常规类型的对象(比如非String等类型的自定义对象)
- Web应用部署方式必须为Tomcat war包部署
参数绑定
初识
参数绑定使程序员编写请求处理时,能够很方便地指定获取的参数以及其类型,并且能够通过请求的参数改变对象的属性:
|
|
如果这里我们访问url:127.0.0.1:8080/test?name=123
,那么testBean
对象的name
属性将会被赋值为123。
嵌套型
Controller:
|
|
User.java:
|
|
Department.java:
|
|
当访问/addUser?name=test&department.name=SEC
时,user对象的name被赋值为test
,并且其成员变量department的name属性被赋值为SEC
环境搭建及复现
选择Spring4Shell PoC Application,加入远程debug相应环境变量。与java 8不同的是,java 9之后的远程调试默认不支持外部IP,需要更改为:
|
|
访问:
http://10.136.127.22:10086/helloworld/greeting
使用自带的exp进行攻击:
|
|
可以去/usr/local/tomcat/webapps/ROOT
看到生成的webshell:
|
|
漏洞分析
首先看Controller,参数绑定了Greeting
类的对象greeting:
|
|
攻击payload为:
|
|
攻击发起的get请求的请求头为:
|
|
简单分析一下,相当于发送了以下参数:
class.module.classLoader.resources.context.parent.pipeline.first.pattern
=带有某前缀和后缀的[jsp webshell]
class.module.classLoader.resources.context.parent.pipeline.first.suffix
=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory
=shell的文件名(不含后缀)
class.module.classLoader.resources.context.parent.pipeline.first.prefix
=shell的储存路径(相对路径,默认为webapps/ROOT)
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
=(空)
其中的webshell大概为(是个有回显的简单的一句话):
|
|
该漏洞中,攻击者可以对绑定对象的class属性进行随意赋值,有点像原型链污染。在exp中,是利用class属性来修改Tomcat的⽇志配置,向⽇志中写⼊shell。
一些细节:
这些payload可以分开发送,也可以合并在一次请求中,这些属性的修改是直接影响内存的(像原型链污染),不会像php一样,下一次请求又复原。
为了简化分析流程,我们仅post传入class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
,其他的参数都是类似的。
我们来跟一下参数绑定的解析过程,首先是org\springframework\spring-web\5.3.15\spring-web-5.3.15-sources.jar!\org\springframework\web\bind\WebDataBinder.java#198
,在WebDataBinder#doBind
方法中:
|
|
跟进去,此处省略n层,到org/springframework/beans/AbstractNestablePropertyAccessor.java#814
,getPropertyAccessorForPropertyPath
方法被传入当前的待解析参数属性,并且尝试解析嵌套的情况,出现嵌套时,从左边开始挨个递归解析,比如此处的属性为class.module.classLoader.resources.context.parent.pipeline.first.suffix
,将会解析最左边的class
:
|
|
然后继续跟进:
|
|
继续跟进:
|
|
此处的ph变量是BeanWrapperImpl对象,该对象装饰了java bean(在这里是Greeting对象),并提供对其属性(这里传入了“class”字符串,获取的是class
属性)的获取和更改(get和set),之后调用的ph.getValue();
会获取Greeting对象的class属性:跟进去之后,在org/springframework/beans/BeanWrapperImpl.java#getValue
中会获取到java.lang.Class java.lang.Object.getClass()
,从而使class
被解析为java.lang.Object.Class
类的对象(具体值为class com.reznok.helloworld.Greeting
):
|
|
这里会返回Greeting的class属性。到这里,单次解析过程分析完毕。我们来对照一下过程:我们现在在解析class.module.classLoader.resources.context.parent.pipeline.first.suffix
中的class
,而当前绑定参数对象为Greeting类的对象,所以实际上是调用了Greeting.getClass()
然后我们来跟下一层解析,即module.classLoader.resources.context.parent.pipeline.first.suffix
中的module
的解析。我们再来看解析的入口,即递归处,此时的nestedPa为上一步解析出的java.lang.Object.Class
对象,根据前一步的分析,这里会尝试获取该对象的module属性,即会调用java.lang.Object.Class.getModule()
|
|
此时获取到的是一个java.lang.Module
对象。
类似地,依次进行各层的解析,总体解析过程为:
|
|
- 需要注意的是,最后一层是整个嵌套解析结束后,调用
setPropertyValue
时会对得到的属性进行赋值。利用时是对org.apache.catalina.AccessLog
属性进行赋值。 - 该过程中,
java.lang.Module.getClassLoader()
得到org.apache.catalina.loader.ParallelWebappClassLoader
这步的跨度较大,这步很关键,在后文war包部署部分会详细介绍。 - 其他的几个属性解析赋值的过程类似。
Tomcat日志与AccessLogValve
下面我们来详细看一下Tomcat日志相关的部分,首先是exp中设置的几个属性,它们的作用为:
- pattern:日志的文件内容格式;其中,pattern有一些特殊的语法,比如
%{xxx}i
表示获取请求的header中的xxx头的值,并将其打印到日志中 - suffix:日志文件名后缀
- directory:日志文件路径
- prefix:日志文件名前缀
- fileDateFormat:文件名日期后缀,默认为
.yyyy-MM-dd
为什么攻击时用于产生日志的get请求中,请求的header会有"c": "Runtime"
?在pattern中是包含了%{c}i
这一格式化记录数据的,所以c的值会被替换为Runtime。只要得到的日志文件没有语法错误,不使用这种方法也能利用成功,但是需要注意的是,http头中的信息在记录到日志文件中时,会对双引号进行转义,而在pattern中直接赋值时不会转义,所以直接在header中传入webshell会导致shell无法成功执行。
为什么部署方式必须为Tomcat war包部署
- LaunchedURLClassLoader是以jar的形式启动Spring boot的加载器来加载/lib下面的jar,LaunchedURLClassLoader和普通的URLClassLoader的不同之处是,它提供了从Archive里加载.class的能力。参考:spring boot应用启动原理分析
- 在利用时,存在
java.lang.Module.getClassLoader()
得到org.apache.catalina.loader.ParallelWebappClassLoader
这一步,ParallelWebappClassLoader
只能是war包部署时的返回值;如果使用jar包的形式进行部署,则此步获取到的对象是org.springframework.boot.loader.LaunchedURLClassLoader
,该类下没有resources成员变量,导致利用链断掉。
为什么要JDK 9+
前面提到,该漏洞是对CVE-2010-1622的绕过,这个绕过是出现在9+之后对模块化的支持上的,在org/springframework/beans/CachedIntrospectionResults.java#CachedIntrospectionResults#289
中有对CVE-2010-1622的修复补丁:
|
|
注意到条件if (Class.class == beanClass && ("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName())))
,该条件的意思是,如果当前的对象的类为Class.class
(即java.lang.Class
),并且下一个要解析的属性名为classLoader
或protectionDomain
,就直接contine,不会再解析该层。而在Spring4shell的绕过中,由于JDK 9+对模块化进行了支持,实现了getModule方法,从而可以通过该方法得到的Module进一步获取classLoader,而不是直接使用Class的getClaasLoader()去获取。
具体来讲就是把
|
|
替换成
|
|
从而绕过该补丁。
可利用程度
首先,我们假设受害者已满足JDK 9+,并且使用了Spring bean,此外还有一个不是很容易满足的条件:
- Web应用部署方式必须为Tomcat war包部署(jar的形式则不行)
然后,攻击者需要知道存在漏洞的url路由,这和应用的业务息息相关。
所以该漏洞虽然是核弹级,但要在真实情况下找到满足这些条件的情形还是比较少的。这也许能解释为什么漏洞爆出后,在野利用的反响远远不如Log4shell那么大。
官方补丁
官方的补丁为:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15,具体来说,修复后的程序仅允许对属性描述符为name或以Name结尾的属性进行赋值,从而防止classLoader和Module以及之后的属性被赋值。
总结
- 该漏洞是在spring bean解析参数绑定时,对对象属性进行赋值时没有对classLoader进行限制造成。
- 开发者要实现的功能:支持参数绑定时向对象的属性进行赋值操作。漏洞出现的原因:能对classLoader之后的其他属性进行赋值,从而修改了一些重要的全局变量导致getshell(比如这里修改了日志文件的文件名、内容,导致写入webshell)。
- 该洞的利用过程是查找一系列的getXXX链,然后可以设置其值为任意字符串,exp中通过设置tomcat日志相关属性以写入webshell。这个链的查找过程应该是很有难度的,除了写日志这条链,还可以尝试去发现其他的利用链,但过程应该很难。
- 该漏洞与原型链污染有相似的地方(污染现有对象的属性达到利用),与python模板注入也有相似的地方(通过链式的形式在对象成员属性或继承链之间跨越,以达到能够利用的属性处)。