Java-десеріалізація з попередньою перевіркою
Як захистити процес десеріалізації неперевірених вхідних даних, не вдаючись до шифрування та ізоляції
Інші пастки десеріалізації
Процес десеріалізації схильний ще до трьох небезпек.
- Зловмисник може "підслухати" повідомлення та перехопити конфіденційні дані. Для запобігання атакам цього типу можна використовувати Transport Layer Security (TLS).
- Зловмисник може підмінити дані, легітимно серіалізовані клієнтським додатком, та порушити бізнес-логіку служби. Як і в інших службах, на сервері повинна виконуватися перевірка вхідних даних, навіть якщо аналогічна перевірка вже виконана на стороні клієнта. У цьому сценарії ефективним контрзаходом також може бути ізоляція об'єкта.
- Зловмисник може встановити private-члени об'єкта, які поводяться не так, як задумали розробники. У такий спосіб зловмисник може змінити внутрішній стан об'єкта. Рішення частково полягає в тому, щоб позначити такі члени як transient.
Подальше обговорення цих проблем та відповідних контрзаходів виходить за межі цієї статті.
Вразливі класи
Служби повинні десеріалізувати об'єкти довільних класів. Чому? Коротка відповідь: тому що в classpath сервері можуть виявитися вразливі класи, якими скористається зловмисник. Ці класи можуть містити код, який дозволить викликати стан "відмова в обслуговуванні" або в крайніх випадках підмішати довільний код.
Слід наголосити, що служба десеріалізує шкідливий об'єкт тільки в тому випадку, якщо:
- Клас шкідливого об'єкта присутня в класі сервера. Зловмисник не може просто відправити серіалізований об'єкт будь-якого класу, тому що служба буде не в змозі завантажити цейклас;
- клас шкідливого об'єкта - серіалізується або екстерналізується. (Тобто клас на сервері повинен реалізовувати інтерфейс java.io.Serializable або інтерфейс java.io.Externalizable .)
Крім того, процес десеріалізації заповнює дерево об'єктів, копіюючи дані із серіалізованого потоку без виклику конструктора. Тому зловмисник не зможе виконати Java-код, що знаходиться всередині конструктора класу об'єкта, що серіалізується.
Але він має інші способи виконання деякого коду на сервері. JVM викликає метод і виконує код усередині нього, коли десеріалізує об'єкт того класу, який реалізує один із наступних трьох методів:
- метод readObject() , який зазвичай використовується розробниками, коли не можна застосувати стандартну серіалізацію, наприклад, якщо потрібно встановити transient -член;
- метод readResolve() , який зазвичай використовується для серіалізації singleton-екземплярів;
- метод readExternal() , що використовується для об'єктів, що екстерналізуються.
Так що якщо в classpath є класи, які використовують будь-який з цих методів, потрібно мати на увазі, що зловмисник може викликати ці методи дистанційно. Свого часу цей вид атак використовувався для злому пісочниці Applet (див. розділ Ресурси); той самий метод можна застосувати проти сервера.
Нижче буде показано, як дозволити десеріалізацію лише того класу (чи класів), який очікується цією службою.
Двійковий формат Java-серіалізації
Білий список
Навіть якщо ви абсолютно впевнені, що служба захищена від атак, що обговорюються в цій статті, пам'ятайте, що з метою безпеки завжди корисно перевірити вхідні дані зі списку відомих допустимих значень (білий список).
Після того, як об'єкт серіалізований, двійкові дані містятьяк метадані (інформацію про структуру даних, таку як ім'я класу, кількість та тип членів) та самі дані. Як приклад візьмемо простий клас Bicycle (велосипед). Цей клас, показаний у лістингу 1, містить три члени ( id , name і nbrWheels ) з відповідними сеттерами та гетерами.
Лістинг 1. Клас Bicycle
Після серіалізації екземпляра класу, представленого у лістингу 1, потік даних виглядає як у лістингу 2.
Лістинг 2. Потік даних серіалізованого класу Bicycle
Використовуючи стандартизований протокол Object Serialization Stream (див. розділ Ресурси), можна побачити подробиці серіалізованого об'єкта, показані у лістингу 3.
Лістинг 3. Детальна інформація про серіалізований об'єкт Bicycle
З лістингу 3 видно, що це серіалізований об'єкт com.ibm.ba.scg.LookAheadDeserializer.Bicycle , його ідентифікатор дорівнює нулю, має одне колесо (wheel), і це одноколісний велосипед (unicycle).
Тут важливо, що двійковий формат містить свого роду заголовки, які дозволяють перевірити вхідні дані.
Попередня перевірка класу
Як видно з лістингу 3, під час читання потоку серіалізованому об'єкту передує опис класу. Ця структура дозволяє програмісту реалізувати свій власний алгоритм читання опису класу та залежно від імені класу вирішувати, чи слід продовжити читання потоку. На щастя, це легко робиться за допомогою Java хука, який зазвичай використовується для завантаження спеціальних класів — а саме, перевизначення методу resolveClass() . Цей хук ідеально підходить для виконання такої перевірки, тому що його можна використовувати для видачі винятку щоразу, коли потік містить несподіваний клас. Достатньо створити підкласjava.io.ObjectInputStream і перевизначити метод resolveClass() . У лістингу 4 цей метод використовується для того, щоб дозволити десеріалізацію тільки екземплярів класу Bicycle.
Лістинг 4. Хук попередньої перевірки
Викликавши метод readObject() для свого екземпляра com.ibm.ba.scg.LookAheadDeserializer , ви запобігаєте десеріалізації неочікуваних об'єктів.
В якості демонстрації в лістингу 5 серіалізуються два об'єкти - екземпляр очікуваного класу (com.ibm.ba.scg.LookAheadDeserializer.Bicycle) і неочікуваний об'єкт (примірник класу java.lang.File) - з подальшою спробою десеріалізувати їх з використанням лістингу 4.
Лістинг 5. Десеріалізація двох об'єктів за допомогою хука попередньої перевірки
При запуску програми, перш ніж спробувати десеріалізувати об'єкт java.lang.File, JVM видає виняток, як показано на малюнку 1.
Рисунок 1. Результат запуску програми
Висновок
У цій статті показано, як зупинити процес десеріалізації Java при виявленні в потоці неочікуваного Java-класу без необхідності виконувати шифрування, ізоляцію або просто перевірку членів щойно десеріалізованого екземпляра у вхідних даних. (Повний вихідний код прикладу цієї статті наведено в розділі Завантаження.)
Пам'ятайте, що десеріалізація будує все дерево об'єктів (кореневий об'єкт з усіма членами). У складніших конфігураціях може знадобитися дозвіл десеріалізації кількох класів.
Ресурси для скачування
- цей контент у PDF
- Вихідний код прикладів для цієї статті (look-ahead-java-deserialization.src.zip 4 КБ)