• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Свое расширение для Burp. Часть 7: Используем gobuster для поиска vhosts

petrinh1988

X-pert
Эксперт
Регистрация
27.02.2024
Сообщения
243
Реакции
493
Автор petrinh1988
Источник https://xss.pro


Какими бы хорошими сканерами не обладал Burp, есть огромная куча инструментов, которые лучше выполняют точечные задачи или позволяют развить атаку. Например, в Burp нет и десятой доли мощи от того же sqlmap. Поэтому, в сообществе появилось соответствующее расширение, которое позволяет интегрировать sqlmap в Burp. Но есть и другие специализированные инструменты, которые было бы неплохо научиться встраивать в инфраструктуру Burp, Один из них, gobuster. В этой статье займемся частичной интеграцией. Почему частичной? Потому что по аналогии вы сможете самостоятельно расширить интеграцию, добавляя однотипные функции. В ином случае, получится полотнище из серии “то да потому”, без смысловой нагрузки.

В этой статье, мы научимся запускать сторонние утилиты и обрабатывать их вывод в рамках расширения Burp. Для примера приведу два варианта запуска. Один из которых нужен скорее для примера, а может быть кому-то подойдет, как база для доведения до идеала. Кроме того, построим интерфейс на базе закладок, а также познакомимся с другими элементами интерфейса, например, выбором файлов. Но главное, для построения интерфейса будем использовать Apache NetBeans 22. Это бесплатная IDE для разработки на Java, которая поможет быстро и удобно собрать нужные формы в конструкторе, а после выгрузить их в виде кода. Тем самым, сильно сократив время разработки и почти полностью исключив ад)))

Vhost vs Subdomain?​

По сути, под vhost я буду подразумевать субдомен, не имеющий DNS-записи. Не получится зайти на него в браузере используя URL типа http://VHOST.domain.com, так как общедоступный DNS не знает IP-адрес связанный с этим хостом. Хотя, по факту, сам виртуальный хост существует и мы можем обратиться к нему по прямому IP или через домен, указав правильный заголовом Host.

Для понимания. Чтобы находить виртуальные хосты при помощи ffuf, строка запуска будет выглядеть примерно так ffuf -w /path/to/vhost/wordlist -u https://target -H "Host: FUZZ.target".

Ну или на примере библиотеки requests Python:

Python:
import requests
response = requests.get('https://subdomain.example.com/')

Подобный запрос спокойно получает ответ от поддомена, но получит ошибку “Failed to resolve 'subdomain.example.com' ([Errno 11001] getaddrinfo failed)” если речь идет о виртуальном хосте. К vhost получится обратиться подобным способом:

Python:
import requests
response = requests.get('https://example.com/', headers={'Host': 'subdomain.example.com'})

Подобный подход позволяет владельцам ресурсов скрывать чувствительные поддомены от прямого фаззинга или индексации поисковыми роботами. В обоих случаях, в ответ прилетит ошибка. Бывает, что владелец сайта просто снес DNS-запись на поддомен, считая, что он теперь недоступен. В любом случае, это еще один возможный вектор атаки.

Соответственно, у gobuster есть специальный режим поиска vhost, запускается следующим образом:

Bash:
gobuster vhost [flags]

Что предстоит сделать?​

Думаю, что в целом задача ясна, просто обговорим детали. Нам потребуется интерфейс, в котором мы настроим все параметры для запуска gobuster. Читай все аргументы типа поиска “vhost”

