还债系列-Java反序列化链条分析补全(1)-CC2&CC4

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

前言

已经有很长一段时间没有写博客了,摆烂摆久了,正好趁着最近入职,来复建学习一波了,之前我们只分析过cc链的第一条利用链,接下来我们会将所有的利用链依次分析,来还Java安全的债。

适用范围:
CommonCollections2 :commons-collections4:4.0
CommonCollections4 :commons-collections4:4.0

以下分析可能会非常详细,显得有些冗长,请见谅。毕竟是复习

调试相关

环境配置:

Java_8u72
ysoserial_0_6

调试ysoserial,我们可以通过查看pom.xml中的主类,来进行调试,如下图:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

这里给出的主类是GeneratePayload,因此我们查看该类需要哪些参数,才能指定调用我们对应的利用链即可,最后配置idea中的运行配置很简单:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

CC2分析

我们先查看ysoserial-cc2链的注释,查看整体的链条构成:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

忽略掉第一个部分,ObjectInputSteam类表示字节流,一般序列化后的对象均会被转化为该类,所以这个利用链最关键的地方是PriorityQueue这个类,我们查看该类的readObject方法:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

该类前面部分对字节流进行读取,然后利用readObject将字节流恢复为相应对象,这里我们能够猜想Queue这个类,应该是实现队列的类,一个队列里可能会存有多个字节流对象,因此后面的for循环遍历这个队列,对队列中的每个对象进行反序列化操作。

根据原程序注释内容,后面的触发点为TransformingComparator.compare方法,因此我们继续跟,关注heapify方法:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4
这里heapify方法,会将queue中的对象传入siftDown方法中,继续跟:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
这里存在一个简单的判断,决定了队列中的对象将被传入那个方法中,但由于PriorityQueue这个类,我们在当初构造payload时,就已经进行了初始化comparator的操作了,因此我们这里会进入siftDownUsingComparator方法:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
该方法体如下:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
这里再判断时,就会调用comparator.compare方法了,这就将TransformingComparator类链接起来,我们继续查看TransformingComparator的对应方法:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

这里使用了transformer对象的transformer方法,由类的整体构造,我们知道transformer属性是Transformer对象,该类是反序列化漏洞触发的关键类。

Transformer类分析

虽然前面有文章也分析过该类,但是这里还是再复习一下,Transformer这个类实际是一个接口,在调用时需要具体实现它,我们需要回到ysoserial中查看其实现类究竟为哪个类。

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

答案是InvokerTransformer这个类,我们查看该类实现的transformer方法:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

实际上该类是实现了一个反射执行的调用过程,该操作我们已经非常熟悉了,通过getClass找到该类,然后getMethod找到对应的方法,套用我们反射的命令执行,这里已经很明显了,显然我们需要传入Runtime类找到其exec方法,然后传入对于参数进行命令执行即可。

到这里其实CC2链便已经分析完成了,总结下来这个链条是利用TransformingComparator类链接Transformer。

ysoserial构造分析

实际上我们回到ysoserial的实际代码中,他并不是直接使用InvokeTransformer中封装的反射执行的方式来调用Runtime类,实际上CC2中的源代码是调用newTransformer方法:

同样我们回到ysoserial整体构造过程,便可以找到答案了
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

再进行动态调试,查看最后得到queue对象辅助理解:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
这里的queue,首元素其实是Templateslmpl,下面我们来阅读整体代码

    public Queue<Object> getObject(String command) throws Exception {
        Object templates = Gadgets.createTemplatesImpl(command);
        //将需要执行命令包装成TemplatesImpl
        InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        //根据InvokerTransformer类的构造方法,三个参数分别为
        //toString-方法名
        //Class[0]- 参数类型 - 空的Class数组,也就是不需要参数
        //Object[0]- 参数数组- 空的参数数组,不需要参数
        PriorityQueue<Object> queue = new PriorityQueue(2, new TransformingComparator(transformer));
        //根据该类构造函数
        //第一个参数可以理解为队列长度且必须大于1
        //第二个参数为比较器,这里就是TransformingComparator
        queue.add(1);
        queue.add(1);
        //填充无意义队列内容
        Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
        // 反射设置方法名为newTransformer
        Object[] queueArray = (Object[])Reflections.getFieldValue(queue, "queue");
        // 反射得到PriorityQueue类属性queue值
        queueArray[0] = templates;
        queueArray[1] = 1;
        // 修改queue属性的内容
        //queue[1]的内容其实可以不更改
        return queue;
        // 返回构造好的PriorityQueue类
    }

