D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор petrinh1988
Источник https://xss.is
Будем программировать помощника для Acunetix. На это у меня есть, минимум, три причины:
Во-первых, окунем пользуется огромное количество людей, в том числе форумчан. Соответственно, вполне актуально для жителей XSS. Во-вторых, сам пользую и сам давно хотел накидать какие-то улучшалки, но как водится…. свои сапоги всегда подождут. Ну и в третьих… у меня была уже статья про работу с Acunetix API. Вы всегда можете обратиться к ней, так как здесь я постараюсь избежать объяснений касающихся апи. Интересно, что через некоторое время после публикации, Litara_B удивил меня прислав скрин…
Кому-то (все знают кому), статья настолько понравилась, что он забрал код себе в канал. Ну забрал и забрал, хотя можно было бы сослаться, если не на статью, то хотя бы на сам XSS.is Значит я не ошибся и тема действительно нравится людям. Что же, самое время углубиться и сделать еще одну приятную утилиту.
Важно! Если вы случайно попали на эту статью и не хотите программировать, а хотите просто само расширение, переходите сразу к инструкции по использованию и скачивайте архивы прикрепленные к статье.
Вторая оговорка в том, что расширение будет написано для браузера Firefox, но нет никаких проблем адаптировать под тот же Chrome.
Просто дадим возможность пользователю в полтора клика добавить активный сайт как новый таргет и запустить сканирования. Установил расширение, добавил в тулбар, на нужном сайте кликнул по иконке и выбрал действие. При этом, важно создать хоть какую-то защиту от дублирования целей и информативность.
Забегая вперед покажу экраны расширения:
Данный экран расширения должен демонстрироваться в случае, если у нас нет никакой информации о таргете. Соответственно, пользователь может просто добавить цель в окуня. Либо создать цель и сразу запустить сканирование.
Если с именем хоста есть несколько таргетов, то пользователь должен иметь возможность выбрать с каким объектом поработать. Да, не самый информативный выбор, но по прочтение статьи вы сможете легко это исправить.
Ну и последний важный сейчас экран, выводит информацию об одном единственном объекте. Мы видим, что по объекту было выполнено сканирование 13-го ноября 2024 года. Критических уязвимостей не нашли, нашли одну с высоким риском. Ну и два кнопки:
JSON: Скопировать в буфер обмена
Соответственно, описываем внешний вид расширения: имя, описание, версию и иконку. Указываем окно опций, причем оно будет встроенным в интерфейс Firefox. Всплывающее окошко, которое и будет основным интерфейсом пользователя. Из интересного, это свойство “browser_specific_settings”.
Ранее этого свойства было достаточно чтобы можно было распространять расширение архивом. Можно было просто заархивировать расширение, после чего свободно устанавливать в браузер. Как я понимаю, сейчас остался только один способ полноценной установки расширения — это загрузить его в стор мозиллы и выкачать, чтобы получить подпись. Без подписи ни в какую не хочет. Поэтому, чтобы полноценно пользоваться расширением, зарегистрируйтесь на мозилле, загрузите туда архив, потом скачайте в виде .xpi и пользуйтесь. Либо, по необходимости, каждый раз добавляйте как расширение разработчика.
Обратите внимание, что предполагается два окна опций. Первое, когда у нас нет никаких данных:
Путь подставиться автоматически. Да, прямо на скрине упоминается, что стандартный адрес это 127… но если ваша машина изранена вмешательствами, как и у меня, url может не прокатить. Поэтому и сделал автоподстановку с localhost.
Все, что нужно пользователю сделать, это добавить ключ API и сохранить. После чего окно будет выглядеть следующим образом:
Здесь у нас появляется выбор стандартного профиля сканирования. Если нажать кнопку “Обновить”, соответственно, должны загрузиться профили из апи. Стандартный профиль используется для указания при сканировании, т.е. пользователь не будет иметь возможности выбрать разные профили для разных таргетов. Но, все у вас в руках… достаточно добавить чуть-чуть кода, если оно вам нужно.
За оживление интерфейса отвечают два скрипта:
JavaScript: Скопировать в буфер обмена
Вряд ли в global что-то интересное, поэтому разберем options.js. Начинается все с инициализации:
JavaScript: Скопировать в буфер обмена
Скрипт запрашивает данные у фонового скрипта. Если сохраненных данных нет, заполняем все стандартными значениями.
JavaScript: Скопировать в буфер обмена
В ином случае, выводим данные хранилища, включая сохраненные профили сканирования:
JavaScript: Скопировать в буфер обмена
В принципе, код вполне понятен. На всякий случай продемонстрирую, как выглядит сам объект сохраненный в локальном хранилище:
Остается назначить обработчики событий на кнопки. Начнем с кнопки инициализации:
JavaScript: Скопировать в буфер обмена
Элементарная защита от дурака и передача фоновому скрипту данных на сохранение. В случае ошибки, просто сбрасываем все данные. Что касается функция фоновых скриптов, я к ним скоро перейду, просто пытаюсь как-то разграничить разные разделы.
Кнопка сохранения настроек:
JavaScript: Скопировать в буфер обмена
Завершаем все назначением события на кнопку обновления профилей сканирования:
JavaScript: Скопировать в буфер обмена
Самое время посмотреть, что происходит в фоновых скриптах. Первым делом, нужно прописать функцию прослушивающую сообщения в рамках расширения:
JavaScript: Скопировать в буфер обмена
Благодаря использованию глобального объекта MSG_ACTIONS, мы без проблем можем вычленить интересующий кусок кода. Понятное дело, что практически все крутиться в пределах трех функций объекта storage.local:
JavaScript: Скопировать в буфер обмена
Обращаю внимание на присвоение к acunetix. Это глобальная переменная, в которой хранится ключ и урл апи. Периодически к нему будем обращаться.
Интересной может быть фукнция readLocalStorage, которая в сущности помогает нам приручить асинхронность при чтении из хранилища и красиво взаимодействовать с полученными данными, избегая коллбэков:
JavaScript: Скопировать в буфер обмена
Мы возвращаем промис, который при наличии нужного ключа возвращает нужное значение через resolve(). В ином случае, соответственно, reject.
Ну и функции получения профилей. Их две, но по сути одна является оберткой над другой. На самом деле их бы по разному назвать, но у меня с фантазией не очень)))
JavaScript: Скопировать в буфер обмена
В коде можно наблюдать вызов и ожидание функции getRequestAPI. Эта функция находится в файле acunetix.js. Подробно разберем файл позже, сейчас важно понимать, что в нем лежит эта функция и функция postRequestAPI(). Функция get используется для получения данных из api, post для сохранения.
На этом все, что касается опций.
Нужно учитывать, что до добавления данных от API, пользователь не может ничего делать. Учитывая, что нет никаких ухищрений, html-код привожу целиком:
HTML: Скопировать в буфер обмена
Вылилось это все в такую функцию:
JavaScript: Скопировать в буфер обмена
Дальше главное не запутаться в функциях и файлах. Самая простая это показать “нужны параметры”:
JavaScript: Скопировать в буфер обмена
Соответственно, hideAllDivs() нужна исключительно чтобы не заморачиваться с сокрытием экранов.
Займемся логикой:
JavaScript: Скопировать в буфер обмена
В фоновых скриптах мы вызываем следующие действия:
JavaScript: Скопировать в буфер обмена
Логика getTargetsInfo() такая:
В любом случае все заканчивается проверкой, не является ли объект массивом. Если это массив, просто возвращаем его, пользователю будет показан список и предложено выбрать. Если у нас одиночный объект, то управление передается функции getTargetDetails().
JavaScript: Скопировать в буфер обмена
Функция делает два запроса к API. Первый запрос, это детальная информация по объекту. Второй запрос, это информация по сканированиям связанных с объектом. После функция упрощает информацию о сканированиях, так как там много лишнего, далее объединяет два объекта.
Так выглядят объекты сканирования до упрощения:
Как видно, на входе достаточно сложный объект. Более того, включающий в себя “target”, информация о котором у нас уже имеется.
Вернемся к коду попапа. В результате выполнения getTargetDetails() мы придем к функции showTargetInfo() демонстрирующей подробную информацию:
JavaScript: Скопировать в буфер обмена
Функция длинная, но в сущности ничего необычного. Просто создается ряд html-элементов, включая таблицу с информацией. После добавляется это все на “экран” приложения. Вместе с информацией выводится две кнопки:
В целом, остальной код в popup не имеет больше каких-то особенностей. Вот, например, функция вывода списка целей:
JavaScript: Скопировать в буфер обмена
Как видно, все тоже самое. Создание html-объектов на лету. Добавление на экран. Назначение событий на кнопки. Соответственно, практически все вызовы кнопок это передача управления фоновому скрипту.
Вернемся к фоновому скрипту и посмотрим на полную функцию обрабатывающую события из других окон.
JavaScript: Скопировать в буфер обмена
Большую часть мы уже разобрали. Более того, в неразобранной части большинство функций известно. Осталось всего несколько сервисных функций, по ним и пробежимся:
JavaScript: Скопировать в буфер обмена
Это функция выбора из списка целей. В ней мы просто получаем детальные данные, далее проверяем код ответа. В случае отсутствия объекта, мы получим “404”. Если просто сохранить данные, то пользователь увидит кучу undefined, что не хорошо. В этом случае просто вернем пустой список, тогда логика попапа отработает правильно и пользователь увидит предложение создать новый объект. Если все ок, просто сохраняем данные и возвращаем подробную инфу.
Обращаю внимание, что при получении объекта URL у меня десериализуется две переменных: hostname и origin. Оставил так сугубо для демонстрации. Я выбрал работу через имя хоста, но это не обязательно правильно и удобно для всех. Возможно, вы захотите отдельно создавать объект по http протокол, отдельно под https. Ну, как пример.
JavaScript: Скопировать в буфер обмена
Функция формирует простейший новый таргет. Подробнее про создание таргетов можно прочитать в соответствующей статье, не вижу смысла на этом останавливаться. Далее этот объект, методом POST пересылается в Acunetix. Так устроен его апи, если гет это получение данных, пост сохранение.
JavaScript: Скопировать в буфер обмена
Функция создания сканирования работает аналогично функции создания нового таргета. Объект, отправить объект на апи, вернуть ответ.
Неразобранной осталась функция создания объекта и нового сканирования. Конечно же, это просто функция-обертка для двух предыдущих функций:
JavaScript: Скопировать в буфер обмена
Вот, практически, и все расширение. Конечно же, написание кода было подольше описания его здесь и сопровождалось сотней переписываний, ну что поделать… участь разработки такова. А у нас остался только один файл, который мы не рассматривали. Это файл acunetix.js
JavaScript: Скопировать в буфер обмена
Всего лишь две функции-обертки, getRequestAPI и postRequestAPI, для requestApi. Которая в свою очередь является простейшей оберткой для fetch. Почему не использовал fetch? Так удобнее. Как минимум, можно запихнуть в функцию console.log и отслеживать все запросы к апи. А в целом, часто в подобную функцию можно что-то и более полезное подпихнуть, поэтому стараюсь максимально оборачивать. Хотя иногда надоедает, но вы заметили, наверное, как у меня сильно меняется код в разных частях))))
Вас никогда не бесило, что нет кнопки “Добавить все”? А не бесил тот факт, что здесь нет никакого указания на то, добавлен ли домен уже в базу таргетов? И даже при добавлении таргета, если уже есть дубль, вы об этом никогда не узнаете!
Как понять, какие из этих 198 хостов уже добавлены как таргеты и оперативно добавить их? Скопировать в файли, почистить, написать скрипт чека (хуже того, чекать руками) есть таргет в окуне, после грузить как CSV?
Долой несправедливость! Нам нужен хороший удобный рабочий инструмент!!!
Хочу следующие функции:
Думаю, теперь вы понимаете, почему я решил сделать отдельную статью. Честно сказать, я аж кайфанул, когда решил это все сделать. Кода немного, зато сколько проблем снимается.
JavaScript: Скопировать в буфер обмена
Все вызовы функции сокрытия повешу на события кнопок и инициализацию. Так правильнее. Раз инициирован какой-то процесс, нужно скрыть интерфейс.
JavaScript: Скопировать в буфер обмена
Для вывода правильного экрана создам отдельную сервисную функцию .
JavaScript: Скопировать в буфер обмена
Останется заменить прямые “включения” экранов на вызов функции с нужным идентификатором.
Такой результат мы должны получить по итогу. На мой взгляд очень приятная картинка. Подгрузил CSV, быстренько пролистал удаляя красные объекты и загружай чистую базу.
Первое, что нужно понимать, это то, что нам потребуются контентные скрипты. Из фона, у нас нет доступа к коду страницы, а данные брать нужно именно оттуда. Я, как и раньше запихну их в манифест:
JavaScript: Скопировать в буфер обмена
Далее есть нюанс. Когда вы жмете на “Import CSV”, не происходит смены адреса. Происходит добавление новых компонентов на страницу. И здесь у нас есть несколько вариантов взаимодействия. Можно в интерфейс попапа добавить экран с которого руками запускать процесс. Но с точки зрения удобства и обучения, рассмотрим более интересный вариант, связанный с использованием MutationObserver и его методом observe().
Этот метод позволяет отслеживать изменения в DOM, причем достаточно тонко фильтруя с чем работаем. Но подробного обзора делать не стану, можете спокойно в гугле найти достаточно хороших примеров работы. В нашем случае код будет выглядеть так:
JavaScript: Скопировать в буфер обмена
Сначала создаем объект обсёрвера, передав в конструктор коллбэк-функцию. Далее вызываем метод observe(), передав ему фильтры. Начнем с того, что нас интересует тэг “acx-add-targets”. Именно в него команда окуня обернула инпуты с параметрами добавляемых таргетов. Передача observe() методу HTML-элемента позволяет избежать обработки лишнего. Ну и, соответственно, наш метод должен работать по дереву ниже от указанного элемента и с его потомками.
Внутри коллбэка нужно перебрать все мутации произошедшие на странице. Опять же, углубляться не вижу смысла, вы и сами можете увидеть два цикла позволяющие обойти все узлы. Внутри узлов мы ищем инпуты и, если есть хоть один, перекидываем url в фоновый скрипт.
Изменения в фоновом скрипте:
JavaScript: Скопировать в буфер обмена
Большая часть кода уже не нова. Слушатель получает сообщение с урлом и вызывает новую функцию чека. Далее, через getTargetsByHostname() проверяется существования таргета. Если есть хотя бы один таргет, отправляем соответствующее сообщение в контентный скрипт. Получается этакий пинг-понг.
Только отправка сообщения в контентный скрипт выполняется не через runtime.sendMessage, а через tabs.sendMessage. Отправка в контентный скрипт производится именно таким способом. Соответственно, здесь у нас появляется узкое место. Нам нужно найти нужный таб. Я сделал это через получение активного таба, соответственно, до окончания работы скрипта нельзя переключаться на другие закладки.
Важно не забыть в объект MSG_ACTIONS файла global.js два новых свойства: checkTargetExists и highlightInput. Плюс добавить туда функцию sleep, она нам потребуется чтобы избежать ошибок. По сути, это всем известный хук:
JavaScript: Скопировать в буфер обмена
Функция sleep нужна для построения цикла поиска HTML-элемента “acx-add-targets”. Дело в том, что наш скрипт подгружается слишком рано, когда объект еще не добавлен. В итоге, MutationObserver выбросит ошибку и ничего работать не будет. Поэтому, добавил такой кусок кода:
JavaScript: Скопировать в буфер обмена
Соответственно, обернув кусок кода в асинхронную функцию. Итоговый код контентного скрипта будет выглядеть так:
JavaScript: Скопировать в буфер обмена
В целом, поступим так же, как и с импортом из CSV. Добавлю еще одну функцию, которая будет вешать MutationObserver. В свою очередь, функцию запускать по window.location.href.
JavaScript: Скопировать в буфер обмена
Но здесь есть слабое место, которое я упустил. Так как переход между экранами Acunetix происходит без перезагрузки страницы, скрипт не перезагружается, а значит наши функции отработают один раз. Соответственно, при переходах скрипт перестанет работать. Чтобы решить эту проблему, сделаем хук, снова обратившись к MutationObserver, но следить будем за изменениями пути:
JavaScript: Скопировать в буфер обмена
Второй момент в том, что по факту, в фоновых скриптах код должен быть практически идентичный предыдущему. Разница только в том, что должен делать контентный скрипт. Чтобы не дублировать код, решил при отправке сообщения добавить еще один параметр — callbackType. В нем будем передавать, какой тип обработки планируется:
JavaScript: Скопировать в буфер обмена
Соответственно, в фоне тоже вносим изменения, добавив проброс параметра:
JavaScript: Скопировать в буфер обмена
И, конечно же, в самой функции чека тоже нужны изменения. Добавить параметр и поменять возврат:
JavaScript: Скопировать в буфер обмена
Пока забудем о фоновых скриптах. Позже нужно будет вернуться и добавить изменения, связанные с запуском нового сканирования и добавлением пачки таргетов, но это позже. Пока добьем контентный скрипт. А именно, добавим подсчет хостов не добавленных в таргеты и выделение их цветом.
Не забудем добавить новое свойство в глобальный MSG_ACTIONS:
JavaScript: Скопировать в буфер обмена
Вот как будет выглядеть результат работы нашего скрипта. Забегая вперед скажу, что это крайне кайфовая штука)))
Начнем с раскраса. Пойду стандартным путем, сначала раскрасив все объекты в серый цвет. После, все найденные объекты покрасим в зелененький. Чтобы реализовать это все, идем в нашу новую функцию updateDiscoveredHosts
JavaScript: Скопировать в буфер обмена
Как и прошлый раз, работаем с обсервером. Но помимо того, что мы отсеиваем все не HTML-элемент, нам нужно отсеить все объекты не содержащие класс “row-space-between”. В ином случае, у нас все пойдет не по плану, так как будут лишние запросы. Например, тот же заголовок.
Осталось добавить функцию окраса в зеленый (highlightHost) и все будет готово. Почему так? Потому что у нас фоновый скрипт отправит сообщение только в том случае, если объект уже существует. Таким образом, мы покрасим только те хосты, которые уже добавлены как таргеты:
JavaScript: Скопировать в буфер обмена
Оптимальным вариантом поиска, конечно же, будет XPath по тексту. Найдя элемент мы переходим к родителю объекта, чтобы окрасить всю строку. Все! Как сказал бы великий мыслитель, мы окрасили строки в те цвета в которые окрасили.
Чтобы реализовать оставшийся функционал, нам потребуется сбегать за костылями. Добавим несколько переменных:
JavaScript: Скопировать в буфер обмена
Название каждого говорит само за себя. Да, есть переменная для подсчета количества, хотя можно было бы вычесть длину массива существующих хостов из массива всех хостов, но… работает не трогай. Была ночь, писал код уставший и запутался в асинхронности. Учитывая, что функционал наращивать не планирую, можно и забить. Да-да, именно так и появляются любимые нами баги…
JavaScript: Скопировать в буфер обмена
Поменял основной if, добавив сброс переменных. Так как скрипт у нас не перезагружается, а переменные глобальные, нужно следить за их чистотой. В ином случае можно сильно удивиться.
Нам нужно изменить обсервер, который обрабатывает карточку с хостами:
JavaScript: Скопировать в буфер обмена
И так же в слушателя добавим код заботливо сохраняющий чекнутые хосты в массив существующих таргетов:
JavaScript: Скопировать в буфер обмена
Осталось дописать функцию updateTitle, добавив вывод количества новых хостов в заголовок:
JavaScript: Скопировать в буфер обмена
Вуаля, теперь у нас поменялся заголовок и мы видим количество новых хостов. Ну не красота ли? Нет, так как мне нужна кнопка!!!
Код добавления кнопки можно много куда запихать, но я решил не заморачиваться и ввернул его прямо в изменение заголовка. В целом, ничего необычного. Просто кнопка, просто стили, просто назначение события.
JavaScript: Скопировать в буфер обмена
Обратите внимание, что в глобальном объекте MSG_ACTIONS, появилось новое свойство. Вот как выглядит объект:
JavaScript: Скопировать в буфер обмена
Осталось исправить фоновые скрипты. Сначала добавим обработку еще одного типа сообщений:
JavaScript: Скопировать в буфер обмена
Кайфушка в том, что нам почти ничего писать и не нужно. Только функцию-обертку для appendManytargets.
JavaScript: Скопировать в буфер обмена
Причем, как вы видите, большая часть кода это отправка сообщения. Точно такая же отправка сообщения, как и в checkTargetExists. По хорошему, нужно вынести этот функционал и получить две отдельных функции, сильно сократив код:
JavaScript: Скопировать в буфер обмена
Мы получили достаточно компактный и красивый код. Но самое главное, он полностью рабочий. Знали бы вы с каким нетерпением я нажал кнопку “Add All”.
2. Получить ключ API, перейдя по адресу https://localhost:3443/app/profile
3. Сохранить ключ в настройках расширения
4. Выбрать дефолтный профиль сканирования и нажать кнопку “Save”
5. Закрепить иконку на тулбаре. На интересующем сайте кликнуть по ней, далее все интуитивно понятно. Если таргетов список, выбрать интересующий. Если таргета нет, создать новый или создать новый и запустить сканирование. В общем, все интуитивно понятно.
Соответственно, чтобы работали фишки связанные с интерфейсом, ничего не требуется. Оно работает само.
Например, с теми же хостами. Кому-то может показаться мелочью. Но мне, когда ты сидишь и пытаешься разобраться с пачкой из 200 найденных хостов…. Копируешь их, приводишь к правильному для окуня CSV, после пытаешься понять какие из них уже есть в окуне, а каких нет… А теперь. вместо этого, кликаешь на одну кнопку и через минуту у тебя уже все эти хосты перебраны, все новые добавлены. Согласитесь, разница существенная.
Да, над расширением можно работать и работать. Например, можно добавить кнопку “Add & Scan”, чтобы все новые хосты сразу летели в сканирование. Причем, код почти весь уже есть. Нужно просто скопировать функцию добавления хостов и к каждому добавлению прилепить запуск сканирования. Или, как вариант, можно удалить стандартную кнопку “Create target” и добавить свою. Которая будет фоном добавлять или же добавлять и открывать новую вкладку с созданным таргетом. Сами знаете, как бесит прыгать туда-сюда, когда пытаешься обнаруженные таргеты добавлять.
Статья вышла случайно. У меня была готовая на 99% общая статья про обнаружение уязвимостей типа XSS, SSTI и т.д. Про изменение интерфейсов и про запуск сторонних приложений из расширения… Но когда я понял, что один из примеров, посвященный Acunetix, можно превратить в действительно интересный инструмент… в общем, надеюсь вы тоже довольны полученным материалом. Ту же статью я тоже выложу в ближайшие дни. Нужно только придумать интересный пример, взамен Acunetix.
Источник https://xss.is
Будем программировать помощника для Acunetix. На это у меня есть, минимум, три причины:
Во-первых, окунем пользуется огромное количество людей, в том числе форумчан. Соответственно, вполне актуально для жителей XSS. Во-вторых, сам пользую и сам давно хотел накидать какие-то улучшалки, но как водится…. свои сапоги всегда подождут. Ну и в третьих… у меня была уже статья про работу с Acunetix API. Вы всегда можете обратиться к ней, так как здесь я постараюсь избежать объяснений касающихся апи. Интересно, что через некоторое время после публикации, Litara_B удивил меня прислав скрин…
Кому-то (все знают кому), статья настолько понравилась, что он забрал код себе в канал. Ну забрал и забрал, хотя можно было бы сослаться, если не на статью, то хотя бы на сам XSS.is Значит я не ошибся и тема действительно нравится людям. Что же, самое время углубиться и сделать еще одну приятную утилиту.
Важно! Если вы случайно попали на эту статью и не хотите программировать, а хотите просто само расширение, переходите сразу к инструкции по использованию и скачивайте архивы прикрепленные к статье.
Вторая оговорка в том, что расширение будет написано для браузера Firefox, но нет никаких проблем адаптировать под тот же Chrome.
Что будет делать расширение?
Важно понимать, что изначально, отдельной статьи по Acunetix, не должно было быть. Расширение было частью другой статьи, которая уже была практически готова к публикации. Но… в какой-то момент я понял, что важно добавить кой-какой функционал и это настолько изменило статью, что пришлось разбить на две. Для сохранения повествования, пока будем исходить из функционала связанного с добавлением объекта в сканер и запуске сканирования.Просто дадим возможность пользователю в полтора клика добавить активный сайт как новый таргет и запустить сканирования. Установил расширение, добавил в тулбар, на нужном сайте кликнул по иконке и выбрал действие. При этом, важно создать хоть какую-то защиту от дублирования целей и информативность.
Забегая вперед покажу экраны расширения:
Данный экран расширения должен демонстрироваться в случае, если у нас нет никакой информации о таргете. Соответственно, пользователь может просто добавить цель в окуня. Либо создать цель и сразу запустить сканирование.
Если с именем хоста есть несколько таргетов, то пользователь должен иметь возможность выбрать с каким объектом поработать. Да, не самый информативный выбор, но по прочтение статьи вы сможете легко это исправить.
Ну и последний важный сейчас экран, выводит информацию об одном единственном объекте. Мы видим, что по объекту было выполнено сканирование 13-го ноября 2024 года. Критических уязвимостей не нашли, нашли одну с высоким риском. Ну и два кнопки:
- Update - обновляет информацию об объекте. Это принудительное получение данных из API, а не из локального хранилища. Но об этом позже.
- New Scan, соответственно, позволяет запустить новое сканирование.
Начнем!
Первым делом, как водится, займемся манифестом.JSON: Скопировать в буфер обмена
Код:
{
"manifest_version": 3,
"name":"Acunetix Helper | XSS.is",
"description": "The extension is intended to demonstrate the development of a browser extension as part of an article for the XSS.is website.",
"version": "0.0.1",
"icons": {
"48": "assets/images/logo.png",
"96": "assets/images/logo.png"
},
"options_ui": {
"page": "assets/html/options.html"
},
"action": {
"default_icon": "assets/images/logo.png",
"default_title": "Acunetix Helper | XSS.is",
"default_popup": "assets/html/popup.html"
},
"background": {
"scripts": ["ext_js/global.js", "ext_js/acunetix.js", "ext_js/background.js"]
},
"permissions": [
"storage",
"unlimitedStorage",
"tabs"
],
"host_permissions":[
"<all_urls>"
],
"browser_specific_settings": {
"gecko": {
"id": "xss_is@extension_example.com"
}
}
}
Соответственно, описываем внешний вид расширения: имя, описание, версию и иконку. Указываем окно опций, причем оно будет встроенным в интерфейс Firefox. Всплывающее окошко, которое и будет основным интерфейсом пользователя. Из интересного, это свойство “browser_specific_settings”.
Ранее этого свойства было достаточно чтобы можно было распространять расширение архивом. Можно было просто заархивировать расширение, после чего свободно устанавливать в браузер. Как я понимаю, сейчас остался только один способ полноценной установки расширения — это загрузить его в стор мозиллы и выкачать, чтобы получить подпись. Без подписи ни в какую не хочет. Поэтому, чтобы полноценно пользоваться расширением, зарегистрируйтесь на мозилле, загрузите туда архив, потом скачайте в виде .xpi и пользуйтесь. Либо, по необходимости, каждый раз добавляйте как расширение разработчика.
Окно опций
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/bootstrap.min.css">
<title>Document</title>
</head>
<body>
<div class="container">
<h3>Acunetix params</h3>
<form>
<div class="mb-3">
<label for="api-url" class="form-label">API URL</label>
<input type="text" class="form-control" id="api-url">
<div id="api-url-help" class="form-text">Standart url is <span class="text-nowrap bg-body-secondary">https://127.0.0.1:3443/api/v1</span></div>
</div>
<div class="mb-3">
<label for="api-key" class="form-label">API Key</label>
<input type="text" class="form-control" id="api-key">
<div id="api-key-help" class="form-text">You can get api key <a href="https://localhost:3443/app/profile">here</a></div>
</div>
<button id="save-and-init" class="btn btn-success" style="display: none;">Save and Initialise</button>
<div id="profiles-info" class="mb-3">
<label for="api-profile-scan" class="form-label">Default profile scan</label>
<div class="row g-2">
<div class="col-11">
<select id="api-profile-scan" class="form-select">
</select>
</div>
<div class="col-1">
<button class="btn btn-outline-secondary" id="update-scan-profiles">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
</svg>
</button>
</div>
</div>
<div id="api-profile-scan-help" class="form-text" style="display: none;">To load the list of available scan profiles, enter the API key and click on the 'Save and Initialise' button.</div>
</div>
<button id="save-btn" class="btn btn-primary">Save</button>
</form>
</div>
<script src="../../ext_js/global.js"></script>
<script src="../js/options.js"></script>
</body>
</html>
Обратите внимание, что предполагается два окна опций. Первое, когда у нас нет никаких данных:
Путь подставиться автоматически. Да, прямо на скрине упоминается, что стандартный адрес это 127… но если ваша машина изранена вмешательствами, как и у меня, url может не прокатить. Поэтому и сделал автоподстановку с localhost.
Все, что нужно пользователю сделать, это добавить ключ API и сохранить. После чего окно будет выглядеть следующим образом:
Здесь у нас появляется выбор стандартного профиля сканирования. Если нажать кнопку “Обновить”, соответственно, должны загрузиться профили из апи. Стандартный профиль используется для указания при сканировании, т.е. пользователь не будет иметь возможности выбрать разные профили для разных таргетов. Но, все у вас в руках… достаточно добавить чуть-чуть кода, если оно вам нужно.
За оживление интерфейса отвечают два скрипта:
- global.js - это общий файл, хранящий объект со всеми возможными действиями при обмене сообщениями между разными частями расширения. Из фоновых в опции или попап и т.д.
- options.js - скрипт работающий только на странице опций, отвечающий за сохранение и вывод данных.
JavaScript: Скопировать в буфер обмена
Код:
const MSG_ACTIONS = {
loadData: 'loadData',
saveData: 'saveData',
clearData: 'clearData',
saveAndInit: 'saveAndInit',
updateProfiles: 'updateProfiles',
getTargetsFromStorage: 'getTargetsFromStorage',
getTargetsFromAPI: 'getTargetsFromAPI',
getTargetsInfo: 'getTargetsInfo',
getTargetDetails: 'getTargetDetails',
createTarget: 'createTarget',
createTargetAndScan: 'createTargetAndScan',
createNewScan: 'createNewScan'
}
Вряд ли в global что-то интересное, поэтому разберем options.js. Начинается все с инициализации:
JavaScript: Скопировать в буфер обмена
Код:
async function init() {
let data = await browser.runtime.sendMessage({
action: MSG_ACTIONS.loadData
})
if (!data)
return fillDefaultValues()
fillApiSettings({...data})
}
init()
Скрипт запрашивает данные у фонового скрипта. Если сохраненных данных нет, заполняем все стандартными значениями.
JavaScript: Скопировать в буфер обмена
Код:
function fillDefaultValues() {
let btnInit = document.querySelector('#save-and-init')
btnInit.style.display = 'block'
btnInit.addEventListener('click', btnSaveAndInitHandler)
document.querySelector('#api-url').value = `https://localhost:3443/api/v1`
document.querySelector('#api-profile-scan-help').style.display = 'block'
document.querySelector('#save-btn').style.display = 'none'
document.querySelector('#profiles-info').style.display = 'none'
}
В ином случае, выводим данные хранилища, включая сохраненные профили сканирования:
JavaScript: Скопировать в буфер обмена
Код:
let globalScanProfiles
function fillApiSettings(acunetix) {
const {apiKey, apiURL, scanProfiles, defaultProfile} = acunetix
document.querySelector('#api-url').value = apiURL
document.querySelector('#api-key').value = apiKey
fillScanProfiles(scanProfiles, defaultProfile)
}
function fillScanProfiles(profiles, defaultProfile) {
if (!profiles || !profiles.length) return
let selProfiles = document.querySelector('#api-profile-scan')
selProfiles.innerHTML = ''
profiles.forEach((scanProfile, index) => {
let option = document.createElement('option')
option.value = scanProfile.profile_id
option.innerHTML = scanProfile.name
selProfiles.append(option)
if (scanProfile.profile_id == defaultProfile)
selProfiles.selectedIndex = index
})
globalScanProfiles = profiles
}
В принципе, код вполне понятен. На всякий случай продемонстрирую, как выглядит сам объект сохраненный в локальном хранилище:
Остается назначить обработчики событий на кнопки. Начнем с кнопки инициализации:
JavaScript: Скопировать в буфер обмена
Код:
async function btnSaveAndInitHandler(event) {
event.preventDefault()
let apiURL = document.querySelector('#api-url').value
let apiKey = document.querySelector('#api-key').value
if (!apiKey || !apiURL) return alert('Api URL or api key is empty')
try {
let scanProfiles = await browser.runtime.sendMessage({
action: MSG_ACTIONS.saveAndInit,
apiURL, apiKey
})
fillScanProfiles(scanProfiles, scanProfiles[0].profile_id)
} catch(e) {
alert('Error!')
browser.runtime.sendMessage({
action: MSG_ACTIONS.clearData
})
}
document.querySelector('#save-and-init').style.display = 'none'
document.querySelector('#api-profile-scan-help').style.display = 'none'
document.querySelector('#save-btn').style.display = 'block'
document.querySelector('#profiles-info').style.display = 'block'
}
Элементарная защита от дурака и передача фоновому скрипту данных на сохранение. В случае ошибки, просто сбрасываем все данные. Что касается функция фоновых скриптов, я к ним скоро перейду, просто пытаюсь как-то разграничить разные разделы.
Кнопка сохранения настроек:
JavaScript: Скопировать в буфер обмена
Код:
document.querySelector('#save-btn').addEventListener('click', event => {
event.preventDefault()
let apiURL = document.querySelector('#api-url').value
let apiKey = document.querySelector('#api-key').value
let profileSelectedIndex = document.querySelector('#api-profile-scan').selectedIndex
let defaultProfile = globalScanProfiles[profileSelectedIndex].profile_id
browser.runtime.sendMessage({
action: MSG_ACTIONS.saveData,
acuSettings: {
apiKey, apiURL, defaultProfile
}
})
})
Завершаем все назначением события на кнопку обновления профилей сканирования:
JavaScript: Скопировать в буфер обмена
Код:
async function updateScanProfiles() {
let response = await browser.runtime.sendMessage({
action: MSG_ACTIONS.updateProfiles
})
}
document.querySelector('#update-scan-profiles').addEventListener('click', event => {
event.preventDefault()
updateScanProfiles()
})
Самое время посмотреть, что происходит в фоновых скриптах. Первым делом, нужно прописать функцию прослушивающую сообщения в рамках расширения:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
switch(data.action) {
case MSG_ACTIONS.loadData:
let acuSettings = await getData()
return acuSettings
case MSG_ACTIONS.saveAndInit:
acunetix = {
apiKey: data.apiKey,
apiURL: removeLastSlash(data.apiURL)
}
await saveData()
let response = await updateProfilesInit()
return response
case MSG_ACTIONS.saveData:
acunetix = {...acunetix, ...data.acuSettings}
await saveData()
return true
case MSG_ACTIONS.clearData:
browser.storage.local.remove('acunetix')
return true
case MSG_ACTIONS.updateProfiles:
let profiles = await updateProfiles()
return profiles
...
}
return true
})
Благодаря использованию глобального объекта MSG_ACTIONS, мы без проблем можем вычленить интересующий кусок кода. Понятное дело, что практически все крутиться в пределах трех функций объекта storage.local:
- Set
- Get
- Remove
JavaScript: Скопировать в буфер обмена
Код:
async function getData(){
try{
let data = await readLocalStorage('acunetix')
if (data && data.apiKey) apiKey = data.apiKey
if (data && data.apiURL) apiURL = data.apiURL
if (data) acunetix = {...data}
return acunetix
} catch(e) {
return null
}
}
async function saveData() {
return await browser.storage.local.set({acunetix: acunetix})
}
Обращаю внимание на присвоение к acunetix. Это глобальная переменная, в которой хранится ключ и урл апи. Периодически к нему будем обращаться.
Интересной может быть фукнция readLocalStorage, которая в сущности помогает нам приручить асинхронность при чтении из хранилища и красиво взаимодействовать с полученными данными, избегая коллбэков:
JavaScript: Скопировать в буфер обмена
Код:
async function readLocalStorage(key){
return new Promise((resolve, reject) => {
browser.storage.local.get([key], function (result) {
if (result[key] === undefined) {
reject();
} else {
resolve(result[key]);
}
});
});
};
Мы возвращаем промис, который при наличии нужного ключа возвращает нужное значение через resolve(). В ином случае, соответственно, reject.
Ну и функции получения профилей. Их две, но по сути одна является оберткой над другой. На самом деле их бы по разному назвать, но у меня с фантазией не очень)))
JavaScript: Скопировать в буфер обмена
Код:
async function updateProfiles() {
let response = await getRequestAPI(acunetix.apiURL, ACUNETIX_CMD.scaningProfiles, acunetix.apiKey)
let json = await response.json()
let result = json.scanning_profiles.map(profile => ({
profile_id: profile.profile_id,
name: profile.name
}))
acunetix = {...acunetix, scanProfiles: result}
return result
}
async function updateProfilesInit() {
let profiles = await updateProfiles()
acunetix = {...acunetix, defaultProfile: profiles[0].profile_id}
await saveData()
return profiles
}
В коде можно наблюдать вызов и ожидание функции getRequestAPI. Эта функция находится в файле acunetix.js. Подробно разберем файл позже, сейчас важно понимать, что в нем лежит эта функция и функция postRequestAPI(). Функция get используется для получения данных из api, post для сохранения.
На этом все, что касается опций.
Интерфейс пользователя
Выше уже приводил основные скрины приложения. Но к ним нужно добавить еще один:Нужно учитывать, что до добавления данных от API, пользователь не может ничего делать. Учитывая, что нет никаких ухищрений, html-код привожу целиком:
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../css/bootstrap.min.css">
<style>
body {
width: 600px;height: 500px;
}
#start, #process, .full-h {
height: 100vh;
}
</style>
<title>Document</title>
</head>
<body>
<!-- Loader screen -->
<div id="process" class="container" style="display: none;">
<div class="row justify-content-center align-items-center full-h">
<div class="col-sm text-center">
<img src="../images/processing.gif" alt="">
</div>
</div>
</div>
<!-- Loader screen -->
<!-- Need settings -->
<div id="not-init" class="container" style="display: none;">
<div id="start" class="row justify-content-center align-items-center full-h">
<div class="col-sm text-center">
<h2>Ooops...</h2>
<p>You will need to set the Acunetix API parameters to use the extension</p>
<button id="open-options" class="btn btn-primary" >Open options</button>
</div>
</div>
</div>
<!-- Need settings -->
<!-- Work screen -->
<div id="new-object" class="container" style="display: none;">
<div class="row justify-content-center align-items-center full-h">
<div class="col-sm text-center">
<h3>Target not found. Create new target in Acunetix.</h3>
<div class="row align-items-center">
<div class="col-6 align-items-center">
<button id="create-btn" class="btn btn-primary">Create</button>
</div>
<div class="col-6 align-items-center">
<button id="create-scan-btn" class="btn btn-success">Create & Scan</button>
</div>
</div>
</div>
</div>
</div>
<!-- Work screen -->
<!-- Work screen -->
<div id="object-list" class="container" style="display: none;">
<div class="row justify-content-center align-items-center full-h">
<div class="col-sm text-center">
<h3>Exists targets: </h3>
<table>
<thead>
<tr>
<th>
Address
</th>
<th>
Description
</th>
<th>
Select
</th>
</tr>
</thead>
<tbody id="targets-list">
</tbody>
</table>
</div>
</div>
</div>
<!-- Work screen -->
<!-- Work screen -->
<div id="object-info" class="container" style="display: none;">
<div class="row justify-content-center align-items-center full-h">
<div id="object-details" class="col-sm text-center">
</div>
</div>
</div>
<!-- Work screen -->
<!-- Starting scan -->
<div id="select-profile" class="container" style="display: none;">
<div id="start" class="row justify-content-center align-items-center">
<div class="col-sm text-center">
<h3>Select scan profile</h3>
<h4 id="scan-url"></h4>
<form>
<div id="profiles-info" class="mb-3">
<select id="api-profile-scan" class="form-select">
</select>
</div>
<button id="save-btn" class="btn btn-primary">Start scan</button>
</form>
</div>
</div>
</div>
<!-- Starting scan -->
<script src="../../ext_js/global.js"></script>
<script src="../js/popup.js"></script>
</body>
</html>
popup.js
Важно понимать, что каждый раз когда появляется всплывающее окно, приложение полностью создается с нуля. Расширение не “помнит”, что вы минуту назад уже открывали попап. Поэтому, каждый раз при создании, нам нужно будет уточнять состояние. Для этого у нас будет несколько косвенных маркеров. Писать отдельно состояние для каждого домена можно, но пошел по этому пути. В моем случае есть несколько условных маркеров:- Нет данных по апи, соответственно выводится экран с кнопкой перехода к опциям
- Нет данных о объекту в локальном хранилище.
- Есть данные по списку объектов
- Есть данные по конкретному объекту
Вылилось это все в такую функцию:
JavaScript: Скопировать в буфер обмена
Код:
async function init() {
let data = await browser.runtime.sendMessage({
action: MSG_ACTIONS.loadData
})
if (!data)
return showNeedSettings()
let targets = await browser.runtime.sendMessage({
action: MSG_ACTIONS.getTargetsInfo
})
if (targets.length > 1) return showTargetsList(targets)
showTargetInfo(targets)
}
init()
Дальше главное не запутаться в функциях и файлах. Самая простая это показать “нужны параметры”:
JavaScript: Скопировать в буфер обмена
Код:
function hideAllDivs() {
document.querySelector('#process').style.display = 'none'
document.querySelector('#not-init').style.display = 'none'
document.querySelector('#new-object').style.display = 'none'
document.querySelector('#object-list').style.display = 'none'
document.querySelector('#object-info').style.display = 'none'
document.querySelector('#select-profile').style.display = 'none'
}
function showNeedSettings() {
hideAllDivs()
document.querySelector('#not-init').style.display = 'block'
}
Соответственно, hideAllDivs() нужна исключительно чтобы не заморачиваться с сокрытием экранов.
Займемся логикой:
JavaScript: Скопировать в буфер обмена
Код:
let targets = await browser.runtime.sendMessage({
action: MSG_ACTIONS.getTargetsInfo
})
В фоновых скриптах мы вызываем следующие действия:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
switch(data.action) {
...
case MSG_ACTIONS.getTargetsInfo:
let targetsInfo = await getTargetsInfo()
return targetsInfo
...
}
return true
})
async function getTargetsInfo() {
let {hostname} = await getURL()
let targets = []
try{
targets = await readLocalStorage(hostname)
if (targets.length > 1) {
return targets
}
if (!targets.target_id) throw new Error('Empty object')
return await getTargetDetails(targets.target_id)
} catch(e) {
targets = []
}
targets = await getTargetsFromAPI(hostname)
if (!targets || !targets.length) return []
await browser.storage.local.set({[hostname]: targets})
if (targets.length > 1) {
return targets
}
return await getTargetDetails(targets[0].target_id)
}
Логика getTargetsInfo() такая:
- Узнаем имя хоста при помощи getURL(). Имя хоста это ключ по которому хранятся данные в storage.local
- Пытаемся прочитать информацию об объекте из локального хранилища.
- Если локальное хранилище пустое, пытаемся получить информацию из апи.
В любом случае все заканчивается проверкой, не является ли объект массивом. Если это массив, просто возвращаем его, пользователю будет показан список и предложено выбрать. Если у нас одиночный объект, то управление передается функции getTargetDetails().
JavaScript: Скопировать в буфер обмена
Код:
async function getTargetDetails(targetId) {
let targetInfoResponse = await getRequestAPI(acunetix.apiURL, ACUNETIX_CMD.targets, acunetix.apiKey,'', targetId)
let targetInfo = await targetInfoResponse.json()
let continueScansResponse = await getRequestAPI(acunetix.apiURL, ACUNETIX_CMD.scans, acunetix.apiKey, `q=target_id:${targetId}`)
let continueScans = await continueScansResponse.json()
let scans = []
let result = {}
if (continueScans && continueScans.scans && continueScans.scans.length) {
scans = continueScans.scans
.filter(el => el.target_id == targetId)
.map(el => ({
profile_name: el.profile_name,
criticality: el.criticality,
current_session: el.current_session,
scan_id: el.scan_id,
apiURL
}))
}
result = {...targetInfo, scans: [...scans]}
return result
}
Функция делает два запроса к API. Первый запрос, это детальная информация по объекту. Второй запрос, это информация по сканированиям связанных с объектом. После функция упрощает информацию о сканированиях, так как там много лишнего, далее объединяет два объекта.
Так выглядят объекты сканирования до упрощения:
Как видно, на входе достаточно сложный объект. Более того, включающий в себя “target”, информация о котором у нас уже имеется.
Вернемся к коду попапа. В результате выполнения getTargetDetails() мы придем к функции showTargetInfo() демонстрирующей подробную информацию:
JavaScript: Скопировать в буфер обмена
Код:
function showTargetInfo(targetInfo) {
if (!targetInfo || !targetInfo.scans || targetInfo.code) return showNewTarget()
let divInfo = document.querySelector('#object-details')
const h3 = document.createElement('h3')
const pDesc = document.createElement('p')
const aAddress = document.createElement('a')
const table = document.createElement('table')
divInfo.innerHTML = ``
h3.innerText = targetInfo.fqdn
pDesc.innerText = targetInfo.description
aAddress.href = targetInfo.address
aAddress.innerText = targetInfo.address
targetInfo.scans.forEach(scan => {
const tr = document.createElement('tr')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const td3 = document.createElement('td')
const td4 = document.createElement('td')
const td5 = document.createElement('td')
const a = document.createElement('a')
a.href = 'https://localhost:3443/#/scans/' + scan.scan_id + '/info'
a.innerText = scan?.profile_name || 'null'
td1.append(a)
td2.innerText = scan?.current_session?.start_date?.split('T')[0] || 'not started'
td3.innerText = scan?.current_session?.severity_counts?.critical || '0'
td4.innerText = scan?.current_session?.severity_counts?.high || '0'
td5.innerText = scan?.current_session?.status || 'null'
td3.classList.add('border', 'border-danger', 'rounded-circle', 'bg-danger', 'text-light')
td4.classList.add('border', 'border-warning', 'rounded-circle', 'bg-warning', 'text-light')
tr.append(td1, td2, td3, td4, td5)
table.append(tr)
})
const buttonUpdate = document.createElement('button')
const buttonScan = document.createElement('button')
const divButtons = document.createElement('div')
divButtons.classList.add('container-fluid', 'align-space-between')
buttonUpdate.innerText = 'Update'
buttonUpdate.classList.add('btn', 'btn-primary')
buttonUpdate.dataset.id = targetInfo.target_id
buttonUpdate.addEventListener('click', clickButtonSelectHandler)
buttonScan.innerText = 'New Scan'
buttonScan.classList.add('btn', 'btn-success')
buttonScan.dataset.id = targetInfo.target_id
buttonScan.addEventListener('click', createNewHandler)
divButtons.append(buttonUpdate, buttonScan)
divInfo.append(h3, aAddress, pDesc, table, divButtons)
hideAllDivs()
document.querySelector('#object-info').style.display = 'block'
}
Функция длинная, но в сущности ничего необычного. Просто создается ряд html-элементов, включая таблицу с информацией. После добавляется это все на “экран” приложения. Вместе с информацией выводится две кнопки:
- Принудительное обновление информации из апи. Я не предусмотрел механизма обновления информации, так как сложно предположить ситуацию, когда на протяжении всего сканирования вы продолжаете копаться на сайте.
- Создание нового сканирования. Эта кнопка нужна в том случае, если когда-то давно вы уже сканили таргет и есть желание запустить заново.
В целом, остальной код в popup не имеет больше каких-то особенностей. Вот, например, функция вывода списка целей:
JavaScript: Скопировать в буфер обмена
Код:
function showTargetsList(targets) {
let divTL = document.querySelector('#targets-list')
divTL.innerHTML = ``
targets.forEach(el => {
const tr = document.createElement('tr')
const tdAddress = document.createElement('td')
const tdDesc = document.createElement('td')
const tdButton = document.createElement('td')
const button = document.createElement('button')
button.innerText = 'Select'
button.classList.add('btn', 'btn-primary')
button.dataset.id = el.target_id
button.addEventListener('click', clickButtonSelectHandler)
tdAddress.innerText = el.address
tdDesc.innerText = el.description
tdButton.append(button)
tr.append(tdAddress, tdDesc, tdButton)
divTL.append(tr)
})
hideAllDivs()
document.querySelector('#object-list').style.display = 'block'
}
Как видно, все тоже самое. Создание html-объектов на лету. Добавление на экран. Назначение событий на кнопки. Соответственно, практически все вызовы кнопок это передача управления фоновому скрипту.
Вернемся к фоновому скрипту и посмотрим на полную функцию обрабатывающую события из других окон.
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
switch(data.action) {
case MSG_ACTIONS.loadData:
let acuSettings = await getData()
return acuSettings
case MSG_ACTIONS.saveAndInit:
acunetix = {
apiKey: data.apiKey,
apiURL: removeLastSlash(data.apiURL)
}
await saveData()
let response = await updateProfilesInit()
return response
case MSG_ACTIONS.saveData:
acunetix = {...acunetix, ...data.acuSettings}
await saveData()
return true
case MSG_ACTIONS.clearData:
browser.storage.local.remove('acunetix')
return true
case MSG_ACTIONS.updateProfiles:
let profiles = await updateProfiles()
return profiles
case MSG_ACTIONS.getTargetsInfo:
let targetsInfo = await getTargetsInfo()
return targetsInfo
case MSG_ACTIONS.getTargetDetails:
return await selectTarget(data.targetId)
case MSG_ACTIONS.createTarget:
let newTarget = await createTarget()
return await getTargetDetails(newTarget.target_id)
case MSG_ACTIONS.createTargetAndScan:
return await createTargetAndScan()
case MSG_ACTIONS.createNewScan:
await createNewScan(data.targetId)
return await getTargetDetails(data.targetId)
}
return true
})
Большую часть мы уже разобрали. Более того, в неразобранной части большинство функций известно. Осталось всего несколько сервисных функций, по ним и пробежимся:
JavaScript: Скопировать в буфер обмена
Код:
async function selectTarget(targetId) {
let {hostname, origin} = await getURL()
let targetInfo = await getTargetDetails(targetId)
if (targetInfo && targetInfo.code && targetInfo.code == '404') {
await browser.storage.local.remove(hostname)
return []
}
await browser.storage.local.set({[hostname]: targetInfo})
return targetInfo
}
Это функция выбора из списка целей. В ней мы просто получаем детальные данные, далее проверяем код ответа. В случае отсутствия объекта, мы получим “404”. Если просто сохранить данные, то пользователь увидит кучу undefined, что не хорошо. В этом случае просто вернем пустой список, тогда логика попапа отработает правильно и пользователь увидит предложение создать новый объект. Если все ок, просто сохраняем данные и возвращаем подробную инфу.
Обращаю внимание, что при получении объекта URL у меня десериализуется две переменных: hostname и origin. Оставил так сугубо для демонстрации. Я выбрал работу через имя хоста, но это не обязательно правильно и удобно для всех. Возможно, вы захотите отдельно создавать объект по http протокол, отдельно под https. Ну, как пример.
JavaScript: Скопировать в буфер обмена
Код:
async function createTarget() {
let {hostname, origin} = await getURL()
let newTarget = {
"address": origin,
"description": "Created By Browser Extension",
"type": "default"
}
let response = await postRequestAPI(acunetix.apiURL, ACUNETIX_CMD.targets, acunetix.apiKey, newTarget)
let newTargetInfo = await response.json()
await browser.storage.local.set({[hostname]: newTargetInfo})
return newTargetInfo
}
Функция формирует простейший новый таргет. Подробнее про создание таргетов можно прочитать в соответствующей статье, не вижу смысла на этом останавливаться. Далее этот объект, методом POST пересылается в Acunetix. Так устроен его апи, если гет это получение данных, пост сохранение.
JavaScript: Скопировать в буфер обмена
Код:
async function createNewScan(targetId) {
const newScanParams =
{
"target_id": targetId,
"profile_id": acunetix.defaultProfile,
"incremental": false,
"schedule": {
"disable": false,
"start_date": null,
"time_sensitive": false
}
}
let response = await postRequestAPI(acunetix.apiURL, ACUNETIX_CMD.scans, acunetix.apiKey, newScanParams)
let newScan = await response.json()
return newScan
}
Функция создания сканирования работает аналогично функции создания нового таргета. Объект, отправить объект на апи, вернуть ответ.
Неразобранной осталась функция создания объекта и нового сканирования. Конечно же, это просто функция-обертка для двух предыдущих функций:
JavaScript: Скопировать в буфер обмена
Код:
async function createTargetAndScan() {
const target = await createTarget()
const scan = await createNewScan(target.target_id)
return getTargetDetails(target.target_id)
}
Вот, практически, и все расширение. Конечно же, написание кода было подольше описания его здесь и сопровождалось сотней переписываний, ну что поделать… участь разработки такова. А у нас остался только один файл, который мы не рассматривали. Это файл acunetix.js
JavaScript: Скопировать в буфер обмена
Код:
const ACUNETIX_CMD = {
scaningProfiles: '/scanning_profiles',
targets: '/targets',
scans: '/scans'
}
async function requestApi(url, options) {
let response = fetch(url, options)
return response
}
async function getRequestAPI(apiURL, apiMethod, apiKey, params, path) {
let url = `${apiURL}${apiMethod}`
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Auth': apiKey
}
}
if (path)
url += '/' + path
if (params)
url += '?' + params
return requestApi(url, options)
}
async function postRequestAPI(apiURL, apiMethod, apiKey, payload) {
let url = `${apiURL}${apiMethod}`
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Auth': apiKey
}
}
if (payload)
options.body = JSON.stringify(payload)
return requestApi(url, options)
}
Всего лишь две функции-обертки, getRequestAPI и postRequestAPI, для requestApi. Которая в свою очередь является простейшей оберткой для fetch. Почему не использовал fetch? Так удобнее. Как минимум, можно запихнуть в функцию console.log и отслеживать все запросы к апи. А в целом, часто в подобную функцию можно что-то и более полезное подпихнуть, поэтому стараюсь максимально оборачивать. Хотя иногда надоедает, но вы заметили, наверное, как у меня сильно меняется код в разных частях))))
И это все?
Когда статья уже была дописана и оставалось её перенести на XSS.is, что-то ёкнуло внутри. А достаточно ли полезным получился хелпер? Может его можно как-то сделать лучше? Все же скопируют, еще и подпишут хреном. Нужно чтобы было не стыдно. Вдруг еще на статью ссылку оставят. Короче, надо было как-то улучшить ситуацию и я придумал как! Всегда хотел это сделать, но всегда откладывал в долгий-долгий ящик. Посмотрите на эту картинку:Вас никогда не бесило, что нет кнопки “Добавить все”? А не бесил тот факт, что здесь нет никакого указания на то, добавлен ли домен уже в базу таргетов? И даже при добавлении таргета, если уже есть дубль, вы об этом никогда не узнаете!
Как понять, какие из этих 198 хостов уже добавлены как таргеты и оперативно добавить их? Скопировать в файли, почистить, написать скрипт чека (хуже того, чекать руками) есть таргет в окуне, после грузить как CSV?
Долой несправедливость! Нам нужен хороший удобный рабочий инструмент!!!
Хочу следующие функции:
- Добавить рядом с Discovered Hosts количество хостов не добавленных как таргеты
- Подсветить цветом новые и старые таргеты
- Добавить кнопку “Добавить все хосты”, которая будет добавлять только новые хосты
- Хочу при импорте большого количества объектов видеть какие из них уже есть в базе или, хотя бы, возможно есть в базе.
- Добавить красоту в виде загрузкичка.
Думаю, теперь вы понимаете, почему я решил сделать отдельную статью. Честно сказать, я аж кайфанул, когда решил это все сделать. Кода немного, зато сколько проблем снимается.
Загрузчик
Начнем с простых улучшений. Меня бесит отсутствие индикатора загрузки. Вернее как отсутствие… в коде страницы попап уже был добавлен “загрузчик”, но я забыл прописать его вывод. Чтобы это исправить, поправлю функцию сокрытия всех экранов. Нужно скрывать все, а процесс, наоборот, показывать.JavaScript: Скопировать в буфер обмена
Код:
function hideAllDivs() {
document.querySelector('#not-init').style.display = 'none'
document.querySelector('#new-object').style.display = 'none'
document.querySelector('#object-list').style.display = 'none'
document.querySelector('#object-info').style.display = 'none'
document.querySelector('#select-profile').style.display = 'none'
document.querySelector('#process').style.display = 'block'
}
Все вызовы функции сокрытия повешу на события кнопок и инициализацию. Так правильнее. Раз инициирован какой-то процесс, нужно скрыть интерфейс.
JavaScript: Скопировать в буфер обмена
Код:
document.querySelector('#create-btn').addEventListener('click', async event => {
event.preventDefault()
hideAllDivs()
let targetInfo = await browser.runtime.sendMessage({
action: MSG_ACTIONS.createTarget
})
showTargetInfo(targetInfo)
})
Для вывода правильного экрана создам отдельную сервисную функцию .
JavaScript: Скопировать в буфер обмена
Код:
function showFinallDiv(finalDivId) {
document.querySelector('#process').style.display = 'none'
document.querySelector(`#${finalDivId}`).style.display = 'block'
}
Останется заменить прямые “включения” экранов на вызов функции с нужным идентификатором.
Чекаем импорт
Такой результат мы должны получить по итогу. На мой взгляд очень приятная картинка. Подгрузил CSV, быстренько пролистал удаляя красные объекты и загружай чистую базу.
Первое, что нужно понимать, это то, что нам потребуются контентные скрипты. Из фона, у нас нет доступа к коду страницы, а данные брать нужно именно оттуда. Я, как и раньше запихну их в манифест:
JavaScript: Скопировать в буфер обмена
Код:
"content_scripts": [
{
"matches": ["https://localhost/*"],
"js": ["ext_js/global.js", "ext_js/import_csv.js"],
"run_at": "document_end"
}
],
Далее есть нюанс. Когда вы жмете на “Import CSV”, не происходит смены адреса. Происходит добавление новых компонентов на страницу. И здесь у нас есть несколько вариантов взаимодействия. Можно в интерфейс попапа добавить экран с которого руками запускать процесс. Но с точки зрения удобства и обучения, рассмотрим более интересный вариант, связанный с использованием MutationObserver и его методом observe().
Этот метод позволяет отслеживать изменения в DOM, причем достаточно тонко фильтруя с чем работаем. Но подробного обзора делать не стану, можете спокойно в гугле найти достаточно хороших примеров работы. В нашем случае код будет выглядеть так:
JavaScript: Скопировать в буфер обмена
Код:
const observer = new MutationObserver((mutations) => {
for(let mutation of mutations) {
for(let node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
let inputs = node.getElementsByTagName('input')
if (inputs && inputs.length) {
browser.runtime.sendMessage({
action: MSG_ACTIONS.checkTargetExists,
inputField: inputs[0].value
})
}
}
}
});
observer.observe(document.querySelector('acx-add-targets'), {
childList: true,
subtree: true,
});
Сначала создаем объект обсёрвера, передав в конструктор коллбэк-функцию. Далее вызываем метод observe(), передав ему фильтры. Начнем с того, что нас интересует тэг “acx-add-targets”. Именно в него команда окуня обернула инпуты с параметрами добавляемых таргетов. Передача observe() методу HTML-элемента позволяет избежать обработки лишнего. Ну и, соответственно, наш метод должен работать по дереву ниже от указанного элемента и с его потомками.
Внутри коллбэка нужно перебрать все мутации произошедшие на странице. Опять же, углубляться не вижу смысла, вы и сами можете увидеть два цикла позволяющие обойти все узлы. Внутри узлов мы ищем инпуты и, если есть хоть один, перекидываем url в фоновый скрипт.
Изменения в фоновом скрипте:
JavaScript: Скопировать в буфер обмена
Код:
async function checkTargetExists(targetURL) {
let {hostname} = new URL(targetURL)
let targets = await getTargetsByHostname(hostname)
if (!targets.length) return
let tabs = await browser.tabs.query({
active: true,
currentWindow: true
})
tabs.forEach(async tab => browser.tabs.sendMessage(
tab.id,
{
action: MSG_ACTIONS.highlightInput,
payload: targetURL
}
))
return true
}
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
switch(data.action) {
...
case MSG_ACTIONS.checkTargetExists:
checkTargetExists(data.inputField)
return true
}
return true
})
Большая часть кода уже не нова. Слушатель получает сообщение с урлом и вызывает новую функцию чека. Далее, через getTargetsByHostname() проверяется существования таргета. Если есть хотя бы один таргет, отправляем соответствующее сообщение в контентный скрипт. Получается этакий пинг-понг.
Только отправка сообщения в контентный скрипт выполняется не через runtime.sendMessage, а через tabs.sendMessage. Отправка в контентный скрипт производится именно таким способом. Соответственно, здесь у нас появляется узкое место. Нам нужно найти нужный таб. Я сделал это через получение активного таба, соответственно, до окончания работы скрипта нельзя переключаться на другие закладки.
Важно не забыть в объект MSG_ACTIONS файла global.js два новых свойства: checkTargetExists и highlightInput. Плюс добавить туда функцию sleep, она нам потребуется чтобы избежать ошибок. По сути, это всем известный хук:
JavaScript: Скопировать в буфер обмена
const sleep = async ms => await new Promise((resolve) => setTimeout(resolve, ms))
Функция sleep нужна для построения цикла поиска HTML-элемента “acx-add-targets”. Дело в том, что наш скрипт подгружается слишком рано, когда объект еще не добавлен. В итоге, MutationObserver выбросит ошибку и ничего работать не будет. Поэтому, добавил такой кусок кода:
JavaScript: Скопировать в буфер обмена
Код:
let mainFrame = document.querySelector('acx-add-targets')
while(!mainFrame) {
console.log('Not found main frame')
mainFrame = document.querySelector('acx-add-targets')
await sleep(500)
}
Соответственно, обернув кусок кода в асинхронную функцию. Итоговый код контентного скрипта будет выглядеть так:
JavaScript: Скопировать в буфер обмена
Код:
async function mainImportCSV() {
let mainFrame = document.querySelector('acx-add-targets')
while(!mainFrame) {
mainFrame = document.querySelector('acx-add-targets')
await sleep(500)
}
const observer = new MutationObserver((mutations) => {
for(let mutation of mutations) {
for(let node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
let inputs = node.getElementsByTagName('input')
if (inputs && inputs.length) {
browser.runtime.sendMessage({
action: MSG_ACTIONS.checkTargetExists,
inputField: inputs[0].value
})
}
}
}
});
observer.observe(document.querySelector('acx-add-targets'), {
childList: true,
subtree: true,
});
}
if (window.location.href == "https://localhost:3443/#/targets/add-multiple") {
mainImportCSV()
}
browser.runtime.onMessage.addListener(async (message) => {
if (message.action == MSG_ACTIONS.highlightInput) {
let input = Array.from(document.querySelectorAll('input')).filter( el => el.value == message.payload)[0]
input.style.background = 'red'
input.style.color = 'white'
}
return true
});
Discovered Hosts
Функции, которые осталось реализовать, по сути, об одном и том же. В том смысле, что нам нужно получить список хостов и исходя из этого списка нужно внести изменения в интерфейс.В целом, поступим так же, как и с импортом из CSV. Добавлю еще одну функцию, которая будет вешать MutationObserver. В свою очередь, функцию запускать по window.location.href.
JavaScript: Скопировать в буфер обмена
Код:
async function updateDiscoveredHosts() {
}
if (window.location.href == "https://localhost:3443/#/targets/add-multiple") {
mainImportCSV()
} else if(window.location.href.startsWith('https://localhost:3443/#/scans/')) {
updateDiscoveredHosts()
}
Но здесь есть слабое место, которое я упустил. Так как переход между экранами Acunetix происходит без перезагрузки страницы, скрипт не перезагружается, а значит наши функции отработают один раз. Соответственно, при переходах скрипт перестанет работать. Чтобы решить эту проблему, сделаем хук, снова обратившись к MutationObserver, но следить будем за изменениями пути:
JavaScript: Скопировать в буфер обмена
Код:
let oldHref = document.location.href;
const body = document.querySelector('body');
const observer = new MutationObserver(mutations => {
if (oldHref !== document.location.href) {
oldHref = document.location.href;
if (window.location.href == "https://localhost:3443/#/targets/add-multiple") {
mainImportCSV()
} else if(window.location.href.startsWith('https://localhost:3443/#/scans/')
&& window.location.hash != '#/scans/') {
console.log('Scan params')
updateDiscoveredHosts()
}
}
});
observer.observe(body, { childList: true, subtree: true });
Второй момент в том, что по факту, в фоновых скриптах код должен быть практически идентичный предыдущему. Разница только в том, что должен делать контентный скрипт. Чтобы не дублировать код, решил при отправке сообщения добавить еще один параметр — callbackType. В нем будем передавать, какой тип обработки планируется:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.sendMessage({
action: MSG_ACTIONS.checkTargetExists,
inputField: inputs[0].value,
callbackType: MSG_ACTIONS.highlightInput
})
Соответственно, в фоне тоже вносим изменения, добавив проброс параметра:
JavaScript: Скопировать в буфер обмена
Код:
case MSG_ACTIONS.checkTargetExists:
checkTargetExists(data.inputField, data.callbackType)
return true
И, конечно же, в самой функции чека тоже нужны изменения. Добавить параметр и поменять возврат:
JavaScript: Скопировать в буфер обмена
Код:
async function checkTargetExists(targetURL, callbackType) {
...
tabs.forEach(async tab => browser.tabs.sendMessage(
tab.id,
{
action: MSG_ACTIONS[callbackType],
payload: targetURL
}
))
...
}
Пока забудем о фоновых скриптах. Позже нужно будет вернуться и добавить изменения, связанные с запуском нового сканирования и добавлением пачки таргетов, но это позже. Пока добьем контентный скрипт. А именно, добавим подсчет хостов не добавленных в таргеты и выделение их цветом.
Не забудем добавить новое свойство в глобальный MSG_ACTIONS:
JavaScript: Скопировать в буфер обмена
highlightHost: 'highlightHost'
Вот как будет выглядеть результат работы нашего скрипта. Забегая вперед скажу, что это крайне кайфовая штука)))
Начнем с раскраса. Пойду стандартным путем, сначала раскрасив все объекты в серый цвет. После, все найденные объекты покрасим в зелененький. Чтобы реализовать это все, идем в нашу новую функцию updateDiscoveredHosts
JavaScript: Скопировать в буфер обмена
Код:
async function updateDiscoveredHosts() {
let hostsFrame = document.querySelectorAll('mat-card')[5]
while(!hostsFrame) {
mainFrame = document.querySelectorAll('mat-card')[5]
await sleep(500)
}
const observer = new MutationObserver((mutations) => {
for(let mutation of mutations) {
for(let node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (!node.classList.contains('row-space-between')) continue;
let host = node.childNodes[0].innerText
node.style.background = '#dddddd'
browser.runtime.sendMessage({
action: MSG_ACTIONS.checkTargetExists,
inputField: host,
callbackType: MSG_ACTIONS.highlightHost
})
}
}
});
observer.observe(hostsFrame, {
childList: true,
subtree: true,
});
browser.runtime.onMessage.addListener((message) => {
if (message.action == MSG_ACTIONS.highlightHost) {
highlightHost(message.payload)
}
return true
});
}
Как и прошлый раз, работаем с обсервером. Но помимо того, что мы отсеиваем все не HTML-элемент, нам нужно отсеить все объекты не содержащие класс “row-space-between”. В ином случае, у нас все пойдет не по плану, так как будут лишние запросы. Например, тот же заголовок.
Осталось добавить функцию окраса в зеленый (highlightHost) и все будет готово. Почему так? Потому что у нас фоновый скрипт отправит сообщение только в том случае, если объект уже существует. Таким образом, мы покрасим только те хосты, которые уже добавлены как таргеты:
JavaScript: Скопировать в буфер обмена
Код:
function highlightHost(host) {
let div = document.evaluate(`//div[text()="${host}"]`, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.parentElement
div.style.background = '#b3eeb3'
}
Оптимальным вариантом поиска, конечно же, будет XPath по тексту. Найдя элемент мы переходим к родителю объекта, чтобы окрасить всю строку. Все! Как сказал бы великий мыслитель, мы окрасили строки в те цвета в которые окрасили.
Чтобы реализовать оставшийся функционал, нам потребуется сбегать за костылями. Добавим несколько переменных:
JavaScript: Скопировать в буфер обмена
Код:
let existsHosts = []
let allHosts = []
let addedButton = false
let countExists = 0
Название каждого говорит само за себя. Да, есть переменная для подсчета количества, хотя можно было бы вычесть длину массива существующих хостов из массива всех хостов, но… работает не трогай. Была ночь, писал код уставший и запутался в асинхронности. Учитывая, что функционал наращивать не планирую, можно и забить. Да-да, именно так и появляются любимые нами баги…
JavaScript: Скопировать в буфер обмена
Код:
if (window.location.href == "https://localhost:3443/#/targets/add-multiple") {
mainImportCSV()
} else if(window.location.href.startsWith('https://localhost:3443/#/scans/')
&& window.location.hash != '#/scans/') {
allHosts = []
existsHosts = []
addedButton = false
countExists = 0
updateDiscoveredHosts()
}
Поменял основной if, добавив сброс переменных. Так как скрипт у нас не перезагружается, а переменные глобальные, нужно следить за их чистотой. В ином случае можно сильно удивиться.
Нам нужно изменить обсервер, который обрабатывает карточку с хостами:
JavaScript: Скопировать в буфер обмена
Код:
const observer = new MutationObserver((mutations) => {
for(let mutation of mutations) {
for(let node of mutation.addedNodes) {
if (!(node instanceof HTMLElement)) continue;
if (!node.classList.contains('row-space-between')) continue;
console.log(node.childNodes[0].innerText)
let host = node.childNodes[0].innerText
node.style.background = '#dddddd'
if (!allHosts.includes(host))
allHosts.push(host)
browser.runtime.sendMessage({
action: MSG_ACTIONS.checkTargetExists,
inputField: host,
callbackType: MSG_ACTIONS.highlightHost
})
}
}
});
И так же в слушателя добавим код заботливо сохраняющий чекнутые хосты в массив существующих таргетов:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener((message) => {
if (message.action == MSG_ACTIONS.highlightHost) {
if (!existsHosts.includes(message.payload)) {
existsHosts.push(message.payload)
countExists++
}
updateTitle(hostsFrame)
highlightHost(message.payload)
}
return true
});
Осталось дописать функцию updateTitle, добавив вывод количества новых хостов в заголовок:
JavaScript: Скопировать в буфер обмена
Код:
function updateTitle(hostFrame, count) {
element = hostFrame.querySelector('.title')
if (!element.innerHTML.includes('New Hosts')) {
element.innerHTML += ` New Hosts <b>${allHosts.length - countExists}</b>`
}else {
element.innerHTML = element.innerHTML.replace(/<b>\d<\/b>/, `<b>${allHosts.length - countExists}</b>`)
}
}
Вуаля, теперь у нас поменялся заголовок и мы видим количество новых хостов. Ну не красота ли? Нет, так как мне нужна кнопка!!!
Код добавления кнопки можно много куда запихать, но я решил не заморачиваться и ввернул его прямо в изменение заголовка. В целом, ничего необычного. Просто кнопка, просто стили, просто назначение события.
JavaScript: Скопировать в буфер обмена
Код:
function updateTitle(hostFrame, count) {
if (!addedButton) {
console.log('Create button')
let button = document.createElement('button')
button.innerText = 'Add All'
button.style.padding = '7px'
button.style.background = '#3f3e49'
button.style.color = 'white'
button.style.border = 'none'
button.style.fontWeight = 'bold'
button.addEventListener('click', event => {
event.preventDefault()
let newHosts = [...new Set(allHosts)].filter(item => !existsHosts.includes(item))
browser.runtime.sendMessage({
action: MSG_ACTIONS.appendAllNewHosts,
hosts: newHosts
})
})
document.querySelectorAll('.card-header')[3].append(button)
addedButton = true
}
...
}
Обратите внимание, что в глобальном объекте MSG_ACTIONS, появилось новое свойство. Вот как выглядит объект:
JavaScript: Скопировать в буфер обмена
Код:
const MSG_ACTIONS = {
loadData: 'loadData',
saveData: 'saveData',
clearData: 'clearData',
saveAndInit: 'saveAndInit',
updateProfiles: 'updateProfiles',
getTargetsFromStorage: 'getTargetsFromStorage',
getTargetsFromAPI: 'getTargetsFromAPI',
getTargetsInfo: 'getTargetsInfo',
getTargetDetails: 'getTargetDetails',
createTarget: 'createTarget',
createTargetAndScan: 'createTargetAndScan',
createNewScan: 'createNewScan',
checkTargetExists: 'checkTargetExists',
highlightInput: 'highlightInput',
highlightHost: 'highlightHost',
appendAllNewHosts: 'appendAllNewHosts'
}
Осталось исправить фоновые скрипты. Сначала добавим обработку еще одного типа сообщений:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
switch(data.action) {
…
case MSG_ACTIONS.appendAllNewHosts:
appendManytargets(data.hosts)
return true
}
return true
})
Кайфушка в том, что нам почти ничего писать и не нужно. Только функцию-обертку для appendManytargets.
JavaScript: Скопировать в буфер обмена
Код:
async function appendManytargets(targets) {
targets.forEach(async target => {
const newTarget = await createTarget(false, target)
let tabs = await browser.tabs.query({
active: true,
currentWindow: true
})
tabs.forEach(async tab => browser.tabs.sendMessage(
tab.id,
{
action: MSG_ACTIONS.highlightHost,
payload: target
}
))
})
}
Причем, как вы видите, большая часть кода это отправка сообщения. Точно такая же отправка сообщения, как и в checkTargetExists. По хорошему, нужно вынести этот функционал и получить две отдельных функции, сильно сократив код:
JavaScript: Скопировать в буфер обмена
Код:
async function sendMessageToActiveTab(callbackType, payload) {
let tabs = await browser.tabs.query({
active: true,
currentWindow: true
})
tabs.forEach(async tab => browser.tabs.sendMessage(
tab.id,
{
action: MSG_ACTIONS[callbackType],
...payload
}
))
}
async function appendManytargets(targets) {
targets.forEach(async target => {
const newTarget = await createTarget(false, target)
await sendMessageToActiveTab(MSG_ACTIONS.highlightHost, {
payload: target
})
})
}
async function checkTargetExists(targetURL, callbackType) {
let {hostname} = new URL(targetURL)
let targets = await getTargetsByHostname(hostname)
if (!targets.length) return
await sendMessageToActiveTab(MSG_ACTIONS[callbackType], {
payload: targetURL
})
return true
}
Мы получили достаточно компактный и красивый код. Но самое главное, он полностью рабочий. Знали бы вы с каким нетерпением я нажал кнопку “Add All”.
Как пользоваться расширением?
1. Добавить расширение в Firefox.2. Получить ключ API, перейдя по адресу https://localhost:3443/app/profile
3. Сохранить ключ в настройках расширения
4. Выбрать дефолтный профиль сканирования и нажать кнопку “Save”
5. Закрепить иконку на тулбаре. На интересующем сайте кликнуть по ней, далее все интуитивно понятно. Если таргетов список, выбрать интересующий. Если таргета нет, создать новый или создать новый и запустить сканирование. В общем, все интуитивно понятно.
Соответственно, чтобы работали фишки связанные с интерфейсом, ничего не требуется. Оно работает само.
Вместо заключения
Надеюсь получилось не сухо, а вполне интересно и полезно. В статье мы прошлись, как по уже осужденным в прошлой статье моментам, но в практическом применении. Так и по, в некоторых смыслах, новым концепциями вроде MutationObserver и вмешательству в существующий интерфейс веб-приложения. Но главное, на выходе у нас получился вполне рабочий инструмент, который каждый может спокойно масштабировать и улучшить. К сожалению, на мой взгляд. Acunetix имеет отвратительнейший и слабоинформативный интерфейс. Разрабатывая подобные решения для собственного использования или “на заказ”, можно значительно улучшить ситуацию.Например, с теми же хостами. Кому-то может показаться мелочью. Но мне, когда ты сидишь и пытаешься разобраться с пачкой из 200 найденных хостов…. Копируешь их, приводишь к правильному для окуня CSV, после пытаешься понять какие из них уже есть в окуне, а каких нет… А теперь. вместо этого, кликаешь на одну кнопку и через минуту у тебя уже все эти хосты перебраны, все новые добавлены. Согласитесь, разница существенная.
Да, над расширением можно работать и работать. Например, можно добавить кнопку “Add & Scan”, чтобы все новые хосты сразу летели в сканирование. Причем, код почти весь уже есть. Нужно просто скопировать функцию добавления хостов и к каждому добавлению прилепить запуск сканирования. Или, как вариант, можно удалить стандартную кнопку “Create target” и добавить свою. Которая будет фоном добавлять или же добавлять и открывать новую вкладку с созданным таргетом. Сами знаете, как бесит прыгать туда-сюда, когда пытаешься обнаруженные таргеты добавлять.
Статья вышла случайно. У меня была готовая на 99% общая статья про обнаружение уязвимостей типа XSS, SSTI и т.д. Про изменение интерфейсов и про запуск сторонних приложений из расширения… Но когда я понял, что один из примеров, посвященный Acunetix, можно превратить в действительно интересный инструмент… в общем, надеюсь вы тоже довольны полученным материалом. Ту же статью я тоже выложу в ближайшие дни. Нужно только придумать интересный пример, взамен Acunetix.