Bash:
Flags:
      --append-domain                     Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.
      --client-cert-p12 string            a p12 file to use for options TLS client certificates
      --client-cert-p12-password string   the password to the p12 file
      --client-cert-pem string            public key in PEM format for optional TLS client certificates
      --client-cert-pem-key string        private key in PEM format for optional TLS client certificates (this key needs to have no password)
  -c, --cookies string                    Cookies to use for the requests
      --domain string                     the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted
      --exclude-length string             exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206
  -r, --follow-redirect                   Follow redirects
  -H, --headers stringArray               Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
  -h, --help                              help for vhost
  -m, --method string                     Use the following HTTP method (default "GET")
      --no-canonicalize-headers           Do not canonicalize HTTP header names. If set header names are sent as is.
  -k, --no-tls-validation                 Skip TLS certificate verification
  -P, --password string                   Password for Basic Auth
      --proxy string                      Proxy to use for requests [http(s)://host:port] or [socks5://host:port]
      --random-agent                      Use a random User-Agent string
      --retry                             Should retry on request timeout
      --retry-attempts int                Times to retry on request timeout (default 3)
      --timeout duration                  HTTP Timeout (default 10s)
  -u, --url string                        The target URL
  -a, --useragent string                  Set the User-Agent string (default "gobuster/3.6")
  -U, --username string                   Username for Basic Auth

Global Flags:
      --debug                 Enable debug output
      --delay duration        Time each thread waits between requests (e.g. 1500ms)
      --no-color              Disable color output
      --no-error              Don't display errors
  -z, --no-progress           Don't display progress
  -o, --output string         Output file to write results to (defaults to stdout)
  -p, --pattern string        File containing replacement patterns
  -q, --quiet                 Don't print the banner and other noise
  -t, --threads int           Number of concurrent threads (default 10)
  -v, --verbose               Verbose output (errors)
  -w, --wordlist string       Path to the wordlist. Set to - to use STDIN.
      --wordlist-offset int   Resume from a given position in the wordlist (defaults to 0)

Соответственно, под параметры потребуются разные компоненты Java.Swing. Где-то текстовая строка, например, для указания имени пользователя и пароля. Где-то флажок, например, для -follow-redirect. Выпадающий список для выбора типа запроса (GET, POST, etc.). Окно выбора файла для словаря или сертификата. Прогрессбар для отслеживания статуса процесса. В общем, будет с чем потренироваться. Сам интерфейс реализуем на базе Tab-ов, чтобы не возникло заморочек, если захотите расширить возможности расширения.

Стартовый URL предлагаю задавать, как просто вводом текста, так и через контекстное меню. Например, зашел пользователь в Target и кликнул на карте сайта “Искать vhosts”. Для пользования удобно и нам лишняя тренировка.

Что касается получения результатов. Хотелось бы, чтобы все происходило интерактивно и найденные виртуальные хосты, сразу добавлялись на карту сайта. С этой целью, будет считывать вывод gobuster. Примерно такой принцип работы: пользователь указал все настройки и нажал кнопку “начать сканирование”. Приложение поменяло свое состояние. С этого момента, слушаем поток вывода данных и поток ошибок. Каждую строку анализируем регулярными выражениями. Если нашли новый поддомен и у него подходящий код ответа, то вызываем метод добавления на карту сайта.

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

Установка необходимого​

Я все делать буду на Windows, хотя нет никаких проблем все повторить в Linux. Для начала пару слов про установку gubouster, если еще не установлен. Качаем последний релиз отсюда. Распаковываем в любую папку, добавляем в системные переменные и, на всякий случай, в исключения антивируса Windows. Запускаем cmd и проверяем работоспособность, вызвав gobuster –help. Обычно проблем не возникает.

Как и писал выше, интерфейс будем собирать в Apache NetBeans. Скачать его можно здесь Особых каких-то настроек не требуется, все ставлю по дефолту.

Построение интерфейсов в Apache NetBeans​

Писать интерфейс руками это ад адский. Один раз мы через него прошли, думаю этого достаточно. В этот раз будем работать в удобной IDE.

Запускаем, выбираем “New Project”. Тип проекта “Maven” -> Java Application.

1723656565661.png


Все довольно стандартно. Имя проекта указываю GobusterTools. Group id вписываю com.xss_articles. Хотя по сути, все это нам особо и не потребуется. Но для порядка стоит указать, если будет много проектов, потом проще будет вспомнить.

1723656548432.png


Наш проект создан самое время добавить форму. Для этого слева кнопаем правой щелкой по нашему пакету и выбираем: New -> JPanel Form…

1723656529669.png


В появившемся окне не важно какое название мы дадим форме, но для простоты называю pnlMain.

1723656487819.png


Все приготовления завершены, перед нами рабочее пространство из пяти основных зон:

1723656443137.png

  1. Структура проекта, входящие в него пакеты. Не забивайте себе сильно голову. Здесь мы почти ничего делать не будем, только картинку накинем. Не забываем, мы тут только творим интерфейс, все остальное в коде Jython
  2. Инспектор объектов. Здесь наглядно видно что во что входит. Помимо этого, нам потребуется для выбора Layout Manager для компонентов
  3. Сам конструктор форм. Тут мы можем мышкой перетаскивать компоненты, дважды кликать на лейблы и менять текст, ресайзить и т.п. Но главное, мы четко видим, что и как выглядит, а значит удобно можем строить формы.
  4. Доступные компоненты. Панельки, labels, чекбоксы и все-все-все доступное по дефолту в Java Swing и AWT
  5. Свойства выбранного компонента. Выделили надпись на форме и поменяли задний фон. Как-то так.

Это большая часть информации, которая потребуется для работы в NetBeans.

Создадим интерфейс для поиска vhosts​

Первым делом, как и в прошлый раз, разобьем интерфейс. Сделаем красивый стилизованный header с логотипом любимого xss.pro, остальное поместим в центральную часть. Соответственно, наиболее удобный менеджер пространства это BorderLayout. Иду в рабочую зону №2, жму по нашей панельке правой кнопкой и выбираю:

1723656422775.png


После чего, панелька поделилась на части. Пока виртуально. В рабочей зоне №4 (справа вверху), нахожу компонент Panel и тащу его в верхнюю часть формы, ищу вот такое выделение рабочих зон:

1723656404154.png


Отлично! Панелька добавилась. Новой панельке задаю лэйаут FlowLayout и перекидываю компонент Label. Это будет логотип XSS.id, а для этого потребуются еще кой какие действия. Кстати, если не увидели Label в рабочей зоне №4, просто разверните список Swing Controls. Когда Label добавлен, в его свойствах (зона №5), находим “icon” и жмем на кнопку с тремя точками.

1723656387607.png


В появившемся окне, жму “Import to Project…”, нахожу файл на диске и со всем соглашаюсь. После вижу картинку загруженную в окно. Жму ок. Картинка добавилась, теперь надо кликнуть дважды по надписи “jLabel1” и удалить ее.

1723656365544.png


Теперь накинем нее еще одну панельку, которая нужна будет для отображения названия и описания нашего расширения. Для этого тащим новую панель из окна компонентов (№4). Тащим к нашему логотипу и размещаем справа. Так как у нас установлен FlowLayout, компоненты размещаются рядо друг с другом. Этой панельке задаю BoxLayout и справа (зона №5). Далее выделяю BoxLayout в зоне №2 (именно сам BoxLayout, а не Panel) и справа указываю свойство “Axis” как “Y Axis”. Таким образом, все что разместиться на этой панельке, будет размещаться друг под другом.

1723656332583.png


Выделил не только все нужные нам свойства и объекты, но и обозначил пунктиром, где должна быть наша панелька. На нее надо “сбросить” два Label. Один будет нашим названием, второй коротким описанием.

1723656311062.png


Если в зоне №2 непорядок и лейблы разместились не так, как нужно — хватайте их мышью прямо там и перетаскивайте в нужный Layout. По итогу, картинка должна выглядеть именно так. Осталось задать соответствующие свойства. А именно:

  1. Двум панелькам хедера и двум лейблам задать background в 47, 66, 85
  2. Обоим лейблам задать foreground в белый (255, 255, 255)
  3. Верхнему лейблу “Gobuster Tool” задать шрифт, кликнув на три точки в строке “font”

Должно получиться примерно так:

1723656251509.png


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

Кстати, для удобства можно работать с каждой панелью отдельно. Для этого, слева внизу ( №2) надо дважды кликнуть по нужной панельке. Тогда отключится все остальное и мы можем спокойно настраивать отдельную панель:

1723656214226.png


Самое время заняться центральной частью. Для этого хватаю еще одну панель и тащу ее на форму. Нужно добиться вот такого выделения:

1723656159833.png

Этой панельке задаю GridBagLayout.

В чем суть? Я хочу разбить область на логические части в которых объединю параметры по смыслу. Например, все что касается авторизации, будет отдельной панелькой. Все, что касается настроек домена будет отдельно панелью. Настройки User-Agent тоже отдельно и т.д. Перечислим все панельки, которые потребуются, а также распределю по ним опции:

  1. Домен
    1. Append domain - чекбокс.
    2. Domain (параметр используемый, если URL указан как IP) - строка
    3. URL - строка (-u, –url)
  2. Wordlist
    1. Путь к словарю - выбор файла
    2. Offset - смещение стартовой позиции в словаре, строка
  3. Параметры запросов
    1. Метод (POST/GET etc) - выпадающий список
    2. Random User Agent - чекбокс
    3. User-Agent - строка
    4. Cookies - строка
    5. Follow redirect - чекбокс
    6. No canonicalize headers - чекбокс
    7. Headers = добавляемые заголовки. Выполним в виде текстового поля, каждый заголовок с новой строки
  4. Авторизация
    1. Username - строка
    2. Password - строка
  5. Общие настройки
    1. Прокси - строка
    2. Количество потоков - строка
    3. Задержка между запросами - строка (158ms)
    4. Timeout - строка, максимальное время выполнения запроса
    5. Retry - чекбокс. Указывает, надо ли повторять попытку при таймауте
    6. Retry attempts - строка, количество попыток при вылете за таймаут
    7. Exclude length - строка. Отсекать результаты по размеру ответа.
    8. No tls validation - чекбокс
  6. Сертификаты
    1. Client-cert-p12
    2. Client-cert-p12
    3. Client-cert-pem
    4. Client-cert-pem-key

Получилось шесть панелек. Но они будут отличаться по размеру. Нужно будет как-то более-менее распределить их. Но сначала, просто накидаем все панельки на центральную область, чтобы они добавились все под наш GridBagLayout. Пока добавляем их, чтобы понять принципы формирования областей. После чего, кликаем правой кнопкой по GridBagLaout и выбираем Customize.

1723656125442.png


В данном конструкторе, можно спокойно хватать панельки и перетаскивать. Таким образом можно перестроить первоначальную сетку нужным образом. По факту, проходит настройка объекта GridBagConstraints, который управляет GridBagLayout менеджером. Помимо самой сетки и перетаскивания по ней, пользователю доступны настройки отступов и позиционирования, а также следующие параметры:

  1. Grid X и Grid Y - указание, где конкретно в сетке размещается компонент. Целое число.
  2. Grid Width и Grid Height - сколько ячеек занимает компонент. Целое число, но доступны также значения Relative и Remainde, “остносительно” и “остаток”, соответственно. Для понимания рекомендую побаловаться.
  3. Fill - заполнение. Помогает менеджеру понять, нужно ли пытаться заполнить пространство компонентом. Доступные значения: None, Both, Vertical, Horizontal.
  4. Internal Padding - соответственно, внутренние отступы по X и по Y
  5. Weight X и Weight Y - определяет отношении размера, т.е. какую часть компонент должен пытаться заполнить собой. Значение от 0.1 до 1.0. Соответственно, 1 - это 100% доступного пространства.
  6. Insets - отступы. Четыре числа, соответственно, для Top, Left, Bottom, Right

Для примера настройки параметров, раскрасил панели в разные цвета и на

1723656076191.png


Вдоволь наигравшись с панеками, удаляем их. Далее надо выделить GridBagLayout и справа в свойствах (зона №5) все свойства (Column Widths, Row Heights, etx) обнулить. После чего можно снова зайти в кастомизацию и уже приступить к построению интерфейса. Интерфейс расширять можно прямо из окна кастомизации, нажав правой кнопкой по сетке и выбрав “Add component”

1723656051264.png


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

1723656019262.png


В результате манипуляций, на выходе получилось 830 строчек Java-кода. Пихать это добро в один файл с другим кодом довольно сомнительное занятие. Проще вынести весь GUI в отдельный класс, отдельный файл и передавать ему все необходимые для работы данные.

Копирую строки, начиная со строки после private void initComponents() и до добавления интерфейса к pnlMain. К слову, в Java-коде pnlMain это текущий класс, нужно не забыть внести коррективы.

Для упрощения переноса, импортирую не компоненты в отдельности, а сразу пакетами swing и awt из javax и java, соответственно. Тогда можно javax.swing просто заменить на swing, ну и с awt та же история. Далее нужно избавиться от точки с запятой, а так же от комментариев. Комментарии ищу простой регуляркой “\/\/.*”. Так же, заменяю все “ new” на пустоту. Оператор new с ведущим пробелом можно заменять все одним скопом, а вот “new “ меняю просматрива каждый вариант, мало ли… Оборачиваю весь код в класс и функцию createGUI(). На этом моменте большинство “красноты” исчезло. Остается только найти и по-удалять листенеры событий, конструкции по типу “<>”, поправить заполнение комбобокса в конструктора, добавить явное указание pnlMain и возврат этой же панели. Не забываем про то, что нужно поменять импорт и установку картинки на следующий код:

Python:
        import os
        print(os.getcwd())
        path = os.getcwd()
        imgLogo = swing.ImageIcon(os.path.join(path, 'logo_xss.png'))
  lblLogo = swing.JLabel(imgLogo)

Покопавшись немного в коде, поменяв всю красноту и бегло пробежавшись, в итоге загружаю расширение в Burp:

1723655976884.png


В принципе, все круто. За исключением того, что я хотел обернуть интерфейс в табы. Помните, в самом начале писал, что хочу сделать задел чтобы потом можно было расширять функционал расширения и каждый мог дописать сам модули, которые ему требуются. Ну или сам в последующих материалах добью расширение до 100% покрытия gobuster. В любом случае, нужно познакомить вас с табами. По сути, там ничего сложного. Просто в самом конце файла интерфейс, вместо добавления pnlBody к pnlMain вставляю промежуточное звено:

Python:
        tbsMain = swing.JTabbedPane()
   
        lblDir = swing.JLabel('Dir')
        lblDns = swing.JLabel('DNS')
        tbsMain.addTab('Dir', lblDir)
        tbsMain.addTab('DNS', lblDns)
        tbsMain.addTab('Vhost', pnlBody)
        pnlMain.add(tbsMain, awt.BorderLayout.CENTER)

JTabbedPane — это компонент, который реализует возможность добавлять закладки в интерфейс. Делается это через addTab(), с указанием имени и компонента, который будет на табе. Первые два таба просто с надписями, т.к. они лишь гости в этой статье. Последний таб, это уже весь наш интерфейс.

1723655954630.png


Познакомимся с работой с диалоговыми окнами, реализуем интерфейс выбора файлов, а именно словаря. Для этого слегка подготовим код. Во-первых, объявлю переменную в которую положу путь к словарю. Во-вторых, pnlMain перенесу на уровень класса, чтобы был доступ. Нам потребуется ссылка на родительское окно при вызове диалогова окна. Также поступлю с lblWLPath, чтобы была возможность в интерфейсе вывести путь. Ну и, конечно же, скорректируем код кнопки “Choose”. У меня она называется “btnChooseWL”. Для начала изменю конструктор, передав в него надпись на кнопке и функцию обработки клика через параметр actionPerformed. Не забудьте удалить кусок “btnChooseWL.setText("Choose")”. Билдер интерфейсов нагенерил нам много ненужного кода, но вы уже и сами видели…

Python:
class GUITab(ITab):
    wordlistFile = None
...
        btnChooseWL = swing.JButton('Choose', actionPerformed =
...
self.onclickChooseFile)
...
    def onclickChooseFile(self, event):
        fileDialog = swing.JFileChooser()
        filterExt = swing.filechooser.FileNameExtensionFilter("wordlists", ["txt", "csv", "lst"])
        fileDialog.addChoosableFileFilter(filterExt)
        selectedFile = fileDialog.showDialog(self.pnlMain, "Wordlist")

        if selectedFile == swing.JFileChooser.APPROVE_OPTION:
            file = fileDialog.getSelectedFile()
            self.wordlistFile = file.getPath()
            self.lblWLPath.setText(file.getPath())
            print('Selected wordlist: ' + self.wordlistFile)

Перезагружаю расширение, запускаю, жму кнопку и… нифига не работает… почему? Потому что забыл добавить один нужный импорт. Без него, интерпретатор не видит filechooser и, соответственно, FileNameExtensionFilter…

Python:
from javax.swing.filechooser import FileNameExtensionFilter
Вот теперь можно свободно выбирать интересующий нас словарь:

1723655931174.png


Что же, последние приготовления перед переходом к главному. А именно, перетащить все переменные ссылающиеся на текстовые поля и чекбоксы в self. У меня все текстовые поля начинаются с “txt”, а значит должно сработать простая замена: Так же с checkbox (chk) и т.д.

Действия на кнопки мы привяжем позже. Теперь все красиво, можно на время отложить наше расширение в сторону и поговорить о главном функционале..

Как запускать сторонние приложения​

Нам потребуется запустить подпроцесс с возможностью контролировать выходной поток данных и поток ошибок. По идее, для этого, есть две альтернативы: стандартный вариант с subprocess Python и вариант с Java Runtime.exe(). Важно подчеркнуть, что здесь область моего незнания. Может я поверхностно подошел к данному вопросу, но я не нашел полноценного человеческого сравнения этих двух подходов. Такое ощущение, что никому в голову не пришло сравнить запуск стороннего приложения из Java и Python (сарказм).

Могу лишь отметить, что большинство расширений Burp с открытым исходным кодом, используют именно Java Runtime. И возможно, потому что при использовании subprocess, Burp перестанет реагировать на любые действия пользователя, пока не будет завершен запущенный подпроцесс. Повторюсь, возможно что-то упустил. Работа с подпроцессами в Python, это достаточно большой раздел, который, так уже сложилось, мне ранее не особо требовался. Для примера набросал простое расширение, которое запускает gobuster из контекстного меню. Без настроек, просто кликаем по любому объекту содержащему IHttpRequestResponse, например по таргету в Target, после чего оно запустит подряд три вызова gobuster. Три вызова,чтобы были разные примеры вывода: сначала вывод справки, потом ошибки, далее запуск скана таргета. Обращаю внимание! Этот файл положите отдельно от того, что мы делали выше. Мы еще вернемся к интерфейсу, пока просто побалуемся.

Python:
from burp import IBurpExtender, IContextMenuFactory
from javax.swing import JMenuItem
import time
import subprocess


class BurpExtender(IBurpExtender):
    def registerExtenderCallbacks(self, cb):
        self._cb = cb
        cb.registerContextMenuFactory(MyContextMenu(cb))

class MyContextMenu(IContextMenuFactory):
    def __init__(self, cb):
        self._cb = cb
        self._helpers = cb.getHelpers()
 
    def createMenuItems(self, invocation):
        menuItem1 = JMenuItem("Run Gobuster", actionPerformed=self.menuClick)
        httpService = invocation.getSelectedMessages()[0].getHttpService()
        self.url = httpService.getProtocol() + '://' + httpService.getHost()

        port = str(httpService.getPort())
        if not port is None and not port in ['80', '443']:
            self.url += ':' + str(httpService.getPort())

        return [menuItem1]
     
    def menuClick(self, event):
        cmd = 'gobuster --help'
        self.runSubprocessAndWait(cmd)
        cmd = 'gobuster vhostmost --help'
        self.runSubprocessAndWait(cmd)    
        cmd = 'gobuster vhost -u ' + self.url + ' --append-domain -w=E:\\SecLists\\Discovery\\DNS\\subdomains-top1million-5000.txt -q'
        self.runSubprocessAndWait(cmd)
   
    def runSubprocessAndWait(self, cmd):
        self.proc = subprocess.Popen(cmd , stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        while(self.proc.poll() is None):
            print('RUNNING OUT')
            print(self.proc.stdout.readline())
            print('RUNNING ERROR')
            print(self.proc.stderr.readline())
            time.sleep(1)
        print('PROCESS END')

Не забудьте поменять путь к словарю, в данном случае захардкожен путь к моему SecLists. В остальном, расширение боль-мень автономное.

1723655822254.png


После запуска, в диспетчере задач появился наш экземпляр запущенного gubuster. Как только наступает момент чтения потока, Burp замирает. Отомрет только когда завершиться. После завершения, наше расширение прочитает потоки вывода и ошибок, выведет результат. Попытки обернуть чтение в отдельный поток Thread, результата не дали. Полностью блокируется ввод/вывод всего Burp. Если вам комфортнее писать расширение максимально на Python, у вас два пути - либо выводить результаты работы в файл и проверять состояние функцией .poll() (если None, поток еще работает), либо искать вариант как читать потоки без блокировки.

Вероятно дело в том, что наш проект Jython проходит прекомпиляцию в Java .class и по факту работает не как скрипт Python. Где-то по этому пути теряется возможность параллельного чтения потоков ввода/вывода. Перепишу все основываясь на доступных классах Java, чтобы работало без замираний.

Python:
from burp import IBurpExtender, IContextMenuFactory
from java.lang import Runtime
from javax.swing import JMenuItem
from java.lang import Runtime
from java.lang import Process
from java.lang import System
from java.lang import Runnable
from java.lang import Thread
from java.io import File
from java.io import Reader
from java.io import BufferedReader
from java.io import InputStreamReader

class BurpExtender(IBurpExtender):
    def registerExtenderCallbacks(self, cb):
        self._cb = cb
        cb.registerContextMenuFactory(MyContextMenu(cb))
   
class StreamGobbler(Runnable):

    def __init__(self, inStream):
        self.inStream = inStream
        return

    def run(self):
        try:
            isr = InputStreamReader(self.inStream)
            br = BufferedReader(isr)
            line=br.readLine()

            while (not line is None):
                line = br.readLine()
                print(line)

        except BaseException as ex:
            print('Could not read input/output/error buffer\n')

class MyContextMenu(IContextMenuFactory):
    def __init__(self, cb):
        self._cb = cb
        self._helpers = cb.getHelpers()
 
    def createMenuItems(self, invocation):
        menuItem1 = JMenuItem("Search VHOSTs", actionPerformed=self.menuClick)
        return [menuItem1]
     
    def menuClick(self, event):
        cmd = 'gobuster --help'
        self.runSubprocessAndWait(cmd)
        cmd = 'gobuster vhostmost --help'
        self.runSubprocessAndWait(cmd)    
        cmd = 'gobuster vhost -u https://example.com --append-domain -w d:\\subdomains-top1million-5000.txt -q'
        self.runSubprocessAndWait(cmd)
   
    def runSubprocessAndWait(self, cmd):
        execMethod = getattr(Runtime.getRuntime(), "exec")
        run_command = cmd.split(' ')
   
        self._process = execMethod(run_command, None, File('d:\\gobuster_Windows_x86_64\\'))
   
        self.errorGobbler = Thread(StreamGobbler(self._process.getErrorStream()))
        self.outputGobbler = Thread(StreamGobbler(self._process.getInputStream()))

        self.errorGobbler.start()
        self.outputGobbler.start()

Вся магия происходит в методе runSubprocessAndWait(). Метод getRuntime() класса Runtime, как бы это странно не звучало (сарказм), предоставляет нам доступ к среде выполнения. Из всего обилия, нас интересует метод exec(). По факту, мы могли бы просто использовать Runtime.getRuntime().exec(). Но читаемость не самая лучшая, для примера не годится. Поэтому, через getattr() получаю этот метод в переменную execMethod, после чего выполняю вызов.

Метод exec() имеет огромное количество перегрузок. Например, я сплитую cmd и передаю в exec() массив, хотя можно было бы спокойно и строку отправить. Но чем больше вариантов мы пробуем, тем больше понимания и лучше усваивается информация. Помимо команды, передаем так же и директорию, которая должна быть Java-типом File(). В целом, если системные переменные прописаны и ОС знает откуда запускать команду, можно не передавать директорию. Тогда достаточно будет exec(cmd). Для справки, вторым параметром, в данной перегрузке, exec() получает массив переменных среды. Подробнее про exec() и варианты запуска здесь.

Результатом выполнения exec() будет объект с типом Process. При помощи него мы можем получить потоки ввода/вывода, получить стандартный exitValue() по завершению, уничтожить процесс или дождаться его выполнения через waitFor(). Последнее не рекомендую, так как без дополнительных манипуляций это заморозит весь BurpSuite. Из Process нам потребуются потоки, возможность получить результат exit code и возможность остановить процесс.

Работа с потоком данных​

Не важно, используем мы Java Runtime или Python subprocess, в любом случае нам нужно получить потоки данных. Для этого создаv специализированный класс GobusterStreamReader, потомка Runnable . Пока мы не добрались до конечной реализации, класс принимает только поток из которого будет производится чтение. Так как класс является дочерним от Runnable, необходимо реализовать метод run(), в котором и происходит чтение. На текущий момент, кроме получения и перенаправления вывода ничего не происходит:

Python:
class GobusterStreamReader(Runnable):

    def __init__(self, inStream):
        self.inStream = inStream
        return

    def run(self):
        try:
            isr = InputStreamReader(self.inStream)
            br = BufferedReader(isr)
            line=br.readLine()

            while (not line is None):
                line = br.readLine()
                print(line)

        except BaseException as ex:
            print('Could not read input/output/error buffer\n')

Чтение происходит построчно, при помощи Java-класса BufferedReader. BufferedReader, как понятно из названия, буферизует поток символов, обеспечивая простой и эффективный способ чтения целыми строками. Но перед чтением, нам обязательно нужно превратить поток байт в строковые символы, для этого используется класс InputStreamReader. Чтение происходит в рамках непрерывного цикла, пока он получает хоть какие-то данные из потока.

Чтобы чтение производилось параллельно, оборачиваем их в отдельные потоки используя Thread. Благодаря этому, нам нет нужды использовать таймеры для задержек внутри циклов чтения..Каждый поток живет своей жизнью и не блокирует расширение или Burp.

Python:
        self.errorGobusterSR = Thread(GobusterStreamReader(self._process.getErrorStream()))
        self.outpuGobusterSR = Thread(GobusterStreamReader(self._process.getInputStream()))

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

Python:
        self.errorGobusterSR.start()
        self.outpuGobusterSR.start()

Собираем все в кучу​

Интерфейс готов, утилита запускается, потоки полностью под нашим контролем. Осталось объединить и добавить совсем немного кода, чтобы все четко работало. Вопрос лишь в том, как организовать взаимодействие между разными классами. Интерфейс должен иметь возможность запускать и прерывать процесс по нажатию на кнопки “Start” и “Stop”. Так же, к процессу должен быть доступ и из контекстного меню. Помните, в самом начале обговаривали о том, что было бы неплохо дать такую возможность. Ну и запуски не должны пересекаться. Если откуда-то запустили процесс сканирования, повторного запуска происходить не должно, у нас на данный случай не предусмотрено никакого механизма. Последним требованием будет то, что контекстное меню должно иметь возможности получить настройки для запуска.

Немного поразмыслив, пришёл к выводу, что оптимальный вариант это не запускать сканирование через контекстное меню, а передавать url в соответствующее текстовое поле и переключать вкладку. Тем более, получится достаточно крутая демонстрация. Вспомним, что расширение встраивается в Burp на уровне класса и получает все соответствующие возможности. В том числе и прямой доступ к классам BurpSuite. В том числе, к закладкам. Но, все по порядку. Сначала добавим класс меню:

Python:
#file main.py

class BurpExtender(IBurpExtender):
    def registerExtenderCallbacks(self, cb):
        self._cb = cb
        self._helpers = cb.getHelpers()
        cb.setExtensionName('Gobuster Tool')
        self._tab = GUITab(self)
        cb.addSuiteTab(self._tab)

...

class MyContextMenu(IContextMenuFactory):
    def __init__(self, extender):
        self._cb = extender._cb
        self._helpers = extender._helpers
        self._extender = extender
 
    def createMenuItems(self, invocation):
        self._invocation = invocation
        menuItem1 = JMenuItem("Search VHOSTs", actionPerformed=self.menuClick)
        menuItem1.setEnabled(not self._extender._tab.isRunning)
        return [menuItem1]
     
    def menuClick(self, event):
        httpService = self._invocation.getSelectedMessages()[0].getHttpService()
        host = httpService.getHost()
        port = httpService.getPort()
        protocol = httpService.getProtocol()
        url = protocol + '://' + host

        if not port in [443, 80]:
            url += url + ':' + str(port)

        self._extender._tab.setUrl(url)
        self._extender._tab.switchTab()

Какие отличия от того, что делали раньше? В меню передается объект BurpExtender со всеми свойствами, в том числе объект закладки. Таким образом получаем возможность управлять доступностью пункта меню:

Python:
#file main.py class MyContextMenu
menuItem1.setEnabled(not self._extender._tab.isRunning)

Второй момент в том, что появляется возможность обратной связи для передачи URL на нашу вкладку и последующего переключения. Код функции setUrl() достаточно прост, просто устанавливается текст у поля. Гораздо интереснее посмотреть на функцию switchTab():

Python:
#file GUI.py class GUITab()

def switchTab(self):
        tabs = self.pnlMain.getParent()
        tabIndex = self.getTabIndex(tabs)
        tabs.setSelectedIndex(tabIndex)
        self.tbsMain.setSelectedIndex(2)
        # self.setBurpTabColor(tabs, tabIndex)

    def getTabIndex(self, tabs):
        for i in range(tabs.getTabCount()):
            if self.tabName in tabs.getTitleAt(i):
                return i
        return None

    def setBurpTabColor(self, tabs, index):
        tabs.setBackgroundAt(index, awt.Color(0xff6633))

Вспоминаем про встраивание расширение, как Java-класс и понимаем, что мы можем обратиться к родителю нашей панели. Родителем является инструмент управления закладками JTabbedPane. Соответственно, мы можем полноценно с ним взаимодействовать. Узнать индекс нашей вкладки, пройдя циклом по всем вкладкам. Открыть не только нашу вкладку, но и любую другую через setSelectedIndex(). Можно скрывать, деактивировать вкладки. Можно установить цвет вкладке через setBackgroundAt(), что зачастую бывает правильнее, чем наглое открытие вкладки. Например, оповещения удобно реализовывать через смену цвета или текста. На всякий случай, оставил два варианта на выбор. Хотите меняйте цвет, хотите оставляйте переключение.

Запуск сканирования​

Что нам предстоит сделать? Для начала, реализую класс, который будет отвечать за запуск и остановку процесса сканирования. Конечно же, совместно с классом чтения потоков вывода и ошибок. Здесь все просто, берем старые наработки, оборачиваем в отдельный класс и добавляем объект в виде переменной к основному классу BerpExtender. Так у интерфейса появится доступ, мы же будем передавать основной класс в конструктор интерфейса:

Python:
#file main.py
class BurpExtender(IBurpExtender):
    def registerExtenderCallbacks(self, cb):
        self._cb = cb
        self._helpers = cb.getHelpers()
        self._runner = GobusterRunner()
        cb.setExtensionName('Gobuster Tool')
        self._tab = GUITab(self)
        cb.addSuiteTab(self._tab)
        cb.registerContextMenuFactory(MyContextMenu(self))

class GobusterRunner():
    def runSubprocessAndWait(self, cmd):
        execMethod  = getattr(Runtime.getRuntime(), "exec")  
        self._process = execMethod(cmd)    
        self.errorGobusterSR = Thread(GobusterStreamReader(self._process.getErrorStream()))
        self.outpuGobusterSR = Thread(GobusterStreamReader(self._process.getInputStream()))
        self.errorGobusterSR.start()
        self.outpuGobusterSR.start()

    def stopScan(self):
        self._process.destroy()
   
class GobusterStreamReader(Runnable):
    def __init__(self, inStream):
        self.inStream = inStream
        return

    def run(self):
        try:
            isr = InputStreamReader(self.inStream)
            br = BufferedReader(isr)
            line=br.readLine()

            while (not line is None):
                line = br.readLine()
                print(line)

        except BaseException as ex:
            print('Could not read input/output/error buffer\n')

Из нового здесь только вызов метода destroy() для уничтожения запущенного нами процесса. Когда будете копировать код, не забудьте поменять путь к gobuster и то, что cmd сразу приходит массивом. До этого сплитовал строку, но там просто тренировались. По факту, передача массивом гораздо удобнее. Тем более, что нам нужно составить командную строку из параметров указанных в интерфейсе, а значит мороки будет меньше.

Самое время добавить обработчики событий для кнопок в интерфейсе. Не буду заморачиваться и прописывать логику компоновки параметров, они будут передаваться на запуск “как есть”. И так уже слишком увлекся, затяну статью, но слов из песен не выкинуть.

Python:
  #file GUI.py class GUITab()
        self.btnStart = swing.JButton('Start', actionPerformed=self.onClickStart)
        self.btnStop = swing.JButton('Stop', actionPerformed=self.onClickStop)

Python:
    #file GUI.py class GUITab()
    def onClickStart(self, event):
        if self.isRunning:
            return
   
        self.isRunning = True
        self.btnStart.setEnabled(False)
        self.btnStop.setEnabled(True)

        cmd = ['gobuster', 'vhost', '-q']

        #Domain
        pAppendDomain = self.chkAppendDomain.isSelected()
        pUrl = str(self.txtUrl.getText())
        pDomain = str(self.txtDomain.getText())

        if not pUrl:
            return
   
        cmd.append('-u ' + pUrl)

        if pAppendDomain:
            cmd.append('--append-domain ')

        if pDomain:
            cmd.append('' + pDomain)
        #Wordlist
        pWLPath = str(self.lblWLPath.getText())
        pWLOffset = str(self.txtWLOffset.getText())

        if not pWLPath:
            return
   
        cmd.append('-w ' + pWLPath)

        if pWLOffset:
            cmd.append('--wordlist-offset ' + pWLOffset)

        #Auth
        pUsername = str(self.txtUsername.getText())
        pPassword = str(self.txtPassword.getText())

        if pUsername:
            cmd.append('-U ' + pUsername)
               
        if pPassword:
            cmd.append('-P ' + pPassword)

        #Requests
        pMethod = str(self.cmbMethod.getSelectedItem())
        pRua = self.chkRua.isSelected()
        pUserAgent = str(self.txtUserAgent.getText())
        pCookies = str(self.txtCookies.getText())
        pRedirect = self.chkRedirect.isSelected()
        pNoCanonical = self.chkNoCanonical.isSelected()
        pCustomHeaders = str(self.txtCustomHeaders.getText())

        cmd.append('-m ' + pMethod)

        if pRua:
            cmd.append('--random-agent')
        elif pUserAgent:
            cmd.append('-a ' + pUserAgent)

        if pRedirect:
            cmd.append('-r')
       

        if pNoCanonical:
            cmd.append('--no-canonicalize-headers')

        if pCookies:        
            cmd.append('--cookies' + pCookies)

        if pCustomHeaders:
            headers = ' '.join(['-H "' + header + '"' for header in pCustomHeaders.split('\n')])
            cmd.append(headers)

        #Common
        pProxy = self.txtProxy.getText()
        pThreads = str(self.spnThreads.getValue())
        pDelay = self.txtDelay.getText()
        pTimeout = self.txtTimeout.getText()
        pRetry = self.chkRetry.isSelected()
        pRetriesCount = str(self.spnRetriesCount.getValue())
        pExcludeSize = self.txtExcludeSize.getText()
        pNoTLS = self.chkNoTLS.isSelected()

        if pProxy:
            cmd.append('--proxy ' + pProxy)
               
        if pThreads:
            cmd.append('-t ' + pThreads)

        if pDelay:
            cmd.append('--delay ' + pDelay + 'ms')

        if pTimeout:
            cmd.append('--timeout ' + pTimeout + 'ms')

        if pRetry:
            cmd.append('--retry')

            if pRetriesCount:
                cmd.append('--retry-attempts ' + pRetriesCount)

        if pExcludeSize:
            cmd.append('--exclude-length ' + pExcludeSize)

        if pNoTLS:
            cmd.append('-k')

        #Cert
        pCertP12 = self.txtCertP12.getText()
        pertP12Passw = self.txtCertP12Passw.getText()
        pCertPem = self.txtCertPem.getText()
        pCertPemKey = self.txtCertPemKey.getText()

        if pCertP12:
            cmd.append('--client-cert-p12 ' + pCertP12)

        if pCertP12:
            cmd.append('--client-cert-p12-password ' + pertP12Passw)

        if pCertP12:
            cmd.append('--client-cert-pem ' + pCertPem)

        if pCertP12:
            cmd.append('--client-cert-pem-key ' + pCertPemKey)

        self._extender._runner.runScanProcess(cmd)
  self._cb.issueAlert('Scanning started')

Все, что делаем, это проверяем есть ли значение, если есть то добавляем в массив параметров запуска. Логика только на уровне “обязательно должен быть URL” и “количество повторных запросов учитывать, только если установлен флажок с повторами”. После формирования строки, запускаем через runScanProcess(). Переименовал функцию, так как старое название было артефактом и не отражало сути.

Python:
  #file GUI.py class GUITab()
  def onClickStop(self, event):
        if self.isRunning:
            self._extender._runner.stopScan()
            self.btnStart.setEnabled(True)
            self.btnStop.setEnabled(False)
            self.isRunning = False
            self._cb.issueAlert('Scanning stopped by user')
Кнопка стоп просто сбрасывает значения состояний, вызывает destroy() процесса и сообщает об этом пользователю через стандартные сообщения.

Python:
#file main.py
class GobusterRunner():
    def __init__(self, extender):
        self._extender = extender

    def runScanProcess(self, cmd):
        print(cmd)
        print(' '.join(cmd))
        execMethod  = getattr(Runtime.getRuntime(), "exec")  
        self._process = execMethod(' '.join(cmd))    
   
        self.errorGobusterSR = Thread(GobusterStreamReader(self._process.getErrorStream()))
        self.outpuGobusterSR = Thread(GobusterStreamReader(self._process.getInputStream()))
        self.errorGobusterSR.start()
        self.outpuGobusterSR.start()    

        self.scan_runner = Thread(GobusterScanProcess(self._process, self._extender._tab))
        self.scan_runner.start()

    def stopScan(self):
        self._process.destroy()

class GobusterScanProcess(Runnable):
    def __init__(self, process, tab):
        self._process = process
        self._tab = tab
        return

    def run(self):
        try:
            self._process.waitFor()
            self._tab.finishScan(self._process.exitValue())
        except:
            print('Scanner error')

class GobusterStreamReader(Runnable):

    def __init__(self, inStream):
        self.inStream = inStream
        return

    def run(self):
        try:
            isr = InputStreamReader(self.inStream)
            br = BufferedReader(isr)
            line=br.readLine()

            while (not line is None):
                line = br.readLine()
                print(line)

        except BaseException as ex:
            print('Could not read input/output/error buffer\n')

Управление процессами уместилось в три класса. Два из них вы уже знаете, третий нужен просто для отслеживания момента окончания сканирования. Как только GobusterScanProcess видит, что процесс завершен, он вызывает функцию завершения в интерфейсе расширения: сброс переменных и сообщение пользователю.

Осталось обработка вывода gobuster. За чтение потоков ввода и вывода, отвечает класс GobusterStreamReader(). Именно в нем логично разместить чекер найденных виртуальных хостов. Для этого достаточно просто обрабатывать подобную строку:
Bash:
Found: beta.example.com Status: 200 [Size: 8533]
Набросал простейшее регулярное выражение, которое шаблонно ищет четыре группы значений. Если на выходе имеем все три группы, то передаем информацию в расширение. Как видно из кода ниже, добавление уязвимости реализовал, расширив класс GobusterRunner. Этот класс отвечает за сам процесс сканирования, поэтому вполне логичным было разместить в нем функционал реагирования. Заодно исправил логическую ошибку в связке классов GobusterRunner, GobusterScanProcess и GUITab. Класс GobusterScanProcess должен взаимодействовать исключительно с GobusterRunner и ничего не знать про интерфейс. Вроде мелочь, но иначе паутинка связей будет разрастаться и поддержка расширения превратиться в ад. Теперь код всех трех классов выглядит следующим образом:

Python:
#file main.py
class GobusterRunner():
    def __init__(self, extender):
        self._extender = extender

    def runScanProcess(self, cmd):
        print(cmd)
        print(' '.join(cmd))
        execMethod  = getattr(Runtime.getRuntime(), "exec")  
        self._process = execMethod(' '.join(cmd))    
   
        self.errorGobusterSR = Thread(GobusterStreamReader(self._process.getErrorStream(), self))
        self.outpuGobusterSR = Thread(GobusterStreamReader(self._process.getInputStream(), self))

        self.errorGobusterSR.start()
        self.outpuGobusterSR.start()    

        self.scan_runner = Thread(GobusterScanProcess(self))
        self.scan_runner.start()

    def stopScan(self):
        self._process.destroy()

    def finishScan(self, returncode):
        self._extender._tab.finishScan(returncode)

    def checkOutputLine(self, line):
        try:
            line = str(line)
            if 'Found' in line:
                results = re.findall('(Missed|Found):\s(\w.*?)\s.*(\d{3}).*Size:\s(\d+)', line)[0]
                if len(results) == 3:
                    self.appendGobusterResult(results)
        except:
            print('Error parse result')


    def appendGobusterResult(self, arrValues):
        pass

class GobusterScanProcess(Runnable):
    def __init__(self, runner):
        self._runner = runner
        self._process = runner._process
        return

    def run(self):
        try:
            self._process.waitFor()
            self._runner.finishScan(self._process.exitValue())
        except:
            print('Scanner error')

class GobusterStreamReader(Runnable):

    def __init__(self, inStream, runner):
        self.inStream = inStream
        self._runner = runner
        return

    def run(self):
        try:
            isr = InputStreamReader(self.inStream)
            br = BufferedReader(isr)
            line=br.readLine()
      self._runner.checkOutputLine(str(line))

            while (not line is None):
                line = br.readLine()
          self._runner.checkOutputLine(str(line))
                print(line)

        except BaseException as ex:
            print('Could not read input/output/error buffer\n')

Здесь возникает дилемма: как лучше сообщать о найденных виртуальных хостах? С одной стороны, это намек на возможную уязвимость и нужно генерировать соответствующий объект IScanIssue. С другой стороны, наличие виртуального хоста, это отличный шанс для дополнительного сканирования, как пауком, так и сканерами. Оба варианта полезны, поэтому реализуем их совместно. Заодно познакомимся с некоторыми новыми методами API Burp Extender и вспомним, как работать с объектами уязвимостей.

У gobuster есть особенность о которой важно знать. Он прекрасно ищет поддомены, используя тот же DNS или VHOST, но в отношении виртуальных хостов есть сюрприз. Даже когда gobuster их находит, он не показывает их как Found. Как Found отмечаются только поддомены. Виртуальные хосты можно отлоивить, ориентируясь на Missed и стутус 200.

1723655784677.png


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

Фиксация найденных VHOSTs на карте сайта и в уязвимостях​


Чтобы добавить новый элемент на карту сайта, в Burp Extender предусмотрен соответствующий коллбэк в IBurpExtenderCallbacks, называется метод addToSiteMap(). Вспоминаем, что Burp не оперирует простыми объектами, типа URL, вместо них используется гораздо более объемный IHttpRequestResponse.

Основная проблема в том, чтобы собрать все данные в кучу и превратить их в IHttpRequestResponse . Чтобы получить IHttpRequestResponse нам нужно выполнить запрос из расширения своими руками. Для этого доступны две перегрузки коллбэка makeHttpRequest(). В первом случае принимает объект IHttpService и byte [] request, во втором добавляется логический оператор указывающий на приоритет HTTP версии 1.1. Именно эти два варианта возвращают IHttpRequestResponse, все остальные перегрузки возвращают массив байт, который нам не подходит.

С Request все просто. Пишу строкой обычный GET-запрос к “/”, добавляя при этом заголовок Host, в который помещаю хост из результатов Gobuster. Использовать хелпер, который соберет GET-запрос на основании данных Burp, не имеет смысла, так как придется обходить заголовки и править хост. Лишняя работа, которая никому не нужна. Нужно только сконвертировать строку в байт-массив при помощи хелпера.

А вот для IHttpService, воспользуюсь хелпером buildHttpService(). Ему надо передать хост (это ответ gobuster), порт и протокол. Чего нет, спокойно достанем из URL.

Python:
#file main.py class GobusterRunner()

    def appendGobusterResult(self, arrValues):
        statusResult, vhost,statusCode,resposneSize = arrValues
        protocol,domain = self._scanUrl.split('://')
        useHttps = protocol == 'https'
        port = self._scanUrl.split(':')[-1]
        if not str(port).isnumeric():
            if useHttps: port = 443
            else: port = 80

        if statusResult == 'Found':
            requestString = 'GET / HTTP/1.1\nHost: ' + vhost
            request = self._extender._helpers.stringToBytes(requestString)
            httpService = self._extender._helpers.buildHttpService(vhost, port, useHttps)
            httpRequestResponse = self._extender._cb.makeHttpRequest(httpService, request)
            self._extender._cb.addToSiteMap(httpRequestResponse)

Супер! Осталось генерация ошибок. Учитывая, что мы ищем виртуальные хосты, уязвимость должна генерироваться только в том случае, если это реально виртуальный хост, а не субдомен. В идентификации нам поможет результат Missed, но со статусом 200. Для подтверждения, выполним два обычных GET-запроса с помощью библиотеки requests. Один с добавлением заголовка Host, второй без. При этом, первый должен пройти и вернуть хоть какой-то статус-код, а второй должен выбросить ошибку, которую мы поймаем при помощи исключения с типом ConnectionError. Если все прошло именно так, уязвимость подтверждена и можно ее добавить.

Обновленная функция добавления subdomains & vhosts
Python:
from VhostIssue import VhostIssue
...
#file main.py class GobusterRunner()

    def appendGobusterResult(self, arrValues):
        statusResult, vhost,statusCode,resposneSize = arrValues
        protocol,domain = self._scanUrl.split('://')
        useHttps = protocol == 'https'
        port = self._scanUrl.split(':')[-1]
        if not str(port).isnumeric():
            if useHttps: port = 443
            else: port = 80

        if statusResult == 'Found':
            requestString = 'GET / HTTP/1.1\nHost: ' + vhost
            request = self._extender._helpers.stringToBytes(requestString)
            httpService = self._extender._helpers.buildHttpService(vhost, port, useHttps)
            httpRequestResponse = self._extender._cb.makeHttpRequest(httpService, request)
            self._extender._cb.addToSiteMap(httpRequestResponse)

        if statusResult == 'Missed' and int(statusCode) in self.goodStatuses:
            response = requests.get(self._scanUrl, headers={'Host': vhost})
            responseCode = response.status_code
            if int(responseCode) in self.goodStatuses:
                try:
                    url = protocol + '://' + vhost
                    if port and not port in [80, 443]:
                        url += ':' + str(port)
                    response = request.get(url)
                except:
                    print('FOUND VHOST: ' + vhost)
                    self._extender._cb.issueAlert('Found hidden vhost: ' + vhost)
                    scanURL = URL(protocol, domain, port, '')
                    httpService = self._extender._helpers.buildHttpService(vhost, port, useHttps)
                    vhostIssue = VhostIssue(scanURL, httpService, vhost)
                    self._extender._cb.addScanIssue(vhostIssue)


Для добавления уязвимости, потребуется хотя бы один объект IHttpRequestResponse. Но при этом, если мы попытаемся выполнить запрос к виртуальному хосту через makeHttpRequest, мы получим ошибку, а не IHttpRequestResponse. Поэтому,

Класс уязвимости
Python:
#file VhostIssue.py

from burp import IScanIssue

class VhostIssue(IScanIssue):
    def __init__(self, url, httpService, vhost):
        self._url = url
        self._httpService = httpService
        self._vhost = vhost

    def getConfidence(self):
        return "Certain"

    def getHttpMessages(self):return None

    def getHttpService(self):
        return self._httpService

    def getIssueDetail(self):
        return 'Hidden virtual host that can be accessed with the header: <b>Host: ' + self._vhost + '</b>'

    def getIssueName(self):
        return 'Hidden VHOST'

    def getSeverity(self):
        return "Medium"

    def getUrl(self):
        return self._url
 
    def getIssueBackground(self): pass
    def getIssueType(self):pass
    def getRemediationBackground(self):pass
    def getRemediationDetail(self):pass

Ура! Расширение работает! В целом, на этом все. Но прежде чем закончить, хотел еще зацепить момент сохранения и загрузки настроек, а заодно исправив момент, который сильно раздражает…

1723655761469.png


Исправлю то, что раздражает​

Первое, что ни в какие ворота — необходимость каждый раз осуществлять длительный поиск директории со словарем. Хочется, чтобы расширение запоминало из какой папки последний раз выполнялось открытие и поиск начинался с нее.

Сам JFileChooser прекрасно принимает в конструктор путь, который будет открыт по умолчанию. Не хватает только механизма хранения. Решим задачу при помощи стандартных коллбэков saveExtensionSetting() и loadExtensionSetting().

Изменения в открытии файла
Python:
#file GUI.py class GUITab()
...
import os
...
class GUITab(ITab):
...
    def __init__(self, extender):
  ...
        self.wordlistFile = self._cb.loadExtensionSetting('gobuster_tool_wl')
        self._wordlistFolderdefault = self._cb.loadExtensionSetting('gobuster_tool_wl_path')
  ...

    def getUiComponent(self):
  ...
        self.lblWLPath.setText("Choose file, please")
        if self.wordlistFile:
            self.lblWLPath.setText(self.wordlistFile)
  ...

    def onclickChooseFile(self, event):
        fileDialog = swing.JFileChooser(self._wordlistFolderdefault)
  ...
        if selectedFile == swing.JFileChooser.APPROVE_OPTION:
      ...
            self._wordlistFolderdefault = os.path.dirname(os.path.abspath(self.wordlistFile))
            self._cb.saveExtensionSetting('gobuster_tool_wl_path', self._wordlistFolderdefault)
      self._cb.saveExtensionSetting('gobuster_tool_wl', self.wordlistFile)

Теперь при инициализации интерфейса будут получаться последние значения с именем файла со словарем и директорией, в которой он лежит. Поиск файла будет стартовать с последней использованной директории. Ну и путь к самому файлу будет подгружаться сам. Если постоянно используется какой-то один словарь, какой смысл его выбирать каждый раз?

Вместо заключения​

В этой статье мы разобрались в нескольких серьезных вопросах, которые помогут вывести расширения на совершенно иной уровень. Во-первых, более эффективный способ построения интерфейсов с использованием NetBeans. В том числе интерфейсов с табами. Во-вторых, научились полноценно запускать сторонние утилиты и обрабатывать их вывод. Разобрались, как сохранять и загружать настройки расширения среди данных Burp. Научились “вторгаться” и манипулировать интерфейсом самим Burp. В целом, получили неплохое расширение. Да, оно не покрывает полность функционал того же Gobuster, но и задачи такой не ставилось. По примеру приведенному в статье, можно спокойно подключить остальные модули Gobuster.

Мне Burp напоминает некий хирургический инструмент. Надеюсь мои статьи помогут вам сделать Burp еще более эффективным в работе. Не важно, для себя, как пет-проект или же как разработка расширений для заказчиков. Впереди еще буквально несколько статей и можно будет сказать, что на xss.pro есть полное руководство по разработке расширений при помощи API Burp Extender.
 

Вложения

  • 1723655846821.png
    1723655846821.png
    8.9 КБ · Просмотры: 9
  • gobuster_vhost.zip
    11.6 КБ · Просмотры: 12
Последнее редактирование модератором:
Елу осилил. Автору респект.
Увидел скрины NetBeans - сперва даже не поверил. Я думал, что он давно мертв и все пересели на жетбрейновских мутантов (если что, то я про потребление ресурсов). Ан нет. Жив курилка.
 
Елу осилил. Автору респект.
Увидел скрины NetBeans - сперва даже не поверил. Я думал, что он давно мертв и все пересели на жетбрейновских мутантов (если что, то я про потребление ресурсов). Ан нет. Жив курилка.
JB мощь, особенно когда был вариант их абсолютно бесплатно пользовать официально. Раньше была тема, что в партнерских обучалках они раздавали лицензии, типа как студентам, на весь софт на халяву. Давно подобного не встречал. Потому и взял абсолютно бесплатную, но далеко не хреновую альтернативу). Плюс, мне так кажется, что NetBeans все же попроще в освоении. Мы и так тут на стыке сидим, вроде Python, а нифига - Jython, добро пожаловать в мир Java, вас тут ждет миллион всего нового))))

