Веб-безопасность/Уязвимости SQLi
Содержание
SQL Injection
Веб-приложения нередко используют SQL-базы данных, т. е. такие БД, взаимодействие с которыми осуществляется через запросы на языке SQL (Википедия). Поиграться с SQL можно просто в SQL Fiddle или с помощью sqlite3.
Веб-приложение формирует и отправляет базе SQL-запросы, которые могут зависеть от параметров, пришедших от клиента (взятых из HTTP-запроса). SQL-запросы текстовые и простой способ сделать запрос, зависящий от параметра - просто подставить этот параметр в текст запроса. Пример кода (на языке Python), подставляющего параметр из HTTP-запроса в SQL-запрос к базе:
Здесь "request.args.get('login')" считывает значение параметра "login" query string запроса. Вот какой вид примет запрос, если параметр login будет иметь значение "testuser".
Значение "testuser" подставилось в строковый литерал в одинарных кавычках. Говорят, что значение попало в контекст строкового литерала. После такой подстановки может оказаться, что смысл SQL-запроса изменился и данные, подставленые в запрос стали не просто значением параметра запроса (скажем, числовым или стокововым литералом), а какими-то еще конструкциями языка SQL. Вот какой запрос будет сформирован, если параметр "login" будет иметь значение ' or 1=1 --
:
В результате выражение в WHERE
-части приняло вид login='' or 1=1
, оно будет истинным для любой строки таблицы, в результате чего в ответ на этот запрос будут возвращены все строки таблицы. Так же можно изменить смысл запроса и передав специфическое значение параметра id для этого кода (для этого даже не придётся добавлять в значение параметра кавычку):
Возможность передать такое значение параметра HTTP-запроса, которое подставится в текст SQL-запроса и при подстановке изменит смысл этого запроса называется SQL injection (OWASP). Хорошие статьи про SQL injection можно найти тут и еще тут.
Фактически, SQLi это один из представителей класса уязвимостей injection, которые получаются, если в какие-то управляющие команды подставляются параметры-данные от пользователя и эти данные могут выйти из контекста, куда подставляются, перестав быть просто данными, и поменять смысл команды.
Полезные трюки
SQL-комментарий
Супер простой трюк. В конце своей инъекции можно добавить символы однострочного SQL-комментария, тогда всё что идет дальше (как правило, остаток исходного SQL-запроса) будет считаться комментарием и проигнорируется. В разных БД символы комментария могут разными, см. документацию, чаще всего это двойной минус --
и/или решетка #
, также бывает классический двойной слеш //
. Фактически, этот трюк уже был использован в примере в предыдущем разделе, двойной минус в конце там заставил базу данных проигнорировать оставшуюся от исходного запроса одинарную кавычку в конце.
UNION SELECT
Очень полезным может быть UNION
(вики,доки postgres), который позволяет присоединить к результату одного SELECT
запроса результат еще одного. Если инъекция в операторе SELECT
(что в реальности довольно часто), с помощью UNION
можно добавить в его результат вывод нового, полностью написанного атакующим SELECT
- запроса из другой таблицы, с другими колонками, условиями и т. д.
Чтобы UNION
сработал, требуется чтобы количество и типы колонок совпадали. Помочь с этим может то что в SQL можно писать на месте колонок просто константы (и тогда они просто проставятся на эти места в каждой из строк результата запроса) и то что чаще всего можно использовать вместо любого значения NULL
. Если количество колонок в исходном запросе неизвестно, можно просто подбирать его.
Еще при использовании UNION
бывает проблема что код приложения отдаёт не все строки результата, а только часть, например только первую (так обычно бывает когда разработчик предполагал что запрос вернет только одну строку). В этом случае можно просто сделать невыполнимое условие в WHERE
исходного запроса - тогда он вернёт 0 строк и в выдачу попадёт строка из присоединенного SELECT
'а.
Метаданные
Как правило, при эксплуатации SQL injection мешает то что неизвестны имена таблиц, имена и типы колонок. Может помочь то что в современных базах данных как правило есть специальные таблицы с метаданными, которые содержат всю схему данных базы. У разных СУБД эти таблицы называются и устроены по-разному, про их устройство можно почитать в документации. Например, у MySQL таблицы с метаданными хранятся в базе INFORMATION_SCHEMA
(документация) - информация о существующих базах данных лежит в таблице SCHEMATA
, о таблицах - в TABLES
, о колонках - в COLUMNS
и так далее.
Fingerprinting
Для атаки бывает необходимо понимать, какая именно СУБД используется - чтобы понимать, как называются таблицы с метаданными какие функции/механизмы/особенности БД можно использовать и т. д. Если из других источников (скажем, доступных исходных кодов приложения или вывода приложением подробной информации о себе) узнать, какая СУБД, не удаётся, это можно узнать от неё самой. Во-первых, если выдаются SQL-ошибки, часто по виду ошибки, который обычно содержит числовой идентификатор, можно однозначно определить базу. Если даже ошибки не выводятся, зафингерпринтить базу можно по её реакции на конструкции SQL, которые работают только в некоторых базах или работают в разных БД по-разному. Яркими примерами является то что в MySQL можно в качестве строковой колонки указать переменную @@version
и функцию version()
и на её место в обоих случаях подставится версия базы, в PostgreSQL и SQLite оператор ||
работает как оператор конкатенации строк, при этом в PostgreSQL поддерживается база information_schema
, а в SQLite - нет. Почитать про database fingerprinting можно на OWASP, здесь, и еще много где, это довольно легко гуглится.
Error-based вывод
В случае, если мы в ответ сервера не выводятся данные, полученные из SQL-запроса, но выводятся ошибки, вытаскивать информацию бывает всё равно можно - через сообщения об ошибке. Про это написано в этой статье, ещё этой, этой и еще много где. Кстати, если даже не выводится ошибка, а есть только бинарный признак (приложение даёт в зависимости от полученных из SQL-запроса данных ответ да/нет) и даже если нет и его вытягивать данные всё равно можно. Как именно - задача на подумать или нагуглить -).
Stacked queries
Иногда бывает можно в одном запросе от приложения к базе данных передать несколько SQL-запросов через ;
. В этом случае SQL injection позволяет сделать уже вообще любой SQL-запрос. Доступность stacked queries зависит от БД и используемого драйвера, к примеру c базами sqlite3 и MySQL обычно нельзя, с базой PostgreSQL и стандартным питоновским драйвером psycopg2 - можно.
Изменения в БД
Часто клиентские библиотеки для баз данных (драйверы) при выполнении SQL-запроса автоматически начинают новую транзакцию (выполняя оператор BEGIN). Любые изменения, сделанные SQL-запросом в транзакции, не будут видны за пределами текущей транзакции, пока она не будет успешно завершена (оператором COMMIT
). Однако, если программист делал запрос, достающий что-то из базы, то есть SELECT
, ему не нужно применять никакие изменения и вполне возможно он не делал COMMIT после своего запроса - поэтому, даже если вставить какой то изменяющий базу запрос после SELECT
'а, он не повлияет на ее состояние, так как не будет подтверждена транзакция. (Про то как это работает в доках psycopg2, см. про функции commit и close). Однако, эту проблему можно (по крайней мере в некоторых случаях) победить - если работают stacked queries, можно просто вставить после своего запроса оператор COMMIT
, и это приведет к подтверждению транзакции (SELECT ... WHERE ... ; INSERT INTO ... ; COMMIT; --
).