Python SSH брутфорс скрипт: пишем вместо hydra и понимаем, зачем нужны потоки

На CTF-стенде hydra отбила 12 попыток и замолчала — SSH-сервер начал сбрасывать соединения после десятого подключения подряд. Параметры -t 1 -W 5 не спасли: каждый перезапуск упирался в тот же лимит. Написал свой python ssh брутфорс скрипт на paramiko с обработкой SSHException и паузой между попытками — пароль из словаря нашёлся через 14 минут. С тех пор рядом с hydra у меня всегда лежит заготовка на Python. И я точно знаю, что происходит на каждом этапе подбора.
Зачем писать свой python ssh брутфорс скрипт
Hydra, medusa, ncrack — готовые инструменты для подбора паролей, покрывают десятки протоколов. Работают. Вопрос в другом: когда сервер ведёт себя нестандартно (сбрасывает соединения, меняет баннер, вводит задержку после N попыток), готовый инструмент превращается в чёрный ящик. Ты видишь «connection refused» и не понимаешь — это сетевая проблема, SSH-демон или fail2ban. Если ещё не сталкивался: fail2ban — утилита, которая мониторит логи и автоматически банит IP после нескольких неудачных попыток входа. На боевых серверах стоит почти всегда.
Свой скрипт даёт контроль. Ты решаешь, что делать при каждой ошибке: подождать, переключиться на другой порт, записать результат. На пентесте это разница между «инструмент не работает» и «я понимаю почему и знаю, как обойти».
В терминах MITRE ATT&CK (открытая база тактик и техник атак — каждая техника имеет свой T-код) подбор пароля SSH — техника Brute Force (T1110, тактика Credential Access). Конкретнее — подтехника Password Guessing (T1110.001): атакующий перебирает пароли по словарю, рассчитывая попасть в слабую комбинацию. Сам SSH как протокол удалённого доступа фигурирует в T1021.004 (тактика Lateral Movement — горизонтальное перемещение по сети). Разница принципиальная: T1021.004 описывает использование SSH уже после получения валидных учётных данных для перемещения между хостами. Сам процесс подбора — это T1110.
Масштаб проблемы: по данным CrowdStrike Global Threat Report 2025, 75% вторжений в 2024 году использовали действительные учётные данные. IBM X-Force фиксирует рост атак с использованием валидных паролей на 71% год к году и оценивает поток свежих учётных записей в dark web в 6000+ штук ежедневно. Verizon DBIR 2025 связывает 38% утечек данных с кражей учётных данных. Брутфорс — не архаика. Это рабочая техника, которая приносит результат, потому что люди продолжают ставить слабые пароли.
Стенд и инструменты для экспериментов
Прежде чем написать брутфорс на Python — нужна безопасная среда. Запускать подбор паролей на чужих хостах — уголовная статья. Нужен собственный SSH-сервер.
Минимальный стенд: виртуальная машина с Linux (Kali, Ubuntu, Debian). Если SSH-сервер не установлен — sudo apt install openssh-server, затем sudo service ssh start. Проверяешь: ssh localhost. Видишь запрос пароля — всё готово. Для наблюдения за попытками подключения открой второй терминал с tail -f /var/log/auth.log — там видна каждая попытка: IP-адрес, имя пользователя, результат. Это помогает понять, дошёл ли запрос до сервера или потерялся по пути. Работаешь с Hack The Box — у каждой машины свой IP и порт, указывай их в скрипте через аргументы командной строки.
Библиотека paramiko — Python-реализация протокола SSH. Позволяет подключаться к серверам, выполнять команды и передавать файлы через Python-код. Установка: pip3 install "paramiko>=3.4.0". Версия не ниже 3.4 — более ранние содержат уязвимости GHSA-f2j6-wrhh-v25m (обход аутентификации) и GHSA-45×7-px36-x8w8 (Terrapin — Prefix Truncation Attack). Paramiko — зрелый проект (с 2003 года), используется в Ansible, Fabric и десятках инструментов автоматизации.
Под капотом paramiko работает через модуль socket — стандартную библиотеку Python для сетевых соединений. Каждый вызов .connect() создаёт TCP-сокет на порт 22, выполняет рукопожатие, обменивается ключами шифрования и только потом отправляет пароль. Знание этого уровня пригодится при отладке: скрипт зависает на connect() — проблема на уровне TCP (порт закрыт, файрвол). Мгновенно получаешь AuthenticationException — TCP и SSH работают, просто пароль неверный.
Словарь паролей — текстовый файл, где каждая строка содержит один пароль-кандидат. В Kali стандартный словарь лежит в /usr/share/wordlists/rockyou.txt (более 14 миллионов строк). Для тестов на стенде хватит файла из 50–100 строк — создай вручную и добавь реальный пароль тестового пользователя, чтобы скрипт гарантированно его нашёл.
Написать брутфорс на Python: однопоточная версия с paramiko
Логика проста: берём пароль из словаря, пытаемся подключиться по SSH, ловим результат. Подключение прошло — пароль найден, останавливаемся. Ошибка аутентификации — следующий. Другая ошибка — обрабатываем отдельно.
Центральная функция — попытка SSH-подключения. В paramiko для этого используется класс SSHClient() с методом .connect(). Перед подключением задаём политику обработки неизвестных хостов: client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) — аналог -o StrictHostKeyChecking=no в обычном SSH-клиенте.
def ssh_attempt(host, user, password):
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(host, port=22, username=user, password=password, timeout=3)
return True # пароль подошёл
except paramiko.AuthenticationException: return False
except socket.timeout: return None # хост недоступен
except paramiko.SSHException as e:
time.sleep(30); raise # вызывающий код должен перехватить и повторить попытку (итеративно, не рекурсивно)
finally: client.close()
Разбор каждой ветки:
client.connect(...)сtimeout=3— если сервер не ответил за 3 секунды, возникаетsocket.timeout. Для локальной сети хватит 1–2 секунд, для удалённых хостов лучше увеличить до 5.paramiko.AuthenticationException— сервер ответил «неверный пароль». Штатная ситуация при брутфорсе: возвращаемFalse, идём дальше.paramiko.SSHException— сервер ограничил количество подключений. Это критический момент, который пропускают большинство русскоязычных руководств: SSH-сервер обнаруживает множество попыток подряд и временно блокирует соединения. Скрипт ждёт 30 секунд и пробрасывает исключение наверх — вызывающий код повторяет попытку итеративно (циклforс ограничением ретраев, скажем до 5 раз). Не используй рекурсию для ретраев: каждый рекурсивный вызов удерживает фрейм в стеке, и при длинной цепочке повторов получишьRecursionError. Без обработки этого исключения скрипт просто упадёт на середине словаря — именно это и случилось у меня с hydra.finally: client.close()— закрываем соединение после каждой попытки. Убери эту строку — через сотню попыток накопятся открытые сокеты, сервер начнёт отклонять все новые подключения.
Вызов функции в цикле: open(wordlist) для чтения файла, итерация по строкам с .strip() (убирает символ переноса строки), вызов ssh_attempt() для каждого пароля. Вернулось True — выводим найденный пароль и выходим через break. Вернулось None — хост недоступен, сообщаем об ошибке.
Как проверить: запусти скрипт на стенде. Одновременно смотри tail -f /var/log/auth.log — там появятся строки Failed password for testuser from 127.0.0.1. Когда скрипт найдёт пароль — Accepted password. Если видишь только socket.timeout — проверь sudo service ssh status.
Почему один поток — очередь в одно окошко
Запусти однопоточный скрипт на словаре из 500 паролей и засеки время. Каждая попытка — сетевой запрос: TCP-рукопожатие (установка соединения на транспортном уровне), обмен ключами SSH (согласование алгоритмов шифрования), отправка пароля, ожидание ответа. Даже при timeout=3 одна итерация занимает 0.5–2 секунды на локальном стенде.
Представь почту с одним окошком. 500 человек в очереди. Пока предыдущий не закончил — следующий ждёт. 500 паролей по 2 секунды = 17 минут. Для словаря в 5000 строк — почти 3 часа. А словарь паролей брутфорс уровня rockyou.txt с 14 миллионами строк при однопоточном подборе потребует недель.
Процессор при этом простаивает 99% времени — ждёт ответа от сети. Вот ключевой момент: брутфорс SSH — задача с ограничением по вводу-выводу (I/O-bound), а не по вычислениям (CPU-bound). Процессору нечего считать, он просто ждёт сетевой пакет.
Открываем второе, третье, десятое окошко — очередь двигается в разы быстрее. Тут и появляется смысл в потоках.
Многопоточный брутфорс Python: threading и concurrent.futures
Потоки (threads) — способ запустить несколько задач параллельно внутри одного процесса. Вместо одного окошка открываем десять. Каждый поток независимо подключается к SSH-серверу со своим паролем, пока остальные ждут ответа.
В Python два основных способа работать с потоками: модуль threading (низкоуровневый — вручную создаёшь и запускаешь каждый поток) и concurrent.futures.ThreadPoolExecutor (высокоуровневый — управляет пулом потоков за тебя). Для python пентест инструментов ThreadPoolExecutor удобнее: сам распределяет задачи, следит за количеством активных потоков и собирает результаты.
found = threading.Event()
def try_password(host, user, pw):
if found.is_set(): return None
if ssh_attempt(host, user, pw):
found.set(); return pw
return None
with ThreadPoolExecutor(max_workers=10) as pool:
futs = [pool.submit(try_password, host, user, p) for p in passwords]
Перед этим блоком нужно прочитать словарь в список: passwords = [line.strip() for line in open("wordlist.txt")], а также добавить импорты from concurrent.futures import ThreadPoolExecutor, as_completed и import threading.
Разбор ключевых элементов:
threading.Event()— потокобезопасный флаг остановки. Один поток нашёл пароль, вызвалfound.set()— все остальные при следующей проверкеfound.is_set()прекращают работу. Без такого флага потоки продолжат долбить сервер после того, как пароль уже найден. Бессмысленная нагрузка.ThreadPoolExecutor(max_workers=10)— пул из 10 потоков. Одновременно выполняется до 10 SSH-подключений. Нюанс: дефолтный порог MaxStartups в sshd — 10 одновременных неаутентифицированных соединений, поэтому даже 10 потоков могут вызвать отбросы. На стенде с дефолтной конфигурацией начни с 3–5 потоков; не ставь больше 20–30 без увеличения MaxStartups (подробнее ниже).pool.submit(...)— отправляет задачу в очередь пула. Свободный поток подхватывает её автоматически.
Для получения результатов добавь цикл с as_completed(futs) — он возвращает результаты по мере завершения потоков, а не в порядке запуска. Первый найденный пароль — break, пул закрывается.
Ожидаемый результат: словарь из 500 паролей при 1 потоке — 17 минут, при 10 потоках — около 2 минут. Замерить можно через time.time() до и после основного блока. Ускорение не линейное (сервер не масштабируется бесконечно), но разница ощутима. На 200 паролях типичные результаты: 1 поток — 120 секунд, 10 потоков — 15–18 секунд.
Ловушки threading в python пентест-скриптах
GIL и I/O-bound задачи
GIL (Global Interpreter Lock) — механизм в CPython, который позволяет выполнять только один поток Python-кода в каждый момент времени. Если проще: интерпретатор пускает потоки по одному, как турникет. Новички часто слышат «GIL убивает многопоточность» и решают, что потоки в Python бесполезны.
Для I/O-bound задач это неверно. Когда поток ожидает ответа от SSH-сервера (а это 99% времени при сетевом брутфорсе), GIL освобождается — другой поток получает возможность работать. GIL мешает только CPU-bound задачам: хеширование, шифрование, математика. Для сетевых операций потоки работают именно так, как от них ждёшь.
Проверяется просто: замерь время однопоточного и многопоточного скрипта. Ускорение при 10 потоках близко к 8–10x — GIL не мешает. Ускорения нет — задача CPU-bound и нужен multiprocessing вместо threading.
Race condition при записи результатов
Race condition (состояние гонки) — когда два потока одновременно пытаются изменить общий ресурс. Опасное место в нашем скрипте — запись найденного пароля в файл. Два потока одновременно вызовут open("result.txt", "w").write(...) — файл может оказаться пустым или содержать мусор из перемешанных строк.
Решение — threading.Lock(): пока один поток пишет, остальные ждут.
lock = threading.Lock()
def save_result(user, password):
with lock:
with open("found.txt", "a") as f:
f.write(f"{user}:{password}\n")
Конструкция with lock гарантирует: внутри блока одновременно находится только один поток. Остальные встают в очередь. Задержка — микросекунды, незаметные на фоне секундных сетевых операций.
Я столкнулся с этим при записи логов: без Lock файл содержал перемешанные строки — начало одной записи врезалось в середину другой. Отладка заняла час, исправление — две строки кода. Если в скрипте несколько потоков пишут в один файл, Lock обязателен.
Как не забанить себя на стенде
Классическая ошибка: поставить max_workers=50, запустить скрипт и через 30 секунд обнаружить, что SSH-сервер перестал отвечать.
fail2ban мониторит /var/log/auth.log и банит IP после N неудачных попыток. Дефолт: 5 попыток за 10 минут = бан на 10 минут. На тестовом стенде отключи на время экспериментов: sudo systemctl stop fail2ban. После тестирования — обязательно включи обратно.
MaxStartups в /etc/ssh/sshd_config ограничивает количество одновременных неаутентифицированных соединений. Дефолтное значение 10:30:60 означает: после 10 соединений сервер начинает случайно отбрасывать новые с вероятностью 30%, а после 60 — отбрасывает все. Именно это произошло у меня на CTF-стенде с hydra: она создавала пачку соединений разом. Для тестирования можно временно увеличить: MaxStartups 100:30:200, затем sudo systemctl restart sshd.
Rate limiting в скрипте: добавь time.sleep(0.1) между запусками задач в пуле. 0.1 секунды задержки при 10 потоках означает, что сервер получает подключения растянуто на секунду, а не пачкой. На реальном пентесте rate limiting ещё критичнее — агрессивный брутфорс виден в логах мгновенно и привлекает внимание SOC-команды (тех, кто мониторит безопасность в режиме реального времени).
Делай раз, делай два, делай три: собираем скрипт
Пошаговая сборка рабочего многопоточного брутфорс python скрипта на своём стенде. Предпосылки: виртуалка с Linux, Python 3, SSH-сервер запущен (sudo service ssh status показывает «active»).
Шаг 1. Тестовый пользователь. Создай учётку с известным паролем: sudo useradd -m testbrute && sudo passwd testbrute. Введи пароль, допустим summer2024. Проверь: ssh testbrute@localhost — пускает, значит стенд готов. Ожидаемый вывод: приглашение командной строки от имени testbrute.
Шаг 2. Словарь. Создай файл test_wordlist.txt с 20–30 паролями, среди которых есть summer2024. По одному паролю на строку: password, admin, 123456, summer2024, letmein и так далее.
Шаг 3. Paramiko. Установи: pip3 install "paramiko>=3.4.0" (версии ниже 3.4 содержат критические уязвимости аутентификации). Проверь: python3 -c "import paramiko; print(paramiko.__version__)" — версия без ошибок импорта.
Шаг 4. Скрипт. Создай файл ssh_brute.py. В начале — импорты: import paramiko, socket, time, threading и from concurrent.futures import ThreadPoolExecutor, as_completed. Затем функция ssh_attempt из кода выше. Затем обёртка try_password с threading.Event. Затем основной блок: чтение словаря в список, запуск ThreadPoolExecutor, цикл as_completed. Добавь print() для каждой попытки — видеть прогресс полезно и для отладки, и для нервов.
Шаг 5. Запуск. Отключи fail2ban: sudo systemctl stop fail2ban. Запусти: python3 ssh_brute.py. В соседнем терминале: tail -f /var/log/auth.log. Ожидаемый результат: в логах — попытки подключения с localhost, в терминале скрипта — перебор паролей. Дойдёт до summer2024 — сообщение об успехе.
Шаг 6. Уборка. Включи fail2ban: sudo systemctl start fail2ban. Удали тестового пользователя: sudo userdel -r testbrute.
Скрипт зависает — скорее всего, сработал MaxStartups. Уменьши max_workers до 3–5 и перезапусти. Видишь AuthenticationException на каждом пароле включая правильный — проверь, что имя пользователя и пароль совпадают с тем, что задал в шаге 1.
Python ssh брутфорс вместо hydra: когда что лучше
| Критерий | Hydra | Свой скрипт на Python |
|---|---|---|
| Скорость запуска | Одна команда в терминале | Нужно написать и отладить |
| Обработка нестандартных ответов | Ограничена встроенной логикой | Полный контроль через try/except |
| Поддержка протоколов | 50+ из коробки | Только то, что реализовал |
| Кастомная логика задержек | Через параметры (ограниченно) | Любая логика на Python |
| Логирование | Стандартный формат вывода | Любой формат: JSON, CSV, БД |
| Обучающая ценность | Минимальная | Понимание протокола изнутри |
| Интеграция с пайплайном | Ограниченная | Nmap, Shodan API, автоматические действия после входа |
Hydra — правильный выбор для быстрой проверки: «на этом порту SSH со слабым паролем?». Команда hydra -l admin -P wordlist.txt ssh://target запускается за 5 секунд и не требует написания кода. Metasploit тоже содержит модуль для SSH-подбора (auxiliary/scanner/ssh/ssh_login) — удобно, если ты уже внутри msfconsole.
Свой ssh brute force python код — когда готовые инструменты буксуют. Сервер с нестандартным поведением, нужна интеграция с предварительным Nmap-сканом, автоматическое выполнение команд после успешного входа. Я использую оба подхода: hydra для первой разведки, свой скрипт — когда hydra выдаёт [ERROR] без внятного объяснения. В скрипте видно конкретное исключение: NoValidConnectionsError, socket.timeout или ConnectionResetError — это экономит часы отладки.
Ещё аргумент за свой скрипт: Python как инструмент (T1059.006 в MITRE ATT&CK, тактика Execution) — навык, который переносится на любой протокол. Написал SSH-брутфорсер на paramiko — FTP-версию на ftplib соберёшь за 15 минут, HTTP-авторизацию на requests — за 20. Каждый новый скрипт пишется быстрее предыдущего, потому что паттерн один: подключение, обработка ошибок, многопоточность.
Начинающие часто спрашивают: «зачем писать своё, если есть готовое?» Ответ не в том, что своё лучше. После написания своего ты начинаешь понимать, как работает готовое — и видишь, когда оно ошибается. Техника Valid Accounts (T1078) — одна из самых результативных для initial access — работает потому, что атакующие адаптируют подход под конкретную цель, а не запускают дефолтный hydra с дефолтным словарём.
Я видел десятки CTF-writeup’ов, где автор писал «запустил hydra, получил пароль» и не мог объяснить, почему hydra сработала на одной машине и не сработала на другой. Потому что не знал про MaxStartups и его вероятностную модель отброса соединений. Потому что не открывал auth.log и не видел, что сервер не отказывает, а просто молчит.
Свой python ssh брутфорс скрипт — не замена hydra как hydra альтернатива python скриптом. Это способ перестать быть пользователем чужих инструментов и стать человеком, который понимает протокол. После того как напишешь и отладишь ssh_attempt с обработкой SSHException, ты уже не забудешь, что SSH-рукопожатие — многоэтапный процесс с обменом ключами и согласованием алгоритмов, а не мгновенная проверка пароля. Подход «сначала разберись, потом автоматизируй» переносится на любой протокол и любую задачу в ИБ. Если хочешь выстроить эту базу системно — на IB Basics показывают не теорию, а как решать первые задачи на практике.
Эту тему и смежные навыки разбирают на практике в курсе «Основы программирования на Python» Codeby Academy.