MAATRIX / Блог / Атака распылением паролей: почему fail2ban её не замечает

Атака распылением паролей: почему fail2ban её не замечает

MAATRIX

Вы поставили fail2ban, выставили maxretry 5, bantime 600 — и график брутфорса в мониторинге действительно затих. А через месяц кто-то заходит в почту финансового директора с первой попытки, не оставив ни одного лишнего лога, за который зацепился бы фильтр. Дело не в дыре в fail2ban — дело в том, что он ловит совсем не ту атаку. Password spraying (распыление паролей) не перебирает пароли для одного аккаунта, он перебирает аккаунты для одного пароля, и вся логика rate-limit по IP просто не видит в этом ничего подозрительного.

Как устроено распыление паролей

Классический брутфорс — это когда против одного логина (root, admin, ivanov) прогоняют сотни или тысячи паролей подряд. Такая атака шумная: один и тот же аккаунт получает всплеск неудачных попыток за короткое время, и любой счётчик по логину или по IP срабатывает мгновенно.

Password spraying устроен наоборот. Атакующий берёт список логинов — выгруженный из утечки, собранный по шаблону имя.фамилия@company.ru, взятый из LinkedIn или из дефолтных сервисных аккаунтов (admin, backup, deploy, svc-mail) — и пробует на каждом из них один и тот же пароль, максимум два-три. Обычно это что-то из верхних строк словарей: Password123!, Welcome1, Company2026!, сезонные варианты вида Autumn2026. Дальше пауза (минуты, иногда часы), и следующий пароль — тоже на весь список логинов.

Типичная структура кампании выглядит так:

ПараметрКлассический брутфорсPassword spraying
Что перебираетсяПароли для одного аккаунтаАккаунты для одного пароля
Паролей на аккаунтСотни–тысячи1–3
Аккаунтов на пароль1Сотни–тысячи
СкоростьВысокая, всплескНизкая, растянута на дни/недели
ИсточникЧасто 1 IPПул из десятков-сотен IP (прокси, скомпрометированные хосты)
Видно по порогу неудачных попыток на IP/аккаунтДаНет

Чаще всего под удар попадают сервисы, где логин угадать легко, а окно входа единое для всей организации: Exchange/OWA и другие веб-порталы почты, VPN-шлюзы, RDP-гейтвеи, панели управления (cPanel, Plesk, ISPmanager), SSH с предсказуемыми сервисными аккаунтами, формы входа CMS вроде wp-login.php. Всё, что даёт атакующему один endpoint и большой список потенциальных логинов, — удобная цель для спрея.

Почему fail2ban и типовой rate-limit её не видят

Модель fail2ban простая и по-своему честная: фильтр разбирает лог (например, auth.log для sshd), находит строки вида Failed password, и джейл считает такие срабатывания по IP-адресу источника в окне findtime. Как только счётчик для конкретного IP достигает maxretry, IP банится на bantime.

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = %(sshd_log)s
maxretry = 5
findtime = 600
bantime  = 600

Эта модель отлично ловит классический брутфорс: один IP долбит один логин, счётчик быстро добирается до порога. Но она структурно слепа к спрею по двум причинам одновременно:

  1. Счётчик привязан к IP, а не к аккаунту. fail2ban ничего не знает про то, что за последний час неудачные попытки входа были зафиксированы для 300 разных логинов. Он видит только «с этого IP было N неудач» — а если атака размазана по пулу из сотен адресов (резидентные прокси, ботнет, арендованные VPS), с каждого конкретного адреса приходит одна-две попытки, и ни один счётчик не добирается до maxretry.
  2. Скорость атаки специально держат ниже порога. Даже если атакующий использует ограниченный набор IP, он не станет повторять попытки по одному и тому же логину — а значит, конкретный аккаунт не создаёт всплеска, по которому можно было бы среагировать через порог неудач *на аккаунт* (такого порога у fail2ban вообще нет — он не смотрит на username). Растянутая на дни кампания с паузами в десятки минут между попытками выглядит в логах как редкий фоновый шум, а не как атака.

Дополнительная проблема — для веб-приложений: если форма логина возвращает HTTP 200 с телом «неверный пароль» вместо 401/403, стандартный фильтр nginx/apache в fail2ban может вообще не матчить такие попытки, пока вы не напишете кастомный regex под конкретное приложение. А если аутентификация идёт через общий балансировщик или API-шлюз, все запросы в логе веб-сервера приходят с одного и того же внутреннего IP — банить его нельзя, он же обслуживает всех легитимных пользователей.

Важно понимать: это не баг конфигурации, который можно поправить, срезав maxretry до 2 или увеличив bantime. Проблема в самой модели «считаем неудачи по IP» — она годится против шумного точечного брутфорса и ничего не может сказать о распределённой low-and-slow активности против множества целей. Разбор архитектурных пределов этого подхода и на что его стоит менять — в статье про то, чем fail2ban и CrowdSec отличаются на практике: у CrowdSec bucket можно группировать не только по IP, но это тоже требует осознанной настройки, из коробки поведение похожее.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как обнаружить password spraying: смотрите на аккаунты, а не на IP

