写点什么

【漏洞分析】jdk9+Spring 及其衍生框架

  • 2022 年 4 月 19 日
  • 本文字数:5174 字

    阅读完需:约 17 分钟

一. 漏洞利用条件

jdk9+

Spring 及其衍生框架

使用 tomcat 部署 spring 项目

使用了 POJO 参数绑定

Spring Framework 5.3.X < 5.3.18 、2.X < 5.2.20 或者其他版本

二. 漏洞分析

通过 API Introspector. getBeanInfo 可以获取到 POJO 的基类 Object.class 的属性 class,进一步可以获取到 Class.class 的其他属性,其中就包括了 classloader,再利用获取到的属性构造利用链,这次爆出来的漏洞既然是绕过,那么原理应该也差不多,首先先搭建环境,构造一个简单的 POJO:


public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
复制代码


再写个简单的 controller:


@RequestMapping("/test")
public String test(User user){
System.out.println(user.getName());
return "hello spring-mvc";
}
复制代码


发送 get 请求:http://localhost:8080/test?name=test 即可完成一次简单的数据绑定。


在开始调试分析之前,首先需要对 spring 的数据绑定体系机构有个简单的了解,其中涉及到一个关键类 org.springframework.validation.DataBinder 类,DataBinder 类实现了 TypeConverter 和 PropertyEditorRegistry 接口,作用主要是把字符串形式的参数转换成服务端真正需要的类型的转换,同时还有校验功能,其中有如下这些属性:


@Nullable
private final Object target;//需要数据绑定的对象
private final String objectName;//给对象起得名字默认target
@Nullable
private AbstractPropertyBindingResult bindingResult;//数据绑定后的结果
@Nullable
private SimpleTypeConverter typeConverter;//当target!=null时不会用到
private boolean ignoreUnknownFields = true;//忽略target不存在的属性,作用于PropertyAccessor的setPropertyValues()方法
private boolean ignoreInvalidFields = false;//忽略target不能访问的属性
private boolean autoGrowNestedPaths = true;//当嵌套属性为空时,是否可以实例化该属性
private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT;//对于集合类型容量的最大值
@Nullable
private String[] allowedFields;//允许数据绑定的资源
@Nullable
private String[] disallowedFields;//不允许的
@Nullable
private String[] requiredFields;//数据绑定必须存在的字段
@Nullable
private ConversionService conversionService;//为getPropertyAccessor().setConversionService(conversionService);
@Nullable
private MessageCodesResolver messageCodesResolver;//同bindingResult的
private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
private final List<Validator> validators = new ArrayList<>();//自定义数据校验器
复制代码


其中 bindingResult 是 BeanPropertyBindingResult 的实例,内部会持有一个 BeanWrapperImpl。


bind()是数据绑定对象的核心方法:将给定的属性值绑定到此绑定程序的目标,源码如下:


**public**void bind(PropertyValues pvs) {MutablePropertyValues mpvs = pvs **instanceof**MutablePropertyValues ? (MutablePropertyValues)pvs : **new**MutablePropertyValues(pvs);**this**.doBind(mpvs);}**protected**void doBind(MutablePropertyValues mpvs) {**this**.checkAllowedFields(mpvs);**this**.checkRequiredFields(mpvs);**this**.applyPropertyValues(mpvs);}
复制代码


再来看看 DataBinder 类的继承关系,DataBinder 有一个子类 WebDataBinder,是一个特殊的 DataBinder,用于从 Web 请求参数到 JavaBean 对象的数据绑定,而 WebDataBinder 的子类 ServletRequestDataBinder 用于执行从 servlet 请求参数到 JavaBeans 的数据绑定,包括对 multipart 文件的支持。


【一>所有资源获取<一】1、很多已经买不到的绝版电子书 2、安全大厂内部的培训资料 3、全套工具包 4、100 份 src 源码技术文档 5、网络安全基础入门、Linux、web 安全、攻防方面的视频 6、应急响应笔记 7、 网络安全学习路线 8、ctf 夺旗赛解析 9、WEB 安全入门笔记



在普通的 Controller 实现参数绑定的过程中自动实例化一个 ServletRequestDataBinder,在客户端请求的过程中使用当前的 ServletRequest 作为参数调用 bind()方法,于是可以来到这个地方下断点,这个过程中会调用到最上级的 DataBinder 类的 dobind()方法,从而调用到 DataBinder 的 applyPropertyValues 方法:


protected void applyPropertyValues(MutablePropertyValues mpvs) {try {this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());} catch (PropertyBatchUpdateException var7) {PropertyAccessException[] var3 = var7.getPropertyAccessExceptions();int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {PropertyAccessException pae = var3[var5];this.getBindingErrorProcessor().processPropertyAccessException(pae, this.getInternalBindingResult());}}
}
复制代码


applyPropertyValues()方法主要是使用 resultBinding 对象内的 BeanWraperImpl 对象完成属性的赋值操作。



后续会调用到。org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath



跟进AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath。


**protected**AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);**if**(pos > -1) {String nestedProperty = propertyPath.substring(0, pos);String nestedPath = propertyPath.substring(pos + 1);AbstractNestablePropertyAccessor nestedPa = **this**.getNestedPropertyAccessor(nestedProperty);**return**nestedPa.getPropertyAccessorForPropertyPath(nestedPath);} **else**{**return****this**;}}
复制代码


这里如果传进来的 propertyPath 包含.符号,pos 则会赋值大于-1,具体的逻辑就不跟了,进入 if 语句后调用 getNestedPropertyAccessor 方法,之后经过如下的调用栈,来到 resultBinding 对象内的 BeanWraperImpl 对象的 getCachedIntrospectionResults 方法。




private CachedIntrospectionResults getCachedIntrospectionResults() {if (this.cachedIntrospectionResults == null) {this.cachedIntrospectionResults = CachedIntrospectionResults.forClass(this.getWrappedClass());}
return this.cachedIntrospectionResults;}
复制代码


CachedIntrospectionResults 缓存了所有的 bean 中属性的信息,通过调试最后 return 的 cachedIntrospectionResults 变量可以看到,能够获取到的 PropertyDescriptor 属性描述器不仅仅有 name,还有关键的 class 属性。



也可以在本地新建一个测试类来获取 user 这个 bean 的属性,如下:



至此,我们还需要了解到怎么去绕过 CachedIntrospectionResults 中的黑名单,看到 CachedIntrospectionResults 的构造方法。



