Java序列化使开发人员可以将Java对象保存为二进制格式,以便可以将其持久保存到文件中或通过网络传输。 远程方法调用(RMI)使用序列化作为客户端和服务器之间的通信介质。 当服务从客户端接受二进制数据并反序列化输入以构造Java实例时,可能会出现几个安全问题。 本文重点介绍其中之一:攻击者可以序列化另一个类的实例,并将其发送到服务。 然后,服务将反序列化恶意对象,并且很可能将其强制转换为服务期望的合法类,从而引发异常。 但是,该异常可能为时已晚,无法确保数据安全。 本文解释了原因,并说明了如何实现安全的替代方案。 (有关与Java反序列化有关的其他安全问题的简要概述,请参见Other反序列化陷阱侧栏。)
弱势阶层
您的服务不应反序列化任意类的对象。 为什么不? 简短的答案是:因为您可能在服务器的类路径中具有易受攻击的类,攻击者可以利用这些类。 这些类包含使攻击者导致拒绝服务条件或在极端情况下注入任意代码的代码。
您可能认为这种攻击是不可能的,但是请考虑在典型服务器的类路径中可以找到多少个类。 它们不仅包括您自己的代码,还包括Java类库,第三方库以及任何中间件或框架库。 此外,类路径可能会在应用程序的整个生命周期中发生变化,或者响应于超出单个应用程序的系统环境变化而被修改。 当试图利用这种弱点时,攻击者可以通过发送多个序列化对象来组合多个操作。
我应该强调, 只有在以下情况下,该服务才会反序列化恶意对象:
- 恶意对象的类存在于服务器的类路径中。 攻击者不能简单地发送任何类的序列化对象,因为该服务将无法加载该类。
- 恶意对象的类是可序列化的或可外部化的。 (也就是说,服务器上的类必须实现
java.io.Serializable
接口或java.io.Externalizable
接口。)
同样,反序列化过程通过从序列化流中复制数据而无需调用构造函数来填充对象树。 因此,攻击者无法执行可序列化对象类的构造函数中的Java代码。
但是攻击者还有其他方法可以在服务器上执行某些代码。 每当JVM反序列化实现以下三个方法之一的类的对象时,JVM都会调用该方法并在其中执行代码:
-
readObject()
方法通常在无法使用标准序列化时(例如,需要设置transient
成员时readObject()
由开发人员使用。 -
readResolve()
方法,通常用于序列化单例实例。 -
readExternal()
方法,用于可外部化的对象。
因此,如果您的类路径中有使用这些方法之一的类,则必须意识到攻击者可以远程调用这些方法。 过去曾使用过这种攻击方式来破坏Applet沙箱(请参阅参考资料 )。 同样的技术也可以应用于服务器。
请继续阅读以了解如何仅允许对您期望为服务提供的类进行反序列化。
Java序列化二进制格式
序列化对象后,二进制数据将包含元数据(有关数据结构的信息,例如类名,成员数和成员类型)和数据本身。 我将以一个简单的Bicycle
类为例。 清单1中所示的类包含三个成员( id
, name
和nbrWheels
)及其对应的setter和getter:
清单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为零,具有一个轮子,并且是一个单轮自行车。
这里的重点是二进制格式包含某种标题,使您可以执行输入验证。
前瞻类验证
如清单3所示 ,读取流时,序列化对象的类描述出现在对象本身之前。 此结构使您可以实现自己的算法来读取类描述,并根据类名称决定是否继续读取流。 幸运的是,您可以使用钩子轻松完成此操作,该钩子Java提供了通常用于自定义类加载的Java钩子,即重写了resolveClass() method
。 这个钩子非常适合提供自定义验证,因为只要流包含意外类,就可以使用它来引发异常。 您需要子类化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.lang.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);
}
}
}
运行应用程序时,JVM会在尝试反序列化java.lang.File
对象之前引发异常,如图1所示:
图1.应用程序输出
结论
本文介绍了如何在流中发现意外的Java类后立即停止Java反序列化过程,而无需对新反序列化实例的成员执行加密,密封或简单的输入验证。 请参阅下载以获取示例的完整源代码。
请记住,整个对象树(带有其所有成员的根对象)是在反序列化期间构造的。 在更复杂的配置中,您可能需要允许对多个类进行反序列化。
翻译自: https://www.ibm.com/developerworks/java/library/se-lookahead/index.html