D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор petrinh1988
Источник https://xss.is
Не так давно на форуме Patr1ck написал интересную статью по разработке антифишингового расширения для браузера Google Chrome. Убедившись, что планы по статьям не пересекаются, решил таки написать на тему хацкерских расширений. Тем более запрос на тему расширений есть.
С одной стороны, у скриптов расширения есть все те же возможности, что и у пользователя. Есть доступ к HTML-коду страницы, к хранилищам используемым веб-приложением, есть возможности автоматизации под видом пользователя. С другой стороны, мы имеем доступ к событийной модели браузера, возможность генерировать новые запросы и т.д. Таким образом, есть возможность повысить эффективность поиска подходящей цели. Вы просто серфите по сайтам, а в это время расширение проводит проверки… если что-то обнаружилось, можно выкинуть уведомление, отправить данные на свой сервер или в Google-таблицу, сохранить скриншоты или вовсе полность автоматизировать взлом по известной уязвимости. Хоть управление голосом прикручивайте, благо для этого есть стандартные возможности и потребуется пару десятков строк кода. Всем, кроме владельцев Mac, на яблоках есть ограничения…
При помощи расширений можно писать сканеры уязвимостей, как пассивные, так и активные. Можно автоматизировать задачи, вплоть до написания спам-ботов или ботов прогревающих аккаунты. Можно реализовать некоторые виды эксплоитов. Можно собирать информацию, например, как Wappalyzer собирает информацию о технологиях. Можно создавать справочники с быстрыми подсказками, например, генераторы reverse-shell’ов.
Начну с расширения определяющего версию Wordpress. В ходе его разработки, вы познакомитесь с тем, как между собой взаимодействуют разные части расширений, а также поймете насколько все легче чем кажется.
Второе расширение будет посвящено более общему сбору информации о веб-приложении и больше будет похоже на несколько мини-расширений, как отдельные модули объединенных в одно:
Писать расширение будем для браузера Firefox. Но стоит помнить, что в целом, разница между API Chrome и Firefox не такая существенная, поэтому практически каждое расширение можно адаптировать под нужный браузер В любом случае, всё всегда начинается с файла manifest.json. Именно этот файл дает браузеру понять, как взаимодействовать с разрешением. Самый простой вариант файла выглядит примерно так:
JavaScript: Скопировать в буфер обмена
Нет, серьезно, это уже готовое расширение, которое браузер “скушает”. Просто оно ничего не делает. Даже иконки не имеет, только название, версию и текстовое описание. Чтобы добавить наше расширение для дебага в браузер, нужно перейти по адресу about:debugging#/runtime/this-firefox и нажать “Load Temporary Add-on”... После чего оно появится во временных расширениях.
Обратите внимание, что использовать будем третью версию манифеста, не вторую. Это влияет не только на структуру самого json, но и в целом на то как браузер будет взаимодействовать с расширением. Третья версия — это актуальная версия для Chrome и Firefox.
Файл манифеста будет выглядеть следующим образом:
JSON: Скопировать в буфер обмена
Я не буду сейчас останавливаться на разрешениях, которые потребуются расширению, чтобы не дублировать текст. Где потребуется остановиться подробнее, я про них напишу. Сейчас же важно понимать только то, что мы просто перечисляем нужные нам объекты API WebExtension.
То, что фоновые скрипты работают в контексте браузера, не говорит об их изоляции. Расширения имеют систему обмена сообщениями, через которую спокойно можно передавать данные. Мы будем пользоваться этим, например, чтобы по итогу работы контентного скрипта выкинуть уведомление браузера. Плюс, фоновые скрипты могут спокойно делать инъекцию javascript-кода.
В определении версии WP, фоновый скрипт носит вспомогательную роль — он просто уведомляет пользователя о факте определения версии:
JavaScript: Скопировать в буфер обмена
Все функции детекта возложены на контентный скрипт, который по итогу выполнения сохранит данные и запустит метод sendMessage(), передав браузеру сообщение. Как видно из кода выше, фоновый скрипт просто вешает слушателя на событие onMessage(), который перехватит тот самый sendMessage().
При получении сообщения, есть возможность принять три аргумента:
Ну и крайний элемент фонового скрипта, это запуск “notifications.create”. На самом деле, очень скудный вариант запуска, так как уведомления имеют множество опций влияющих на внешний вид и поведение. Можно даже кнопки прикрутить, если оно того требует.
Свойство “matches”, помогает задать шаблон урла к которому будет подгружаться контентный скрипт. Соответственно, можно задать конкретный сайт или, например, работу только с сайтами базирующимися на “http://*” или использовать универсальный "<all_urls>".
Свойство “run_at” может иметь ключевую роль в вашем расширении, так как оно указывает когда должна производится инъекция. В нашем случае, скрипт добавляется после загрузки всего.
Обратите внимание, что свойства “js” и “scripts” (в background) массивы. Загрузка массива происходит в порядке указанном в манифесте. Поэтому, если требуются общие переменные или функции, их нужно объявлять в первом загружаемом файле скриптов. Мы будем пользоваться этой особенностью во втором расширении, чтобы добавлять новые модули.
JavaScript: Скопировать в буфер обмена
Напомню, что скрипт загружается после загрузки всего документа сайта. Поэтому, оборачиваю код в самовызывающуюся функцию. Асинхронной делаю её для того, чтобы была возможность дождаться выполнения нужных асинхронных функций, напрример, данных из локального хранилища:
JavaScript: Скопировать в буфер обмена
Сейчас данные нужны только для того, чтобы избежать повторных определений. В ином случае, расширение вас закалупает оповещениями.
JavaScript: Скопировать в буфер обмена
Если у нас есть мета-тэг указывающий на генерацию вордпрессом, сохраняем информацию в массив потенциальных версий. Каждый вариант указывающий на возможную версию, храним в массиве versions в виде объекта. Не думаю, что есть смысл комментировать каждое его свойство.
JavaScript: Скопировать в буфер обмена
Немаловажную роль, в определении версии, имеет параметр “ver”, который классически цепляется ко всем JS и CSS в WP. Для сбора этих параметров написал стрелочную функцию getEntries(). В ином случае, пришлось бы почти полностью дублировать процесс сбора css и js отдельно. Хотя между ними разница исключительно в запросе поиска (“link” против “script”) и интересующем параметре (“href” против “src”).
Функция получает все HTML-объекты подходящие под поиск и преобразует в HTML-коллекцию в обычный массив. Но нам нужны не объекты, а ссылки, поэтому первым “map” исправляем несправедливость, оставив только строки. Фильтром, используя регулярное выражение “verRegexp” избавляемся от строк не содержащих “wp-include” и “ver”. После чего преобразуем массив строк в массив объектов по структуре предыдущего. Ну и завершаем действие объединением, сложив полученные значения в “versions” через деструктуризацию.
Следующим шагом сделаем запрос к фиду сайта. Кстати, дополнить это действие можно запросом к файлу лицензии и readme.
JavaScript: Скопировать в буфер обмена
Обернул действо в try-catch, т.к. фида может не быть, а заморачиваться с проверками очень не хотелось. Хотя можно было меньше писанины сделать…
С шагами, думаю, все понятно: стандартным fetch выполнил запрос, дождался результата, получил текст, регуляркой вытащил значение и добавил еще один объект в потенциальные версии.
Самое время подсчитать веса определить победителя:
JavaScript: Скопировать в буфер обмена
Все, что осталось добавить — это оповещение пользователя и сохранение результатов. Для оповещения, как описывал в фоновом скрипте, нужно передать сообщение в runtime методом sendMessage():
JavaScript: Скопировать в буфер обмена
В параметрах только объект с данными, так как мы не ждем какой-то реакции. Можно спокойно сохранить данные:
browser.storage.local.set({[window.location.host]: {versions, maybe: maybeVersion.version}})
На этом моменте остановлюсь подробнее. У расширения, как у любого сайта, есть свое хранилище. Перейдите в отладку расширений about:debugging#/runtime/this-firefox и кликните по кнопке “Inspect” или “Исследовать”. В открывшемся окне выберите “Хранилище”:
На скрине видно, что данные сохраняются в хранилище расширений. В нашем случае, это локальное хранилище. Оно не будет уничтожено, когда закончится сессия, не будет синхронизировано с другими устройствами. Данные хранятся локально и на одном устройстве. Есть варианты использовать не “local”, а тот же “sync”, но в данном случае нет никакого смысла. На всякий случай вот разница между типами хранилищ:
Чтобы получить доступ к хранилищу, в манифесте мы запрашивали разрешение на “storage”. Но если внимательно изучить наш манифест, там же в разрешениях есть пункт “unlimitedStorage”. Дело в том, что при запросе доступа только к хранилищу, на нас накладывается ограничение в пять мегабайт. Т.е. Расширение не может хранить больше информации. В целом, 5Mb это неплохо, но мы же с широкой душой и планируем путешествовать по большому количеству сайтов. Поэтому и указал, что нужно безграничное пространство. Оно будет заполняться до тех пор, пока профиль пользователя помещается на диске. Удаление данных произойдет только при удалении расширения… ну или при чистке хранилища.
Данные в хранилище лежат в виде объекта, поэтому при сохранении указываем ключ и значение. В данном случае, ключом выступает имя хоста.
Полный код контентного скрипта:
JavaScript: Скопировать в буфер обмена
Здесь описывается иконка на панели приложений. Мы просто говорим браузеру, какую иконку использовать, какую выводить всплывающую подсказку и указываем обычный html-документ, который будет выведен как Popup. Кстати, вот html для окна “WP Version Detect”:
HTML: Скопировать в буфер обмена
С интерфейсом не заморачивался, все же ключевое это продемонстрировать принципы. Суть интерфейса простая: сверху версия, которую скрипт посчитал наиболее вероятной, снизу таблица с параметрами по которым считал скрипт.
За наполнение и поведение окошка отвечает скрипт popup.js:
JavaScript: Скопировать в буфер обмена
Весь код обернул в асинхронную самозапускающуюся функцию. Асинхронную, чтобы не городить кучу коллбэков. Разберу, что тут происходит:
JavaScript: Скопировать в буфер обмена
Для извлечения данных из локального хранилища, нам нужно имя хоста. Как вы помните, имя хоста выступает ключом в хранилище расширения. Из скриптов popup-окон нет прямого доступа к контенту и контексту сайта, соответственно, получить имя хоста из переменной location не вариант. Приходится обратиться к вкладкам, запросив активную вкладку активного окна. Вообще, по идее, можно проигнорить “currentWindow”, но если открыто несколько окон, может выйти незадача.
Чтобы использовать “tabs”, нужно в manifest.json указать это разрешение. В этот раз мы используем объект “tabs” поверхностно, но по факту это мощный инструмент, который позволяет не только программно управлять вкладками, как это делал бы обычный пользователь, но и реагировать на события.
Когда мы получили домен, можем загрузить данные из локального хранилища и вывести их. Для этого используем метод get() локального хранилища, передав ему ключ и функцию обрабатывающую результат:
JavaScript: Скопировать в буфер обмена
Думаю объяснять процесс вывода данных в интерфейс нет никакого смысла. Обычная работа с HTML-документом
На этом расширение для детекта версии Wordpress отложим. Безусловно, даже не пытаюсь называть подобный алгоритм определения версии точным, но все же он способен дать неплохие результаты. Все же, главное в том, что вы с нуля написали достаточно полезное расширение, при этом поработав с телом страницы, системой обмена сообщений и хранилищем расширений. Самое время переходить к более интересным моментам.
JSON: Скопировать в буфер обмена
Соответственно, вместо многоточия будут подставляться скрипты. Итоговый файл manifest.json будет приведен в конце статьи. Обращаю внимание, что многоточие в background идет перед скриптом background.js, а в контентных скриптах наоборот. В основном фоновом скрипте будет обработчик событий, который вызывает функции из других скриптов. Это значит, что все вызываемые функции должны быть объявлены до основного скрипта. В контентных скриптах, в общий файл поместятся переиспользуемые функции, поэтому он стоит первым.
Как видно из манифеста, структура папок будет следующая:
Структуру папок вы можете определить по своему усмотрению, нет каких-то жёстких требований.
Помимо перечисленных выше папок, я добавил папку bootstrap, в которой лежат css и js для наведения красоты.
JavaScript: Скопировать в буфер обмена
Функция принимает имя хоста, ключ хранения и объект для хранения. Сначала получает текущий сохраненный объект по хосту, далее через деструкцию сохраняет объект, перезаписывая конкретный ключ.
На всякий случай пояснения для тех, кто не сильно знаком с особенностями Javascript. При написании [key] и [domain], мы получим имя ключа хранящаяся в переменной. Если написать просто “key”, то имя ключа будет “key”. При использовании квадратных скобок, именем ключа станет то что храниться в “key”
Перезапись данных можно представить себе следующим образом:
Вернемся к нашему коду. Теперь, сохранение данных можно выполнить двумя способами, в зависимости откуда мы хотим произвести сохранение:
JavaScript: Скопировать в буфер обмена
Так, если мы хотим передать данные на сохранение из контентного или попап-скрипта. Из фоновых скриптов, соответственно, напрямую вызываем функцию сохранения:
JavaScript: Скопировать в буфер обмена
Обратите внимание, что в фоновом скрипте я вызвал “saveDataFunction”, а не “saveData”. Все дело в области видимости. Так как функции фоновых скриптов расположены в разных файлах, они могут не знать друг о друге. Поэтому, при вызове функции собирающей какие-то данные, я передаю функцию сохранения в виде коллбэка через дополнительный параметр “saveDataFunction”, который впоследствии вызываю как функцию:
JavaScript: Скопировать в буфер обмена
Искать будем при помощи метода “search” строки. Прелесть в том, что, запихивать в него можем хоть строку, хоть регулярное выражение. Код файла cs_comments.js:
JavaScript: Скопировать в буфер обмена
Обратите внимание, что массив “sensetiveSearchValues” используется исключительно для примера. Если у вас нет идей, чем можно наполнить этот массив, можно обратить внимание на следующие словари: Но, даже если не наполнять словарь, уже гораздо удобнее работать с комментами, чем копаться в исходниках.
Логика кода очень проста и вряд ли вызовет много вопросов. Получил html-код страницы, объявил регулярку для поиска комментариев и массив с интересующими совпадениями. Дальше, применив регулярку к коду страницы, получил массив совпадений которые надо чекнуть на предмет “интересности”. В этом помогает функция “map”, внутри которой вывод подменяется на объект. Объект содержит всего два свойства:
Завершается скрипт сохранением данных:
JavaScript: Скопировать в буфер обмена
Обратите внимание, что в конце функции я возвращаю “false”. Это нужно на случай, если какой-то из ифов окажется без возврата результата. Важно из асинхронного обработчика события onMessage делать возврат, иначе посыпятся ошибки в связи с непониманием скриптов, где же происходит завершение выполнения функции.
Первые результаты в хранилищи:
Вхождения сохраняю, чтобы была максимально полная информация при выводе. Кстати, файл всплывающего окна, пока, будет выглядеть так:
HTML: Скопировать в буфер обмена
Ничего необычного, простой проект с подключенным бутстрап. Разве что задана ширина body, эта ширина учитывается браузером при создании всплывающего окошка.
Вся “магия” происходит в файле popup.js:
JavaScript: Скопировать в буфер обмена
Если вы прочитали часть об определению версии WP, код вам покажется очень знакомым. Снова получаем таб, снова берем хостнейм и так далее. Разве что вывод реализован через установку innerHTML для дива с аккордеонами. Когда есть совпадения, указывается их количество и текст выделяется красным, иначе “0” и зеленым.
Ну и перед выводом происходит подмена “<” на html-код. Это, конечно, забавно… но я на самом деле минут 10 не мог понять, что не так с кодом… почему не выводится текст комментария… ))))
На этому работу с комментами завершаем. Но помните, каждый раз при загрузке страницы будут перезаписываться данные по комментам в рамках активного домена. Думаю вы знаете как это можно исправить.
Соответственно, расширение может получить доступ к кукам с двух сторон: из контекста веб-приложения и из контента браузера. Мы будем работать из контекста браузера, так как в этом случае, нас ничего не ограничивает. Главное не забыть прописать “cookies” в списке требуемых разрешений в манифесте.
JavaScript: Скопировать в буфер обмена
Чтобы получить куки, достаточно вызвать метод getALL() передав в него фильтры. Если не передать, браузер высыпает все куки. Мы можем каждый раз обрабатывать все доступные куки, но лучше если ограничим хотя бы доменом. Тогда вызов будет примерно таким:
JavaScript: Скопировать в буфер обмена
К слову о фильтрах, их огромное множество и можно использовать их в любой конфигурации… вместе или по отдельности. Например, мы можем запросить все httpOnly куки хранящиеся в браузере:
JavaScript: Скопировать в буфер обмена
Наше расширение будет работать с активной страницей, поэтому код пишем в контентный скрипт “cs_cookie.js” (не забудьте добавить путь к сприпту в манифест):
JavaScript: Скопировать в буфер обмена
В background.js перехватываем сообщение и запускаем обработку:
JavaScript: Скопировать в буфер обмена
Если просто вывести куки XSS.is, увидим примерно такую картинку:
Приступим к написанию чекера checkCookies(). Схема работы будет следующей:
В коде я выразил это следующим образом (bg_cookie.js):
JavaScript: Скопировать в буфер обмена
Просто проходим по каждой куке последовательно применяя специализированные функции. Обратите внимательно где и какие переменные используются, чтобы разобраться к каким данным применяется та или иная функция. Оптимально, если накидаете консоль-логов и посмотрите вывод.
JavaScript: Скопировать в буфер обмена
Здесь все просто, JWT состоит из трех отдельных частей. Если нет, то возвращается значение которое и получили. Если да, то возвращается среднее значение, так как оно в base64, а следующим действом мы будем пытаться декодировать значение:
JavaScript: Скопировать в буфер обмена
Соответственно, сначала наращиваем недостающие символы до корректного значения, после применяем декодирование. Как и в прошлой функции, если что-то пошло не так, возвращаем входящее значение.
JavaScript: Скопировать в буфер обмена
Функции простые и полностью соответствуют своим названиям, поэтому задерживаться на них не буду. Завершает файл функция чека на наличие интересных значений:
JavaScript: Скопировать в буфер обмена
Как и с комментариями, просто ищем вхождения. Самое время заняться выводом и он очень-очень будет похож на вывод инфы по комментариям. Начну с popup.html и сразу внесу в него то, что хочу увидеть в итоге. Если точнее, то добавлю табы для переключения между разными блоками информации и все блоки:
HTML: Скопировать в буфер обмена
Соответственно, в popup.js нужно повесить обработчики кликов по табам.
JavaScript: Скопировать в буфер обмена
Что касается вывода инфы по кукам, он будет идентичный комментариям. Забегая вперед скажу, что вывод спаршенных ссылок со страницы будет точно таким же. Городить три одинаковых функции, которые будут отличаться только парой параметров…. ну неет. Лучше сделаю отдельную сервисную функцию. Еще один момент, который мне не нравится, это то что URL получаем прямо в popup-скрипте. По хорошему, даже взаимодействие с хранилищем стоило бы вытащить в фоновый скрипт. Но хотя бы получение урла перетащил в фон, тем более там тоже его надо будет поулчать
JavaScript: Скопировать в буфер обмена
Стало гораздо лучше. Теперь, если надо будет еще один подобный вывод, достаточно будет добавить новую функцию print с параметрами и все. Но не забываем, что background.js тоже надо бы поменять:
JavaScript: Скопировать в буфер обмена
Во-первых, добавились две переменных для хранения урла: одна в виде текста, вторая в виде объекта URL. Обе переменных получает функция getCurrentURL(). Функция эта может быть вызвана в двух случаях: при переключении пользователем вкладки или же если на момент запроса через сообщение “'getCurrentTabUrl'” переменная окажется пустой.
JavaScript: Скопировать в буфер обмена
Просто собираю все тэги a, link, script. Чищу от дублей и ищу чувствительные вхождения, типа “админ” и т.п. Можно обратить внимание, что передаю ссылку на сохранение привидя объект к строке. Ссылок может быть невероятное множество, поэтому перестраховываюсь.
Остается только чуть-чуть поправить вывод (popup.js):
JavaScript: Скопировать в буфер обмена
Альтернативой для анализа запросов может выступать объект API “proxy”. В этой статье я не буду его разбирать, при желании вы можете добавить его в манифест в разрешения, после чего в фоновом скрипте подписаться на событие proxy.onRequest через метод addListener и самостоятельно изучить, какую информацию можно получить.
JavaScript: Скопировать в буфер обмена
В статье, будем анализировать приходящие к нам данные и искать в них ссылки. Хотя, конечно же, это не единственный вариант использования. Можно собирать пересылаемые параметры, искать ендпоинты API, анализировать запросы на предмет SSRF ну и т.д. Вплоть до модификации запросов и поиска XSS, или других уязвимостей, в полностью автоматическом режиме. К этому вернемся позже, а пока подпишемся на событие:
JavaScript: Скопировать в буфер обмена
Как понятно из названия, событие происходит перед отправкой запроса. При назначении слушателя, передаю ему функцию прослушивания, объект с фильтрами и массив экстра-параметров. Фильтры, соответственно, позволяют игнорировать ненужный контент, например, картинки. Или ограничивать адреса с которых обрабатывать запросы. Экстра-параметры, помогают настроить поведение.
В функцию слушателя передается параметр “details”, как его называют в MND(в моем случае “requestInfo”), в нем находится объект полностью описывающий запрос.
Что касается объекта фильтров… использую два параметра: urls и types, оба массива. В целом, они сами себя документируют. Обрабатываться будут все урлы, из которых будут выбираться:
В примере передаются параметры requestBody и blocking. “requestBody” указывает FF, что надо к объекту с деталями запроса добавить тело запроса. “blocking” более интересный параметр. Без него мы не сможем получить ответ сервера. Кроме того, с этим параметром мы можем вносить изменения в запрос перед отправкой или вовсе отменить его. Это нужно, например, если ваше расширение анализирует запрос перед отправкой и нашло какие-то опасные данные. Тогда можно настроить поведение, например, перекинув подобный запрос на страницу блокировки или просто отменив запрос.
Из ближайшего примера, могу вспомнить таргет, в котором о любом изменении в профиле пользователя улетало уведомление самому пользователю и админу. Забравшись в админку и, желая найти вторичную SQLi, я с досадой обнаружил дополнительный вопрос на sendNotify.php при каждом сохранении. Да, это странно, ведь отправку уведомления можно прилепить к функции отправки, но кто я такой чтобы спорить с разработчиком того ресурса?
Чтобы минимизировать риск обнаружения при помощи расширения, достаточно было добавить блокирующий экстра-параметр ["blocking"] для домена. Плюс, заменить возвращаемое значение на {cancel: true}; для запросов содержащих “sendNotify.php”. Не забыв добавить в манифест разрешение на “webRequestBlocking”.
JavaScript: Скопировать в буфер обмена
Другой вариант возвращать, вместо “cancel” , “redirectUrl” в котором передавать нужный URL. Например, выкидывать “ататат-страницу” при попытки получить взрослый контент.
Вернемся к нашему расширению.В манифест добавлю разрешения:
JavaScript: Скопировать в буфер обмена
Перепишу beforeRequestHandler(), добавив в него фильтрацию ответов:
JavaScript: Скопировать в буфер обмена
Обратите внимание, что мы получаем не готовые данные, фильтр отдает поток данных и нам нужно обрабатывать событие “ondata”. Причем, обращаться вальяжно с потоком нельзя: обработка события блокирует поток, поэтому в конце обработки должна быть обязательная запись в поток данных, полученных в event.data. В ином случае, запрос прервется и данные никогда не попадут на вывод. К слову, декодировав поток, в него можно спокойно вносить изменения как в обычную строковую переменную, тем самым меняя поведение приложения... Но наша задача сегодня, это поиск ссылок. Для этого создам вспомогательную функцию, которая будет проходить регулярками по данным и сохранять полученные значения. Функцию сделаем асинхронной, чтобы максимально избежать блокирования потоков на время обработки.
Потребуется какое-то или какие-то регулярные выражения. Можно заморочиться и написать свои варианты, но когда есть готовое, нафига оно надо? Предлагаю обратиться к репозиторию JS Link Finder, это достаточно популярное расширение для Burp. У них есть достаточно интересное регулярное выражение.
Python: Скопировать в буфер обмена
Нужно просто привести его в порядок, откинув комментарии и переносы строк. На выходе получил такую функцию:
JavaScript: Скопировать в буфер обмена
Тестирование прошло успешно, осталось избавиться от дублей, undefined и записать все в ссылки.
Но есть нюанс… если мониторить все ответы, система будет серьезно нагружена. Кроме того, что каждый ответ, в рамках всего браузера, надо будет прогнать через регулярку, нагрузку на систему даст процесс чистки от дублей и постоянного сохранения. Наиболее оптимальным вариантом вижу включение мониторинга пользователем. Во всплывающем окне уже добавлена кнопка, осталось её оживить и изменить процесс запуска мониторинга. Начну с popup.js:
JavaScript: Скопировать в буфер обмена
Появилось две функции: одна отвечает за поведение кнопки, вторая проверяет нет ли уже спаршенных ссылок, если есть то добавляет их. Дело в том, что для корректного парсинга ссылок, нужно перехватить запросы, как при начальной загрузке страницы, так и возможные запросы когда казалось бы ничего не происходит и сайт уже загружен. Второй нюанс в том, что скрипт popup.js срабатывает каждый раз при открытии попапа. Каждое открытие это чистый лист. Если не организовать какого-то временного хранения, каждый раз ссылки будут удаляться из интерфейса.
Чтобы решить эту проблему, организовал временное хранение спаршенных ссылок. Когда нажимается кнопка “Старт”, включается перехватчик запросов, список ссылок чиститься и страница обновляется. Перехватчик записывает найденные ссылки в хранилище до тех пор, пока не будет нажата кнопка “Стоп”. При открытии окна, через функцию checkHasParsedLinks() подгружаются уже сохраненные ссылки. Надеюсь не запутал)
Завершает все прослушиватель события в попап-скрипте. Он нужен для добавления новых ссылок, когда окно попапа открыто. Альтернативой может быть слушатель, который реагирует на изменения в хранилище (да, изменения в хранилище можно прослушивать), но в данном случае, это не очень рационально.
Файл background.js так же имеет ряд новых кусков кода:
JavaScript: Скопировать в буфер обмена
Появился массив спаршенных ссылок. Благодаря ему происходит отсеивание дублей плюс в popup пересылаются новые ссылки для добавления. Посмотрите на searchLinks(). Сначала запоминаем размерность массива с уже добытыми ссылками. Дальше объединяем уже добытые ссылки с новой партией и, при помощи Set, сносим дубли. После вычленяем только новые значения, отбрасывая все уже имеющееся в parsedLinks, чтобы переслать их в popup. Ну и сохраняем значения в хранилище.
Обратите внимание, как происходит добавление прослушивателя события, проверка на его наличие и удаление — везде используется только название функции. Никаких идентификаторов, ничего другого. Ниже мы еще вернемся к этой особенности, назначив персональный парсер для сервиса.
Так как это запрос к стороннему ресурсу, чтобы ничего не мешало запрос выполняем из background-скрипта:
JavaScript: Скопировать в буфер обмена
Если вы добрались до этого места, код должен быть предельно понятным. Запросили у сервиса данные в формате JSON, почистили и сохранили. Остановлюсь только на доменном имени. У нас в распоряжении есть имя хоста, что не очень хорошо. CRT.sh отдаст нам гораздо меньше информации, чем при использовании домена. Поэтому использовал самый простой хук: сплитанул хостнейм, удалил все кроме домена и TLD, после чего снова собрал. Вуаля, у нас есть чистый домен…
Для вывода в попап-скрипт добавляем к проверкам еще одну:
JavaScript: Скопировать в буфер обмена
Ну и функция добавления поддоменов списком:
JavaScript: Скопировать в буфер обмена
Очень люблю данные с censys, dnsinfo и других серисов. Но есть проблема в виде WAF и каптчи. Решать её можно по разному: пытаться обмануть CloudFlare, подключить сервис гадания каптчи, обращаться напрямую к API, подгружать страницу с каптчей пользователю и т.д. В статье буду использовать самый тупой и топорный вариант: добавлю кнопку, по нажатию которой откроются интересующие вкладки. Плюс добавлю парсер ответов dnsinfo, как пример. На его основе каждый сможет создать свой парсер.
Первым делом, назначим действие кнопке во всплывающем окне:
JavaScript: Скопировать в буфер обмена
Соответственно, в фоновом скрипте надо добавить обработчик. Ссылки на интересующие сервисы буду хранить в массиве, используя “$$$” для подстановки доменного имени:
JavaScript: Скопировать в буфер обмена
С DNSInfo поступим хитро. Перед открытием вкладок назначим перехватчик со следующими параметрами:
JavaScript: Скопировать в буфер обмена
Соответственно, перехватчик будет реагировать только на определенный поиск в dnsinfo, причем завязанный на конкретном домене. Обрабатывать будем “main_frame” по той причине, что вся нужная информация прилетает сразу в ответе, без каких-то дополнительных подгрузок. Имя хоста запомним, чтобы сохранить данные. Именно что запомним, на случай если до получения информации произойдет переключение вкладок.
Функция обработки
JavaScript: Скопировать в буфер обмена
Во многом уже знакомая. После назначения фильтра, сразу же удаляю прослушивание события. А зачем оно теперь нам? Как только сработает фильтр, мы получим нужные данные и забудем обо всем.
Внутри обработчика потока две регулярки. Одна парсит айпи, вторая таблицу. Да, чтобы не заморачиваться, просто заберу таблицу целиком. Чтобы проблем с хранением таблицы не возникло, кодирую её в base64.
Для вывода нужно совсем немногое:
JavaScript: Скопировать в буфер обмена
Такой вот хитрый “бич”-способ парсить нужные данные с сервисов. Массово не напарсишься, но точечно поработать с объектом очень даже получится.
Сначала добавлю в манифест разрешение на “dns”. После зайду в пустующий content.js (ну должны же мы в него хоть что-то положить) и добавлю:
JavaScript: Скопировать в буфер обмена
Соответственно, в фоновый скрипт надо добавить обработчик соответствующего события:
JavaScript: Скопировать в буфер обмена
Здесь мы просим браузер дать нам информацию с учетом:
JavaScript: Скопировать в буфер обмена
Еще раз напомню, что получившиеся в итоге расширения хоть и можно использовать в своей деятельности, но стоит рассматривать больше как пример и ввод в концепцию. Есть множество нюансов, которые нужно допиливать под каждый конкретный случай. Например, сохранение данных. Сейчас сохранение происходит по полному хосту. Правильно ли это в вашем случае? Может оптимальнее накапливать данные по домену в целом? Тогда встает вопрос о смене структуры хранения и удобном, для анализа, выводе информации.
Или, например, надо ли сохранять все спаршенные данные в локальное хранилище? Например, какие-то парсинги можно хранить в сессионном хранилище. Или, может быть есть смысл собирать данные в динамике, чтобы отслеживать произошедшие на сайте изменения?
В целом, мы поговорили о нескольких видах парсинга данных: парсинг с использованием возможностей DOM, прямой парсинг кода HTML-страницы и парсинг сопутствующих файлов (скрипты перехватываемые через webRequest). Обсудили варианты хранения данных. Работу с вкладками, проброс сообщений между разными частями расширения и многое-многое другое. Я планирую написать еще 1-2 статьи, чтобы раскрыть вопросы активного сканирования и создания автоматических эксплоитов на базе расширений браузеров, но усвоив информацию одной только этой статьи, вы уже можете все это проделать самостоятельно без каких-либо проблем.
Если остались какие-то вопросы, есть уточнения или пожелания, не стесняйтесь написать об этом в комментариях. Ну и, если тема зашла, дайте знать об этом.
Источник https://xss.is
Не так давно на форуме Patr1ck написал интересную статью по разработке антифишингового расширения для браузера Google Chrome. Убедившись, что планы по статьям не пересекаются, решил таки написать на тему хацкерских расширений. Тем более запрос на тему расширений есть.
Чем хорошо расширение для браузера?
С одной стороны, у скриптов расширения есть все те же возможности, что и у пользователя. Есть доступ к HTML-коду страницы, к хранилищам используемым веб-приложением, есть возможности автоматизации под видом пользователя. С другой стороны, мы имеем доступ к событийной модели браузера, возможность генерировать новые запросы и т.д. Таким образом, есть возможность повысить эффективность поиска подходящей цели. Вы просто серфите по сайтам, а в это время расширение проводит проверки… если что-то обнаружилось, можно выкинуть уведомление, отправить данные на свой сервер или в Google-таблицу, сохранить скриншоты или вовсе полность автоматизировать взлом по известной уязвимости. Хоть управление голосом прикручивайте, благо для этого есть стандартные возможности и потребуется пару десятков строк кода. Всем, кроме владельцев Mac, на яблоках есть ограничения…
При помощи расширений можно писать сканеры уязвимостей, как пассивные, так и активные. Можно автоматизировать задачи, вплоть до написания спам-ботов или ботов прогревающих аккаунты. Можно реализовать некоторые виды эксплоитов. Можно собирать информацию, например, как Wappalyzer собирает информацию о технологиях. Можно создавать справочники с быстрыми подсказками, например, генераторы reverse-shell’ов.
Что мы сделаем в рамках статьи
Запихать все в одну статью невозможно, поэтому будет небольшая серия в которой реализуем практически все описанное выше, от сканеров до эксплоитов и ботов. В этой статье, сконцентрируюсь на вопросах сбора информации, пассивного сканирования сайта. Сделаю несколько небольших расширений, которые дадут понимание процесса разработки, а так же будут неплохой основой для доведения их до интересной вам кондиции.Начну с расширения определяющего версию Wordpress. В ходе его разработки, вы познакомитесь с тем, как между собой взаимодействуют разные части расширений, а также поймете насколько все легче чем кажется.
Второе расширение будет посвящено более общему сбору информации о веб-приложении и больше будет похоже на несколько мини-расширений, как отдельные модули объединенных в одно:
- Поиск чувствительной информации в комментариях к коду
- Сбор ссылок, в том числе из JS-файлов
- Анализ кукисов и локального хранилища
- Анализ запросов/ответов
- Поиск поддоменов веб-приложения и получение информации о DNS
Стартуем
Писать расширение будем для браузера Firefox. Но стоит помнить, что в целом, разница между API Chrome и Firefox не такая существенная, поэтому практически каждое расширение можно адаптировать под нужный браузер В любом случае, всё всегда начинается с файла manifest.json. Именно этот файл дает браузеру понять, как взаимодействовать с разрешением. Самый простой вариант файла выглядит примерно так:
JavaScript: Скопировать в буфер обмена
Код:
{
"manifest_version": 3,
"name": "Passive Hack | XSS.is",
"description": "Extension for passive scanning of web applications. Developed specifically for the article on the XSS .is website",
"version": "0.0.1"
}
Нет, серьезно, это уже готовое расширение, которое браузер “скушает”. Просто оно ничего не делает. Даже иконки не имеет, только название, версию и текстовое описание. Чтобы добавить наше расширение для дебага в браузер, нужно перейти по адресу about:debugging#/runtime/this-firefox и нажать “Load Temporary Add-on”... После чего оно появится во временных расширениях.
Обратите внимание, что использовать будем третью версию манифеста, не вторую. Это влияет не только на структуру самого json, но и в целом на то как браузер будет взаимодействовать с расширением. Третья версия — это актуальная версия для Chrome и Firefox.
Файлы расширения WP Detect
Чтобы более-менее точно определить версию Wordpress, нам потребуется подобная структура::- Контентный скрипт, который будет отвечать за обработку HTML-кода и дополнительный запрос с целью найти фид и по нему определить версию
- Фоновый скрипт, так как контентный скрипт знать не знает о существовании возможности отправить уведомление в браузере.
- Попап-окно, в котором будем выводить развернутые данные по определению версии
Файл манифеста будет выглядеть следующим образом:
JSON: Скопировать в буфер обмена
Код:
{
"manifest_version": 3,
"name": "WordPress version detector | XSS.is",
"description": "WordPress version detector. Developed specifically for the article on the XSS .is website",
"version": "0.0.1",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"action": {
"default_icon": "images/logo.png",
"default_title": "WP Version Detect | XSS.is",
"default_popup": "popup.html"
},
"permissions": ["notifications", "storage", "tabs", "unlimitedStorage"]
}
Я не буду сейчас останавливаться на разрешениях, которые потребуются расширению, чтобы не дублировать текст. Где потребуется остановиться подробнее, я про них напишу. Сейчас же важно понимать только то, что мы просто перечисляем нужные нам объекты API WebExtension.
Свойство background
Эти скрипты работают в контексте браузера. У них нет прямого доступа к контенту сайта, зато они имеют доступ ко всем интерфейсам API WebExtensions: управление вкладками, пунктами контекстного меню, bookmarks, уведомлениями и т.д. Кроме того, фоновые скрипты позволяют спокойно делать запросы на любой урл, независимо от политик. При необходимости есть вариант использовать фоновую страницу, загружая в нее любой урл. Достаточно, в background добавить свойство “page”. Причем, можно спокойно использовать отдельно или совместно со “scripts”.То, что фоновые скрипты работают в контексте браузера, не говорит об их изоляции. Расширения имеют систему обмена сообщениями, через которую спокойно можно передавать данные. Мы будем пользоваться этим, например, чтобы по итогу работы контентного скрипта выкинуть уведомление браузера. Плюс, фоновые скрипты могут спокойно делать инъекцию javascript-кода.
В определении версии WP, фоновый скрипт носит вспомогательную роль — он просто уведомляет пользователя о факте определения версии:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(async (data, sender) => {
if (data.action == 'notify') {
browser.notifications.create({
type: "basic",
title: `Possible Wordpress Version: ${data.host}`,
message: `${data.host} site is probably built on Wordpress version ${data.maybeVersion.version}. This is indicated by ${data.maybeVersion.count} mentions of the version`,
});
}
return false
})
Все функции детекта возложены на контентный скрипт, который по итогу выполнения сохранит данные и запустит метод sendMessage(), передав браузеру сообщение. Как видно из кода выше, фоновый скрипт просто вешает слушателя на событие onMessage(), который перехватит тот самый sendMessage().
При получении сообщения, есть возможность принять три аргумента:
- Данные. Это может быть что угодно, но я предпочитаю объекты. Так можно передать тип события и сами данные.
- Sender - понятно из названия, это объект описывающий отправителя
- sendResponse - функция, при помощи которой можно вернуть реакцию на сообщение. У нас функция асинхронная да и в целом не предполагается наличие ответа, поэтому всегда возвращаем “false”
Ну и крайний элемент фонового скрипта, это запуск “notifications.create”. На самом деле, очень скудный вариант запуска, так как уведомления имеют множество опций влияющих на внешний вид и поведение. Можно даже кнопки прикрутить, если оно того требует.
Свойство content_scripts
Это скрипты, которые встраиваются в само веб-приложение. Практически так же, как владелец сайта подключил какие-то свои js, браузер подключает ваши контентные скрипты. Скрипты помещаются в контекст страницы и могут взаимодействовать с DOM, вызывать функции сайта, менять значение переменных и т.д.Свойство “matches”, помогает задать шаблон урла к которому будет подгружаться контентный скрипт. Соответственно, можно задать конкретный сайт или, например, работу только с сайтами базирующимися на “http://*” или использовать универсальный "<all_urls>".
Свойство “run_at” может иметь ключевую роль в вашем расширении, так как оно указывает когда должна производится инъекция. В нашем случае, скрипт добавляется после загрузки всего.
Обратите внимание, что свойства “js” и “scripts” (в background) массивы. Загрузка массива происходит в порядке указанном в манифесте. Поэтому, если требуются общие переменные или функции, их нужно объявлять в первом загружаемом файле скриптов. Мы будем пользоваться этой особенностью во втором расширении, чтобы добавлять новые модули.
content.js WP Version Detect
Для определения версии WordPress: из нескольких мест берутся потенциальные версии с неким “весом вероятности” , после чего веса суммируются и максимальный вариант считается правильным. Как только найдена “правильная версия”, сохраняем значения в локальное хранилище и оповещаем пользователя. Пройдем по шагам:JavaScript: Скопировать в буфер обмена
Код:
(async function() {
...
})();
Напомню, что скрипт загружается после загрузки всего документа сайта. Поэтому, оборачиваю код в самовызывающуюся функцию. Асинхронной делаю её для того, чтобы была возможность дождаться выполнения нужных асинхронных функций, напрример, данных из локального хранилища:
JavaScript: Скопировать в буфер обмена
Код:
let savedValue = await browser.storage.local.get([window.location.host])
if (Object.keys(savedValue).length) return
Сейчас данные нужны только для того, чтобы избежать повторных определений. В ином случае, расширение вас закалупает оповещениями.
JavaScript: Скопировать в буфер обмена
Код:
let versions = []
const metaWP = document.querySelector('meta[name="generator"]')
if (metaWP) {
versions.push({
originalValue: metaWP.content,
version: metaWP.content.replace('WordPress ', ''),
type: 'Meta Generator',
weight: 10
})
}
Если у нас есть мета-тэг указывающий на генерацию вордпрессом, сохраняем информацию в массив потенциальных версий. Каждый вариант указывающий на возможную версию, храним в массиве versions в виде объекта. Не думаю, что есть смысл комментировать каждое его свойство.
JavaScript: Скопировать в буфер обмена
Код:
const verRegexp = /(?<=wp-includes).*(?<=ver=)(.*)/
const getEntries = (query, param, title) => Array.from(document.querySelectorAll(query)).map(el => el[param].toString()).filter(el => verRegexp.exec(el)).map(el => ({
originalValue: el,
version: /(?<=ver=)\d\.\d{1,2}\.\d{1,2}/.exec(el)[0],
type: title,
weight: 1
}))
let cssWP = getEntries('link[rel="stylesheet"]', 'href', 'CSS Version')
let jsWP = getEntries('script', 'src', 'JS Version')
versions.push(...cssWP)
versions.push(...jsWP)
Немаловажную роль, в определении версии, имеет параметр “ver”, который классически цепляется ко всем JS и CSS в WP. Для сбора этих параметров написал стрелочную функцию getEntries(). В ином случае, пришлось бы почти полностью дублировать процесс сбора css и js отдельно. Хотя между ними разница исключительно в запросе поиска (“link” против “script”) и интересующем параметре (“href” против “src”).
Функция получает все HTML-объекты подходящие под поиск и преобразует в HTML-коллекцию в обычный массив. Но нам нужны не объекты, а ссылки, поэтому первым “map” исправляем несправедливость, оставив только строки. Фильтром, используя регулярное выражение “verRegexp” избавляемся от строк не содержащих “wp-include” и “ver”. После чего преобразуем массив строк в массив объектов по структуре предыдущего. Ну и завершаем действие объединением, сложив полученные значения в “versions” через деструктуризацию.
Следующим шагом сделаем запрос к фиду сайта. Кстати, дополнить это действие можно запросом к файлу лицензии и readme.
JavaScript: Скопировать в буфер обмена
Код:
try{
let response = await fetch(`${window.location.protocol }://${window.location.host }/feed/`)
let feedText = await response.text()
let feedRegex = /(?<=<generator>).*wordpress.*(\d\.\d{1,2}\.\d{1,2})(?=<\/generator>)/g
let feedGroups = feedRegex.exec(feedText)
if (feedGroups.length < 2) throw new Error('Empty wp')
versions.push({
originalValue: feedGroups[0],
version: feedGroups[1],
type: 'Feed Response',
weight: 5
})
} catch (e) {
console.log('Feed not found')
}
Обернул действо в try-catch, т.к. фида может не быть, а заморачиваться с проверками очень не хотелось. Хотя можно было меньше писанины сделать…
С шагами, думаю, все понятно: стандартным fetch выполнил запрос, дождался результата, получил текст, регуляркой вытащил значение и добавил еще один объект в потенциальные версии.
Самое время подсчитать веса определить победителя:
JavaScript: Скопировать в буфер обмена
Код:
if (!versions.length)
return
let versionWeights = versions.reduce((acc, curr) => {
if (acc[curr.version]) {
acc[curr.version] += curr.weight
return acc
}
return {...acc, [curr.version]: curr.weight}
}, {})
let maybeVersion = {
version: "",
count: 0
}
for(let key in versionWeights) {
if (versionWeights[key] > maybeVersion.count)
maybeVersion = {
version: key,
count: versionWeights[key]
}
}
if (!maybeVersion.count) return browser.storage.local.set({[window.location.host]: {}})
Все, что осталось добавить — это оповещение пользователя и сохранение результатов. Для оповещения, как описывал в фоновом скрипте, нужно передать сообщение в runtime методом sendMessage():
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.sendMessage({
action: 'notify',
host: window.location.host,
maybeVersion: maybeVersion
})
В параметрах только объект с данными, так как мы не ждем какой-то реакции. Можно спокойно сохранить данные:
browser.storage.local.set({[window.location.host]: {versions, maybe: maybeVersion.version}})
На этом моменте остановлюсь подробнее. У расширения, как у любого сайта, есть свое хранилище. Перейдите в отладку расширений about:debugging#/runtime/this-firefox и кликните по кнопке “Inspect” или “Исследовать”. В открывшемся окне выберите “Хранилище”:
На скрине видно, что данные сохраняются в хранилище расширений. В нашем случае, это локальное хранилище. Оно не будет уничтожено, когда закончится сессия, не будет синхронизировано с другими устройствами. Данные хранятся локально и на одном устройстве. Есть варианты использовать не “local”, а тот же “sync”, но в данном случае нет никакого смысла. На всякий случай вот разница между типами хранилищ:
- local - хранится локально на диске компа и больше нигде. Данные удаляются только когда сносится расширение или принудительно через API
- sync - тоже самое, но при этом работает механизм синхронизации. Не путать с синхронной записью в хранилище, запись всегда асинхронная. Если вы на двух компах залогинены в браузере, данные хранилища будут синхрониться. Удобно, когда вы трудоголик.
- session - данные не сохраняются на диске. Оперативная память, кэш… закрыли вкладку, данные удалились.
- managed - данные задаются админом домена, мы можем их только читать. Никогда не пользовался. Может в какой-то корпоративной сети и есть смысл в подобном подходе.
Чтобы получить доступ к хранилищу, в манифесте мы запрашивали разрешение на “storage”. Но если внимательно изучить наш манифест, там же в разрешениях есть пункт “unlimitedStorage”. Дело в том, что при запросе доступа только к хранилищу, на нас накладывается ограничение в пять мегабайт. Т.е. Расширение не может хранить больше информации. В целом, 5Mb это неплохо, но мы же с широкой душой и планируем путешествовать по большому количеству сайтов. Поэтому и указал, что нужно безграничное пространство. Оно будет заполняться до тех пор, пока профиль пользователя помещается на диске. Удаление данных произойдет только при удалении расширения… ну или при чистке хранилища.
Данные в хранилище лежат в виде объекта, поэтому при сохранении указываем ключ и значение. В данном случае, ключом выступает имя хоста.
Полный код контентного скрипта:
JavaScript: Скопировать в буфер обмена
Код:
(async function() {
let savedValue = await browser.storage.local.get([window.location.host])
if (Object.keys(savedValue).length) return
let versions = []
const metaWP = document.querySelector('meta[name="generator"]')
if (metaWP) {
versions.push({
originalValue: metaWP.content,
version: metaWP.content.replace('WordPress ', ''),
type: 'Meta Generator',
weight: 10
})
}
const verRegexp = /(?<=wp-includes).*(?<=ver=)(.*)/
const getEntries = (query, param, title) => Array.from(document.querySelectorAll(query)).map(el => el[param].toString()).filter(el => verRegexp.exec(el)).map(el => ({
originalValue: el,
version: /(?<=ver=)\d\.\d{1,2}\.\d{1,2}/.exec(el)[0],
type: title,
weight: 1
}))
let cssWP = getEntries('link[rel="stylesheet"]', 'href', 'CSS Version')
let jsWP = getEntries('script', 'src', 'JS Version')
versions.push(...cssWP)
versions.push(...jsWP)
try{
let response = await fetch(`${window.location.protocol }://${window.location.host }/feed/`)
let feedText = await response.text()
let feedRegex = /(?<=<generator>).*wordpress.*(\d\.\d{1,2}\.\d{1,2})(?=<\/generator>)/g
let feedGroups = feedRegex.exec(feedText)
if (feedGroups.length < 2) throw new Error('Empty wp')
versions.push({
originalValue: feedGroups[0],
version: feedGroups[1],
type: 'Feed Response',
weight: 5
})
} catch (e) {
console.log('Feed not found')
}
if (!versions.length)
return
let versionWeights = versions.reduce((acc, curr) => {
if (acc[curr.version]) {
acc[curr.version] += curr.weight
return acc
}
return {...acc, [curr.version]: curr.weight}
}, {})
let maybeVersion = {
version: "",
count: 0
}
for(let key in versionWeights) {
if (versionWeights[key] > maybeVersion.count)
maybeVersion = {
version: key,
count: versionWeights[key]
}
}
if (!maybeVersion.count) return browser.storage.local.set({[window.location.host]: {}})
browser.runtime.sendMessage({
action: 'notify',
host: window.location.host,
maybeVersion: maybeVersion
})
browser.storage.local.set({[window.location.host]: {versions, maybe: maybeVersion.version}})
})();
Свойство action
JSON: Скопировать в буфер обмена
Код:
"action": {
"default_icon": "images/logo.png",
"default_title": "WP Version Detect | XSS.is",
"default_popup": "popup.html"
},
Здесь описывается иконка на панели приложений. Мы просто говорим браузеру, какую иконку использовать, какую выводить всплывающую подсказку и указываем обычный html-документ, который будет выведен как Popup. Кстати, вот html для окна “WP Version Detect”:
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WP Version Detect | XSS.is</title>
</head>
<body>
<script src="popup.js"></script>
<div id="header" style="padding: 10px 0;">
<span id="title">Detected WP version:</span>
<span id="version"></span>
</div>
<table>
<thead>
<tr>
<th>Type</th>
<th>Version</th>
<th>Full value</th>
</tr>
</thead>
<tbody id="list">
</tbody>
</table>
</body>
</html>
С интерфейсом не заморачивался, все же ключевое это продемонстрировать принципы. Суть интерфейса простая: сверху версия, которую скрипт посчитал наиболее вероятной, снизу таблица с параметрами по которым считал скрипт.
За наполнение и поведение окошка отвечает скрипт popup.js:
JavaScript: Скопировать в буфер обмена
Код:
(async function() {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
browser.storage.local.get([domain], savedValue => {
let data = savedValue[domain]
let list = document.querySelector('#list')
if (!data) return
document.querySelector('#version').innerHTML = data.maybe
list.innerHTML = ''
data.versions.forEach(el => {
const tr = document.createElement('tr')
const tdType = document.createElement('td')
const tdVer = document.createElement('td')
const tdVal = document.createElement('td')
tdType.innerHTML = el.type
tdVer.innerHTML = el.version
tdVal.innerHTML = el.originalValue
tr.append(tdType, tdVer, tdVal)
list.append(tr)
})
})
})()
Весь код обернул в асинхронную самозапускающуюся функцию. Асинхронную, чтобы не городить кучу коллбэков. Разберу, что тут происходит:
JavaScript: Скопировать в буфер обмена
Код:
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
Для извлечения данных из локального хранилища, нам нужно имя хоста. Как вы помните, имя хоста выступает ключом в хранилище расширения. Из скриптов popup-окон нет прямого доступа к контенту и контексту сайта, соответственно, получить имя хоста из переменной location не вариант. Приходится обратиться к вкладкам, запросив активную вкладку активного окна. Вообще, по идее, можно проигнорить “currentWindow”, но если открыто несколько окон, может выйти незадача.
Чтобы использовать “tabs”, нужно в manifest.json указать это разрешение. В этот раз мы используем объект “tabs” поверхностно, но по факту это мощный инструмент, который позволяет не только программно управлять вкладками, как это делал бы обычный пользователь, но и реагировать на события.
Когда мы получили домен, можем загрузить данные из локального хранилища и вывести их. Для этого используем метод get() локального хранилища, передав ему ключ и функцию обрабатывающую результат:
JavaScript: Скопировать в буфер обмена
Код:
browser.storage.local.get(domain, savedValue => {
let data = savedValue[domain]
let list = document.querySelector('#list')
if (!data) return
document.querySelector('#version').innerHTML = data.maybe
list.innerHTML = ''
data.versions.forEach(el => {
const tr = document.createElement('tr')
const tdType = document.createElement('td')
const tdVer = document.createElement('td')
const tdVal = document.createElement('td')
tdType.innerHTML = el.type
tdVer.innerHTML = el.version
tdVal.innerHTML = el.originalValue
tr.append(tdType, tdVer, tdVal)
list.append(tr)
})
})
Думаю объяснять процесс вывода данных в интерфейс нет никакого смысла. Обычная работа с HTML-документом
На этом расширение для детекта версии Wordpress отложим. Безусловно, даже не пытаюсь называть подобный алгоритм определения версии точным, но все же он способен дать неплохие результаты. Все же, главное в том, что вы с нуля написали достаточно полезное расширение, при этом поработав с телом страницы, системой обмена сообщений и хранилищем расширений. Самое время переходить к более интересным моментам.
Второе расширение
Как и писал ранее, по большей части это расширение состоящее из отдельных модулей, которые можно дописывать и наращивать. Модульность будет реализована на основе возможности добавления новых скриптов в массивы, как фоновых так и контентных скриптов.JSON: Скопировать в буфер обмена
Код:
{
"manifest_version": 3,
"name": "Passive Hack | XSS.is",
"description": "Extension for passive scanning of web applications. Developed specifically for the article on the XSS .is website",
"version": "0.0.1",
"background": {
"scripts": [... ,"js/background.js"]
},
"action": {
"default_icon": "images/logo.png",
"default_title": "Passive Hack | XSS.is",
"default_popup": "popup/popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/content.js", ...],
"run_at": "document_end"
}
],
"permissions": [
"tabs",
"cookies",
"webRequest",
"storage",
"unlimitedStorage"
]
}
Соответственно, вместо многоточия будут подставляться скрипты. Итоговый файл manifest.json будет приведен в конце статьи. Обращаю внимание, что многоточие в background идет перед скриптом background.js, а в контентных скриптах наоборот. В основном фоновом скрипте будет обработчик событий, который вызывает функции из других скриптов. Это значит, что все вызываемые функции должны быть объявлены до основного скрипта. В контентных скриптах, в общий файл поместятся переиспользуемые функции, поэтому он стоит первым.
Как видно из манифеста, структура папок будет следующая:
- js - здесь будут храниться все скрипты относящиеся к контентным и фоновым
- popup - папка для всего, что касается всплывающего окна
- images - изображения проекта
Структуру папок вы можете определить по своему усмотрению, нет каких-то жёстких требований.
Помимо перечисленных выше папок, я добавил папку bootstrap, в которой лежат css и js для наведения красоты.
Хранилище
Так как расширение будет представлять из себя достаточно разрозненную структуру и нужно организовать процесс сохранения множества отдельных объектов, решил написать отдельную функцию сохранения данных:JavaScript: Скопировать в буфер обмена
Код:
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
Функция принимает имя хоста, ключ хранения и объект для хранения. Сначала получает текущий сохраненный объект по хосту, далее через деструкцию сохраняет объект, перезаписывая конкретный ключ.
На всякий случай пояснения для тех, кто не сильно знаком с особенностями Javascript. При написании [key] и [domain], мы получим имя ключа хранящаяся в переменной. Если написать просто “key”, то имя ключа будет “key”. При использовании квадратных скобок, именем ключа станет то что храниться в “key”
Перезапись данных можно представить себе следующим образом:
Вернемся к нашему коду. Теперь, сохранение данных можно выполнить двумя способами, в зависимости откуда мы хотим произвести сохранение:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'comments',
object: [...comments]
})
Так, если мы хотим передать данные на сохранение из контентного или попап-скрипта. Из фоновых скриптов, соответственно, напрямую вызываем функцию сохранения:
JavaScript: Скопировать в буфер обмена
saveDataFunction(hostname, 'cookies', {...cookies})
Обратите внимание, что в фоновом скрипте я вызвал “saveDataFunction”, а не “saveData”. Все дело в области видимости. Так как функции фоновых скриптов расположены в разных файлах, они могут не знать друг о друге. Поэтому, при вызове функции собирающей какие-то данные, я передаю функцию сохранения в виде коллбэка через дополнительный параметр “saveDataFunction”, который впоследствии вызываю как функцию:
JavaScript: Скопировать в буфер обмена
Код:
// Функция обработки кук в файле bg_cookie.js
function checkCookies(hostname, cookiesData, saveDataFunction) {
...
// Обработка данных
...
// Вызываю callback
saveDataFunction(hostname, 'cookies', {...cookies})
}
// Основная функция в background.js, которые вызывает checkCookies()
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
...
} else if (data.action == 'getCookies') {
cookies = await browser.cookies.getAll({
hostname: data.hostname
})
// Передаю saveData как callback
checkCookies(data.hostname, cookies, saveData)
}
...
})
Ищем чувствительный контент
Начнем с простого. Исходный код страницы может дать нам много полезной информации, можно наткнуться на чувствительные комментарии разработчиков, которые они забыли удалить, собрать ссылки, определить технологии и т.д. Сконцентрируюсь на комментариях. Да, вероятность найти в комментариях кредсы стремиться к нулю, но бывает попадается что-то интересное… закомментированные ссылки на админку, упоминания о поддоменах или айпишники. В процессе разработки, какие только пометки не оставляют программисты. Всегда проверяю комменты, настало время сделать этот процесс удобным.Искать будем при помощи метода “search” строки. Прелесть в том, что, запихивать в него можем хоть строку, хоть регулярное выражение. Код файла cs_comments.js:
JavaScript: Скопировать в буфер обмена
Код:
const html = document.body.innerHTML
commentsRegex = /<!--[^>]*-->/g
sensetiveSearchValues = ["passwd", "password", "pass", "creds", /(?<=db).*/, /(https|http|ftp)(.*)/ ,/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/]
const comments = html.match(commentsRegex).map( comment => {
const sensValues = sensetiveSearchValues.map(sens => {
if (comment.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {
comment,
sensValues
}
})
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'comments',
object: [...comments]
})
Обратите внимание, что массив “sensetiveSearchValues” используется исключительно для примера. Если у вас нет идей, чем можно наполнить этот массив, можно обратить внимание на следующие словари: Но, даже если не наполнять словарь, уже гораздо удобнее работать с комментами, чем копаться в исходниках.
Логика кода очень проста и вряд ли вызовет много вопросов. Получил html-код страницы, объявил регулярку для поиска комментариев и массив с интересующими совпадениями. Дальше, применив регулярку к коду страницы, получил массив совпадений которые надо чекнуть на предмет “интересности”. В этом помогает функция “map”, внутри которой вывод подменяется на объект. Объект содержит всего два свойства:
- Саму строку комментария
- Интересные вхождения — здесь именно та строка или регулярка, которая выдала совпадение.
Завершается скрипт сохранением данных:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.onMessage.addListener(onMessageHandler)
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
}
return false
}
Обратите внимание, что в конце функции я возвращаю “false”. Это нужно на случай, если какой-то из ифов окажется без возврата результата. Важно из асинхронного обработчика события onMessage делать возврат, иначе посыпятся ошибки в связи с непониманием скриптов, где же происходит завершение выполнения функции.
Первые результаты в хранилищи:
Вхождения сохраняю, чтобы была максимально полная информация при выводе. Кстати, файл всплывающего окна, пока, будет выглядеть так:
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="../bootstrap/bootstrap.min.css">
<script src="../bootstrap/bootstrap.min.js"></script>
<title>Document</title>
</head>
<body style="width: 600px;padding: 20px 0; ">
<div class="container">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Comments</a>
</li>
</ul>
<div class="accordion" id="comments">
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
Ничего необычного, простой проект с подключенным бутстрап. Разве что задана ширина body, эта ширина учитывается браузером при создании всплывающего окошка.
Вся “магия” происходит в файле popup.js:
JavaScript: Скопировать в буфер обмена
Код:
(async function() {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
browser.storage.local.get(domain, savedValue => {
let data = savedValue[domain]
if (!data || !data.comments || !data.comments.length) return
let commentsDiv = document.querySelector('#comments')
const commentsHTML = data.comments.map((comment, ind) => {
console.log(comment.sensValues.length, comment.comment)
let body, button = `<span class="accordion-button collapsed text-success">${comment.sensValues.length} | ${comment.comment.replaceAll('<', '<')}</span>`
if (comment.sensValues.length) {
button = `<button class="accordion-button collapsed text-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse${ind}"
aria-expanded="false" aria-controls="collapse${ind}">
${comment.sensValues.length} | ${comment.comment.replaceAll('<', '<')}
</button>`
body = `<div class="accordion-body">${comment.sensValues.join(', ')}</div>`
}
return `<div class="accordion-item">
<h2 class="accordion-header">
${button}
</h2>
<div id="collapse${ind}" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
${body}
</div>
</div>`
})
commentsDiv.innerHTML = commentsHTML.join('')
})
})()
Если вы прочитали часть об определению версии WP, код вам покажется очень знакомым. Снова получаем таб, снова берем хостнейм и так далее. Разве что вывод реализован через установку innerHTML для дива с аккордеонами. Когда есть совпадения, указывается их количество и текст выделяется красным, иначе “0” и зеленым.
Ну и перед выводом происходит подмена “<” на html-код. Это, конечно, забавно… но я на самом деле минут 10 не мог понять, что не так с кодом… почему не выводится текст комментария… ))))
На этому работу с комментами завершаем. Но помните, каждый раз при загрузке страницы будут перезаписываться данные по комментам в рамках активного домена. Думаю вы знаете как это можно исправить.
Разбираем Cookies
Забавно, но недавно мне попадался ресурс с cookie “role”. Любой пользователь мог вместо “user” установить “admin” и без каких-либо проблем попасть в админку. Подобный баг может годами тянуться из какой-то давней-давней версии веб-приложения. Не редко кука представляет собой кодированный base64 JSON или другой формат. Неплохо было бы реализовать чекер, который в фоне тихонько нормализует куки и чекнет их на какие-то интересные параметры. Например: role, id, userid и т.п.Соответственно, расширение может получить доступ к кукам с двух сторон: из контекста веб-приложения и из контента браузера. Мы будем работать из контекста браузера, так как в этом случае, нас ничего не ограничивает. Главное не забыть прописать “cookies” в списке требуемых разрешений в манифесте.
JavaScript: Скопировать в буфер обмена
Код:
let getting = browser.cookies.getAll(
details // object
)
Чтобы получить куки, достаточно вызвать метод getALL() передав в него фильтры. Если не передать, браузер высыпает все куки. Мы можем каждый раз обрабатывать все доступные куки, но лучше если ограничим хотя бы доменом. Тогда вызов будет примерно таким:
JavaScript: Скопировать в буфер обмена
Код:
cookies = await browser.cookies.getAll({
domain:domain
})
К слову о фильтрах, их огромное множество и можно использовать их в любой конфигурации… вместе или по отдельности. Например, мы можем запросить все httpOnly куки хранящиеся в браузере:
JavaScript: Скопировать в буфер обмена
Код:
cookies = await browser.cookies.getAll({
secure: true
})
Наше расширение будет работать с активной страницей, поэтому код пишем в контентный скрипт “cs_cookie.js” (не забудьте добавить путь к сприпту в манифест):
JavaScript: Скопировать в буфер обмена
Код:
let response = browser.runtime.sendMessage({
action: 'getCookies',
domain: window.location.hostname
})
В background.js перехватываем сообщение и запускаем обработку:
JavaScript: Скопировать в буфер обмена
Код:
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
}
return false
})
Если просто вывести куки XSS.is, увидим примерно такую картинку:
Приступим к написанию чекера checkCookies(). Схема работы будет следующей:
- Проверяем не является ли значение куки JWT-токеном. Если да, расшифровываем все параметры и продолжаем их анализ в отдельности.
- Пытаемся декодировать из base64, на всякий случай дополнив недостающими символами.
- Анализируеми, не является ли значение JSON или XML
- Ищем в строке намек на чувствительные данные
В коде я выразил это следующим образом (bg_cookie.js):
JavaScript: Скопировать в буфер обмена
Код:
function checkCookies(hostname, cookiesData, saveDataFunction) {
let cookies = cookiesData.map(({name, value, secure, session}) => {
let cookieDecode = parseJwt(value)
let cookieBS64Decode = checkBase64Value(cookieDecode)
let isJSON = checkJSONValue(cookieBS64Decode)
let isXML = checkXMLValue(cookieBS64Decode)
let sensName = checkSensCookies(name)
let sensValue = checkSensCookies(cookieBS64Decode)
return {
name, value, secure, session, isJSON, isXML, sensName, sensValue, base64decode: cookieDecode == cookieBS64Decode ? null : cookieBS64Decode
}
})
saveDataFunction(hostname, 'cookies', cookies)
}
function checkSensCookies(value) { ... }
function checkBase64Value(originalValue) { ... }
function checkXMLValue(value) { ... }
function checkJSONValue(value) { ... }
function parseJwt (token) { ... }
Просто проходим по каждой куке последовательно применяя специализированные функции. Обратите внимательно где и какие переменные используются, чтобы разобраться к каким данным применяется та или иная функция. Оптимально, если накидаете консоль-логов и посмотрите вывод.
JavaScript: Скопировать в буфер обмена
Код:
function parseJwt (token) {
let parts = token.split('.')
if (parts != 3) return token
return parts[1]
}
Здесь все просто, JWT состоит из трех отдельных частей. Если нет, то возвращается значение которое и получили. Если да, то возвращается среднее значение, так как оно в base64, а следующим действом мы будем пытаться декодировать значение:
JavaScript: Скопировать в буфер обмена
Код:
function checkBase64Value(originalValue) {
let checkedValue = originalValue
while(checkedValue.length % 4) checkedValue += '='
try {
decodeValue = atob(checkedValue)
return decodeValue
} catch (except) {
return originalValue
}
}
Соответственно, сначала наращиваем недостающие символы до корректного значения, после применяем декодирование. Как и в прошлой функции, если что-то пошло не так, возвращаем входящее значение.
JavaScript: Скопировать в буфер обмена
Код:
function checkXMLValue(value) {
try{
const parser = new DOMParser()
const doc = parser.parseFromString(value, "application/xml")
const errorNode = doc.querySelector("parsererror")
if (errorNode) {
return false
} else {
return true
}
} catch (e) {
return false
}
}
function checkJSONValue(value) {
try {
const json = JSON.parse(value)
return true
} catch(e) {
return false
}
}
Функции простые и полностью соответствуют своим названиям, поэтому задерживаться на них не буду. Завершает файл функция чека на наличие интересных значений:
JavaScript: Скопировать в буфер обмена
Код:
function checkSensCookies(value) {
const sensetiveCookieNames = ['role', 'token', 'secret', 'id', 'userid', 'username', 'password']
const sensValues = sensetiveCookieNames.map(sens => {
if (value.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {value, sensValues}
}
Как и с комментариями, просто ищем вхождения. Самое время заняться выводом и он очень-очень будет похож на вывод инфы по комментариям. Начну с popup.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="../bootstrap/bootstrap.min.css">
<script src="../bootstrap/bootstrap.min.js"></script>
<title>Document</title>
</head>
<body style="width: 600px;padding: 20px 0; ">
<div class="container">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" data-id="comments"href="#">Comments</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="cookies"href="#">Cookies</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="links"href="#">Links</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="dns"href="#">DNS Info</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="subdomains"href="#">Subdomains</a>
</li>
<li class="nav-item">
<a class="nav-link active" data-id="about"href="#">About</a>
</li>
</ul>
<div class="accordion tab-div" id="comments" style="display: none;">
Comments is empty
</div>
<div class="accordion tab-div" id="cookies" style="display: none;">
Cookies is empty
</div>
<div class="accordion tab-div" id="links" style="display: none;">
<h2>Parsed from page</h2>
<div class="container">
</div>
<h2>Links is empty</h2>
<button id="responseMonitoring">Start background monitoring</button>
<ul id="parsedLinks">
</ul>
</div>
<div class="container tab-div" id="dns" style="display: none;">
<h3>Canonical Name</h3>
<span id="dnsCanonicalName"></span>
<h3>IP</h3>
<ul id="dnsIP">
</ul>
</div>
<div class="container tab-div" id="subdomains" style="display: none;">
<h2>Subdomains</h2>
<button id="loadSubdomains">Parse from CRT.sh</button>
<ul id="subdomains-list-crt"></ul>
<h2>Other platforms:</h2>
<button id="loadDNSInfo">Open tabs</button>
</div>
<div class="container tab-div" id="about">
<h2>About</h2>
<p>
Расширение не является готовым рабочим решением. Данное расширение было разработано исключительно в образовательных целей для статьи на сайте <a href="https://xss.is">XSS.is</a>. Прочитав статью, вы можете спокойно модифицировать расширение под свои задачи. Код поставляется "как есть" и не поддерживается автором.
</p>
<p>
This extension was developed for educational purposes only for the <a href="https://xss.is">XSS.is</a> website. After reading the article, you can modify the extension to suit your needs. The code is provided "as is" and is not maintained by the author.
</p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
Соответственно, в popup.js нужно повесить обработчики кликов по табам.
JavaScript: Скопировать в буфер обмена
Код:
document.querySelectorAll('.nav-link').forEach(navLink => {
navLink.addEventListener('click', event => {
event.preventDefault()
document.querySelectorAll('.nav-link').forEach(elem => elem.classList.remove('active'))
event.currentTarget.classList.add('active')
document.querySelectorAll('.tab-div').forEach(elem => elem.style.display = 'none')
document.querySelector(`#${event.currentTarget.dataset.id}`).style.display = 'block'
})
})
Что касается вывода инфы по кукам, он будет идентичный комментариям. Забегая вперед скажу, что вывод спаршенных ссылок со страницы будет точно таким же. Городить три одинаковых функции, которые будут отличаться только парой параметров…. ну неет. Лучше сделаю отдельную сервисную функцию. Еще один момент, который мне не нравится, это то что URL получаем прямо в popup-скрипте. По хорошему, даже взаимодействие с хранилищем стоило бы вытащить в фоновый скрипт. Но хотя бы получение урла перетащил в фон, тем более там тоже его надо будет поулчать
JavaScript: Скопировать в буфер обмена
Код:
let url
(async function() {
url = await getCurrentURL()
browser.storage.local.get([url.hostname], savedValue => {
let data = savedValue[url.hostname]
if (!data) return
if (data.comments && data.comments.length) printComments(data)
if (data.cookies && data.cookies.length) printCookies(data)
})()
async function getCurrentURL() {
return new URL(await browser.runtime.sendMessage({action: 'getCurrentTabUrl'}))
}
function printComments({comments}) {
printCheckedValues('#comments', comments, 'comment')
}
function printCookies({cookies}) {
printCheckedValues('#cookies', cookies, 'cookie')
}
function printCheckedValues(selector, data, paramName) {
let elemDiv = document.querySelector(selector)
const elemsHTML = data.map((item, ind) => {
console.log(item.sensValues.length, item[paramName])
let body, button = `<span class="accordion-button collapsed text-success">${item.sensValues.length} | ${item[paramName].replaceAll('<', '<')}</span>`
if (item.sensValues.length) {
button = `<button class="accordion-button collapsed text-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse${ind}"
aria-expanded="false" aria-controls="collapse${ind}">
${item.sensValues.length} | ${item[paramName].replaceAll('<', '<')}
</button>`
body = `<div class="accordion-body">${item.sensValues.join(', ')}</div>`
}
return `<div class="accordion-item">
<h2 class="accordion-header">
${button}
</h2>
<div id="collapse${ind}" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
${body}
</div>
</div>`
})
elemDiv.innerHTML = elemsHTML.join('')
}
Стало гораздо лучше. Теперь, если надо будет еще один подобный вывод, достаточно будет добавить новую функцию print с параметрами и все. Но не забываем, что background.js тоже надо бы поменять:
JavaScript: Скопировать в буфер обмена
Код:
let currentURL, currentUrlObj
async function getCurrentURL(activeInfo) {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
currentURL = activeTab[0].url
currentUrlObj = new URL(currentURL)
return currentURL
}
browser.runtime.onInstalled.addListener(() => {
browser.tabs.onActivated.addListener(getCurrentURL)
browser.runtime.onMessage.addListener(onMessageHandler)
})
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCurrentTabUrl') {
if (currentURL) return currentURL
return currentURL
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
}
return false
}
Во-первых, добавились две переменных для хранения урла: одна в виде текста, вторая в виде объекта URL. Обе переменных получает функция getCurrentURL(). Функция эта может быть вызвана в двух случаях: при переключении пользователем вкладки или же если на момент запроса через сообщение “'getCurrentTabUrl'” переменная окажется пустой.
Парсинг ссылок со страницы
Подробно останавливаться на процессе нет смысла, так как он практически идентичен обработке комментариев и кук, поэтому сразу к коду cs_linkSearch.jsJavaScript: Скопировать в буфер обмена
Код:
sensetiveLinkValues = ["admin", "control", "api", "panel", "dev"]
const aHref = Array.from(document.querySelectorAll('a')).map(link => encodeURI(link.href).toString()).filter(Boolean)
const linkHref = Array.from(document.querySelectorAll('link')).map(link => encodeURI(link.href).toString()).filter(Boolean)
const scriptSrc = Array.from(document.querySelectorAll('script')).map(link => encodeURI(link.src).toString()).filter(Boolean)
const allLinks = Array.from(new Set([...aHref, ...linkHref, ...scriptSrc]))
const pageLinks = allLinks.map( link => {
if (link[0] == '#') return false
const sensValues = sensetiveLinkValues.map(sens => {
if (link.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {
link,
sensValues
}
}).filter(Boolean)
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'pageLinks',
object: JSON.stringify(pageLinks)
})
Просто собираю все тэги a, link, script. Чищу от дублей и ищу чувствительные вхождения, типа “админ” и т.п. Можно обратить внимание, что передаю ссылку на сохранение привидя объект к строке. Ссылок может быть невероятное множество, поэтому перестраховываюсь.
Остается только чуть-чуть поправить вывод (popup.js):
JavaScript: Скопировать в буфер обмена
Код:
...
if (data.pageLinks && data.pageLinks.length) printPageLinks(data)
...
function printPageLinks({pageLinks}) {
printCheckedValues('#links', pageLinks, 'link')
}
Обработка запросов/ответов
Чтобы погрузиться глубже, нам потребуется объект webRequest. Этот объект позволяет отслеживать и взаимодействовать запросам и ответами на огромном количестве этапов. На портале разработчико Mozilla, есть схема демонстрирующая все этапы запросов/ответов. Эта схема красной нитью будет проходить практически через любое хакерское расширение, если речь не идет о каком-нибудь банальном справочнике:Альтернативой для анализа запросов может выступать объект API “proxy”. В этой статье я не буду его разбирать, при желании вы можете добавить его в манифест в разрешения, после чего в фоновом скрипте подписаться на событие proxy.onRequest через метод addListener и самостоятельно изучить, какую информацию можно получить.
JavaScript: Скопировать в буфер обмена
Код:
browser.proxy.onRequest.addListener(
listener, // function
filter, // object
extraInfoSpec // optional array of strings
)
В статье, будем анализировать приходящие к нам данные и искать в них ссылки. Хотя, конечно же, это не единственный вариант использования. Можно собирать пересылаемые параметры, искать ендпоинты API, анализировать запросы на предмет SSRF ну и т.д. Вплоть до модификации запросов и поиска XSS, или других уязвимостей, в полностью автоматическом режиме. К этому вернемся позже, а пока подпишемся на событие:
JavaScript: Скопировать в буфер обмена
Код:
function beforeRequestHandler(requestInfo) {
console.log('requestInfo', requestInfo)
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls: ["<all_urls>"],
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
}, ['blocking', 'requestBody']
);
Как понятно из названия, событие происходит перед отправкой запроса. При назначении слушателя, передаю ему функцию прослушивания, объект с фильтрами и массив экстра-параметров. Фильтры, соответственно, позволяют игнорировать ненужный контент, например, картинки. Или ограничивать адреса с которых обрабатывать запросы. Экстра-параметры, помогают настроить поведение.
В функцию слушателя передается параметр “details”, как его называют в MND(в моем случае “requestInfo”), в нем находится объект полностью описывающий запрос.
Что касается объекта фильтров… использую два параметра: urls и types, оба массива. В целом, они сами себя документируют. Обрабатываться будут все урлы, из которых будут выбираться:
- Main_frame - это основной документ, который возвращает сервер (например, html-страница)
- Скрипты
- XML-файлы, которые так же могут содержать ссылки, а мы охотимся именно за ссылками.
В примере передаются параметры requestBody и blocking. “requestBody” указывает FF, что надо к объекту с деталями запроса добавить тело запроса. “blocking” более интересный параметр. Без него мы не сможем получить ответ сервера. Кроме того, с этим параметром мы можем вносить изменения в запрос перед отправкой или вовсе отменить его. Это нужно, например, если ваше расширение анализирует запрос перед отправкой и нашло какие-то опасные данные. Тогда можно настроить поведение, например, перекинув подобный запрос на страницу блокировки или просто отменив запрос.
Из ближайшего примера, могу вспомнить таргет, в котором о любом изменении в профиле пользователя улетало уведомление самому пользователю и админу. Забравшись в админку и, желая найти вторичную SQLi, я с досадой обнаружил дополнительный вопрос на sendNotify.php при каждом сохранении. Да, это странно, ведь отправку уведомления можно прилепить к функции отправки, но кто я такой чтобы спорить с разработчиком того ресурса?
Чтобы минимизировать риск обнаружения при помощи расширения, достаточно было добавить блокирующий экстра-параметр ["blocking"] для домена. Плюс, заменить возвращаемое значение на {cancel: true}; для запросов содержащих “sendNotify.php”. Не забыв добавить в манифест разрешение на “webRequestBlocking”.
JavaScript: Скопировать в буфер обмена
Код:
function beforeRequestHandler(requestInfo) {
if (requestInfo.url.indexOf('sendNotify.php') > -1)
return {cancel: true}
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler, {
urls: ["https://your_target.com/*"],
["blocking"]
});
Другой вариант возвращать, вместо “cancel” , “redirectUrl” в котором передавать нужный URL. Например, выкидывать “ататат-страницу” при попытки получить взрослый контент.
Вернемся к нашему расширению.В манифест добавлю разрешения:
JavaScript: Скопировать в буфер обмена
Код:
"webRequest",
"webRequestBlocking",
"webRequestFilterResponse",
Перепишу beforeRequestHandler(), добавив в него фильтрацию ответов:
JavaScript: Скопировать в буфер обмена
Код:
function beforeRequestHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
searchLinks(str)
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls: ["<all_urls>"],
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
}, ['blocking', 'requestBody']
);
Обратите внимание, что мы получаем не готовые данные, фильтр отдает поток данных и нам нужно обрабатывать событие “ondata”. Причем, обращаться вальяжно с потоком нельзя: обработка события блокирует поток, поэтому в конце обработки должна быть обязательная запись в поток данных, полученных в event.data. В ином случае, запрос прервется и данные никогда не попадут на вывод. К слову, декодировав поток, в него можно спокойно вносить изменения как в обычную строковую переменную, тем самым меняя поведение приложения... Но наша задача сегодня, это поиск ссылок. Для этого создам вспомогательную функцию, которая будет проходить регулярками по данным и сохранять полученные значения. Функцию сделаем асинхронной, чтобы максимально избежать блокирования потоков на время обработки.
Потребуется какое-то или какие-то регулярные выражения. Можно заморочиться и написать свои варианты, но когда есть готовое, нафига оно надо? Предлагаю обратиться к репозиторию JS Link Finder, это достаточно популярное расширение для Burp. У них есть достаточно интересное регулярное выражение.
Python: Скопировать в буфер обмена
Код:
regex_str = """
(?:"|') # Start newline delimiter
(
((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or //
[^"'/]{1,}\. # Match a domainname (any character + dot)
[a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path
|
((?:/|\.\./|\./) # Start with /,../,./
[^"'><,;| *()(%%$^/\\\[\]] # Next character can't be...
[^"'><,;|()]{1,}) # Rest of the characters can't be
|
([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with /
[a-zA-Z0-9_\-/]{1,} # Resource name
\.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action)
(?:[\?|/][^"|']{0,}|)) # ? mark with parameters
|
([a-zA-Z0-9_\-]{1,} # filename
\.(?:php|asp|aspx|jsp|json|
action|html|js|txt|xml) # . + extension
(?:\?[^"|']{0,}|)) # ? mark with parameters
)
(?:"|') # End newline delimiter
"""
Нужно просто привести его в порядок, откинув комментарии и переносы строк. На выходе получил такую функцию:
JavaScript: Скопировать в буфер обмена
Код:
async function searchLinks(text) {
let linkRegEx = /(?:"|')(((?:[a-zA-Z]{1,10}:\/\/|\/\/)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:\/|\.\.\/|\.\/)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}\/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|/][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:\?[^"|']{0,}|)))(?:"|')/ig
console.log(typeof text, text)
text.matchAll(linkRegEx, "ig").forEach(console.log)
}
Тестирование прошло успешно, осталось избавиться от дублей, undefined и записать все в ссылки.
Но есть нюанс… если мониторить все ответы, система будет серьезно нагружена. Кроме того, что каждый ответ, в рамках всего браузера, надо будет прогнать через регулярку, нагрузку на систему даст процесс чистки от дублей и постоянного сохранения. Наиболее оптимальным вариантом вижу включение мониторинга пользователем. Во всплывающем окне уже добавлена кнопка, осталось её оживить и изменить процесс запуска мониторинга. Начну с popup.js:
JavaScript: Скопировать в буфер обмена
Код:
(async function() {
...
initMonitoringButton(await checkRunningMonitoring())
checkHasParsedLinks()
...
})()
...
async function checkHasParsedLinks() {
let response = await browser.runtime.sendMessage({
action: 'hasParsedLinks'
})
if (response.length) appendParsedLinks(response)
}
async function checkRunningMonitoring() {
let response = await browser.runtime.sendMessage({
action: 'checkMonitoring'
})
return response == 'running'
}
async function initMonitoringButton(running = false) {
let btn = document.querySelector('#responseMonitoring')
async function start(event) {
event.preventDefault()
console.log('Start Monitoring')
await browser.runtime.sendMessage({
action: 'startMonitoring'
})
initMonitoringButton(await checkRunningMonitoring())
}
async function stop(event) {
event.preventDefault()
console.log('Stop Monitoring')
await browser.runtime.sendMessage({
action: 'removeMonitoring'
})
initMonitoringButton(await checkRunningMonitoring())
}
if (running) {
btn.innerHTML = `Stop`
btn.removeEventListener('click', start)
btn.addEventListener('click', stop)
} else {
btn.innerHTML = `Start`
btn.removeEventListener('click', stop)
btn.addEventListener('click', start)
}
}
browser.runtime.onMessage.addListener(updateParsedInfoHandler)
async function updateParsedInfoHandler(data) {
if (data.action == 'response_links_update') {
appendParsedLinks(data.newLinks)
}
}
Появилось две функции: одна отвечает за поведение кнопки, вторая проверяет нет ли уже спаршенных ссылок, если есть то добавляет их. Дело в том, что для корректного парсинга ссылок, нужно перехватить запросы, как при начальной загрузке страницы, так и возможные запросы когда казалось бы ничего не происходит и сайт уже загружен. Второй нюанс в том, что скрипт popup.js срабатывает каждый раз при открытии попапа. Каждое открытие это чистый лист. Если не организовать какого-то временного хранения, каждый раз ссылки будут удаляться из интерфейса.
Чтобы решить эту проблему, организовал временное хранение спаршенных ссылок. Когда нажимается кнопка “Старт”, включается перехватчик запросов, список ссылок чиститься и страница обновляется. Перехватчик записывает найденные ссылки в хранилище до тех пор, пока не будет нажата кнопка “Стоп”. При открытии окна, через функцию checkHasParsedLinks() подгружаются уже сохраненные ссылки. Надеюсь не запутал)
Завершает все прослушиватель события в попап-скрипте. Он нужен для добавления новых ссылок, когда окно попапа открыто. Альтернативой может быть слушатель, который реагирует на изменения в хранилище (да, изменения в хранилище можно прослушивать), но в данном случае, это не очень рационально.
Файл background.js так же имеет ряд новых кусков кода:
JavaScript: Скопировать в буфер обмена
Код:
let currentURL, currentUrlObj
let parsedLinks = []
async function getCurrentURL(activeInfo) {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
currentURL = activeTab[0].url
currentUrlObj = new URL(currentURL)
return currentURL
}
async function searchLinks(text) {
let startLength = parsedLinks.length
let linkRegEx = /(?:"|')(((?:[a-zA-Z]{1,10}:\/\/|\/\/)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:\/|\.\.\/|\.\/)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}\/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|/][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:\?[^"|']{0,}|)))(?:"|')/ig
let results = text.match(linkRegEx)
if (!results) return
let allLinks = Array.from(new Set([...results, ...parsedLinks]))
let newLinks = allLinks.filter(el => !parsedLinks.includes(el))
parsedLinks = allLinks
if (parsedLinks.length > startLength)
browser.runtime.sendMessage({
action: 'response_links_update',
newLinks
})
}
function beforeRequestHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
searchLinks(str)
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
}
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
browser.runtime.onInstalled.addListener(() => {
browser.tabs.onActivated.addListener(getCurrentURL)
browser.runtime.onMessage.addListener(onMessageHandler)
})
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCurrentTabUrl') {
if (currentURL) return currentURL
return currentURL
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
} else if (data.action == 'checkMonitoring') {
let hasListener = browser.webRequest.onBeforeRequest.hasListener(beforeRequestHandler)
if (hasListener) return 'running'
return 'not running'
} else if (data.action == 'startMonitoring') {
parsedLinks = []
let urls = [
`${currentUrlObj.protocol}//${currentUrlObj.hostname}/*`,
`${currentUrlObj.protocol}//*.${currentUrlObj.hostname}/*`
]
await browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls,
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
},
['blocking', 'requestBody']
);
browser.tabs.query({active:true, currentWindow: true}).then(tabs => {
let tab = tabs[0]
browser.tabs.reload(tab.id, { bypassCache: true })
})
return 'running'
} else if (data.action == 'removeMonitoring') {
parsedLinks = []
let hasListener = browser.webRequest.onBeforeRequest.removeListener(beforeRequestHandler)
return hasListener
} else if (data.action == 'hasParsedLinks') {
return parsedLinks
}
return false
}
Появился массив спаршенных ссылок. Благодаря ему происходит отсеивание дублей плюс в popup пересылаются новые ссылки для добавления. Посмотрите на searchLinks(). Сначала запоминаем размерность массива с уже добытыми ссылками. Дальше объединяем уже добытые ссылки с новой партией и, при помощи Set, сносим дубли. После вычленяем только новые значения, отбрасывая все уже имеющееся в parsedLinks, чтобы переслать их в popup. Ну и сохраняем значения в хранилище.
Обратите внимание, как происходит добавление прослушивателя события, проверка на его наличие и удаление — везде используется только название функции. Никаких идентификаторов, ничего другого. Ниже мы еще вернемся к этой особенности, назначив персональный парсер для сервиса.
Собираем поддомены
Админ-панели, версия для разработчиков, бэта-версии веб-приложений, phpMyAdmin… чего только не встретишь среди поддоменов. Подробно о способах сбора, можно посмотреть, например, в этой теме. Предлагаю использовать несколько сервисов и начать с crt.sh, так как это не требует какого-то гемороя. Достаточно выполнить GET-запрос https://crt.sh/?q=domain.tld&output=jsonТак как это запрос к стороннему ресурсу, чтобы ничего не мешало запрос выполняем из background-скрипта:
JavaScript: Скопировать в буфер обмена
Код:
async function getSubdomains(hostname, saveDataFunction) {
let domain = hostname.split('.').slice(-2).join('.')
let headers = new Headers({
"Content-Type" : "application/json",
"User-Agent" : navigator.userAgent
});
let response = await fetch(`https://crt.sh/?q=${domain}&output=json`, {
method: 'GET',
headers
})
let responseJson = await response.json()
let subdomains =Array.from(new Set(responseJson.map(item => item.common_name)))
saveDataFunction(hostname, 'subdomains', subdomains)
return subdomains
}
Если вы добрались до этого места, код должен быть предельно понятным. Запросили у сервиса данные в формате JSON, почистили и сохранили. Остановлюсь только на доменном имени. У нас в распоряжении есть имя хоста, что не очень хорошо. CRT.sh отдаст нам гораздо меньше информации, чем при использовании домена. Поэтому использовал самый простой хук: сплитанул хостнейм, удалил все кроме домена и TLD, после чего снова собрал. Вуаля, у нас есть чистый домен…
Для вывода в попап-скрипт добавляем к проверкам еще одну:
JavaScript: Скопировать в буфер обмена
if (data.subdomains && data.subdomains.length) printSubdomains(data.subdomains)
Ну и функция добавления поддоменов списком:
JavaScript: Скопировать в буфер обмена
Код:
function printSubdomains(subdomains) {
let subdomainsList = document.querySelector('#subdomains-list-crt')
subdomains.forEach(subdomain => {
let li = document.createElement('li')
li.innerText = subdomain
subdomainsList.append(li)
})
}
Очень люблю данные с censys, dnsinfo и других серисов. Но есть проблема в виде WAF и каптчи. Решать её можно по разному: пытаться обмануть CloudFlare, подключить сервис гадания каптчи, обращаться напрямую к API, подгружать страницу с каптчей пользователю и т.д. В статье буду использовать самый тупой и топорный вариант: добавлю кнопку, по нажатию которой откроются интересующие вкладки. Плюс добавлю парсер ответов dnsinfo, как пример. На его основе каждый сможет создать свой парсер.
Первым делом, назначим действие кнопке во всплывающем окне:
JavaScript: Скопировать в буфер обмена
Код:
document.querySelector('#loadDNSInfo').addEventListener('click', event => {
event.preventDefault()
browser.runtime.sendMessage({action: 'openDNSTabs', hostname: url.hostname})
})
Соответственно, в фоновом скрипте надо добавить обработчик. Ссылки на интересующие сервисы буду хранить в массиве, используя “$$$” для подстановки доменного имени:
JavaScript: Скопировать в буфер обмена
Код:
let domainCheckSites = [
`https://viewdns.info/reverseip/?host=$$$&t=1`,
`https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=$$$`,
`https://www.shodan.io/search?query=$$$`
]
else if (data.action == 'openDNSTabs') {
let domain = data.hostname.split('.').slice(-2).join('.')
domainCheckSites.forEach(link => {
let linkToOpen = link.replace('$$$', domain)
browser.tabs.create({
active: false,
url: linkToOpen
})
})
}
С DNSInfo поступим хитро. Перед открытием вкладок назначим перехватчик со следующими параметрами:
JavaScript: Скопировать в буфер обмена
Код:
else if (data.action == 'openDNSTabs') {
let domain = data.hostname.split('.').slice(-2).join('.')
parsedDNSInfoHost = data.hostname
browser.webRequest.onBeforeRequest.addListener(parseDNSInfoHandler,
{
urls: [`https://viewdns.info/reverseip/?host=${domain}*`],
types: ['main_frame'],
},
['blocking']
);
domainCheckSites.forEach(link => {
let linkToOpen = link.replace('$$$', domain)
browser.tabs.create({
active: false,
url: linkToOpen
})
})
}
Соответственно, перехватчик будет реагировать только на определенный поиск в dnsinfo, причем завязанный на конкретном домене. Обрабатывать будем “main_frame” по той причине, что вся нужная информация прилетает сразу в ответе, без каких-то дополнительных подгрузок. Имя хоста запомним, чтобы сохранить данные. Именно что запомним, на случай если до получения информации произойдет переключение вкладок.
Функция обработки
JavaScript: Скопировать в буфер обмена
Код:
function parseDNSInfoHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
let ipRegex = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
let tableRegex = /(?<=<br><br>)<table.*<\/table>/mi
let ip = str.match(ipRegex)[0]
let table = str.match(tableRegex)
if (table && table.length) {
saveData(parsedDNSInfoHost, 'dnsinfo', {
ip, table: btoa(table)
})
}
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
browser.webRequest.onBeforeRequest.removeListener(parseDNSInfoHandler)
}
Во многом уже знакомая. После назначения фильтра, сразу же удаляю прослушивание события. А зачем оно теперь нам? Как только сработает фильтр, мы получим нужные данные и забудем обо всем.
Внутри обработчика потока две регулярки. Одна парсит айпи, вторая таблицу. Да, чтобы не заморачиваться, просто заберу таблицу целиком. Чтобы проблем с хранением таблицы не возникло, кодирую её в base64.
Для вывода нужно совсем немногое:
JavaScript: Скопировать в буфер обмена
Код:
function printDNSInfo({dnsinfo}){
document.querySelector('#dnsinfo-ip').innerText = dnsinfo.ip
document.querySelector('#dnsinfodata').innerHTML = atob(dnsinfo.table)
}
Такой вот хитрый “бич”-способ парсить нужные данные с сервисов. Массово не напарсишься, но точечно поработать с объектом очень даже получится.
JS API интерфейс DNS
Среди объектов API доступных расширениям, есть “dns”. При помощи него можно получить немного, но все же, информации.Сначала добавлю в манифест разрешение на “dns”. После зайду в пустующий content.js (ну должны же мы в него хоть что-то положить) и добавлю:
JavaScript: Скопировать в буфер обмена
Код:
browser.runtime.sendMessage({
action: 'getDNSInfo',
hostname: window.location.hostname
})
Соответственно, в фоновый скрипт надо добавить обработчик соответствующего события:
JavaScript: Скопировать в буфер обмена
Код:
else if (data.action == 'getDNSInfo') {
let response = await browser.dns.resolve(data.hostname, [
"allow_name_collisions",
"bypass_cache",
"canonical_name",
])
saveData(data.hostname, 'dns', response)
}
Здесь мы просим браузер дать нам информацию с учетом:
- allow_name_collisions - не фильтровать конфликты. Если прилетает противоречивая информация, браузер пытается её отфильтровать. Попросим его отключить этот фильтр.
- bypass_cache - отключаем кэш поиска DNS
- canonical_name - просим вывести CNAME
JavaScript: Скопировать в буфер обмена
Код:
(async function() {
...
if (data.dns) printDns(data)
...
})()
function printDns({dns}) {
document.querySelector('#dnsCanonicalName').innerText = dns.canonicalName
document.querySelector('#dnsIP').innerHTML = `<li>${dns.addresses.join('</li><li>')}</li>`
}
Заключение
Статья получилась не маленькая. Вероятно даже тяжеловатая, хоть и пытался постепенно вводить и максимально разбирать каждый момент.Еще раз напомню, что получившиеся в итоге расширения хоть и можно использовать в своей деятельности, но стоит рассматривать больше как пример и ввод в концепцию. Есть множество нюансов, которые нужно допиливать под каждый конкретный случай. Например, сохранение данных. Сейчас сохранение происходит по полному хосту. Правильно ли это в вашем случае? Может оптимальнее накапливать данные по домену в целом? Тогда встает вопрос о смене структуры хранения и удобном, для анализа, выводе информации.
Или, например, надо ли сохранять все спаршенные данные в локальное хранилище? Например, какие-то парсинги можно хранить в сессионном хранилище. Или, может быть есть смысл собирать данные в динамике, чтобы отслеживать произошедшие на сайте изменения?
В целом, мы поговорили о нескольких видах парсинга данных: парсинг с использованием возможностей DOM, прямой парсинг кода HTML-страницы и парсинг сопутствующих файлов (скрипты перехватываемые через webRequest). Обсудили варианты хранения данных. Работу с вкладками, проброс сообщений между разными частями расширения и многое-многое другое. Я планирую написать еще 1-2 статьи, чтобы раскрыть вопросы активного сканирования и создания автоматических эксплоитов на базе расширений браузеров, но усвоив информацию одной только этой статьи, вы уже можете все это проделать самостоятельно без каких-либо проблем.
Если остались какие-то вопросы, есть уточнения или пожелания, не стесняйтесь написать об этом в комментариях. Ну и, если тема зашла, дайте знать об этом.