Как я написал на Python парсер логов аутентификации, чтобы найти brute-force раньше SIEM

На ночной смене в SOC (центр мониторинга безопасности — место, где аналитики круглосуточно смотрят в дашборды и пьют кофе литрами) я заметил всплеск CPU на SSH-бастионе. Splunk ещё не проиндексировал свежие логи — задержка около 15 минут, очередь форвардеров забита. Открыл /var/log/auth.log по SSH, прогнал grep "Failed password" — 800+ строк с одного IP за полчаса. Через 40 минут SIEM наконец выдал алерт. К тому моменту IP уже заблокирован, таймлайн собран, тикет в L3. После того случая я написал Python-скрипт на 60 строк, который делает то же самое за секунды. Запускаю его каждый раз, когда нужно проверить гипотезу до того, как правила корреляции отработают. SIEM никто не отменял — но ждать его, когда можно не ждать, глупо.
Что лежит в auth.log и почему SIEM опаздывает
auth.log (на Debian/Ubuntu) или secure (на CentOS/RHEL) — файл, куда Linux через rsyslog пишет все события аутентификации: неудачные и успешные входы по SSH, команды sudo, смену паролей. На системах с systemd события также доступны через journalctl -u ssh, но для парсинга текстового файла подход из статьи применим именно к auth.log/secure. Каждая строка — одно событие с меткой времени, именем хоста, процессом и описанием.
Типичная строка неудачного входа: Jan 5 14:22:01 server sshd[12345]: Failed password for root from 192.168.1.100 port 54321 ssh2. Разберём по частям. Jan 5 14:22:01 — дата и время (два пробела перед однозначным днём — нюанс, к нему вернёмся). server — hostname машины. sshd[12345] — процесс SSH-демона с PID. Failed password for root — неудачная попытка ввода пароля для пользователя root. from 192.168.1.100 — IP атакующего. port 54321 ssh2 — порт на стороне клиента и протокол.
Если атакующий пробует несуществующего пользователя, формат чуть другой: Failed password for invalid user admin from 10.0.0.1 port 54322 ssh2. Между for и именем пользователя появляется invalid user. Кроме паролей SSH пишет и неудачные попытки аутентификации по ключу: Failed publickey for root from 192.168.1.100 port 54323 ssh2. Оба типа — следы атаки, оба нужно ловить.
Корпоративные SIEM-системы (Splunk, QRadar, MaxPatrol SIEM) собирают логи через forwarder-агенты, индексируют, нормализуют и только потом прогоняют правила корреляции. В нагруженной инфраструктуре задержка между событием в auth.log и алертом на экране аналитика — от 5 до 30 минут. Для brute-force атаки (перебор паролей, техника T1110 в MITRE ATT&CK — это открытая база тактик и техник атак, где каждому приёму присвоен T-код для единообразной классификации) этого хватит, чтобы атакующий подобрал пароль к слабой учётке и начал двигаться по сети.
Проблема не теоретическая. По данным IBM X-Force Threat Intelligence Index 2025, использование действительных учётных данных в атаках выросло на 71% за год. По их же оценке, в даркнете ежедневно появляется около 6000 свежих учётных записей. Verizon DBIR 2025 фиксирует: 38% утечек данных связаны с кражей credentials. Brute-force — не абстрактная угроза из учебника, а повседневность для любого SSH-сервера с доступом из интернета. OWASP относит такие проблемы к категории A07:2021 — Identification and Authentication Failures (ошибки идентификации и аутентификации), а недостаточный мониторинг логов — к A09:2021 — Security Logging and Monitoring Failures (недостатки логирования и мониторинга).
Парсинг логов аутентификации на Python: regex и подсчёт
Задача скрипта: прочитать auth.log, найти все строки с неудачными попытками входа, вытащить из каждой IP-адрес и имя пользователя, подсчитать количество попыток с каждого адреса и пометить те, где число попыток превышает порог.
Что понадобится: Python 3.8+, доступ к файлу /var/log/auth.log (нужны права root или членство в группе adm). На рабочей станции можно тренироваться на скопированном файле — sudo cp /var/log/auth.log ~/test_auth.log && sudo chown "$(whoami)":"$(whoami)" ~/test_auth.log.
Регулярные выражения для логов SSH
Ключевая задача — написать regex (регулярное выражение — шаблон для поиска и извлечения текста по его структуре), который ловит все четыре варианта строк Failed: пароль для существующего пользователя, пароль для несуществующего, ключ для существующего, ключ для несуществующего.
Пропуск Failed publickey — одна из самых частых ошибок. Пентестеры и боты тестируют и ключи тоже, эти попытки не менее подозрительны.
import re
from collections import defaultdict
FAILED = re.compile(
r"(\w+\s+\d+\s+[\d:]+)\s+\S+\s+sshd\[\d+\]:\s+"
r"Failed (?:password|publickey) for"
r"(?: invalid user)?\s+(\S+)\s+from\s+(\S+)" # NB: пробел между 'for' и username обеспечивается \s+ после опциональной группы
)
Разберём построчно — тут каждый кусок выполняет конкретную работу:
(\w+\s+\d+\s+[\d:]+)— первая группа захвата, метка времени.\w+ест название месяца (Jan, Feb…),\s+— один или несколько пробелов (ловит иJan 5, иJan 15),\d+— день,[\d:]+— часы:минуты:секунды.\S+— пропускает hostname. Для анализа brute-force он не нужен.sshd\[\d+\]:— ловит процесс sshd с любым PID в квадратных скобках.Failed (?:password|publickey) for— матчит оба типа неудачной аутентификации.(?:...)— группировка без захвата, чтобы не засорять результат.(?: invalid user)?— опциональный блок. Пользователь не существует в системе — блок появится; существует — regex его пропустит.(\S+)— вторая группа захвата, имя пользователя.\S+ловит и root, и test-user, и john.doe, и user@domain.from\s+(\S+)— третья группа захвата, IP-адрес.\S+вместо[\d.]+— осознанный выбор, чтобы ловить и IPv6 (вроде2001:db8::1или::1).
Обнаружение подбора пароля SSH по порогу
Теперь проходим по файлу, применяем regex к каждой строке и считаем попытки по IP:
def parse_auth_log(path, threshold=5):
attempts = defaultdict(list)
with open(path, "r", errors="replace") as f:
for line in f:
m = FAILED.search(line)
if m:
ts, user, ip = m.groups()
attempts[ip].append((ts, user))
brute = {ip: e for ip, e in attempts.items()
if len(e) >= threshold}
return attempts, brute
Что тут происходит. Функция принимает путь к файлу и порог (по умолчанию 5 попыток). defaultdict(list) — словарь, где для каждого нового IP автоматически создаётся пустой список. Для каждой строки файла: если regex сработал — извлекаем метку времени, имя пользователя и IP, складываем кортеж в список по IP. В конце фильтруем: IP с числом попыток >= threshold попадают в словарь brute. Функция возвращает оба словаря — полный и отфильтрованный.
Если запустить parse_auth_log("/var/log/auth.log") и вывести brute, увидите словарь вида {"185.220.101.42": [("Jan 5 03:14:01", "root"), ("Jan 5 03:14:03", "admin"), ...]} — ключ это IP, значение — список всех неудачных попыток с временем и пользователем. Словарь пуст — brute-force не обнаружен (или порог слишком высокий).
Последний блок — вывод результатов в терминал:
def report(brute):
for ip, events in sorted(
brute.items(), key=lambda x: -len(x[1])):
users = {u for _, u in events}
print(f"[ALERT] {ip}: {len(events)} failures, "
f"users: {', '.join(sorted(users))}, "
f"first: {events[0][0]}, "
f"last: {events[-1][0]}")
Ожидаемый вывод: [ALERT] 185.220.101.42: 312 failures, users: admin, root, test, first: Jan 5 03:14:01, last: Jan 5 03:47:22. Одна строка — один потенциальный brute-force с количеством попыток, целевыми учётками и временным окном атаки.
Порог 5 попыток — не магическое число. Таков дефолт в конфигурации fail2ban на большинстве дистрибутивов (jail.conf: maxretry = 5). На проде я обычно ставлю 3 для привилегированных учёток (root, admin) и 10 для обычных — так отсекается шум от легитимных ошибок ввода пароля.
Грабли при анализе auth.log: edge cases
Первый скрипт, который я написал, сломался на реальных логах за первые сутки. Вот грабли, на которые наступают при написании парсера логов на Python.
Однозначные дни и двойные пробелы. В auth.log первое число месяца записывается с ведущим пробелом: Jan 5 (два пробела), двузначное — с одним: Jan 15. Если разбираете строку через split() — количество элементов поменяется и индексы поедут. Regex с \s+ решает это автоматически, потому что жадно ест любое количество пробелов. Мелочь, но именно на ней ломается каждый второй самописный парсер.
IPv6-адреса. Боты с IPv6 — реальность. Строка с Failed password for root from 2001:db8::1 port 22 ssh2 или даже from ::1 (localhost) встречается на серверах с dual-stack. Если в regex стоит [\d.]+ для IP — IPv6 не поймается. В нашем шаблоне используется \S+, который ловит оба формата. Обратная сторона: если формат лога нестандартный, может захватить лишнее. Для auth.log работает стабильно.
Пустой файл и отсутствие Failed-строк. Если auth.log свежий, на сервере нет SSH или файл просто пустой — скрипт не должен падать с IndexError. В нашем коде это обработано: если regex ни разу не сработал, attempts остаётся пустым defaultdict, а brute — пустым словарём. Функция report просто ничего не выведет. Но если вы обращаетесь к events[0] в другом месте — проверяйте длину списка.
Необычные имена пользователей. Атакующие пробуют не только root и admin. В реальных логах встречаются test-user, john.doe, user@domain, чисто цифровые 12345 и даже одиночный символ _. Regex с \S+ ловит всё это, но если строите whitelist допустимых учёток — будьте готовы к экзотике.
Часовые пояса. auth.log не содержит информации о таймзоне — только локальное время сервера. При сравнении логов с серверов в разных часовых поясах нужно приводить время к UTC вручную. Для скрипта на одном хосте это не проблема, но для корреляции между серверами — обязательно.
Детектирование атак без SIEM: что делать с результатами
Парсер выдал список IP с превышением порога. Дальше — конкретные шаги, которые я выполняю на каждом дежурстве.
Шаг 1: проверить IP по AbuseIPDB. AbuseIPDB — сервис репутации IP-адресов с бесплатным API (до 1000 запросов в день). Confidence score (оценка от 0 до 100 — чем выше, тем больше независимых жалоб на этот адрес) выше 75 — рекомендация к блокировке. Но перед этим исключите CGNAT-адреса (за одним IP может сидеть тысяча пользователей провайдера) и проверьте, не Tor exit node ли это (список выходных узлов публикуется на torproject.org/exit-addresses). Блокировка Tor — отдельный вопрос корпоративной политики.
Шаг 2: классифицировать атаку по MITRE ATT&CK. Три разновидности brute-force, которые различаются прямо в логах:
| Паттерн в логах | Техника MITRE | Признак |
|---|---|---|
| Один IP, один пользователь, сотни попыток | T1110.001 — Password Guessing | Классический перебор пароля |
| Один IP, десятки пользователей, 1-2 попытки на каждого | T1110.003 — Password Spraying | Один пароль на много учёток, обходит блокировку по числу попыток |
| Много IP, одинаковые пары user + password | T1110.004 — Credential Stuffing | Утёкшие базы данных (HIBP фиксирует миллионы таких записей) |
Скрипт позволяет различить эти паттерны: смотрите на количество уникальных пользователей для каждого IP в словаре attempts. IP перебирает только root — guessing. Пробует admin, test, oracle, postgres по одному разу — spraying. Одни и те же пары приходят с разных IP — credential stuffing, и тут уже нужно смотреть на корреляцию между адресами.
Шаг 3: проверить успешные входы. Критический момент, который новички пропускают. Если с IP из списка brute-force в auth.log есть строка Accepted password или Accepted publickey — это уже не попытка атаки, а возможная компрометация. Техника T1078 (Valid Accounts) — атакующий получил действительные учётные данные и использует их. CrowdStrike фиксирует среднее время lateral movement (горизонтальное перемещение по сети после первого проникновения) после initial access в 62 минуты. Рекорд — 51 секунда. Каждая минута задержки в детектировании — минута, которую атакующий тратит на закрепление.
Шаг 4: заблокировать и эскалировать. Блокировка IP через iptables -A INPUT -s <IP> -j DROP или добавление в fail2ban jail. Параллельно — тикет с таймлайном: первая попытка, последняя попытка, целевые учётки, наличие или отсутствие успешного входа. Есть успешный вход — эскалация в L3 немедленно.
Шаг 5: автоматизировать. Когда скрипт работает стабильно, добавьте его в cron: */5 * * * * python3 /opt/scripts/auth_parser.py --threshold 5 >> /var/log/brute_alerts.log — проверка auth.log каждые 5 минут, алерты пишутся в отдельный файл. Это не замена SIEM, а дополнительный слой мониторинга логов аутентификации, который срабатывает мгновенно без задержки на индексацию.
Половина аналитиков, с которыми я работал, считают скрипты для анализа логов безопасности костылём — мол, станет не нужен, когда «нормально настроят SIEM». На практике SIEM настраивают годами, очередь индексации растёт вместе с инфраструктурой, а правила корреляции покрывают не все сценарии. Скрипт на 60 строк не конкурирует со Splunk — он закрывает окно между событием и алертом. Я видел инциденты, где эти 15 минут разницы определяли, успеет ли атакующий выгрузить хэши из SAM или будет остановлен на этапе подбора. Аналитик, который умеет открыть терминал и за минуту написать парсер под конкретную гипотезу, полезнее того, кто ждёт, пока дашборд обновится. Это вопрос привычки. Чем раньше она формируется, тем лучше. На IB Basics показывают не «что такое CIA-триада», а как делать первые задачи в SOC и blue team — примерно с этих навыков и начинается реальная работа.
Эту тему и смежные навыки разбирают на практике в курсе «Основы программирования на Python» Codeby Academy.