По объему... все стараюсь короче статьи делать, но люди разные читают, что-то упустишь и капец, нихрена не понятно. Сам ненавижу статьи читать подобные. Вроде все ок-ок, а потом немой вопрос "какого хрена? откуда ты это взял?. А все просто, то что для автора очевидно, далеко не всегда очевидно для читателя. Вот и пытаюсь балансировать. По секрету скажу, что опубликованный материал это где-то 2/3, если не половина, от того что изначально накатал, а потом жестоко порезал. Хоть кофе-брейки в статьи вставляй по номерам, чтобы читателю комфортно осилить было)
 
Видимо из-за того, что добавил сообщение, уже не могу редактировать статью. Поэтому напишу отдельным сообщением:

Для того, чтобы прикрутить оставшиеся части Gobuster, необходимо произвести ряд изменений. Если говорить конкретнее:
  1. Раздробить интерфейс на несколько частей, выделив pnlBody в отдельные классы под каждый из табов.
  2. Сам класс, отвечающий за общий интерфейс, должен превратиться в некий мидл, который будет связывать сканер и конкретный таб через промежуточные функции и переменные состояний. Действия кнопок должны взаимодействовать с общим интерфейсом, а он уже с самим объектом GobusterRunner
  3. Необходимо организовать блокировку интерфейса на время сканирования. Например, подменой табов на экран состояния с выводом данных gobuster. Это исключит параллельный запуск сразу нескольких инструментов, что может приводить к снижению точности сканирования или блокировке атакующего IP на сервере.

Если необходимо, могу сделать вторую часть, в которой опишу весь процесс и прикручу, например, dir из арсенала gobuster. Соответственно, как-то дайте знать об этом.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх