对Java解序列化早作防备

对Java解序列化早作防备(译)
对Java解序列化早作防备
    本文是 IBM developerWorks中的 一篇文章,介绍了不使用加密与签章技术,如何防止对不可信数据输入的解序列化。(2013.01.18最后更新)

    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解序列化早作防备)