В рамках решения второй части задания CTB-500 из квалификационного раунда ruCTF - 2010, встала задача эксплуатации "слепой"(blind) SQL-injection. Далее в вольной форме описывается способ достижения результата с наименьшими затратами времени, а также технические детали, относящиеся к СУБД MySQL, которые важны для успешного применения этого способа на практике.
Задача
Пусть найдена blind SQL-injection в скрипте. Наша цель - извлечь информацию из базы данных. Конкретный скрипт(взято из задания CTB500 ruCTF2010 quals):
<?
@mysql_connect("127.0.0.1", "user","pass") or die("Cannot connect to DB!");
@mysql_select_db("base") or die("Cannot select DB!");
$text=$_REQUEST['text'];
if($text=='')
$text='none';
$text = str_replace("\\","",$text);
$text = str_replace("\"","",$text);
$text = str_replace("'","",$text);
echo "$text";
$result=mysql_query("select $text");
# yep, this is all...
?>
Как мы видим, по результатам запроса на экран ничего не выдается, так что об использовании UNION-based атаки или даже о простейшем blind SQL exploit через изменения в страницах в зависимости от истинности или ложности выражения, говорить не приходится. Что остается?
Остаются out-of-band channels и time-based атаки.
Out-Of-Band channels
Смысл этой техники в том, что мы извлекаем информацию не через тот канал, через который поступает наш ввод для скрипта. Это может быть email, файлы и т.п.
Про OOB хорошо написано
тут.
Собственно, в MySQL возможности в этом плане более чем скромные, как можно прочесть в означенной статье. INTO OUTFILE использовать в данном случае не получится, увы. Почему? А потому, что
после INTO OUTFILE должна стоять
строго строка в кавычках (http://www.owasp.org/index.php/Testing_for_MySQL#Write_in_a_File). А кавычки в данном случае экранируются, так что это нам не подходит.
Есть еще метод, который требует внедрения UDF(User-Defined Function) - этот метод описан в статье выше. Совершенно очевидно, что это нам не подходит.
Остаются атаки, основанные на временных задержках.
Time-based blind SQL injection technique
Вкратце опишу эту технику:
Итак, у нас есть запрос, в который мы можем инжектить. Во всех СУБД существует возможность вызвать временнУю задержку. Для разных СУБД способы разные, все это описано в моей настольной книге: The Web Application Hacker's Handbook (WAHH) в chapter 9(Injecting Code).
Для MySQL'a есть два способа вызвать временную задержку: benchmark() и SLEEP() . Оба работают хорошо, но sleep более надежен в плане контроля времени задержки. В нашем случае proof-of-concept выглядит так:
http://sqldarkness.quals2010.ructf.org/nightmare.php?text=if( 1 , sleep(5), 1)
Сервер отвечает через 5 секунд.
http://sqldarkness.quals2010.ructf.org/nightmare.php?text=if( 0 , sleep(5), 1)
Сервер ответит сразу.
Так, мы убедились в возможности извлекать информацию. Наши возможности весьма скромны - порядка одного бита в секунду, так что надо серьезно задуматься об оптимизации количества запросов, вытягивая только интересующую нас информацию в максимальном темпе.
Метаинформация в СУБД MySQL
Цитата из WAHH: "there is no database metadata in MySQL". Да, именно так до последнего времени обстояли дела, и приходилось прибегать ко всевозможным ухищрениям, чтобы получить имена таблиц и их полей. В MySQL последних версий(>=5), однако, имеется целая база данных, в которой содержится вся информация о базах данных, таблицах и их полях, а также еще много чего интересного. База называется information_schema, и в ней нас интересуют таблицы:
schemata, tables, и columns.
Первая из них содержит имена баз данных, вторая - имена таблиц, третья - имена столбцов таблиц.
Другими словами, схема извлечения данных выглядит так:
- select schema_name from information_schema.schemata -> Имена баз
- select table_name from information_schema.tables where table_schema = Имя базы -> имена таблиц в базе
- select column_name from information_schema.columns where table_name = Имя Таблицы and table_schema=Имя Базы -> имена полей в таблице
- select (список_нужных_полей) from Имя_базы.Имя_таблицы -> содержимое таблиц.
В моем конкретном случае первый шаг можно пропустить, так как имя базы зашито в скрипте(см выше).
Разработав план действий, приступим к его реализации, а именно, научимся извлекать результаты запросов, используя временные задержки.
Извлечение информации через таймауты - технические тонкости.
Величина таймаута
Как я говорил уже выше, таймаут можно вызвать двумя способами - benchmark и sleep. Будем использовать второй, если он доступен. Возникает вопрос - какой таймаут ставить. Вообще говоря, ответ на него неоднозначен. Все зависит от качества соединения с сервером, от быстродействия сервера, от наличия времени. Хорошо идет значение в полторы секунды - достаточно надежно, хотя для вытаскивания большого объема инфы не подойдет.
Вообще, в моём случае ошибки в одной-двух буквах вполне допускались, так как все имена были человеко-понятными и ошибки легко исправлялись.
Извлечение данных целого типа
Пусть нам надо узнать некий числовой параметр, про который известно, что его величина колеблется в некоторых пределах. Быстрейший способ использует метод половинного деления, он же - метод бинарного поиска. Тут, в общем, всё ясно. Заметим, что для целых чисел с неизвестным диапазоном значений этот метод не подойдет. Пример:
( 1<=a<=10)
- select if(a > 5,sleep(5),1) -> нет задержки, значит (1<=a<=5)
- select if(a > 3,sleep(5),1) -> есть задержка, значит (4<=a<=5)
- select if(a>4, sleep(5),1) -> нет задержки, значит a = 5.
Извлечение строковых данных
Для начала извлекаем длину строки через функцию length (см. предыдущий пункт).
Зная длину и используя функцию substring, сводим задачу к извлечению отдельного символа.
Тут способ такой же, как и для чисел, но есть свои тонкости.
Вариант 1: кавычки не фильтруются
Тогда достаточно сравнивать символ с другими символами, причем именно через непосредственное указание их в виде литерала (
'a', а не
char(65)). В этом случае по умолчанию
сравнение будет case-insensitive
, что в большинстве случаев нам и надо, поскольку сокращает пространство поиска.
Вариант 2: кавычки фильтруются
В этом случае мы можем задавать строковые литералы только в виде hex-значений: 0x41 == 'A' и т.п.
Фокус в том, что в этом случае сравнение по умолчанию будет case-sensitive, что нас мало устраивает. Для того, чтобы обойти это неудобство, используем следующую конструкцию:
(CONVERT (0x123456 USING latin1) COLLATE latin1_general_ci )
Извлечение других типов данных
Для извлечения данных других типов проще всего преобразовать их в строки.
Хинт: можно, используя concat, слить информацию сразу из нескольких полей в виде одной строки. Это позволит выбирать отдельные записи из таблицы целиком. Для извлечения всей таблицы, надо извлекать записи по одной, для чего нам пригодится конструкция LIMIT 1 OFFSET N.
Замечания
В том случае, если можно сделать какие-то догадки относительно тех или иных данных, их легко проверить на истинность, используя приведение к строке и конструкцию str1 REGEXP str2. Если кавычки экранируются, шаблон надо сконвертировать в hex и изменить collation, как описано выше.
P.S. К сожалению, в течение квалификации не хватило времени, чтобы использовать эту технику. При грамотном подходе, однако, время, необходимое для успешного решения таска, вряд ли превысило бы 30 минут.
P.P.S. Предпринимались попытки приспособить утилиту SQLmap для решения поставленной задачи, однако безрезультатно. Вероятно, сказывается нестандартное расположение внедряемого параметра в запросе - сразу после select. Хотя в принципе в sqlmap заявлена поддержка time-based атак.
Спасибо за внимание.
Полезные ссылки
Про получение метаинформации в СУБД MySQL:
Подборка статей по SQL-injection: