对Java序列化早作防备(译)

标签: java 序列化 | 发表时间:2013-01-17 22:39 | 作者:Sha Jiang
出处:http://www.blogjava.net
对Java序列化早作防备
本文是 IBM developerWorks中的 一篇文章,介绍了不使用加密与签章技术,如何防止对不可信数据输入的解序列化。(2013.01.17最后更新)

    Java序列化允许开发者将Java对象保存为二进制格式,以便将该对象持久化到一个文件中或将其在网络中进行传递。远程方法调用(RMI)使用序列化作为客户端与服务器端之间的通信媒介。当服务从客户端接收二进制数据,以及将输入的数据进行解序列化去构造Java实例时,就会产生多种安全问题。本文关注其中一种问题:骇客可能序列化另一个类的实例,并将其传给服务程序。那么服务程序就会解序列化该恶意对象,并很可能将该对象强制转换为服务所期望得到的合法类型,而这将导致异常的发生。然而,该异常对于确保数据安全性则显得太晚了。本文解释了为什么要以及怎样去实现一种安全的序列化。

脆弱的类
    你的服务程序不可能反序列化任意类的对象。为什么不能呢?简单的回答是:因为在服务器端的类路径中可能存有被骇客利用的脆弱类。这些类所包含的代码为骇客造就了拒绝服务(DOS)的条件,或者--在极端情况下--会允许骇客注入任意代码。
    你可能会相信存在这种攻击的可能性,但考虑到一个典型的服务器端程序的类路径中存在太多的类,不仅包含你自己的代码,还包括Java核心类库,第三方的类库,以及其它的中间件或框架中的类库。另外,在应用程序的生命周期中,类路径可能会被改变,或者为了应对底层运行环境的变化,应用程序的类路径也可能被修改。当试图去利用这样的弱点时,通过传送多个序列化对象,骇客能够将这些操作组合到一块。
    我应该强调一下,仅当满足如下条件时,服务才会解序列化一个恶意对象:
    1. 恶意对象的类存在于服务器端的类路径中。骇客不可能随便地传递任意类的序列化对象,因为应用服务可能无法加载这个类。
    2. 恶意对象的类要么是可序列化的,要么是可外部化的。(即,服务器端的这个类要实现java.io.Serializable或java.io.Externalizable)

    另外,通过从序列化流中直接复制数据,在不调用构造器的情况下,解序列化操作就能产生对象树,所以骇客不可能执行序列化对象类的构造器中的Java代码。
    但骇客还有其它途径在服务器端去执行代码。无论JVM在何时去解序列化一个对象,都将实现如下三个方法中的一个,都将调用并执行该方法中的代码:
    1. 方法readObject(),当标准的序列化机制不适用时,开发者一般就会用到该方法。例如,当需要对transient成员变量进行赋值时。
    2. 方法readResolve(),一般用于序列化单例对象。
    3. 方法readExternal(),用于外部化对象。

    所以,如果在你的类路径中存在着使用上述方法的类,你就必须意识到骇客可能会在远程调用这些方法。此类攻击在过往曾被用于破坏Applet安全沙箱;同样地,相同的攻击技术也可用于服务器端应用。
    继续读下去,将会看到如何才能只允许应用服务对其期望的类的对象进行解序列化。

Java序列化二进制格式
    一个对象被序列化之后,二进制数据将包含有元数据(指与数据的结构相关的信息,例如类的名称,成员的数量,以及成员的类型),及对象数据本身。我将以一个简单的Bicycle类作为例子,如清单1所示,该类包含三个成员变量(id,name和nbrWheels)以及与之对应的set与get方法。
清单1. Bicycle类
package com.ibm.ba.scg.LookAheadDeserializer;

public class Bicycle implements java.io.Serializable {

    private static final long serialVersionUID = 5754104541168320730L;

    private int id;
    private String name;
    private int nbrWheels;

    public Bicycle(int id, String name, int nbrWheels) {
        this.id = id;
        this.name = name;
        this.nbrWheels = nbrWheels;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public int getNbrWheels() {
        return nbrWheels;
    }

    public void setNbrWheels(int nbrWheels) {
        this.nbrWheels = nbrWheels;
    }
}

    在一个清单1所示类的实例被序列化之后,其数据流如清单2所示:
清单2. Bicycle类的序列化流
000000: AC ED 00 05 73 72 00 2C 63 6F 6D 2E 69 62 6D 2E |········com.ibm.|
000016: 62 61 2E 73 63 67 2E 4C 6F 6F 6B 41 68 65 61 64 |ba.scg.LookAhead|
000032: 44 65 73 65 72 69 61 6C 69 7A 65 72 2E 42 69 63 |Deserializer.Bic|
000048: 79 63 6C 65 4F DA AF 97 F8 CC C0 DA 02 00 03 49 |ycle···········I|
000064: 00 02 69 64 49 00 09 6E 62 72 57 68 65 65 6C 73 |··idI··nbrWheels|
000080: 4C 00 04 6E 61 6D 65 74 00 12 4C 6A 61 76 61 2F |L··name···Ljava/|
000096: 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 70 00 00 |lang/String;····|
000112: 00 00 00 00 00 01 74 00 08 55 6E 69 63 79 63 6C |·········Unicycl|
000128: 65                                              |e|