中间看起来有些没有太大意义的代码,但实际上都是有用的,主要是考虑到前面compare的逻辑以及必须提前填充一定长度的元素,否则无法直接修改元素值两个问题。

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

在进行填充元素,也就是调用add方法时,就会调用PriorityQueue中的比较器进行比较,这里实际上会运行填充元素.iMethodName(),所以我们在这里如果提前设置newTransformer方法,那么会导致运行1.newTransformer(),便会抛出异常,无法序列化,所以这里的iMethodName,必须是比较元素都有的方法,ysoserial选择的是toString。

下面还剩下一个问题,这里为什么会选择TemplatesImpl类,作为命令执行的入口,而不是直接使用Runtime类,在这之前的文章并没有说清楚这点,我们可以进行一下测试,稍微修改一下ysoserial的代码,如下:

public Queue<Object> getObject(final String command) throws Exception {
  final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
  Runtime rt = Runtime.getRuntime();
  final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
  queue.add(1);
  queue.add(1);
  Reflections.setFieldValue(transformer, "iMethodName", "exec");
  final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
  queueArray[0] = rt;
  queueArray[1] = 1;
  return queue;
}

这里其实invoke设置有瑕疵,但不影响说明的问题,上述代码不会是这边猜测的抛出exec方法无参的异常,而是无法序列化Runtime类

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

我们知道反序列化的条件之一是,需要类实现java.io.Serializable接口,但是很显然Runtime类没有实现该接口,我们无法直接将Runtime类带入反序列化的过程中,所以我们需要其他类,进行命令执行的操作。

顺着这个思路,同时结合Java的语言特性,便还有一个方法那就是字节码,我们知道Java代码的运行,需要将.java文件,编译为.class,也就是转化成为字节码,才能在JVM中运行,因此我们需要找到某个类,能够恢复出字节码,并加载到JVM中加载运行,这里ysoserial选择的类为TemplatesImpl。

TemplatesImpl类有下面几个特点:

  1. TemplatesImpl实现java.io.Serializable
    还债系列-Java反序列化链条分析补全(1)-CC2&CC4
  2. TemplatesImpl有loadClass和defineClass方法,有做过内存马分析的同学,应该不会陌生这两个方法,简单来说loadClass可以通过名字加载类,并创建实例,而defineClass则是直接从字节码中加载并创建实例。
  3. TemplatesImpl从字节码中加载类时,调用的是属性,而非其他给定传入的值,也就是说字节码可控,这个入口便是newTransformer()方法

newTransformer方法体如下:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

继续跟
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

这里直接取出_bytecodes属性中的内容,进行defineClass,从而导致字节数组中的类被恢复出来,并创建了对应实例,如果这里我们将Runtime.exec这个实例恢复出来,则可直接导致命令执行。

ysoserial是利用javaassist包进行字节码基本的操作,最主要的是后面前面部分可以当作javaassist使用范例,重点在于框起来的部分:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

这里设置了三个属性:

  1. _bytecodes是defineClass函数的参数,类的字节码从里面取出来并恢复,所以必须设置
  2. _name是进入defineClass的条件,必须不等于null,否则会直接返回,不会进行字节码恢复的操作
    还债系列-Java反序列化链条分析补全(1)-CC2&CC4
  3. _tfactory是保证defineTransletClasses顺利运行的条件,_tfactory为null无法调用loadClass
    还债系列-Java反序列化链条分析补全(1)-CC2&CC4

总结

整体流程可以看作下面这个流程图:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

CC4分析

首先先看这个链条,ysoserial是如何解释的,在源码中并没有将整个链条进行解释,只是说明了InvokerTransformer这个类,被替换掉了:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

那么我们就来看,这个类被替换成了什么类,答案是InstantiateTransformer,根据前文分析的,InvokerTransformer类在整个链条的作用是什么,答案是创建字节码加载类TemplatesImpl,InstantiateTransformer同样具有此特性,我们来看看他的transform方法体:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