**for**(int var5 = 0; var5 < var4; ++var5) {PropertyDescriptor pd = var3[var5];**if**(Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())) {**if**(logger.isTraceEnabled()) {logger.trace("Found bean property '" + pd.getName() + "'" + (pd.getPropertyType() != **null**? " of type [" + pd.getPropertyType().getName() + "]" : "") + (pd.getPropertyEditorClass() != **null**? "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));}
pd = **this**.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);**this**.propertyDescriptorCache.put(pd.getName(), pd);}}
复制代码


在第一次获取 Bean 的属性信息过程中,会初始化 CachedIntrospectionResults 从而去调用到其构造方法,但其中有个 classLoader 和 protectionDomain 的黑名单,导致于在所有 jdk 版本下面都不能直接去通过 class 属性中的 classloader 进行漏洞利用,所以到这里即便能够操作从 bean 中获得的动态 class,也无法进行进一步利用。


绕过方法就是利用 jdk9+的新特性,也就是 module 机制,简称模块化系统,在 jdk9+中 Class 类有一个名为 getModule()的新方法,它返回该类作为其成员的模块引用,而包含的模块引用当中就有 classloader,如下:



于是可以通过 class 中的 module 去间接获取 classloader,使 CachedIntrospectionResults 初始化时的黑名单无效化。


后面的利用思路,就是去思考能利用哪些可控的属性去完成漏洞利用,首先去枚举都有哪些属性,这里贴个小脚本:



<%!**public**void processClass(Object instance, javax.servlet.jsp.JspWriter out, java.util.HashSet set, String poc){**try**{Class<?> c = instance.getClass();set.add(instance);Method[] allMethods = c.getMethods();**for**(Method m : allMethods) {**if**(!m.getName().startsWith("set")) {**continue**;}**if**(!m.toGenericString().startsWith("public")) {**continue**;}Class<?>[] pType = m.getParameterTypes();**if**(pType.length!=1) **continue**;
**if**(pType[0].getName().equals("java.lang.String")||pType[0].getName().equals("boolean")||pType[0].getName().equals("int")){String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);out.print(poc+"."+fieldName + "<br>");}}**for**(Method m : allMethods) {**if**(!m.getName().startsWith("get")) {**continue**;}**if**(!m.toGenericString().startsWith("public")) {**continue**;}Class<?>[] pType = m.getParameterTypes();**if**(pType.length!=0) **continue**;**if**(m.getReturnType() == Void.TYPE) **continue**;Object o = m.invoke(instance);**if**(o!=**null**){**if**(set.contains(o)) **continue**;processClass(o,out, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));}}} **catch**(java.io.IOException x) {x.printStackTrace();} **catch**(java.lang.IllegalAccessException x) {x.printStackTrace();} **catch**(java.lang.reflect.InvocationTargetException x) {x.printStackTrace();}}%><%java.util.HashSet set = **new**java.util.HashSet<Object>();String poc = "class.module.classLoader";User user = **new**User();processClass(user.getClass().getModule().getClassLoader(),out,set,poc);%>
复制代码


这段脚本只获取了 int、string 与 boolean 这些基本类型参数的属性,访问得到如下:



枚举出来大概有两百八多个属性,在这些属性当中有几个控制着在 tomcat 上生成的 access log 的文件名,其默认值如下:


class.module.classLoader.resources.context.parent.pipeline.first.directory =logs//将放置由此阀创建的日志文件的目录的绝对路径名或相对路径名。class.module.classLoader.resources.context.parent.pipeline.first.prefix =localhost_access_log//前缀添加到每个日志文件名称的开头class.module.classLoader.resources.context.parent.pipeline.first.suffix = .txt//后缀添加到每个日志文件名称的末尾class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat =.yyyy-mm-dd//日志文件名中的自定义日期格式class.module.classLoader.resources.context.parent.pipeline.first.pattern//一种格式化布局,用于标识要记录的请求和响应中的各种信息字段,或单词 common或combined选择标准格式
复制代码


其中比较值得注意的是其中的 pattern,在 tomcat 中其属性的值由文字文本字符串组成,与前缀为“%”字符的模式标识符组合,还支持从 cookie,传入头,传出响应头,Session 或 ServletRequest 中的其他内容中写入信息,有如下模型:


%{xxx}i 传入请求头
%{xxx}o 用于传出响应头
%{xxx}c 对于特定的请求cookie
%{xxx}r xxx是ServletRequest中的一个属性
%{xxx}s xxx是HttpSession中的一个属性
复制代码


然后通过调试也可以观察出各属性默认值:



于是就可以构造请求包,将 log 日志文件后缀改为.jsp,在请求头加入标识符变量值在 pattern 中构造 webshell 内容,发送完 payload 后重新调试可发现已成功修改日志配置。



在实际生产环境中,如果是 tomcat 直接单独启动的话,可以直接控制写入相对路径为“./webapps/ROOT/”下即可正常访问 webshell。


三. 修复方式

目前官方已经发布了补丁,在最新版本 v5.3.18 和 v5.2.20 中已经完成了修复



修复过后 class 类中缓存的属性只包含以下这几个:



用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
【漏洞分析】jdk9+Spring及其衍生框架_网络安全_网络安全学海_InfoQ写作社区