Главный сдвиг в детектировании — перестать спрашивать «сколько неудач с этого IP» и начать спрашивать «сколько разных логинов получили неудачу за последние N минут, независимо от источника». Признаки спрея в логах достаточно характерны:

  • Растёт число уникальных логинов с неудачной попыткой за короткое окно, при этом каждый конкретный логин фигурирует всего один-два раза — не десятки, как при брутфорсе.
  • Попытки против несуществующих или явно словарных логинов — если у вас есть логика оповещения на invalid user, всплеск таких записей с разными именами (не повтор одного и того же несуществующего логина) — верный признак перебора по списку.
  • Попытки входа в сервисные/технические аккаунты, которые не должны логиниться интерактивно вообще (backup, www-data, deploy, svc-*) — для них любая попытка входа уже аномалия.
  • Источники сосредоточены в одной подсети или автономной системе (ASN), нетипичной для вашей аудитории — например, десятки IP из диапазонов известных дата-центров вместо обычных провайдеров пользователей.
  • Временная кластеризация: попытки приходят волнами с заметными интервалами (атакующие скрипты часто спят между раундами, чтобы не привлекать внимание).

Без централизованного лог-хранилища эти паттерны всё равно можно вытащить прямо из auth.log. Вот рабочий скрипт, который сводит неудачные SSH-попытки к паре «логин — IP» и считает, сколько разных логинов атаковали с каждого адреса, а также — что важнее для спрея — сколько логинов встретились всего один раз (это и есть сигнатура распыления):

# Свести строки "Failed password" к паре "логин IP"
grep "Failed password" /var/log/auth.log | \
  sed -E 's/.*for (invalid user )?(\S+) from ([0-9.]+).*/\2 \3/' \
  > /tmp/attempts.txt

# Сколько разных логинов пробовали с каждого IP
echo "IP -> число разных логинов:"
sort -u /tmp/attempts.txt | awk '{print $2}' | sort | uniq -c | sort -rn | head -20

# Сколько логинов встретились ровно один раз за весь период — сигнатура спрея
echo "Логины с ровно одной неудачной попыткой:"
awk '{print $1}' /tmp/attempts.txt | sort | uniq -c | awk '$1==1' | wc -l

echo "Логины с многократными попытками (похоже на классический брутфорс):"
awk '{print $1}' /tmp/attempts.txt | sort | uniq -c | awk '$1>5' | sort -rn

Если первая цифра (логины с одной попыткой) на порядок больше второй — вы, скорее всего, смотрите на спрей, а не на брутфорс. Для веб-приложений тот же принцип применяется к access-логу nginx: считаете уникальные значения поля username (если оно логируется) или сравниваете паттерн POST-запросов к /wp-login.php, /owa/auth.owa и подобным эндпоинтам.

Если логи уже стекаются в Graylog или Grafana Loki, задача упрощается — можно построить агрегирующий запрос: сгруппировать неудачные попытки по временному окну (например, 10 минут) и посчитать count(distinct username) независимо от source_ip. Порог для алерта подбирается под ваш трафик: для сервера с десятком легитимных пользователей 15-20 разных неудачных логинов за 10 минут — почти наверняка не совпадение, а для крупного портала с тысячами аккаунтов порог придётся поднимать и калибровать по историческим данным, иначе алерты потонут в фоновом шуме от опечаток. Отдельно стоит следить не только за общим количеством уникальных логинов, но и за долей аккаунтов, которых раньше вообще не существовало в вашей системе, — это отсекает часть легитимного шума.

MFA — барьер, который не зависит от гигиены паролей

Всё, что описано выше, — детектирование постфактум. Но у спрея есть простое структурное слабое место: он ломается, если угаданного пароля недостаточно для входа. Многофакторная аутентификация не мешает атакующему подобрать пароль — она делает подбор бесполезным, потому что второй фактор атакующий не контролирует.

Для SSH это libpam-google-authenticator поверх PubkeyAuthentication, для панелей управления и веб-порталов — TOTP или, лучше, WebAuthn/passkeys, которые к тому же устойчивы к фишингу второго фактора. Мы подробно разбирали настройку в статьях про двухфакторную аутентификацию для панелей управления и про установку 2FA для SSH на VPS — оба сценария закрывают именно тот случай, когда пароль уже скомпрометирован или угадан.

Есть нюанс, о котором стоит знать заранее: там, где второй фактор — push-уведомление в приложении (а не одноразовый код), спрей эволюционирует в MFA fatigue — атакующий, уже угадавший пароль, засыпает пользователя push-запросами в надежде, что тот случайно подтвердит один из них. Это отдельная угроза со своими мерами (number matching, ограничение количества push-запросов в минуту, обязательный TOTP вместо push для привилегированных аккаунтов), но важно понимать: она возникает уже ПОСЛЕ того, как спрей нашёл рабочий пароль, — то есть MFA всё равно останавливает подавляющее большинство попыток на первом рубеже, а push-fatigue — это следующий уровень защиты, а не аргумент против включения MFA вообще.

