lz520520@深蓝攻防实验室

漏洞介绍

Spring MVC 框架的参数绑定功能提供了将请求中的参数绑定控制器方法中参数对象的成员变量,攻击者通过构造恶意请求获取AccessLogValve 对象并注入恶意字段值触发 pipeline 机制可写入任意路径下的文件。
简答来说就是参数绑定造成的变量覆盖漏洞,漏洞点spring-beans包中。

漏洞编号

影响范围

JDK>=9
springMVC 全版本

漏洞分析

属性注入分析

根据之前的漏洞分析
http://rui0.cn/archives/1158,其中一个点在于获取BeanInfo,位置在CachedIntrospectionResults构造方法,下个断点,发送请求断下

之前的补丁是把classLoader等危险属性过滤掉了。

跟踪如上堆栈发现,新版本和旧版本参数绑定的类有所不一样,具体位置如下,setPropertyValue也叫属性注入
org\springframework\beans\AbstractNestablePropertyAccessor.class#setPropertyValue

补充:AbstractNestablePropertyAccessor 类通过其成员属性提供了一种支持嵌套属性的数据结构

这段的理解参考 https://www.cnblogs.com/binarylei/p/10267928.html

// 1. 递归获取 propertyName 属性所在的 beanWrapper,如 director.info.name 获取 name 属性所在的 info bean
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
// 2. 获取属性的 token
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
// 3. 设置属性值
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));

getPropertyAccessorForPropertyPath 根据属性(propertyPath)获取所在 bean 的包装对象 beanWrapper。如果是类似director.info.name 的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法 getNestedPropertyAccessor 完成。
可以看到getPropertyAccessorForPropertyPath的递归调用

protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
    // 1. 获取第一个点之前的属性部分。eg: director.info.name 返回 department
    int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);

    // 2. 递归处理嵌套属性
    // 2.1 先获取 director 属性所在类的 rootBeanWrapper
    // 2.2 再获取 info 属性所在类的 directorBeanWrapper
    // 2.3 依此类推,获取最后一个属性 name 属性所在类的 infoBeanWrapper
    if (pos > -1) {
        String nestedProperty = propertyPath.substring(0, pos);
        String nestedPath = propertyPath.substring(pos + 1);
        AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
        return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
    // 3. 当前对象直接返回
    } else {
        return this;
    }
}

也就是bean属性的递归获取,比如我这个环境中,最外层BeanWrapperImpl封装的是UserInfo,我传参是

对应的就是我controller中的入参类

我传参class.module.classLoader.resources.context.parent.appBase
第一次获取class对应的BeanWrapperImpl

this.getNestedPropertyAccessor(nestedProperty)
    this.getPropertyValue(tokens)
        this.getLocalPropertyHandler(actualName)
            this.getCachedIntrospectionResults().getPropertyDescriptor(propertyName) // propertyName=class

利用链分析

UserInfo除了基础的四个属性以外,还有一个class

并且在嵌套属性nestedPropertyAccessors(这个是缓存后有的,直接在beanInfo里也能看)中也直观看到class

展开可以看到套娃一样的嵌套属性

class.module.classLoader.resources.context用对象如何获取呢,对应代码如下,也就是多个getter串起来

((org.apache.catalina.loader.ParallelWebappClassLoader)  new UserInfo().getClass().getModule().getClassLoader()).getResources().getContext()

获取到的就是StandardContext,是不是在内存马也见过。

进一步获取appBase也就顺理成章,最终聚焦下parent,也就是StandardHost
class.module.classLoader.resources.context.parent

((org.apache.catalina.loader.ParallelWebappClassLoader)  new UserInfo().getClass().getModule().getClassLoader()).getResources().getContext().getParent()

公开的利用链也就是pipeline下的AccessLogValue的利用,这个类用来设置日志存储参数,包括路径、后缀,修改参数即可达到写入任意文件的目的。
[

](https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html)

AccessLogValue构造

AccessLogValue的属性可参考tomcat官方文档
https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html
通过属性注入修改AccessLogValue的几个属性如下

class.module.classLoader.resources.context.parent.appBase=./
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{Prefix123}i+1231231+%25{Suffix123}i
class.module.classLoader.resources.context.parent.pipeline.first.suffix=random1111.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory=.
class.module.classLoader.resources.context.parent.pipeline.first.prefix=webapps/ROOT/random1111
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=time_fomrat_random111

由于%会被过滤,pattern里通过引用头部来实现构造。
PS: 注意每次写新的文件,需要修改suffix、prefix以及fileDateFormat,否则文件路径不会修改。
fileDataFormat:默认是.yyyy-MM-dd,尽量只用数字,因为字母会被解析格式化
suffix:只要有后缀即可
prefix:可任意
pattern:格式一般是%h %l %u %t "%r" %s %b ,所以%会被格式化,但通过%{xxx}i可引用请求头字段,即可保证任意字符写入,并且可以实现字符拼接,绕过webshell检测。

%{xxx}i 请求headers的信息
%{xxx}o 响应headers的信息
%{xxx}c 请求cookie的信息
%{xxx}r xxx是ServletRequest的一个属性
%{xxx}s xxx是HttpSession的一个属性