    对上述数据应用标准的对象序列化流协议,你将看到如清单3所示的序列化对象:
清单3. 被序列化的Bicycle对象的细节
STREAM_MAGIC (2 bytes) 0xACED 
STREAM_VERSION (2 bytes) 5
newObject
    TC_OBJECT (1 byte) 0x73
    newClassDesc
        TC_CLASSDESC (1 byte) 0x72
        className
            length (2 bytes) 0x2C = 44
            text (59 bytes) com.ibm.ba.scg.LookAheadDeserializer.Bicycle
        serialVersionUID (8 bytes) 0x4FDAAF97F8CCC0DA = 5754104541168320730
        classDescInfo
            classDescFlags (1 byte) 0x02 = SC_SERIALIZABLE
            fields
                count (2 bytes) 3
                field[0]
                    primitiveDesc
                        prim_typecode (1 byte) I = integer
                        fieldName
                            length (2 bytes) 2
                            text (2 bytes) id
                field[1]
                    primitiveDesc
                        prim_typecode (1 byte) I = integer
                        fieldName
                            length (2 bytes) 9
                            text (9 bytes) nbrWheels
                field[2]
                    objectDesc
                        obj_typecode (1 byte) L = object
                        fieldName
                            length (2 bytes) 4
                            text (4 bytes)  name
                        className1
                            TC_STRING (1 byte) 0x74
                                length (2 bytes) 0x12 = 18
                                text (18 bytes) Ljava/lang/String;

            classAnnotation
                TC_ENDBLOCKDATA (1 byte) 0x78

            superClassDesc
                TC_NULL (1 byte) 0x70
    classdata[]
        classdata[0] (4 bytes) 0 = id
        classdata[1] (4 bytes) 1 = nbrWheels
        classdata[2]
            TC_STRING (1 byte) 0x74
            length (2 bytes) 8
            text (8 bytes) Unicycle

    从清单3中你可以看到该序列化对象的类型为com.ibm.ba.scg.LookAheadDeserializer.Bicycle,它的ID为0,只有一个轮子,即它是一个独轮车。
    重点是这个二进制格式包含一种文件头,这就允许你对输入进行校验。

类校验
    如你在清单3中所看到的,在读取该二进制流时,在对象本身出现之前,首先会看到该序列化对象的类型描述。这种结构就允许实现自己的算法去读取类型描述,并依靠类的名称去决定是否继续读取该序列化流。幸运地是,通过使用Java提供的一个常用于定制类加载的"钩子",你能很容易地实现该功能--即,覆盖resolveClass()方法。这个"钩子"方法非常适合用于提供定制的校验功能,无论序列化流何时包含了不被期望的类,你都可以用这个方法去抛出一个异常。你需要继承类java.io.ObjectInputStream,并覆盖其中的resolveClass()方法。清单4中的代码就利用该项技术确保只有Bicycle类的实例才可被解序列化。
清单4. 定制校验"钩子"程序
package com.ibm.ba.scg.LookAheadDeserializer;

import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

import com.ibm.ba.scg.LookAheadDeserializer.Bicycle;

public class LookAheadObjectInputStream extends ObjectInputStream {

    public LookAheadObjectInputStream(InputStream inputStream)
            throws IOException {
        super(inputStream);
    }

    /**
     * Only deserialize instances of our expected Bicycle class
     */
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,
            ClassNotFoundException {
        if (!desc.getName().equals(Bicycle.class.getName())) {
            throw new InvalidClassException(
                    "Unauthorized deserialization attempt",
                    desc.getName());
        }
        return super.resolveClass(desc);
    }
}

    通过对com.ibm.ba.scg.LookAheadDeserializer类的实例调用readObject()方法,就可以防止对不被期望的对象进行解序列化操作。
    作为一个示例应用程序,清单5序列化了两个对象--一个是期望的类(com.ibm.ba.scg.LookAheadDeserializer.Bicycle)的实例,另一个是不被期望的类(java.io.File)的实例--然后使用清单4中的定制校验"钩子"程序去尝试它们进行解序列化。
清单5. 使用定制的"钩子"程序
package com.ibm.ba.scg.LookAheadDeserializer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import com.ibm.ba.scg.LookAheadDeserializer.Bicycle;

public class LookAheadDeserializer {

    private static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        byte[] buffer = baos.toByteArray();
        oos.close();
        baos.close();
        return buffer;
    }

    private static Object deserialize(byte[] buffer) throws IOException,
            ClassNotFoundException {
        ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
        
        // We use LookAheadObjectInputStream instead of InputStream
        ObjectInputStream ois = new LookAheadObjectInputStream(bais);
        
        Object obj = ois.readObject();
        ois.close();
        bais.close();
        return obj;
    }
    
