Переходим ко второй атаке, которая связана с двоичной сериализацией в Java. Мы, как ты помнишь, были несколько ограничены в возможностях: да, поменять данные объектов можно, а вот отправлять произвольные объекты и получить в итоге RCE (как бывает в случае других языков) можем лишь при определенных условиях :).
Java, как мы видим, берет на себя все необходимое для сериализации и десериализации. Но это не всегда удобно. Есть ряд ситуаций, когда программисту необходимо дополнить или изменить эти механизмы. Например, добавить шаги, выполняемые несериализуемым родителем класса в конструкторе с параметрами. Для этих целей программист может переопределить для класса методы readObject
и writeObject
и добавить туда необходимые команды.
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();
System.out.println("It happens");
}
Самый важный момент для нас с точки зрения атаки в том, что readObject
всегда вызывается раньше приведения типа (casting) объекта.
ObjectInputStream ois = new ObjectInputStream(fis);
Employee emp1 = (Employee) ois.readObject();
Другими словами, сначала будет вызван readObject
и создастся некий объект, а уже его попробуют привести к нужному классу.
Эти два факта нам дают вот что. В приложение мы можем отправлять любой объект, класс которого «известен» приложению (то есть в classpath приложения), и при этом будет вызван readObject
этого класса.
Правда, вторая необходимая для атаки часть зависит от конкретных уязвимостей. Нужен класс с уязвимым readObject
. Поясню на примере. Есть такая библиотека, как Apache Common Uploads, она используется для управления загрузкой файлов на сервер и очень распространена. В ней есть куча различных классов. Один из них — DiskFileItem
.
С его помощью можно добиться того, чтобы файл, загруженный пользователем в приложение, разместился во временной директории, а потом при необходимости был сериализован, перенесен на другую машину или кластер и воссоздан там при помощи десериализации. Это суперфича, так что патчить ее вряд ли будут.
Но получается, что класс DiskFileItem
сериализуем. Кроме того, у него переопределены writeObject
и readObject
. Код в readObject
очень размазан, так что приведу лишь его суть. В соответствии с указанной выше логикой, при десериализации должен быть создан временный файл и в него должен попасть контент из объекта. Важно и то, что путь до временного файла состоит из двух частей: путь к директории хранится в private переменной DiskFileItem
(repository
), а имя задается случайным образом в процессе readObject
. Суть же уязвимости библиотеки заключалась в том, что она «не беспокоилась» о null-байте в имени директории в repository. В repository мы могли указать полный путь до файла, а не только до директории (/any/path/in/OS/including.file.name\x00
), а добавляемое при readObject
случайное имя файла обрезалось бы уже нативными библиотеками ОС (для которых null-байт — конец строки).
Таким образом, если у нас есть приложение, которое читает сериализуемые данные, то мы можем отправить ему объект класса DiskFileItem
с переменной repository
, исправленной на произвольный путь (с private поможет предыдущая задачка). И в итоге при десериализации приложение выполнит readObject
класса DiskFileItem
и создаст файл с нашим контентом в произвольном месте. На практике это дает нам RCE.
Конечно, был выпущен патч, в котором readObject
при десериализации проверяет присутствие null-байта. Да и к тому же с какой-то из версий Java 1.7, саму возможность атаки с null-байтом прикрыли. Тем не менее остается возможность контролировать полный путь до директории, что тоже часто очень существенно.
Важнее всего то, что приложение ждет на вход объект определенного класса (например, Employee), а мы ему подаем DiskFileItem
и при этом из-за readObject
можем как-то влиять (атаковать) на саму ОС или приложение.
Если крупное приложение работает с сериализуемыми данными, то оно должно быть уверено, что ни в одном сериализуемом классе (из тех, что есть в classpath) нет readObject
с уязвимостями.
Я здесь сконцентрировал внимание на readObject
(writeObject
). Но есть еще readResolve
(writeReplace
), которая представляет аналогичный интерес. А также есть интерфейс Externalizable. Это дочерний класс Serializable, но с ним программист должен сам полностью описывать процесс сериализации и десериализации объекта.
Спасибо за внимание и успехов в познании нового!