http://xiaobaoqiu.github.io/blog/2014/12/30/tomcat-access-logpei-zhi/
[

](http://xiaobaoqiu.github.io/blog/2014/12/30/tomcat-access-logpei-zhi/)

bypass原因分析

JDK1.8下测试,class bean下没有module bean,导致后续无法利用,如果是class.classLoader则会被黑名单拦截。

查了下module对应的类是java.lang.Module,是JDK9引入的,JDK9引入了模块系统。
https://blog.csdn.net/charles_neil/article/details/114460702

补充下:
黑名单判断逻辑,beanClass非Class或者属性name非(classLoader|protectionDomain),满意一个即可,而jdk8中没有module,只能用class.classLoader调用,而这样两个条件都不满足,导致无法绕过。

Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())

jdk9为什么能绕过呢,因为他多了一个module,如class.module.classLoader,这样module就满足第一个条件,而不去判断第二个条件,从而绕过黑名单。

如何遍历嵌套属性

之前struts2出过类似漏洞,有人编写了一个jsp来遍历
https://cs.github.com/julianvilas/rooted2k15/blob/a00055f906502dd038b908a84907b74b38e26b20/struts-tester/struts-tester.jsp
细节看代码,主要把这个替换成自己的模型类,也就是控制器方法的入参类,以此为起点来获取gettersetter

效果如下,其他嵌套属性有啥用,就待大家自行摸索了。

做了属性值的获取,通过getPropertyAccessorForPropertyPath获取即可,示例代码如下

BeanWrapperImpl bw = new BeanWrapperImpl(this.initarget);
String propertyName = poc + "." + fieldName;
Integer offset = propertyName.indexOf(prefix);
if (offset != -1) {
    propertyName = propertyName.substring(offset + prefix.length());
}
String value = "";
String type = "";
try {
    BeanWrapperImpl bwl = (BeanWrapperImpl)invoke(bw, "getPropertyAccessorForPropertyPath", propertyName);
    // TODO: 这里私有属性和getter对应不上,应该不算bean,默认还是通过私有属性反射
    Object v = getFieldValue(bwl.getWrappedInstance(), fieldName);

    value = v.toString();
    type = v.getClass().getName();
} catch (Exception  e) {
    value = "";
}

再看一下JDK9 springboot下的,和springMVC的classloader不一样,是AppClassLoader,没有getResources()。
springMVC是ParallelWebappClassLoader

然后看下JDK8的springMVC,如下所示,protectionDomain是在黑名单内,所以连属性值都获取不到。

PS: 没有解析到值的,说明那个属性是错误的,不是bean的属性。
这里是打印的存在setter的属性,不打印只有getter的属性。

利用条件

所以总结下

  • >=JDK9
  • springMVC。
  • 请求接口对应的是控制器方法。
  • 方法入参是非基础类,不能是String,int等
  • 请求方法要和控制器方法对应上,否则无法访问。
    ##

修复建议

官方补丁
https://github.com/spring-projects/spring-framework/commit/afbff391d8299034cd98af968981504b6ca7b38c

临时方案
(一)WAF防护
在WAF等网络防护设备上,根据实际部署业务的流量情况,实现对“class.”“Class.”“.class.”“.Class.”等字符串的规则过滤,并在部暑过滤规则后,对业务运行情况进行测试,避免产生额外影响。

(二)临时修复措施
需同时按以下两个步骤进行漏涧的临时修复:
1.在应用中全局搜索@InitBinder注解,看看方法体内是否调用dataBinder.setDisallowedFields方法,如果发现此代码片段的引入,则在原来的黑名单中,添加{"class.","Class. ",". class.", ".Class."}。 (注:如果此代码片段使用较多,需要每个地方都追加)

  1. 在应用系统的项目包下新建以下全局类,并保证这个类被Spring 加载到(推荐在Controller 所在的包中添加).完成类添加后,需对项目进行重新编译打包和功能验证测试。并重新发布项目。
    import org.springframework.core.annotation.Order;
    import org.springframework.web.bind.WebDataBinder;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.InitBinder;
    @ControllerAdvice
    @Order(10000)
    public class GlobalControllerAdvice{ 
     @InitBinder
     public void setAllowedFields(webdataBinder dataBinder){
         String[]abd=new string[]{"class.*","Class.*","*.class.*","*.Class.*"};
         dataBinder.setDisallowedFields(abd);
     }
    }
    

补丁分析

https://github.com/spring-projects/spring-framework/commit/afbff391d8299034cd98af968981504b6ca7b38c
2022年3月31日18:31:58
Refine PropertyDescriptor filtering
补丁很简单,当beanClass是class.Class时,只允许添加name属性。
并且如果属性是ClassLoaderProtectionDomain,也会被忽略。

参考链接

class.classLoader.resources.dirCon想·text.docBase
CVE-2010-1622 http://rui0.cn/archives/1158
http://xiaobaoqiu.github.io/blog/2014/12/30/tomcat-access-logpei-zhi/
https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html
https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement

其他注意事项

利用需要找控制器方法入参是一个非基础类,比如入参是String则无法触发参数绑定。

点击收藏 | 3 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