Статья Пишем свой фаззер на Python

Привет, Codeby!

Расскажу тебе о том, как написать простой фаззер на Python.

Ты наверное хочешь спросить: зачем писать фаззер на Python, если уже есть wfuzz? Я тебе отвечу - мне нужна была возможность использовать http запросы, представленные в текстовом формате, которые были получены из Burp Suite, например, так умеет sqlmap с параметром -r, в котором передается путь до текстового файла c http запросом.

Пример:
HTTP:
GET /get?test=FUZZ HTTP/1.1
Host: httpbin.org
User-Agent: test-agent
Accept: application/json
Content-Type: application/json;charset=utf-8
Connection: close
Content-Length: 16

Но ты скажешь, что есть расширение Turbo Intruder для Burp Suite, позволяющее производить фаззинг не покидая Burp,
я же тебе отвечу: во-первых в Turbo Intruder нет поддержки прокси, либо я этого не знаю, а если я ошибаюсь, то кинь в меня ссылкой :), и во-вторых, могу же написать что то свое, почему бы не попробовать, согласись?

Сама идея возникла, когда я искал уязвимости при авторизации по номеру телефона через числовые коды в SMS. Нужен был удобный инструмент для подбора SMS-кодов.
В итоге, все это вылилось в фаззер, под названием rafuzz.




Фаззер работает через консоль, и принимает следующие параметры:

ПараметрОписаниеОбязательный параметр
frequestПуть к файлу с http запросомда
protocolПротокол запроса http/httpsда
fproxiesПуть к файлу со списком проксинет
range_startНачало диапазона для SMS-кодада
range_endКонец диапазона для SMS-кодада
range_stepШаг для диапазонанет
min_lenМинимальная длина коданет
stop_statusHttp статус при получении которого фаззер останавливает работунет
output_typeТип вывода all/found, по умолчанию found
all - выводит в консоль все ответы API цели
found - выводит в консоль ответ API цели если был угадан SMS-код
нет
print_requestВывод в консоль http запросанет

Пример запуска:

рисунок.png


Bash:
python3 rafuzz.py --frequest request.txt --range_start 1 --range_end 5 --min_len 4 --protocol http --fproxies proxies.txt --stop_status 200 --output_type all --print_request True

Одна из основных частей фаззера это класс для парсинга http запроса из Burp Suite:
Python:
class BurpRequestParser(object):
    def __init__(self, text: str):
        self.text = text

    def parse_text(self):
        result = {
            "headers": {}
        }
        lines = self._split_into_list(self.text)
        request_method, path, http_version = lines[0].split(" ")
        result["method"] = request_method
        result["path"] = path
        result["http_version"] = http_version

        start_post_data = False
        i = 1
        for line in lines[1:]:
            if line in ("\n", ""):
                start_post_data = True
                break
      
            values = line.split(":")

            header = values[0].strip()
            value = ":".join(values[1:]).strip()
            result["headers"][header] = value
            i += 1
 
        post_data = ""
        if start_post_data and len(lines) > i:
            for line in lines[i+1:]:
                post_data += line

        if post_data:
            result["post_data"] = post_data

        url = result["headers"]["Host"] + path

        result["url"] = url

        return Request(result)

    def _split_into_list(self, text):
        return text.split("\n")

В классе BurpRequestParser все просто, без использования регулярных выражений проходим построчно по http запросу и сохраняем в словаре результаты в ключах url, headers, method, path, http_version, post_data, в headers при этом также создаются пары ключ-значение по информации по заголовкам.

Вторая важная часть - это класс описания запроса
Python:
class Request(object):
    def __init__(self, data: dict):
        self.data = data
 
    def __getattr__(self, name: str):
        try:
            return self.data[name]
        except KeyError:
            return None

    def __setitem__(self, key, item):
        self.data[key] = item

    def __getitem__(self, key):
        return self.data[key]
 
    def __str__(self):
        request_text = f"{self.method} {self.path} {self.http_version}\n"
        headers = "\n".join(["{}: {}".format(h, v) for h, v in self.headers.items()])
        request_text += headers
        if self.post_data:
            request_text += "\n\n" + self.post_data

        return request_text

    def items(self):
        return self.data.items()

    def set_data(self, data: dict):
        self.data = data
 
    def get_data(self):
        return self.data

Результат парсинга http запроса передается в конструктор класса Request, в итоге получаем объект запроса с которым достаточно удобно обращаться к параметрам запроса, например request.url, request.method, request.post_data и др.


Основная логика фаззера описана в функции main:
Python:
async def main(args):
    backend_parser = BurpRequestParser
    rp = RequestParser(args.frequest, backend_parser)
    request = rp.parse_text()
    request.url = args.protocol + "://" + request.url

    key_request, subkey_request = find_world_in_request(request)
    if args.fproxies:
        ph = ProxiesHandler(args.fproxies)
    else:
        ph = None
 
    response_handler = ResponseHandler(
        stop_status=args.stop_status,
        output_type=args.output_type,
        print_request=args.print_request,
    )
    range_step = args.range_step or 1
 
    async with aiohttp.ClientSession() as session:
        for value in get_values_from_range(args.range_start, args.range_end, range_step, args.min_len):
            replica_request = Request(request.get_data().copy())
            replica_request.url = request.url
      
            replace_world_in_request(replica_request, key_request, subkey_request, value)
    
            no_answer = True
            attempt_request_limit = 10
            request_count = 0
            response = None
      
            while no_answer:
                proxy = None
                if ph:
                    if not ph.proxies:
                        print("List of working proxies is empty")
                        sys.exit()

                    proxy = ph.get_random_proxy()
          
                try:
                    response = await do_request(session, replica_request, proxy=proxy)
                except (aiohttp.client_exceptions.ClientProxyConnectionError, asyncio.TimeoutError):
                    print(f"There was failed request attempt with the proxy {proxy}")
                    if ph:
                        ph.exclude_not_working_proxy(proxy)
                else:
                    no_answer = False
                finally:
                    request_count += 1
          
                if no_answer and request_count > attempt_request_limit:
                    print("you have exceeded maximum request limit")
                    break

            await response_handler.handle(replica_request, response)


Если коротко описать логику работы этой функции, то в нем выполняются следующие шаги:
  1. Парсинг http запрос из Burp Suite,
  2. Обработка списка прокси если они были указаны
  3. Создание сессия через aiohttp
  4. Итерация по диапазонам SMS-кода
  5. Замена в объекте запроса ключевого слова FUZZ на новый SMS-код
  6. Выполнение запроса через aiohttp
  7. Исключение прокси из списка если через него не проходил запрос
  8. Печать в консоль результата

Из минусов в текущей реализации:
  1. Работа только с числовыми значениями для перебора
  2. Отсутствие подробной документации
  3. Малое количество тестов
  4. Отсутствие замеров по производительности
В планах все эти минусы исправить, конечно же, если найдется на это время.
Я думаю это все, что я тебе хотел рассказать про фаззер. Не суди строго, это моя первая статья здесь.
Ссылка на репозиторий GitHub - radaram/rafuzz: Fuzzer for working with an http request presented in text format from Burp Suite. Ставьте пальцы вверх, подписывайтесь :)

До встречи, Codeby!
 
Последнее редактирование модератором:

Muxtar

Green Team
02.06.2021
131
53
Знаю я полный НУБ, но хотел спросить что такое фазер? Чекал в инете - Ничего не понял. Объясните пожалуйста :)
 
Мы в соцсетях: