In English: https://blog.deteact.com/dql-injection
В современных веб-приложениях всё реже бывают инъекции, все используют подготовленные запросы и ORM, но мы до сих пор встречаем инъекции при пентестах.
Особый интерес представляют диалекты SQL, встроенные в ORM-библиотеки. Это дополнительная абстракция, которая также подвержена инъекциям, при этом могут возникать уязвимости и при трансляции выражений из диалекта в конкретную реализацию SQL.
Введение
ORM — это библиотека, связывающая объекты и их атрибуты в коде с таблицами и полями в базе данных.
Абстракция ORM позволяет представлять реляционные таблицы БД в виде обычных объектов и обращаться с ними, как с объектами.
ORM позволяет разделить задачи базы данных и приложения, так что программист может даже не писать SQL-запросы, а просто выполнять действия с объектами, и соответствующие SQL-запросы будут сгенерированы ORM-библиотекой.
Для чего используют ORM?
Понятно, что отсутствие необходимости вручную писать сотни SQL-запрос упрощает процесс разработки, особенно в крупных проектах.
В то же время генерируемые библиотекой запросы сложнее оптимизировать, да и сама библиотека добавляет оверхед.
Само по себе использование ORM не является средством защиты от инъекций, но при правильном использовании библиотеки предоставляют средства для параметризованных и подготовленных запросов.
Doctrine и DQL
Существует множество ORM-библиотек для различных языков программирования и фреймворков. Остановимся подробнее на проекте Doctrine, написанном на PHP, и эксплуатации инъекций в Doctrine Query Language. Doctrine по умолчанию используется в популярном PHP-фреймворке Symfony.
Пользоваться Doctrine можно как осуществляя действия над объектами в PHP-коде (при помощи QueryBuilder), так и вручную выполняя DQL-запросы. Также возможно выполнение «сырых» запросов напрямую в SQL.
Язык DQL основан на HQL (Hibernate Query Language в Java-библиотеке Hibernate) и является подмножеством SQL, но в нём всё равно довольно много возможностей, которые могут помочь и при эксплуатации инъекций.
DQL поддерживает привычные операторы SELECT, UPDATE, DELETE, однако нет реализации операторов INSERT и UNION, выражения LIMIT (необходимо использование метода setMaxResults). Оператор UNION авторы библиотеки не стали реализовывать ввиду строгой типизации DQL (а UNION подразумевает возможность выборки данных разного типа).
Также в DQL реализована поддержка подзапросов, а также выражений JOIN, WHERE, ORDER BY, HAVING, IN и т.д.
Описание синтаксиса DQL: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/dql-doctrine-query-language.html
Ниже приведён список встроенных функций в DQL, которые можно использовать после выражений SELECT, WHERE и HAVING. Также после выражений SELECT и GROUP BY можно использовать функции AVG, COUNT, MIN, MAX, SUM.
Как и во многих СУБД, в Doctrine можно создать собственную реализацию функции (User Defined Function) на PHP и сделать её доступной из DQL.
Инъекции в DQL
Вот как выглядит создание SQL-запроса для выборки данных в Doctrine при работе с объектами в коде:
А ниже показана разница между DQL-запросом и SQL-запросом:
1 |
$dqlQuery = "SELECT p FROM App\Entity\Post p WHERE id = '$query' ORDER BY p.publishedAt DESC"; |
1 |
$sqlQuery = "SELECT * FROM post WHERE id = '$query' ORDER BY publishedAt DESC"; |
Очевидно, в обоих случаях есть конкатенация некоторой переменной с запросом. Если это пользовательские данные, возможно проведение DQL-инъекции.
Принципы эксплуатации DQL инъекций, конечно, не отличаются от эксплуатации SQL-инъекций но необходимо понимать, что атакующий не может полностью контролировать запрос, который будет отправлен в СУБД. Работа на самом деле идёт не с базой данных, а с моделями, поэтому, например, не получится извлекать данные из таблиц, для которых не определены модели в коде.
Посмотрим, что происходит при создании такого запроса (QueryBuilder вызван из метода класса Post):
DQL-запрос преобразуется в синтаксическое дерево, после чего генерируется уже SQL-запрос в грамматике подключённой СУБД.
Техники инъекций
В зависимости от используемой СУБД, типа запроса, контекста инъекции и настроек (наличие debug-режима), возможны различные алгоритмы эксплуатации инъекции, такие как Boolean Based и Error Based.
- Boolean Based
Функция substring и подзапросы дают возможность перебором посимвольно извлекать значения атрибутов моделей:
1 |
1 or 1=(select 1 from App\Entity\User a where a.id=1 and substring(a.password,1,1)='$') |
Из скриншотов видно, что мы получили значение первого символа хеш-суммы пароля («$»). При этом в операторе SELECT мы использовали полное имя модели User. Нет простого способа получить перечень всех моделей.
- Error Based (SQLite)
При использовании СУБД SQLite есть ещё одна особенность — диалект SQLite достаточно бедный, а DQL обеспечивает одинаковый интерфейс независимо от используемой СУБД. Поэтому, при отсутствии каких-то нативных функций в SQLite, приходится писать их реализацию на PHP.
Это касается функций udfSqrt, udfMod, udfLocate (соответствующие DQL-функции: SQRT, MOD, LOCATE). При передаче некорректных данных в эти функции возникает исключение на уровне PHP, а не на уровне СУБД, поэтому, при отображении ошибок, возможна утечка результата SQL-подзапроса целиком.
Ошибка:
Результат SQL-подзапроса с хешем пароля:
Понятно, что при отсутствии debug-режима приложение вряд ли отобразит эти данные, но, тем не менее, возможна эксплуатация Error Based инъекции перебором (извлекать бит информации по наличию или отсутствию внутренней ошибки).
- Инъекции в ORDER BY
Грамматика DQL не предусматривает использование сложных выражений и подзапросов после ORDER BY и GROUP BY, так что эксплуатация инъекции в таком контексте не представляется возможной, парсер пропустит лишь литералы.
- Инъекция в IN
В качестве аргументов выражения IN можно передать подзапрос, что даёт различные возможности для эксплуатации инъекций, например, при помощи техники Error Based:
1 |
$dqlQuery = "SELECT p FROM App\Entity\Post p WHERE p.id IN (select sqrt(a.password) from App\Entity\User a where a.id=2)"; |
- Инъекция в UPDATE.
Оператор UPDATE позволяет записывать в значение атрибута модели результат выполнения подзапроса, так что можно извлечь данные целиком по стороннему каналу (записав секретные данные в таблицу с публичными данными):
1 |
UPDATE App\Entity\Post p SET p.title = (SELECT u.password FROM App\Entity\User u WHERE u.id = 2), slug = testslug, summary = testsum, content = testcon WHERE id = 25 |
Выводы
Использование ORM — не панацея от SQL-инъекций. Необходимо тщательно валидировать и санитизировать передаваемые пользователями данные, использовать подготовленные запросы.
Многие разработчики привыкли, что фреймворки выполняют всю работу за них, и не нужно заботиться о безопасности своего кода.
Подробнее о том, какие методы являются безопасными в DQL, можно прочитать в документации: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/security.html.
Что делать?
Если вашей команде разработки не хватает помощи по организации процесса безопасности, обращайтесь к нам за услугами и продуктами в области Application Security и DevSecOps: https://appsec.deteact.com/.