最主要就是上面框起来的两行代码,这里直接通过获取input这个输入参数的构造方法,然后利用newInstance调用该构造方法进行类的实例化,因此这里显然我们的input参数必须是某个类,且构造函数的参数为TemplateImpl类,这个类为TrAxFilter类,我们后文再分析。

构造InstantiateTransformer时,需要考虑InstantiateTransformer的构造函数问题,此处需求两个参数,iParamTypes表示构造函数所需参数类型,iArgs为具体参数,先在InstantiateTransformer构造前填充好默认值,再使用反射进行设置对应值。

我们在分析整体ysoserial的构造流程时,自然会注意到下面一行代码:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

还是同样的,先来看看背后的源码,ConstantTransformer.transformer方法很简单,就是给啥返回啥:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

关键在于为什么需要TrAXFilter类,而非其他类,注意看TrAXFilter的构造函数:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

TrAXFilter的构造函数,会直接将TemplateImpl类进行初始化,这刚好符合InstantiateTransformer的transformer方法所需,再回顾一下:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

CC4链的构造问题

InstantiateTransformer怎么和TrAXFilter进行连接

我们会发现ysoserial在构造时,实际上利用类ChainedTransformer将两个不同的transformer进行连接,而非单纯的传入一个transformer数组进行比较,因此ChainedTransformer的transformer方法就是连接的关键了:

    public T transform(T object) {
        Transformer[] arr$ = this.iTransformers;
        //iTransformers为transformer数组
        int len$ = arr$.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            Transformer<? super T, ? extends T> iTransformer = arr$[i$];
            // 首先获得一个transformer,这里如果是第一位
            // 则运行i.transform(null)
            // 第一位时constTransformer,
            // 此时object等于TrAXFilter
            object = iTransformer.transform(object);
            // 进入下一次循环object有值,则运行
            // i.transformer(TrAXFilter)
        }

        return object;
    }

这也是为什么我们经常可以看到chainedTransformer的原因了,该类除了首元素外,将前一个元素的transformer结果,作为下一个元素的transformer参数进行链式调用,如下图:

还债系列-Java反序列化链条分析补全(1)-CC2&CC4

为什么不一开始填充TemplatesImpl

还债系列-Java反序列化链条分析补全(1)-CC2&CC4
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
之所以不直接填充好需要构造的值,是因为在构造整个链条时,最外层的queue类需要将元素进行添加,而添加过程中会调用compare方法,导致transformer方法被调用,如果这里直接填充,则会导致以下错误:
还债系列-Java反序列化链条分析补全(1)-CC2&CC4
还债系列-Java反序列化链条分析补全(1)-CC2&CC4

填充时的源码如下:

 public Queue<Object> getObject(final String command) throws Exception {
  Object templates = Gadgets.createTemplatesImpl(command);
  ConstantTransformer constant = new ConstantTransformer(String.class);
  Class[] paramTypes = new Class[] { templates.getClass() };
  Object[] args = new Object[] { templates };

  InstantiateTransformer instantiate = new InstantiateTransformer(
    paramTypes, args);
  paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
  args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

  ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
  PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
  Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
  return queue;
 }

这里不再粘贴大量源码,下面简单叙述一下过程:

  1. 必须保证queue中有元素,否则无法执行compare方法触发利用链条。
  2. compare方法必须传入两个对象,在这里一个为ConstantTransformer,另一个为InstantiateTransformer。
  3. compare方法会调用transformer方法,在这条利用链构造时,如果添加队列的话,则是ChainedTransformer.transformer(1)。
  4. 序列化时根据之前分析,会调用1.getConstructor(iParamTypes),所以如果直接将TemplatesImpl的构造参数,则会抛出无法找到构造方法的异常,因此这里只能先给定一个大家都有的默认值,待完成构造后再使用反射进行修改。

参考链接

Java安全之Jdk7u21链分析
Java反序列化利用链挖掘之CommonsCollections2,4,8
Java动态加载字节码

赞 (0)

评论区

评论一下~


40+11=?

暂无评论,要不来一发?

回到顶部