Введение в практическую безопасность (2019)/Эксплуатация SQL injection

Материал из SecSem Wiki
< Введение в практическую безопасность (2019)
Версия от 11:16, 18 марта 2019; Asterite (обсуждение | вклад) (Fingerprinting базы данных)
(разн.) ← Предыдущая | Текущая версия (разн.) | Следующая → (разн.)
Перейти к навигации Перейти к поиску

Краткое введение в то что такое SQL injection было в предыдущей статье, теперь о некоторых вещах которые может делать атакующий.

SQL-комментарий

Супер простой трюк. В конце своей инъекции можно добавить символы однострочного SQL-комментария, тогда всё что идет дальше (как правило, остаток исходного SQL-запроса) будет считаться комментарием и проигнорируется. В разных БД символы комментария могут разными, см. документацию, чаще всего это двойной минус -- и/или решетка #, также бывает классический двойной слеш //. Фактически, этот трюк уже был использован в примере в предыдущей статье, двойной минус в конце там заставил базу данных проигнорировать оставшуюся от исходного запроса одинарную кавычку в конце.

UNION SELECT

Очень полезным может быть UNION (вики,доки postgres,курс "Основы современных БД"), который позволяет присоединить к результу одного SELECT запроса результат еще одного. Если инъекция в операторе SELECT (что в реальности довольно часто), с помощью UNION можно добавить в его результат вывод нового, полностью написанного атакующим SELECT - запроса из друой таблицы, с другими колонками, условиями и т. д.

Чтобы UNION сработал, требуется чтобы количество и типы колонок совпадали. Помочь с этим может то что в SQL можно писать на месте колонок просто константы (и тогда они просто проставятся на эти места в каждой из строк результата запроса) и то что чаще всего можно использовать вместо любого значения NULL. Если количество колонок в исходном запросе неизвестно, можно просто подбирать его.

Для такого кода

Sqli-code.png

если параметр login будет иметь значение admin' UNION SELECT * FROM users WHERE login = 'Kobzon' # результирующий запрос примет вид Union-triv.png

Этот запрос "достанет" из базы данных две записи - соответствующие пользователям "admin" и "Kobzon". Это можно попробовать на стенде: http://sql1.stands.course.secsem.ru/by-login?login=admin'+UNION+SELECT+*+FROM+users+WHERE+login+=+'Kobzon'+%23 Обратите внимание что решетка закодирована URL-кодированием (вики) и выглядит как %23. В данном случае это необходимо, т. к. решетка в URL имеет специальное значение - отделяет хеш урла (вики), который не посылается на сервер вообще (как и сама решетка). То есть если её не закодировать то по неё URL просто обрежется.

Конечно, пример тривиальный, так как мы получаем те же колонки из той же таблицы, это можно было бы сделать и без UNION. Вот более интересный пример: если значение login будет admin' UNION SELECT id,password,NULL,NULL,NULL FROM p4ssww0rdz WHERE id >= 2 # то результирующий SQL-запрос примет вид

Union-p4ssww0rdz.png

И в его результат попадут пароли всех пользователей с id начиная с 2. На стенде: http://sql1.stands.course.secsem.ru/by-login?login=admin'+UNION+SELECT+id,password,NULL,NULL,NULL+FROM+p4ssww0rdz+WHERE+id+%3e%3d+2+%23 (символ > здесь также URL-закодирован). Здесь NULL "забивает" лишние колонки, то что их нужно было именно 3 можно понять из схемы данных (код стенда выложен), там в таблице users 5 колонок, также это можно было получить подбором.

Еще при использовании UNION бывает проблема что код приложения отдаёт не все строки результата, а только часть, например только первую (так обычно бывает когда разработчик предполагал что запрос вернет только одну строку). В этом случае можно просто сделать невыполнимое условие в WHERE исходного запроса - тогда он вернёт 0 строк и в выдачу попадёт строка из присоединенного SELECT'а. Например nonexistent' and 1=0 UNION SELECT id,password,NULL,NULL,NULL FROM p4ssww0rdz WHERE id >= 2 #.