Меры защиты: считать не количество, а паттерн

MFA закрывает основной риск, но не отменяет смысла детектирования — атаку стоит видеть и останавливать до того, как она доберётся до перебора вторых факторов или найдёт единственный аккаунт без MFA (сервисные учётки, забытые исключения — обычно именно там спрей и выигрывает). Набор мер, которые реально работают против паттерна, а не только против объёма с одного IP:

  • Аккаунт-приманка (honeypot). Заведите логин вроде administrator или admin-legacy, который никогда не используется легитимно и не имеет привилегий. Любая попытка входа в него — стопроцентный сигнал разведки или спрея, реагировать можно немедленно и жёстко: банить исходный IP, поднимать всю подсеть/ASN в список наблюдения.
  • Rate-limit на уровне аккаунта, а не только IP. Это требует централизованной точки — обратного прокси, WAF или auth-прокси перед приложением, — которая ведёт счётчик неудачных попыток по логину независимо от того, с какого адреса они пришли. На уровне одного хоста fail2ban этого физически не может — его модель заточена под IP.
  • Список запрещённых паролей. Заблокируйте очевидные кандидаты для спрея: производные от названия компании, сезонные шаблоны, Password1/123. Это не отменяет мифа про «чем длиннее пароль, тем безопаснее» — миф разобран отдельно в статье про длину пароля и реальную защиту сервера — но именно словарные, предсказуемые пароли и есть топливо для спрея.
  • Удалите или переименуйте предсказуемые сервисные логины. admin, administrator, test, backup без явной необходимости в интерактивном входе — первая цель любого списка для спрея. Чем меньше предсказуемых имён, тем меньше вероятность, что атакующий вообще попадёт в существующий аккаунт.
  • Гео/ASN-аномалии как повод для step-up. Если у вас есть система identity-провайдера с условным доступом, вход из неожиданного региона или из диапазона известного дата-центра — повод потребовать дополнительное подтверждение, даже если пароль верный.
  • Централизуйте логи, если сервисов больше одного. Пока auth-события лежат в auth.log на каждом сервере отдельно, посчитать «сколько разных логинов атаковали организацию за последний час» невозможно в принципе — картина складывается только на уровне свода логов в одно место с единым запросом по времени.

Ни одна из этих мер не заменяет остальные — они закрывают разные звенья: honeypot и rate-limit по аккаунту нужны, чтобы увидеть атаку рано; MFA — чтобы даже угаданный пароль ничего не дал; централизация логов — чтобы вообще была возможность построить правило поверх паттерна, а не поверх счётчика на один IP.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Чем password spraying отличается от credential stuffing?

Credential stuffing использует пары «логин:пароль», украденные в реальной утечке, и рассчитывает на повторное использование пароля пользователем. Password spraying не опирается на утечку конкретной пары — берётся произвольный список логинов и один популярный пароль, в расчёте на то, что хотя бы у кого-то он совпадёт. Меры защиты частично пересекаются (MFA останавливает обе атаки), но детектируются они по разным сигнатурам в логах.

Поможет ли просто уменьшить maxretry в fail2ban до 1-2?

Незначительно. Это чуть снизит порог для отдельного IP, но не решает главную проблему — счётчик всё равно ведётся по IP, а не по аккаунту, и распределённая атака с пула адресов всё равно останется невидимой. Более низкий maxretry скорее увеличит число ложных банов реальных пользователей, которые просто ошиблись паролем один раз.

Может ли CrowdSec поймать спрей лучше, чем fail2ban?

У CrowdSec более гибкий движок сценариев — bucket можно группировать не только по IP-адресу, но и по другим полям. Но из коробки стандартный SSH-сценарий устроен по той же логике «считаем неудачи с одного источника», так что реальный выигрыш появляется только после того, как вы осознанно настроите сценарий под группировку по логину, а не по IP.

Как отличить спрей от опечаток реальных пользователей?

Ключевой отличающий признак — распределение. Опечатки дают редкие одиночные неудачи по существующим, реальным логинам, разбросанные во времени без явной кластеризации. Спрей даёт всплеск неудач по множеству логинов (включая несуществующие) за короткое окно, часто с одинаковым или очень похожим паролем на всех целях — это видно, если логировать хотя бы хэш введённого пароля для сравнения (не сам пароль).

Нужен ли отдельный SIEM, или хватит скрипта поверх auth.log?

Для одного-двух серверов скрипт на awk/sed, запускаемый по крону с алертом в Telegram или на почту, закрывает задачу полностью. SIEM или связка Graylog/Loki становится нужна, когда серверов и точек входа много и вопрос «сколько разных логинов атаковали организацию за последний час» требует свода данных из нескольких источников в одном запросе.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →