Содержание статьи
- Что такое «слепые» SQL-инъекции
- Факторы, влияющие на скорость эксплуатации
- Общие идеи повышения производительности
- Работа с сессиями
- Многопоточность
- Основные задачи эксплуатации
- Определение количества записей и длины строк
- Поиск целых чисел
- Работа с диапазонами поиска
- Сужение диапазона поиска
- Определение ошибок
- Поиск в диапазонах с разрывами
- Подстройка диапазона поиска
- Другие идеи оптимизации
- Соединение строк
- Работа с UNICODE
- Сжатие данных
- А что там sqlmap?
- Работа с потоками
- Определение длины строки
- Поиск символов
- Выбор диапазона символов
- Unicode
- Выводы
Что такое «слепые» SQL-инъекции
Здесь мы разберем случаи, при которых возникают Blind SQL-инъекции, а также описана базовая техника их эксплуатации. Если ты уже c ними знаком и понимаешь, что такое бинарный поиск, — смело переходи к следующему разделу.
Blind SQL-инъекции возникают, когда мы не можем напрямую достать данные из приложения, но можем различить два разных состояния веб‑приложения в зависимости от условия, которое мы определяем в SQL-запросе.
Как работает Blind SQL-инъекция
Представим следующую инъекцию (для простоты используем язык PHP, а в качестве СУБД возьмем MySQL):
$query = "SELECT id,name FROM products ORDER BY ".$_GET['order'];
Здесь мы можем указать в параметре order
не только имя колонки, но и некоторое условие, например:
order=(if( (select 1=1),id,name))order=(if( (select 1=0),id,name))
В зависимости от истинности логического выражения (
или (
результат будет отсортирован СУБД либо по колонке id
, либо по колонке name
. В ответе веб‑приложения мы увидим, по какой колонке отсортированы products
, и сможем отличить истинное условие от ложного. Это позволяет нам «задавать вопросы» СУБД, на которые мы будем получать ответы «да» или «нет».
Например, «спросим» у СУБД — в таблице information_schema.
больше 50 строк или нет?
order=(if( (select count(*)>50 from information_schema.tables),id,name))
Если говорить более формально, то за один запрос мы можем получить 1 бит информации из базы данных. Для получения текстовой информации можно делать полный перебор всех возможных символов следующим образом:
order=(if( (select substring(password,1,1)='a' from users where username='admin'),id,name))order=(if( (select substring(password,1,1)='b' from users where username='admin'),id,name))order=(if( (select substring(password,1,1)='c' from users where username='admin'),id,name))...
Но это долго — нужно сделать столько запросов, сколько букв мы хотим проверить. Именно поэтому, чтобы ускорить процесс, прибегают к бинарному поиску.
Как выполнить бинарный поиск
Переведем символ искомой строки в его ASCII-код и сделаем некоторое предположение о возможных значениях этого символа. Например, можно предположить, что он лежит в нижней половине ASCII-таблицы, то есть имеет код в диапазоне от 0x00
до 0x7f
. Разделим этот диапазон пополам и спросим у БД — в какой половине диапазона находится символ?
order=(if( (select ascii(substring(password,1,1))>0x3f from users where username='admin'),id,name))
Если символ окажется больше 0x3f
, значит, целевой диапазон сужается до 0x40-0x7f
, если меньше — до 0x00-0x3f
. Далее находим середину диапазона и снова спрашиваем у БД: целевой символ больше середины или меньше? Потом снова сужаем диапазон и продолжаем до тех пор, пока в диапазоне не останется ровно одно значение. Это значение и будет ответом.
В данном случае для поиска точного значения символа нам потребуется ровно запросов.
Факторы, влияющие на скорость эксплуатации
Теперь разберемся, что ограничивает скорость эксплуатации инъекции:
При эксплуатации Blind SQL-инъекции атакующий отправляет запросы на один и тот же эндпоинт. Один такой запрос обрабатывается веб‑сервером некоторое время. Обозначим среднее время выполнения запроса — . Разброс времени выполнения запросов обычно невелик.
Веб‑сервер может обрабатывать некоторое количество запросов параллельно. Оно зависит от количества физических веб‑серверов за балансировщиком, потоков веб‑приложения, ядер на сервере СУБД и т.д. Его можно выяснить экспериментально. Обозначим количество запросов к веб‑серверу — .
Соответственно, приложение не будет обрабатывать больше чем запросов в секунду. Мы можем отправить и больше запросов в секунду, но они будут попадать в очередь и ждать обработки, поэтому общая скорость обработки запросов не превысит . Данный параметр может меняться в зависимости от текущей нагрузки на приложение и быть непостоянным, но не суть.
Вывод: cкорость, с которой мы можем вынимать данные, составляет ~= бит в секунду. Это основное ограничение по скорости эксплуатации слепых SQL-инъекций.
Общие идеи повышения производительности
Работа с сессиями
Веб‑приложение может не выполнять паралельные запросы в рамках одной сессии (например, так работает PHP). Если мы видим, что подбор идет в один поток, то нужно создать по сессии для каждого потока подбора.
Также, если веб‑приложение выполняется на нескольких серверах и использует балансировку на основе Sticky Sessions, все запросы в рамках сессии отправляются на один и тот же сервер. Мы можем создать несколько сессий на разных серверах и распределить запросы равномерно по ним. В обработку запросов будет вовлечено большее количество серверов — следовательно, повыситься, а вместе с ним и общая скорость атаки.
Многопоточность
Получить больше чем битов в секунду мы не можем никак. Однако мы можем грамотно распорядиться имеющейся скоростью и тем самым увеличить общую скорость проведения атаки.
Как правило, наша задача — вытащить несколько колонок из одной таблицы (возможно, с применением фильтра WHERE
). Реализация такой атаки «в лоб» состоит из следующих задач:
- Определить количество строк, которое надо вернуть.
- Для каждой строки и колонки, которые надо вернуть, определить длину строки.
- Выполнить поиск каждого символа целевой строки. Если мы считаем, что нам нужны только стандартные символы из нижней половины ASCII-таблицы (
0x00-0x7f
), то это 7 запросов на один символ.
Банальная реализация
При банальной реализации «в лоб» распараллеливание делается только на третьем этапе. На первом и втором этапе идет подбор в 1 поток, это означает, что скорость составляет запросов в секунду вместо доступных .
На третьем этапе распараллеливание делается таким образом. Допустим, веб‑приложение обрабатывает одновременных запросов, а длина строки составляет . Таким образом мы можем выполнять параллельных задач, каждую из которых удобно поместить в отдельный поток.
Сначала определяем первые символов строки параллельно. Нужно помнить, что мы не можем использовать все доступные потоки для определения какого‑то одного символа, так как бинарный поиск требует получения ответа на предыдущий запрос для формирования нового запроса.
По мере завершения поиска символов мы запускаем новые потоки для следующих символов той же строки. Но так как скорость выполнения запросов примерно одинакова, новые потоков мы запустим примерно тогда, когда первые потоков завершатся, а именно через секунд (если ищем в диапазоне 0x00-0x7f
).
Если длина строки кратна , то все хорошо и все потоки будут работать на максимальной скорости. Если же нет, то последняя группа после кратности будет работать в потоков. Остальные потоки будут простаивать. Наконец, если длина строки случайная, то очевидно, что в среднем последняя группа будет работать только на 50% от максимальной скорости.
Более производительный вариант
Для увеличения скорости можно лучше использовать параллельность: запустить первый поток для решения задачи 1, а оставшиеся свободных потоков сразу запустить на определение длин строк для первых строк, где — количество колонок. Если окажется, что строк нужно запросить меньше, чем мы определили, то часть потоков отработала бессмысленно. Но иначе они бы просто простаивали, так что скорость атаки не упадет, но может увеличиться.
Можно пойти и еще дальше, сделав вероятное допущение о том, что целевые строки имеют длину не меньше, например, 3-х символов. Не завершив ни задачу 1, ни задачу 2, начать выполнять задачу 3, начать определять первые 3 символа первых целевых строк. После такого старта, если грамотно распорядиться потоками (например, иметь уже определенные длины для нескольких следующих строк заранее), можно поддерживать общую скорость ~=.
Основные задачи эксплуатации
В данном разделе рассмотрим две типовые задачи, возникающие при эксплуатации слепых SQL-инъекций и способы их эффективного решения: определение количества записей и длины строк и поиск целых чисел.
Определение количества записей и длины строк
Определение количества возвращаемых записей (row) и длины строк — схожие задачи, но они не так тривиальны, как могут изначально показаться.
Бинарный поиск может искать значение в рамках какого‑то диапазона, у которого есть минимальное и максимальное значение. Так, при определении символов мы знаем, что они, скорее всего, лежат в диапазоне 0x00-0x7f
и точно попадают в 0x00-0xff
(Unicode пока опустим, хотя и для него тоже можно задать максимальную границу диапазона).
Для определения же количества записей и длины строк мы не обладаем такой информацией, поэтому теоретически целевое значение может быть абсолютно любым.
Также важно отметить, что определение количества записей — это операция, которая делается один раз для каждого запроса. А вот длина строки определяется многократно, поэтому оптимизация определения длины строки является более приоритетной задачей.
Определяем длину строки (банальный способ)
Сначала я приведу банальный алгоритм, который позволяет определить длину строки. Изучив его и поняв сопутствующие проблемы, мы сможем обсудить проблемы оценки эффективности таких алгоритмов.
Первой идеей, которая приходит в голову, является попытка определения значения, которое больше длины строки (далее рассуждения будут касаться только длины строки, для определения количества row
они аналогичны). После обнаружения такого значения мы уже можем свободно запустить бинарный поиск.
Так как бинарный поиск имеет наибольшую эффективность при работе с диапазонами, размер которых равен точной степени двойки (это будет показано далее в статье), можно отталкиваться от какого‑то стартового числа — точной степени двойки, например 256.
Алгоритм получается следующий:
- Сравниваем длину строки с 256.
- Если длина меньше, то запускаем бинарный поиск, который будет требовать запросов.
- Если длина больше, то нам нужно увеличить максимальный диапазон. Например, можно умножить старую верхнюю границу на и сравнить с ней. Если окажется меньше, то запускаем бинарный поиск, если больше, то умножаем на еще раз и так далее.
Для расчета эффективности такого алгоритма нам нужно сделать предположение о вероятностном распределении длин искомых строк. Такое предположение сделать непросто, т.к. строки будут сильно зависеть от смысла, который они несут. Например, email-адреса имеют одно распределение, текстовые посты — другое, а хеши паролей вообще всегда имеют одинаковую длину.
Также стоит учитывать обычные практические задачи. Эксплуатация слепых SQL-инъекций — дело небыстрое. Скорее всего, если мы нашли строку, длина которой составляет 10 000 символов, нам не будет интересно вынимать ее целиком. Возможно, мы заходим посмотреть на первые 100 символов, чтобы понять, что вообще в ней содержится.
Таким образом, если длина строки больше, чем, например, 128 символов, то нас и не интересует ее точная длина — можно в выводе просто указать, что длина больше 128 символов, и при этом найти только эти самые первые 128 символов. В итоге получаем, что на определение длины строки мы тратим 7 запросов, как на 1 символ. На мой взгляд — вполне приемлемо.
Определяем длину строки (небанальный способ)
Также есть техника, в которой длину строки можно не определять в принципе.
При определении символов, как правило, используется конструкция
ASCII(substring(target_col,n,1))
Если n
больше, чем длина строки, то функция substring
вернет пустую строку, а функция ASCII
от пустой строки вернет 0x00
(это верно для MySQL и PostgreSQL). Соответственно, как только мы обнаружили нулевой символ, мы считаем, что мы обнаружили последний символ и дальше искать не надо.
В данном подходе мы осуществляем поиск символа за последним, что дает лишних 7 запросов. Траты аналогичны тратам на определение длины строки. Также учтем, что в MSSQL и SQLite функция substring
вернет не пустую строку, а NULL, как и функция ASCII/
. Можно создавать особые конструкции, приводящие NULL в 0, однако это требует увеличения длины запроса. Кроме того, мы должны аккуратно выстраивать параллельность: подбор нескольких символов одновременно может привести к бесполезному поиску за концом строки и лишнему расходу запросов.
Если нам все же требуется определить длину строки точно (как и точное количество возвращаемых записей), возможные техники приведены далее.
Поиск целых чисел
Если искомая колонка является целочисленной, то у нас есть банальный вариант — привести число к строке и получить его как строку. Также мы можем воспользоваться бинарным поиском, но нам надо решить уже упомянутую проблему — определить верхнюю границу бинарного поиска:
Будем считать, что размер числа ограничен 64 битами. При этом число не отрицательное — unsigned. Для signed-чисел можно применить те же рассуждения, оценка не изменится.
Максимальное 64-битное число представляется строкой в 20 символов. Таким образом, на определение длины строки нужно 5 запросов, т.к. — ближайшая сверху точная степень двойки (на самом деле немного меньше, как будет показано ниже, но пока округлим в большую сторону).
Дальше в зависимости от длины строки нужно потратить 3-4 запроса на каждую цифру (почему так — смотри ниже в разделе «Сужение диапазона поиска»). Длина строки равна , где — искомое число.
Таким образом, общее количество запросов — .
Максимальное количество запросов для максимальной длины — 73. Банальный бинарный поиск на все 64 бита требует 64 запроса вне зависимости от размера числа.
Для улучшения алгоритма мы можем:
Определить, сколько в числе битов (). Возможные значения — от 1 до 64, то есть нам потребуется 6 запросов ().
Дальше в зависимости от количества битов еще нужно столько запросов, сколько в числе битов. Битов в числе — , то есть в сумме получается .
-
Для сравнения двух вариантов уберем округление и приведем формулу оценки общего количества запросов к логарифму по основанию 2:
.
Видно, что разница формул в основном описывается дробью , которая не сильно отличается от , то есть приведенные алгоритмы имеют примерно одинаковую эффективность. Первый алгоритм удобно использовать, когда мы не знаем заранее тип колонки, — мы можем всегда все колонки конвертировать в текстовый тип и получать работающую атаку (правда, ценой небольшого удлинения запроса).
Работа с диапазонами поиска
В данном разделе рассмотрены различные подходы к эксплуатации слепых SQL-инъекций при использовании разных диапазонов поиска.
Сужение диапазона поиска
До этого мы говорили о том, что для определения одного символа требуется 7 запросов, за которые мы можем определить значение из диапазона 0x00-0x7f
(алфавит объема ), что соответствует нижней (английской) половине таблицы ASCII. Идея ускорения состоит в том, что мы можем искать символы не среди всей нижней половины ASCII-таблицы, а среди какого‑то ее подмножества.
Давайте разберем пример, когда нам известно, что целевая строка состоит исключительно из цифр, и оценим скорость подбора.
Алфавит подбора имеет мощность 10. Для точных степеней 2 мы могли бы взять просто логарифм по основанию 2 от мощности алфавита. Однако 10 не является степенью 2, а значит, определить количество запросов для определения символа чуть сложнее:
Первый запрос покажет, входит символ в множество
[
или в0, 1, 2, 3, 4] [
.5, 6, 7, 8, 9] -
Второй запрос разобьет полученную группу из 5 элементов на группы из 2 и 3 элементов:
-
[
и0, 1] [
для первого случая;2, 3, 4] -
[
и5, 6] [
для второго случая.7, 8, 9]
-
Третий запрос найдет значение в случае, если целевой символ был в диапазонах
[
и0, 1] [
.5, 6] -
Для диапазонов
[
и2, 3, 4] [
одним запросом мы можем сделать разделение на поддиапазоны из одного и двух символов:7, 8, 9] -
[
разобьем на2, 3, 4] [
и2] [
;3, 4] -
[
разобьем на7, 8, 9] [
и7] [
.8, 9]
-
Таким образом символы
[
и2] [
будут найдены третьим запросом.7] Если целевой символ находится в
[
или3, 4] [
, то понадобится еще один (четвертый) запрос.8, 9]
Получается, для 6 возможных значений мы делаем 3 запроса, а для оставшихся 4 значений — 4 запроса. Итого в среднем мы делаем 3-4 запроса на один символ. Это более чем в 2 раза лучше, чем полный бинарный поиск в диапазоне из возможных 128 значений.
Математическая оценка
Определить среднее количество запросов для символа из алфавита мощностью N можно по формуле
где — ближайшая снизу к N полная степень числа 2: .
from math import log,floordef questions(N): pow2 = 2**floor(ln2(N)) return ((N-pow2)*2*(ln2(pow2)+1) + (pow2*2-N)*ln2(pow2))/Ndef ln2(x): return log(x)/log(2)
Функцию можно апроксимировать как , но реальное будет всегда чуть больше, чем для N, не являющихся точной степенью двойки.
Определение ошибок
Для дальнейших рассуждений разберем пример, в котором мы предполагаем, что целевой символ является маленькой английской буквой из диапазона a-z
:
Будем искать значение соответствующего ASCII-кода в диапазоне чисел
97-122
. Серединой диапазона является число 109,5.-
Определим, больше ASCII-код целевого символа, чем 109,5, или меньше:
(ascii(substr(target_col,5,1)) from target_table limit 2,1)>109Если ASCII-код меньше, то целевой диапазон уменьшится до
97-109
, если больше — до110-122
. Далее новый дипазон также бьется на две части и так далее, пока символ не будет определен.
Таким образом, среднее количество запросов получается .
Канареечные символы
Если наше изначальное предположение «целевым символом является маленькая английская буква» — неверное, то мы получим в результате бинарного поиска одно из граничных значений — a
или z
. Отличить такой случай от настоящих букв «a» или «z» без дополнительных запросов мы не сможем. Для решения этой проблемы мы можем добавить на обеих границах диапазона по символу‑канарейке — то есть искать целевое значение мы будем не в диапазоне 97-122
, как в примере выше, а в диапазоне 96-123
. Если в результате поиска будет получено канареечное значение 96
или 123
, мы сможем понять, что изначальное предположение неверно.
Такая техника позволяет использовать диапазонный поиск в случае, когда мы с хорошей долей вероятности можем предположить об алфавите конкретного символа, но не уверены на 100%. При этом стоит помнить, что расширение диапазона канареечными символами приводит к увеличению среднего количество запросов на один символ: с до .
Также необходимо отметить, что если символ отсутствовал в диапазоне поиска, то нам потребуется к уже потраченным запросам добавить:
- , если значение меньше 97;
- , если больше 122.
Если считать, что символы распределены равномерно, то в сумме получается , что намного больше изначальных 7 запросов. То есть предположение о диапазоне должно иметь достаточно высокую вероятность, иначе попытка ускорения может обернуться потерей производительности.
Поиск в диапазонах с разрывами
Теперь рассмотрим поиск в наборе возможных значений, которые не образуют непрерывного диапазона в ASCII-таблице. Например, наша цель — шестнадцатеричная строка, набор значений [
. Для решения данной задачи можно предложить две техники:
- инъекция с помощью операторов
IN
иNOT
;IN - поиск с игнорированием разрывов.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»