Java-десериализация с предварительной проверкой

Как защитить процесс десериализации непроверенных входных данных, не прибегая к шифрованию и изоляции

Когда для обмена информацией между клиентом и сервером используется Java™-сериализация, злоумышленники могут попытаться подменить легитимный сериализованный поток вредоносными данными. В статье объясняется природа этой угрозы и предлагается простой способ защиты от нее. Читатель узнает, как остановить процесс десериализации сразу же, как только в потоке будет обнаружен неожидаемый Java-класс.

Пьер Эрнст, этичный хакер, работающий в области бизнес-анализа, IBM

Фото Пьера ЭрнстаПьер Эрнст (Pierre Ernst) - старший сотрудник группы IBM Business Analytics Security Competency при лаборатории IBM в Оттаве (Канада). Бывший разработчик, ставший испытателем на проникновение, он отвечает за поиск уязвимостей в системе безопасности приложений IBM перед их выпуском. Его работа, сочетающая ручное тестирование с проверкой безопасности кода, дополняет автоматизированные сканеры уязвимостей. Пьер отвечает также за обучение разработчиков способам решения проблем безопасности.



13.05.2013

Java-сериализация позволяет разработчикам сохранить Java-объект в двоичном формате, так чтобы его можно было записать в файл или передать по сети. Сериализация используется как средство связи между клиентом и сервером при удаленном вызове метода (Remote Method Invocation - RMI). Когда служба принимает двоичные данные от клиента и десериализует их, создавая Java-экземпляр, могут возникнуть некоторые проблемы безопасности. Эта статья посвящена одной из них: злоумышленник может сериализовать и направить в службу экземпляр другого класса. Служба десериализует вредоносный объект и может выдать его за ожидаемый легитимный класс, что вызовет исключение. Однако это исключение может опоздать, и данные окажутся под угрозой. В предлагаемой статье объясняется, почему, и предлагается безопасная альтернатива. (См. врезку Другие ловушки десериализации с кратким обзором других проблем безопасности, связанных с Java-десериализацией).

Другие ловушки десериализации

Процесс десериализации подвержен еще трем опасностям.

  • Злоумышленник может "подслушать" сообщение и перехватить конфиденциальные данные. Для предотвращения атак этого типа можно использовать Transport Layer Security (TLS).
  • Злоумышленник может подменить данные, легитимно сериализованные клиентским приложением, и нарушить бизнес-логику службы. Как и в случае других служб, на сервере должна выполняться проверка входных данных, даже если аналогичная проверка уже выполнена на стороне клиента. В этом сценарии эффективной контрмерой также может служить изоляция объекта.
  • Злоумышленник может установить private-члены объекта, которые ведут себя не так, как задумали разработчики. Этим способом злоумышленник может изменить внутреннее состояние объекта. Решение отчасти заключается в том, чтобы пометить такие члены как transient.

Дальнейшее обсуждение этих проблем и соответствующих контрмер выходит за рамки этой статьи.

Уязвимые классы

Службы не должны десериализовать объекты произвольных классов. Почему? Краткий ответ: потому что в classpath сервера могут оказаться уязвимые классы, которыми воспользуется злоумышленник. Эти классы могут содержать код, который позволит вызвать состояние "отказ в обслуживании" или — в крайних случаях — подмешать произвольный код.

Если вы уверены, что такие атаки невозможны, подумайте о том, как много классов могут находиться в classpath типичного сервера. Это не только ваш собственный код, но и библиотека классов Java, сторонние библиотеки и любые библиотеки промежуточного ПО или инфраструктуры. Кроме того, classpath может изменить срок действия приложения или изменяться в ответ на внешние изменения в системе, выходящие за рамки одного приложения. Пытаясь использовать эти уязвимости, злоумышленник может объединить несколько операций, отправив несколько сериализованных объектов.

Следует подчеркнуть, что служба десериализует вредоносный объект только в том случае, если:

  • класс вредоносного объекта присутствует в classpath сервера. Злоумышленник не может просто отправить сериализованный объект любого класса, потому что служба будет не в состоянии загрузить этот класс;
  • класс вредоносного объекта ― сериализуемый или экстернализуемый. (То есть, класс на сервере должен реализовывать интерфейс java.io.Serializable или интерфейс java.io.Externalizable.)

Кроме того, процесс десериализации заполняет дерево объектов, копируя данные из сериализованного потока без вызова конструктора. Поэтому злоумышленник не сможет выполнить Java-код, находящийся внутри конструктора сериализуемого класса объекта.

Но у него есть другие способы выполнения некоторого кода на сервере. JVM вызывает метод и выполняет код внутри него, когда десериализует объект того класса, который реализует один из следующих трех методов:

  • метод readObject(), обычно используемый разработчиками, когда нельзя применить стандартную сериализацию, например, если нужно установить transient-член;
  • метод readResolve(), обычно используемый для сериализации singleton-экземпляров;
  • метод readExternal(), используемый для экстернализуемых объектов.

Так что если в classpath есть классы, использующие любой из этих методов, нужно иметь в виду, что злоумышленник может вызывать эти методы дистанционно. В свое время этот вид атак использовался для взлома песочницы Applet (см. раздел Ресурсы); тот же метод можно применить и против сервера.

Ниже будет показано, как разрешить десериализацию только того класса (или классов), который ожидается данной службой.


Двоичный формат Java-сериализации

Белый список

Даже если вы абсолютно уверены, что служба защищена от атак, обсуждаемых в этой статье, помните, что в целях безопасности всегда полезно проверить входные данные по списку известных допустимых значений (белому списку).

После того как объект сериализован, двоичные данные содержат как метаданные (информацию о структуре данных, такую как имя класса, количество и тип членов) и сами данные. В качестве примера возьмем простой класс Bicycle (велосипед). Этот класс, показанный в листинге 1, содержит три члена (id, name и nbrWheels) с соответствующими сеттерами и геттерами.

Листинг 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|

Применяя стандартизированный протокол Object Serialization Stream (см. раздел Ресурсы), можно увидеть подробности сериализованного объекта, показанные в листинге 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, его идентификатор равен нулю, у него одно колесо (wheel), и это одноколесный велосипед (unicycle).

Здесь важно, что двоичный формат содержит своего рода заголовки, которые позволяют выполнять проверку входных данных.


Предварительная проверка класса

Как видно из листинга 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);
    }

    /**
     * Десериализуются только экземпляры ожидаемого класса Bicycle
     */
    @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);
    }
}

Вызвав метод readObject() для своего экземпляра com.ibm.ba.scg.LookAheadDeserializer, вы предотвратите десериализацию неожидаемых объектов.

В качестве демонстрации в листинге 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);

        // Используем LookAheadObjectInputStream вместо InputStream
        ObjectInputStream ois = new LookAheadObjectInputStream(bais);

        Object obj = ois.readObject();
        ois.close();
        bais.close();
        return obj;
    }
	
    public static void main(String[] args) {
        try {
            // Сериализация экземпляра Bicycle
            byte[] serializedBicycle = serialize(new Bicycle(0, "Unicycle", 1));

            // Сериализация экземпляра File
            byte[] serializedFile = serialize(new File("Pierre Ernst"));

            // Десериализация экземпляра Bicycle (легитимный случай использования)
            Bicycle bicycle0 = (Bicycle) deserialize(serializedBicycle);
            System.out.println(bicycle0.getName() + " has been deserialized.");

            // Десериализация экземпляра File (ошибка)
            Bicycle bicycle1 = (Bicycle) deserialize(serializedFile);

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

При запуске приложения, прежде чем попытаться десериализовать объект java.lang.File, JVM выдает исключение, как показано на рисунке 1.

Рисунок 1. Результат запуска приложения
Результат запуска приложения

Заключение

В этой статье показано, как остановить процесс десериализации Java при обнаружении в потоке неожидаемого Java-класса без необходимости выполнять шифрование, изоляцию или просто проверку членов только что десериализованного экземпляра во входных данных. (Полный исходный код примера для этой статьи приведен в разделе Загрузка.)

Помните, что при десериализации строится все дерево объектов (корневой объект со всеми членами). В более сложных конфигурациях может потребоваться разрешение десериализации нескольких классов.


Загрузка

ОписаниеИмяРазмер
Исходный код примеров для этой статьиlook-ahead-java-deserialization.src.zip4 КБ

Ресурсы

  • Оригинал статьи: Look-ahead Java deserialization.
  • Пять секретов... сериализации объектов Java (developerWorks, апрель 2010 г.): статья, посвященная некоторым вопросам безопасности, связанным с сериализацией.
  • Спецификация сериализации объектов Java: см. раздел Object Serialization Stream Protocol и приложение Security in Object Serialization Спецификации.
  • Java Remote Method Protocol: JRMP, протокол для удаленных вызовов Java-Java с использованием сериализации.
  • Безопасность Java, глава 2, раздел 2.1.1: о безопасности процессов сериализации/десериализации.
  • Интересный случай взлома песочницы JRE (CVE-2012-0507): разновидность атаки, описанной в этой статье, использовалась для взлома песочницы Applet.
  • CWE-502: Десериализация ненадежных данных: перечень MITRE Common Weakness, содержащий все виды атак, описанных в этой статье.
  • Известные уязвимости, связанные с сериализацией Java:
    • CVE-2004-2540: readObject в JRE позволяет удаленным злоумышленникам вызывать отказ в обслуживании с помощью специальных сериализованных данных.
    • CVE-2008-5353: JRE неправильно применяет контекст объектов ZoneInfo при десериализации, что позволяет удаленным злоумышленникам несанкционированно выполнять апплеты и приложения в привилегированном контексте, как это продемонстрировано на «десериализации объектов Calendar».
    • CVE-2010-0094: Неизвестная уязвимость в JRE позволяет удаленным злоумышленникам нарушать конфиденциальность, целостность и готовность системы с помощью неизвестных атак, относящихся к десериализации объектов RMIConnectionImpl, вызывая Java-функции системного уровня с использованием загрузчика десериализуемого конструктора.
    • CVE-2011-3521: Неизвестная уязвимость в JRE позволяет ненадежным удаленным приложениям Java Web Start и Java-апплетам нарушать конфиденциальность, целостность и готовность системы с помощью неизвестных атак, связанных с десериализацией.
    • CVE-2012-0505: Неизвестная уязвимость в JRE позволяет ненадежным удаленным приложениям Java Web Start и Java-апплетам нарушать конфиденциальность, целостность и готовность системы с помощью неизвестных атак, связанных с сериализацией.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=929512
ArticleTitle=Java-десериализация с предварительной проверкой
publish-date=05132013