    public static void main(String[] args) {
        try {
            // Serialize a Bicycle instance
            byte[] serializedBicycle = serialize(new Bicycle(0, "Unicycle", 1));

            // Serialize a File instance
            byte[] serializedFile = serialize(new File("Pierre Ernst"));

            // Deserialize the Bicycle instance (legitimate use case)
            Bicycle bicycle0 = (Bicycle) deserialize(serializedBicycle);
            System.out.println(bicycle0.getName() + " has been deserialized.");

            // Deserialize the File instance (error case)
            Bicycle bicycle1 = (Bicycle) deserialize(serializedFile);

        } catch (Exception ex) {
            ex.printStackTrace(System.err);
        }
    }
}

当运行该应用程序时,在试图去java.io.File的对象进行解序列化之前,JVM就抛出异常,如图1所示:
图1. 应用程序输出


结论
    本文向你展示了在序列化流中发现不被期望的类之后,若不使用加密,签章,或简单的成员变量校验等手段,如何能尽快地停止Java解序列化操作。
    需求记住的是,整棵对象树(根对象,及其所有的成员对象)是在解序列化过程中进行组建的。在更为复杂的情况下,你可能必须允许更多的类可被解序列化。


本文链接

相关 [java 序列化] 推荐:

java序列化java.io.Externalizable

- - Java - 编程语言 - ITeye博客
这次我们讲的是控制对象的序列化和反序列化. 控制序列化就是有选择的序列化对象,而不是把对象的所以内容都序列化,前篇我们的例子中介绍了transit变量和类变量(static)不被序列化,现在我们还有一种更为灵活的控制对象序列化和反序列方法,可以在序列化过程中储存其他非this对象包含的数据. 我们现在再来介绍一个接口 java.io.Externalizable.

JAVA 反序列化攻击

- - OneAPM 博客
Java 反序列化攻击漏洞由. FoxGlove 的最近的一篇博文爆出,该漏洞可以被黑客利用向服务器上传恶意脚本,或者远程执行命令. 由于目前发现该漏洞存在于 Apache commons-collections, Apache xalan 和 Groovy 包中,也就意味着使用了这些包的服务器(目前发现有WebSphere, WebLogic,JBoss),第三方框架(Spring,Groovy),第三方应用(Jenkins),以及依赖于这些服务器,框架或者直接/间接引用这些包的应用都会受到威胁,这样的应用的数量会以百万计.

java序列化与反序列化以及浅谈一下hadoop的序列化

- - CSDN博客云计算推荐文章
1、什么是序列化和反序列化. 神马是序列化呢,序列化就是把 内存中的对象的状态信息,转换成 字节序列以便于存储(持久化)和网络传输. (网络传输和硬盘持久化,你没有一定的手段来进行辨别这些字节序列是什么东西,有什么信息,这些字节序列就是垃圾). 反序列化就是将收到 字节序列或者是硬盘的持久化数据,转换成 内存中的对象.

讲解Java中的序列化

- - IT江湖
serialVersionUID的作用. serialVersionUID适用于JAVA的序列化机制. 简单来说,Java的序列化机制是通过判断类的serialVersionUID来验证版本一致性的. 在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException.

理解Java对象序列化

- - 博客 - 伯乐在线
来源: jiangshapub 的博客( @jiangshapub). 关于Java序列化的文章早已是汗牛充栋了,本文是对我个人过往学习,理解及应用Java序列化的一个总结. 此文内容涉及Java序列化的基本原理,以及多种方法对序列化形式进行定制. 在撰写本文时,既参考了 Thinking in Java, Effective Java,JavaWorld,developerWorks中的相关文章和其它网络资料,也加入了自己的实践经验与理解,文、码并茂,希望对大家有所帮助.

对Java序列化早作防备(译)

- - BlogJava_首页
本文是 IBM developerWorks中的 一篇文章,介绍了不使用加密与签章技术,如何防止对不可信数据输入的解序列化. (2013.01.17最后更新).     Java序列化允许开发者将Java对象保存为二进制格式,以便将该对象持久化到一个文件中或将其在网络中进行传递. 远程方法调用(RMI)使用序列化作为客户端与服务器端之间的通信媒介.

java 序列化 serializable接口 serialVersionUID

- - 互联网 - ITeye博客
如果一个类实现了serializable接口,那么就会要求一个serialVersionUID. 序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类. 如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException.

Java序列化理解与总结

- - CSDN博客编程语言推荐文章
Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长. 但在现实应用中,就可能要求在JVM停止运行之后能够保存指定的对象,并在将来重新读取被保存的对象. Java对象序列化就能够帮助我们实现该功能.

java反序列化工具ysoserial分析

- - WooYun知识库
关于java反序列化漏洞的原理分析,基本都是在分析使用 Apache Commons Collections这个库,造成的反序列化问题. 然而,在下载老外的 ysoserial工具并仔细看看后,我发现了许多值得学习的知识. 不同反序列化 payload玩法. 灵活运用了反射机制和动态代理机制构造POC.

Java下利用Jackson进行JSON解析和序列化

- - 企业架构 - ITeye博客
Java下常见的Json类库有Gson、JSON-lib和Jackson等,Jackson相对来说比较高效,在项目中主要使用Jackson进行JSON和Java对象转换,下面给出一些Jackson的JSON操作方法. 首先去官网下载Jackson工具包,下载地址http://wiki.fasterxml.com/JacksonDownload.