Spring MVC RCE 分析(一)
CVE-2010-1622 (未完)
参考:
SpringMVC RCE (CVE-2010-1622) – JohnFrod's Blog
从零开始,分析Spring Framework RCE - 跳跳糖 (tttang.com)
spring rce 从cve-2010-1622到CVE-2022-22965 篇一 - 先知社区 (aliyun.com)
前天尝试入门最新的Spring Beans RCE (CVE-2022-22965) ,后来看各种博文和师兄的复现历程决定先从这个漏洞的前身开始(CVE-2010-1622)。在Spring把CVE-2010-1622洞修上之后,
java9版本出现了一个能绕这个修复判断条件的方法,所以这里再来尝试分析下这个洞来加深下理解,希望能有所帮助。
漏洞条件
- Spring 3.0.0 to 3.0.2
2.5.0 to 2.5.6.SEC01 (community releases)
2.5.0 to 2.5.7 (subscription customers) 以及更早的版本 - tomcat6.0.28 之前的版本
Java Beans API
JavaBean是一种特殊的类,主要用于传递数据信息,这种类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。如果在两个模块之间传递信息,可以将信息封装进JavaBean中。这种JavaBean的实例对象称之为值对象(Value Object),因为这些bean中通常只有一些信息字段和存储方法,没有功能性方法,JavaBean实际就是一种规范,当一个类满足这个规范,这个类就能被其它特定的类调用。一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。
归纳来说:java bean就是
- 所有的类必须声明为
public
,这样才能够被外部所访问; - 类中所有的属性都必须封装,即:使用
private
声明; - 封装的属性如果需要被外部所操作,则必须编写对应的
setter
、getter
方法; - 一个JavaBean中至少存在一个无参构造方法
而关于“一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。”,这个可以结合下面的“内省”的概念一起理解:
内省(Introspector) 是Java 语言对 JavaBean 类属性、事件的一种缺省处理方法。其中的propertiesDescriptor
实际上来自于对Method的解析。
这里我理解为内省机制就是通过对类里面的
getter
/setter
方法的名字来判断这个类有哪些属性
关于property
此外,通常把一组对应的读方法(
getter
)和写方法(setter
)称为属性(property
)。例如,Person类中的name
的属性:
- 对应的读方法是
String getName()
- 对应的写方法是
setName(String)
example:
如我们现在声明一个JavaBean—Student
Student
public class Student {
private String id;
private String name;
public String getPass() {
return null;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
在类Test中有私有属性id,我们可以通过getter
/setter
方法来访问或设置这个属性。在Java JDK中提供了一套 API 用来访问某个属性的 getter
/setter
方法,这就是内省。
因为内省操作非常麻烦,所以Apache开发了一套简单、易用的API来操作Bean的属性——BeanUtils工具包。
Java Beans API的Introspector类提供了两种方法来获取类的bean信息:
BeanInfo getBeanInfo(Class beanClass)
BeanInfo getBeanInfo(Class beanClass, Class stopClass)
这里就出现了一个使用时可能出现问题的地方,即没有使用stopClass
,这样会使得访问该类的同时访问到Object.class
。
因为在java中所有的对象都会默认继承Object基础类。而又因为Object基础类存在一个getClass()
方法(只要有 getter
/setter
方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性),所以会找到class属性。
demo(没有使用stopClass
)
public static void main(String[] args) throws IntrospectionException
{
BeanInfo beanInfo = Introspector.getBeanInfo(Student.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for(PropertyDescriptor pd:propertyDescriptors){
System.out.println("Property: "+pd.getName());
}
}
使用了stopClass
:
public static void main(String[] args) throws IntrospectionException
{
BeanInfo beanInfo = Introspector.getBeanInfo(Student.class,Object.class);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for(PropertyDescriptor pd:propertyDescriptors){
System.out.println("Property: "+pd.getName());
}
}
可以看到在不使用stopClass
获取到的BeanInfo里面存在class属性,这个属性对应就是Object.class。然后我们还发现,Test类里面实际上是没有pass这个属性的,但是这里却获取到pass属性,这是因为Test类有getPass
方法,所以内省机制根据方法名字认为存在pass属性。
如果我们接着调用
Introspector.getBeanInfo(Class.class)
可以看到关键的classLoader
出现了,能够让我们实现任意类加载。
spring bean
简介
Spring Bean是事物处理组件类和实体类(POJO)对象的总称,是能够被实例化、能够被spring容器管理的java对象。
可以把spring bean理解为java bean的增强版,spring bean是由 Spring IoC 容器管理的,bean 是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象。这些 bean 是由用容器提供的配置元数据创建的,在spring中可以由xml配置文件来创建bean,也就是创建所需要的对象。
假设存在一个配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="bean1" class="com.Person">
<constructor-arg value="panda"/>
<constructor-arg value="18"/>
</bean>
</beans>
则Spring相当于调用如下代码:
Bean bean = new com.Person("panda","18");
这看着很简单,但实际上Spring为我们做了很多事情,具体可以借用一张流程图来说明:
SpringMVC如何实现数据绑定
一般一个springboot会包含webmvc框架+tomcat中间件等
首先SpringMVC中当传入一个http请求时会进入DispatcherServlet的doDispatch,然后前端控制器请求HandlerMapping查找Handler,接着HandlerAdapter请求适配器去执行Handler,然后返回ModelAndView,ViewResolver再去解析并返回View,前端解析器去最后渲染视图。
在这个过程中我们这里主要关注再适配器中invokeHandler调用到的参数解析所进行的数据绑定(在调用controller中的方法传入参数调用前进行的操作)。
无论是spring mvc的数据绑定(将各式参数绑定到@RequestMapping注解的请求处理方法的参数上),还是BeanFactory(处理@Autowired注解)都会使用到BeanWrapper接口。
过程如上,BeanWrapperImpl具体实现了创建,持有以及修改bean的方法。
其中的setPropertyValue
方法可以将参数值注入到指定bean的相关属性中(包括list,map等),同时也可以嵌套设置属性。
example:
tb中有个spouse的属性,也为TestBean
TestBean tb = new TestBean();
BeanWrapper bw = new BeanWrapperImpl(tb);
bw.setPropertyValue("spouse.name", "tom");
//等价于tb.getSpouse().setName("tom");
变量覆盖
User类
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
可以看到:其中User的name和UserInfo中id有get
和set
方法
Userinfo类
public class UserInfo {
private String id ;
private String number;
private User user=new User();
private String names[] = new String[]{"1"};
public String getId() {
return id;
}
public String getNumber() {
return number;
}
public void setId(String id) {
this.id = id;
}
public User getUser() {
return user;
}
public String[] getNames() {
return names;
}
}
可以看到:UserInfo中的user,number和names[]数组只有get
方法,id则都有
controller
@RequestMapping(value = "/test", method = RequestMethod.GET)
public void test(UserInfo userInfo) {
System.out.println("id:"+userInfo.getId());
System.out.println("number:"+userInfo.getNumber());
System.out.println("class:"+userInfo.getClass());
System.out.println("user.name:"+userInfo.getUser().getName());
System.out.println("names[0]:"+ userInfo.getNames()[0]);
System.out.println("classLoader:"+ userInfo.getClass().getClassLoader());
}
直接访问/test, 结果如下,符合预期
但是如果:
http://localhost:8080/test?id=1&name=test&class.classLoader=org.apache.catalina.loader.StandardClassLoader&class=java.lang.String&number=123&user.name=ruilin&names[0]=33333
据说是会
但是我本地复现的时候报错了:java.lang.IllegalArgumentException: Invalid character found in the request target
随后我修改为post传参,又可以复现了,有点迷,参考代码如下
@RequestMapping(value = "/test", method = RequestMethod.POST)
public void test(UserInfo userInfo) {
System.out.println("id:"+userInfo.getId());
System.out.println("number:"+userInfo.getNumber());
System.out.println("class:"+userInfo.getClass());
System.out.println("user.name:"+userInfo.getUser().getName());
System.out.println("names[0]:"+ userInfo.getNames()[0]);
System.out.println("classLoader:"+ userInfo.getClass().getClassLoader());
}
如上图所示,这里id由于具有set方法可以被设置,UserInfo其他变量无法被设置(除了User类),所以可见id,number,没有收到传参的影响。但是,names这个字符串数组在没有设置set方法的情况下却成功设置了
调试分析
上面提到了BeanWrapperImpl.setPropertyValue
方法是用来绑定赋值的,所以我们在此处打上断点,一起调试一下看一下。
(如果你遇到了搜不到函数的情况,很可能是没能把文件源代码拉下来,一般可以在pom.xml右键选择下载source,也可以参考这个(23条消息) idea无法下载源码 Sources not found for: org.springframework:spring-context:5.1.5.RELEAS_程序员劝退师-TAO的博客-CSDN博客_idea下载spring源码提示)
值得注意的是,我的BeanWrapperImpl是继承自AbstractNestablePropertyAccessor的,setPropertyValue在AbstractNestablePropertyAccessor有实现
最后根据自身情况,我把断点打在AbstractPropertyAccessor.setPropertyValues,debug启动,随后post参数,命中断点
首先在AbstractPropertyAccessor类的setPropertyValues
方法传入一个ArrayList,里面包含了请求参数的beans键值对,然后循环取出每一个bean调用
AbstractPropertyAccessor的setPropertyValue
单步进AbstractPropertyAccessor的setPropertyValue
后,
可以看到最终还是需要调用BeanWrapperImpl的setPropertyValue
,进去看一下
其实是AbstractPropertyAccessor的setPropertyValue, 从可以看出很明显的参数设置的样子
然后我们循环到处理names[0]
这轮循环进入BeanWrapperImpl的setPropertyValue
,看看在这里是怎么讲没有set方法的字符串数组进行覆盖的
步入setPropertyValue
字符串数组在pv里,追踪一下
进入nestedPa.setPropertyValue
(由于我是post的,和原文略有出入(SpringMVC RCE (CVE-2010-1622) – JohnFrod's Blog))
步入这个
步入这个
仍然会出现getPropertyValue,并且和原文高度一致,步入
几乎是一样了,但是没有看到我们要的那个PropertyDescriptor,步入看看
原来是被封装了,这里看函数意思应该可以理解:所以自此,我们它发现是从CachedIntrospectionResults获取PropertyDescriptor。
如果步入进去看,可以看看它的cachedIntrospectionResults是怎么来的
这里是个属性,那咱们去函数构造方法或者声明成员变量的地方看看
发现它的变量类型既然也是CachedIntrospectionResults
ctrl进去看看
看到了熟悉的Introspector.getBeanInfo
。这也就是我们上面讲过的内省,因此可以解释names[]
它为什么它能去获取到没有set
的属性,因为有get
就能够获取到属性了。
咱们跳出来到这个:
继续调看看是如何赋值的。仔细分析
看代码可以知道当判断为Array时会直接调用Array.set
,由此绕过了pojo的set
方法,直接调用底层赋值,也就是说及时pojo没有写set
方法也能够赋值。后面同样List,Map类型的字段也有类似的处理,也就是说这三种类型是不需要set
方法的。对于一般的值,直接调用java反射中的writeMethod
方法给予赋值。
最后是调用dobind把它们都绑定起来,感兴趣的可以自己进去看看实现