Метаданные

Как правило, при эксплуатации SQL injection мешает то что неизвестны имена таблиц, имена и типы колонок. Может помочь то что в современных базах данных как правило есть специальные таблицы с метаданными, которые содержат всю схему данных базы. У разных СУБД эти таблицы называются и устроены по-разному, про их устройство можно почитать в документации. Например, у MySQL таблицы с метаданными хранятся в базе INFORMATION_SCHEMA(документация) - информация о существующих базах данных лежит в таблице SCHEMATA, о таблицах - в TABLES, о колонках - в COLUMNS и так далее.

C таким payload nonexistent' and 1=0 UNION SELECT 0,SCHEMA_NAME,NULL,NULL,NULL FROM INFORMATION_SCHEMA.SCHEMATA # можно получить список всех существующих баз данных (http://sql1.stands.course.secsem.ru/by-login?login=nonexistent'+and+1=0+UNION+SELECT+0,SCHEMA_NAME,NULL,NULL,NULL+FROM+INFORMATION_SCHEMA.SCHEMATA+%23), с таким nonexistent' and 1=0 UNION SELECT 0,concat(concat(table_name, ' '),column_name),NULL,NULL,NULL FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'webapp' # - список всех таблиц с названиями колонок в БД webapp (http://sql1.stands.course.secsem.ru/by-login?login=nonexistent'+and+1=0+UNION+SELECT+0,concat(concat(table_name,+'+'),column_name),NULL,NULL,NULL+FROM+INFORMATION_SCHEMA.COLUMNS+WHERE+TABLE_SCHEMA+=+'webapp'+%23 ). В этом последнем запросе использована ещё одна довольно полезная штука - функция SQL, в данном случае функция MySQL CONCAT (документация).

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 после своего запроса - поэтому, даже если вставить какой то изменяющий базу запрос после селекта, он не повлияет на ее состояние, так как не будет подтверждена транзакция. (Про то как это работает в доках psycopg2, см. про функции commit и close). Однако, эту проблему можно (по крайней мере в некоторых случаях) победить - если работают stacked queries, можно просто вставить после своего запроса оператор COMMIT, и это приведет к подтверждению транзакции (SELECT ... WHERE ... ; INSERT INTO ... ; COMMIT; --).

sqlmap

Существует ряд инструментов которые умеют обнаруживать и/или эксплуатировать SQL injection. Главный из них - sqlmap. Это тулза напитоне, которая умеет брать запрос к сайту и подставлять в значения его параметров разные атаки на SQLi. Как им пользоваться отлично описано в их usage, базовое использование - передача sqlmap'у урла через параметр -u (этого достаточно если уязвимый параметр в URL и можно эксплуатировать GET-запросом)

python sqlmap.py -u http://sql1.stands.course.secsem.ru/by-login?login=admin

либо файла с HTTP-запросом через -r (это может понадобиться если уязвим параметр, передаваемый в теле POST-запроса или, скажем, HTTP-заголовке)

python sqlmap.py -r request.txt

где в request.txt записан HTTP-запрос, например

GET /by-login?login=admin HTTP/1.1
Host: sql1.stands.course.secsem.ru
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

Одна вещь, которую надо помнить про sqlmap это то что он может кэшировать результаты запросов, так что если в базе что-то обновилось (или вы получили при прошлом запуске неполные данные), повторный запуск может выдавать (устаревшие) данные из кэша, а не взаимодействовать с уязвимым приложением для получения новых. Чтобы сделать запрос в обход кэша можно использовать флаг --fresh-queries, также можно очистить кэш с помощью флага --flush-session.

Еще материалы

Это уже писалось в прошлой статье, но отличая дока по SQL injection есть на форуме rdot.