java语言的前瞻性_前瞻性Java反序列化

java语言的前瞻性

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中所示的类包含三个成员( idnamenbrWheels )及其对应的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.应用程序输出
控制台输出的屏幕快照,显示一条消息,表明Unicycle对象已反序列化,并抛出java.io.InvalidClassException以进行未经授权的反序列化java.io.File的尝试

结论

本文介绍了如何在流中发现意外的Java类后立即停止Java反序列化过程,而无需对新反序列化实例的成员执行加密,密封或简单的输入验证。 请参阅下载以获取示例的完整源代码。

请记住,整个对象树(带有其所有成员的根对象)是在反序列化期间构造的。 在更复杂的配置中,您可能需要允许对多个类进行反序列化。


翻译自: https://www.ibm.com/developerworks/security/library/se-lookahead/index.html

java语言的前瞻性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值