Содержание статьи
Burp идеально подходит для сбора информации по конкретному таргету. Acunetix шикарно выполняет массовые сканирования. У обоих инструментов различные наборы сканеров, и часто они находят непересекающиеся уязвимости. Идея в том, чтобы встроить в интерфейс Burp расширение, которое даст возможность запустить дополнительное сканирование в Acunetix и получить найденные уязвимости в Burp.
Основа
В предыдущей статье я разбирал нюансы, которые пригодятся, если планируешь применять на практике изложенное здесь. Мы подготовили рабочую среду, создали и запустили расширения на Jython + Burp Extender API. Затем реализовали коннект Acunetix API, интерфейс для ввода данных и хранения их между запусками Burp.
Готовые исходники расширения ты можешь скачать с моего GitHub.
Список файлов:
-
burp-acunetix.— основной файл расширения со всеми настройками и предустановками;py -
acu_client.— класс с методами Acunetix API;py -
jtransport.— класс, реализующий запросы по HTTP/HTTPS;py -
store.— класс для хранения информации о состоянии расширения;py -
run_api_task.— класс для реализации многопоточности;py -
menu.— реализация пунктов контекстного меню Burp;py -
ui.— файл с интерфейсом расширения.py
В статье буду избегать объяснений по части интерфейса, иначе и без того большой материал превратится в гигантский.
Ищем и загружаем таргеты из Acunetix
Итак, предположим, что ты скачал перечисленные выше файлы. Открываем проект и продолжаем реализацию задуманного. Начнем с правок в файле ui.. Он отвечает за интерфейс расширения. В текущей версии интерфейс — это два текстовых поля для кред к Acunetix API и пара кнопок.
Для поиска таргетов удобно добавить таблицу (компонент Java JTable), текстовое поле JTextField и кнопку поиска JButton. Разместить элементы нужно на новой панели JPanel, которую прикрепим к основной панели. В файле ui. в функцию _build_ui добавляем код:
# Найди эту строкуmain_container.add(Box.createVerticalStrut(15))# Добавь код нижеself.dynamic_panel = JPanel(BorderLayout())self._show_search_panel()# Конец кодаmain_container.add(self.dynamic_panel)Создай функцию _show_search_panel, в которой пропиши создание компонентов для новой панели:
def _show_search_panel(self): self.dynamic_panel.removeAll() panel = JPanel(GridBagLayout()) panel.setBorder(BorderFactory.createTitledBorder("Search Targets")) gbc = GridBagConstraints() gbc.insets = Insets(6, 6, 6, 6) gbc.anchor = GridBagConstraints.WEST gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE panel.add(JLabel("Search:"), gbc) gbc.gridx = 1; gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL self.search_field = JTextField() panel.add(self.search_field, gbc) gbc.gridx = 2; gbc.weightx = 0; gbc.fill = GridBagConstraints.NONE self.search_btn = JButton("Search", actionPerformed=self.on_search_targets) self.search_btn.setPreferredSize(Dimension(100, 28)) panel.add(self.search_btn, gbc) columns = ["ID", "Address", "Description"] self.table_model = DefaultTableModel(columns, 0) self.targets_table = JTable(self.table_model) self.targets_table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION) self.targets_table.getSelectionModel().addListSelectionListener(self.on_target_selected) table_scroll = JScrollPane(self.targets_table) table_scroll.setPreferredSize(Dimension(0, 180)) gbc.gridx = 0; gbc.gridy = 1; gbc.gridwidth = 3; gbc.weighty = 1.0 gbc.fill = GridBagConstraints.BOTH; gbc.insets = Insets(10, 0, 0, 0) panel.add(table_scroll, gbc) self.dynamic_panel.add(panel, BorderLayout.CENTER) self.dynamic_panel.revalidate() self.dynamic_panel.repaint()def on_search_targets(self, event): # Обработка события нажатия кнопки поиска passdef on_target_selected(self, event): # Обработка события выбора таргета pass
В файле acu_client. найди функцию _get_targets. Она отвечает за получение таргетов из Acunetix. Принимает два параметра: query и normalize. Первый — это строка поиска. Второй параметр указывает, что нужен объект, подготовленный к выводу в таблице. Значение True заставит функцию удалить все лишние поля.
Создай функцию‑обертку search_targets, это чисто стилистическая вещь, которая позволит сделать код читаемым и удобным для масштабирования. Функция передает управление _get_targets, устанавливая normalize в True:
def search_targets(self, query): return self._get_targets(query=query, normalize=True)Вернись в файл интерфейса ui.. Чтобы оживить кнопку Search, замени код функции on_search_targets:
def on_search_targets(self, event): # Получаем значение поисковой строки self.query = self.search_field.getText().strip() # Отключаем кнопку, чтобы избежать повторных нажатий self.search_btn.setEnabled(False) # Меняем надпись на кнопке, чтобы пользователь видел разницу self.search_btn.setText("Searching...") # Передаем управление функции поиска и вывода данных self._search_targets()Когда пользователь кликнет по кнопке, мы изменим ее вид и передадим управление в _search_targets. Внутри выполним запрос к API, получив ответ, положим его в таблицу.
def _search_targets(self): api_key, api_url = self.get_credentials_from_fields() if not api_key or not api_url: self._callbacks.issueAlert("API credentials are missing. Please fill in the fields and save the settings.") JOptionPane.showMessageDialog(self.panel, "API access data is missing. Please enter the data in the text fields and save.", "Error", JOptionPane.ERROR_MESSAGE) return def do_search(): self.client.set_credentials(api_url, api_key) self._callbacks.printOutput("[SEARCH] Searching targets: %s" % self.query) return self.client.searc_targets(self.query) def on_success(result): success, message = result if success: self._update_search_table(message) return err = "Search failed: %s" % message self._callbacks.issueAlert(err) JOptionPane.showMessageDialog(self.panel, err, "Error", JOptionPane.ERROR_MESSAGE) def on_error(error): self._callbacks.printError("[SEARCH] Search error: %s" % str(error)) def on_finally(): self._callbacks.printOutput("[SEARCH] Finished") self.search_btn.setText("Search") self.search_btn.setEnabled(True) self.task_runner.run("SEARCH", do_search, on_success, on_error, on_finally)Чтобы интерфейс не зависал, запросы к Acunetix API делаем в отдельном процессе. Для этого у нас есть класс ApiTaskRunner, в котором лежит метод run(. Алгоритм любой функции, работающей с клиентом API (класс AcunetixClient), выглядит так:
def _search_targets(self): # Какие-то действия def do_search(): # Выполнение запроса к API через клиент AcunetixClient return self.client.<some_function>() def on_success(result): # Функция выполняется в случае успешного запроса def on_error(error): # Выполняется в случае ошибки # Обычно: self._callbacks.printError("[<SERVICE-NAME>] <описание>: %s" % str(error)) def on_finally(): # Необязательная функция. Если указана, выполняется независимо от результата self.task_runner.run("<SERVICE-NAME>", do_search, on_success, on_error, on_finally)Отлично! Твое расширение умеет запрашивать список таргетов по поисковой строке. Осталось сделать вывод результатов в таблицу. Создай функцию:
def _update_search_table(self, targets): self.table_model.setRowCount(0) if not targets: self._callbacks.printOutput("[UI] No targets to display") return for t in targets: addr = t.get("address", "N/A") desc = (t.get("description") or "")[:120] if len(desc) == 120: desc += "..." tid = t.get("id", "N/A") self.table_model.addRow([tid, addr, desc]) self._callbacks.printOutput("[UI] Updated table with %d targets" % len(targets))Эта функция очистит таблицу через setRowCount(, а после добавит найденные цели. Обнови расширение в Burp и проверь работу поиска.

Особенности пагинации Acunetix
Рано или поздно ты столкнешься со слишком большим объемом данных. Это могут быть, например, сотни уязвимостей на слабом таргете. Придется использовать механизм пагинации Acunetix, которая довольно специфична.
Нельзя просто указать страницу и получить нужный срез. API в параметре ?c= ждет курсор страницы. Курсор можно получить, выполнив запрос к API, в ответе будет свойство pagination. Внутри свойства — массив cursors:
"pagination": { "count": 1322, "cursor_hash": "9192d370043f1e7f14729e9c6fb143fb", "cursors": [ null, "100", "200" ], "sort": null }Первое значение null указывает, что данные получены с первой страницы, мы в начале списка. Следующие два значения — это указатель на следующую страницу и страницу «через одну» (текущая + 2). При запросе я установил параметр &l= (количество запрашиваемых объектов) в 100. Первая страница выводит объекты 0–99, вторая — 100–199, третья — 200–299...
Если ты предположил, что первое значение массива cursors указывает на предыдущую страницу, то ошибся. Первое значение — это текущая страница. Чтобы реализовать полноценную навигацию, запоминай значение предыдущих страниц.
Чтобы понять, что ты в конце списка, проверяй количество элементов cursors. Предпоследняя страница вернет только два элемента: текущую и следующую страницу. Последняя страница вернет один курсор, указывающий на текущую страницу.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
