副标题#e#
本文是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; } }
#p#副标题#e#
在一个清单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所示的序列化工具:
#p#分页标题#e#
清单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()要领,就可以防备对不被期望的工具举办解序列化操纵。
#p#分页标题#e#
作为一个示例应用措施,清单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解序列化操纵。
需要记着的是,整棵工具树(根工具,及其所有的成员工具)是在解序列化进程中举办组建的。在更为巨大的环境下,你大概必需答允更多的类可被解序列化。