В качестве большой темы в этот раз я решил выбрать двоичную сериализацию в Java и две связанные с ней атаки. Из-за специфики не хотелось бы разделять их. Однако начнем с теории.
Статья в Википедии гласит: «Сериализация — процесс перевода какой-либо структуры данных в последовательность битов». Эту последовательность можно передать по сети или сохранить в файл, после чего полностью с помощью десериализации восстановить в изначальное состояние.
Идея тут проста. С сериализацией мы можем сохранить значения сложных типов данных. Например, массив объектов произвольного класса, который был создан в программе. Итоговый формат данных зависит от вида сериализации. Есть двоичные, есть такие, в результате работы которых создается документ XML. Фактически формат может быть любой. Мы же рассмотрим нативную двоичную сериализацию в Java.
Давай представим, что у нас есть класс Employee.
public class Employee implements Serializable {
private int employeeId;
private String employeeName;
public int getEmployeeId() {
return employeeId;
}
public Employee (int employeeId, String employeeName) {
this.employeeId = employeeId;
this.employeeName = employeeName;
}
public String getEmployeeName() {
return employeeName;
}
}
У него есть две переменные (свойства) — id
и имя. Обе приватные (private), то есть вне класса они недоступны. Еще есть конструктор, который выставляет значения переменным, и два метода для доступа к переменным (начинаются с get).
Для того чтобы этот класс можно было сериализовать, нужно добавить интерфейс-маркер Serializable (implements Serializable).
Теперь мы спокойно можем создать объект этого класса в нашей программе и сохранить в файл.
Employee emp1 = new Employee(1, "admin");
fos = new FileOutputStream("test1.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(emp1);
Вот и все — реализация сохранения последовательности ложится на плечи Java. Для чтения объекта мы должны сделать следующее.
fis = new FileInputStream("test1.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
Employee emp1 = (Employee) ois.readObject();
С учетом того что FileInputStream и FileOutputStream используются лишь для сохранения объектов в файлы, на практике для сохранения и восстановления сериализованного объекта нам требуется всего две строки.
Здесь примерно такая же ситуация, как с наследованием. Все, что может быть сериализовано, сериализуется. Например, объекты внутренних и родительских классов.
Вот как устроен итоговый формат. Сначала идет magic-string ACED
(плюс некий номер версии) обозначающая сериализованный объект. Дальше идет толстое и полное описание каждого из свойств класса, и потом сами значения свойств (полей) объекта.
Важно, что сериализуются именно объекты. Сюда входят все значения свойств самого объекта (private, public, protected), а временные переменные (к примеру, какого-нибудь из методов) и код самих методов не входит.
Для удобства просмотра сериализованных объектов можно воспользоваться тулзой jdeserialize. Она помогает смотреть, какие поля хранятся в объекте и какие у них значения. Иногда это может быть полезно, в особенности когда нет доступа к самому классу объекта.
Первая атака заключается в следующем. Иногда сериализацию используют для передачи данных. Например, есть некое веб-приложение, клиенты подключаются к нему по HTTP и в качестве запросов передаются сериализованные объекты. Конечно, для подключения требуется специальное клиентское ПО: реализовано оно чаще всего в виде Java-апплета. В интернете такое встречается редко, а вот в корпоративном ПО это более распространенное решение.
Атака представляет собой обычный parameter tampering. Мы пытаемся подменить значения свойств объектов, посылаемых от клиента на сервер, и таким образом достичь обхода тех или иных ограничений. В данном случае мы надеемся на то, что разработчики воспринимают сериализацию как нечто более доверенное, чем обычные HTTP-запросы. Так может показаться из-за более «страшного» формата данных и из-за наличия контроля доступа к полям (private, public). В нашем примере два поля (employeeId
и employeeName
) — это private. В рамках виртуальной машины Java ничто, кроме самого объекта, не сможет их менять.
Для нас как для атакующих обе этих причины не преграда. Мы спокойно можем изменить любое из значений, так как мы работаем с «сырыми» данными. Фактически нам требуется перехватить HTTP-запрос с объектом от клиента к серверу и поменять несколько байтов информации (в Burp во вкладке Hex, например). Сервер десериализует ответ и получит уже измененные данные. К сожалению, этот способ годится только в случае самых простых объектов.
Это связано со сложностью самого формата данных. Поля имеют разную длину, и при изменениях, сделанных вручную, испортить объект проще простого. Поэтому нам требуется автоматизировать процесс. В этом поможет специальный плагин для Burp Suite — BurpJDSer.
Плагин работает следующим образом. Когда сериализованный объект проходит через Burp в запросе, плагин отправляет этот объект в специальную библиотеку XStream. Это библиотека позволяет сериализовать объекты в виде XML. Его видно в Burp и можно менять. При отправке запроса далее XStream сериализует его обратно в двоичный вид. Что вдвойне приятно, плагин поддерживает все основные возможности Burp — и proxy, и intruder, и repeater.
Для корректной работы плагина необходимо добавить его, xstream.jar и, самое главное, файл класса (jar), который должен быть сериализован в classpath.
java -classpath burp.jar;burpjdser.jar;xstream-1.4.2.jar;[client_jar] burp.StartBurp
Я отметил возможность ручного изменения объектов (hex-редактором), потому что этот способ не требует доступа к самому классу объекта, а вот в случае с XSteam это необходимо.