Защита почтовой системы от ботнетов
В силу служебных обязанностей приходится заниматься почтовой системой. И вот однажды, разблокируя учетную запись после очередной заявки, задумался о том, что надо с этим что-то делать. Про это что-то и будет дальнейшее повествование.
В нашем случае система состоит из нескольких серверов: WEB-клиент, прокси-сервер, сервера с ящиками, MTA и т.д. Т.е. получается - что авторизация пользователя производится в одном месте, почта храниться в другом а сообщения принимаются и рассылаются в третьем. И все это надо как-то вместе связать, проанализировать и решить что делать.
Сбор и анализ журналов
Первым делом было решено свести журналы (логи) в одно место для более детального и удобного изучения. Для этого (точнее не только для этого) был развернут кластер elasticsearch (в последствии opensearch). На все узлы участвующие в работе почтовой системы были установлены агенты, в роли которых выступает filebeat. И сырые данные полетели прямиком в эластик без преобразований.
Пример настройки filebeat на одном из ящиков:
# Многострочные сообщения с форматом начала 2019-10-20
- type: log
enabled: true
paths:
- /opt/zimbra/log/mailbox.log
ignore_older: 2h
close_inactive: 30m
#close_timeout: 5m
multiline.pattern: '^\d{4}-\d{2}-\d{2} '
multiline.negate: true
multiline.match: after
processors:
- drop_event.when:
contains.message: "Caught ServiceException trying to compare ZimbraHit"
- drop_event.when:
contains.message: "Error reading unread flag. :invalid request: missing required"
output.elasticsearch:
protocol: "https"
hosts: ["node1:9200", "node2:9200", "node3:9200"]
username: "user"
password: "password"
ssl.verification_mode: none
pipeline: "maillog-with-geoip"
index: maillog_mailbox1-%{+yyyy.MM.dd}
А следует уточнить - что данные эти, т.е. логи программ, они сильно разные: от postfix формат один, от pop3/imap сервера другой и так далее. И вот, по прошествии некоторого времени и многочисленных попыток поиска нужной информации, были настроены правила преобразования данных в структурированный формат, посредством ingest-pipeline и GROK-фильтров. Дело это было не простое и болезненное, так как форматов много. В итоге какие-то данные решено было вообще не трогать, какие-то исключить.
Вот пример не самого сложного шаблона (часть mailboxlog pipeline):
{
"grok" : {
"field" : "message",
"patterns" : [
"""%{LOGDATE:mail.log_datetime} %{WORD:mail.severity}(\s.+)\[%{MAILSERVICE}\] \[(|%{USERDATA1}|%{USERDATA2})\] mailop - (%{MOVINGMESSAGE}|%{ADDINGMESSAGE}|%{DELETEMESSAGE})"""
],
"pattern_definitions" : {
"MAILSERVICE" : "%{DATA:mail.service}(|-%{POSINT:mail.pid})(|:%{DATA:mail.http_method}:%{URI:mail.http_request_url})",
"ACCOUNT" : "(%{WORD}|%{EMAIL})",
"USERDATA1" : "name=%{ACCOUNT:mail.client_name};mid=%{POSINT:mail.mid};oip=%{IP:mail.remote_ip};port=%{POSINT:mail.port};ua=%{DATA:mail.ua};soapId=%{DATA:mail.soap_id};",
"USERDATA2" : "name=%{ACCOUNT:mail.client_name};mid=%{POSINT:mail.mid};(||ip=%{IP:mail.ip};)",
"ADDINGMESSAGE" : "%{DATA:mail.command}: id=%{NONNEGINT:mail.id}, Message-ID=<%{DATA:mail.message_id}>, parentId=%{DATA}, folderId=%{NONNEGINT:mail.folder_id}, folderName=%{DATA:mail.folder_name} acct=%{NUMBER:mail.acct}.",
"MOVINGMESSAGE" : """%{DATA:mail.command} \(id=%{NONNEGINT:mail.id}\) to Folder %{DATA:mail.folder_name} \(id=%{NONNEGINT:mail.folder_id}\)""",
"DELETEMESSAGE" : """%{DATA:mail.command} \(id=%{NONNEGINT:mail.id}\).""",
"LOGDATE" : "20%{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME}",
"EMAIL" : "([?a-zA-Z0-9_.+-=:]+)@%{HOSTNAME}"
},
"ignore_failure" : true
}
},
{
"grok" : {
"field" : "message",
"patterns" : [
"""%{LOGDATE:mail.log_datetime} %{WORD:mail.severity}(\s.+)\[%{MAILSERVICE}\] \[(|%{USERDATA1}|%{USERDATA2})\] nginxlookup - %{DATA:mail.reason}:%{GREEDYDATA:mail.client_name}"""
],
"pattern_definitions" : {
"MAILSERVICE" : "%{DATA:mail.service}-%{POSINT:mail.pid}(|:%{DATA:mail.http_method}:%{URI:mail.http_request_url})",
"ACCOUNT" : "(%{WORD}|%{EMAIL})",
"USERDATA1" : "name=%{ACCOUNT:mail.client_name};mid=%{POSINT:mail.mid};oip=%{IP:mail.remote_ip};port=%{POSINT:mail.port};ua=%{DATA:mail.ua};soapId=%{DATA:mail.soap_id};",
"USERDATA2" : "name=%{ACCOUNT:mail.client_name};mid=%{POSINT:mail.mid};ip=%{IP:mail.ip};",
"LOGDATE" : "20%{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{TIME}",
"EMAIL" : "([?a-zA-Z0-9_.+-=:]+)@%{HOSTNAME}"
},
"ignore_failure" : true
}
}
Для определения географической принадлежности создан еще один "трубопровод" с поддержкой GeoIP, maillog-with-geoip:
{
"processors" : [
{
"pipeline" : {
"name" : "mailboxlog"
}
},
{
"pipeline" : {
"name" : "fail2ban"
}
},
{
"geoip" : {
"field" : "mail.remote_ip",
"ignore_missing" : true
}
},
{
"geoip" : {
"field" : "f2b.remote_ip",
"ignore_missing" : true
}
}
]
}
Таким образом, данные с серверов попадают в ingest pipeline, разный для разных серверов. Потом уже раскидываются по соответствующим полям в индексах.
Т.е. на примере данных от "ящика" (сервер с ящиками пользователей), сперва данные попадают в pipeline maillog-with-geoip, где обогащаются географической информацией, потом mailboxlog (где разбиваются по полям на основе шаблонов), потом уже попадают в нужный индекс.
Защита почты штатными средствами
Штатно, zimbra позволяет устанавливать различные политики по защите: сложность пароля, блокировка учетных записей и так далее. Примем, что количество попыток установлено равное 5, по прошествии которых учетка будет заблокирована.
На сетевом уровне (данное определение не относится с модели OSI) используется fail2ban.
fail2ban - это программное обеспечение может анализировать текстовый файл (лог) в режиме реального времени и на основе правил (шаблонов) может блокировать (как вариант) вредный ip-адрес.
Правила для f2b пишутся в виде регулярных выражений, при попадании записи в файле под данное выражение, адрес будет заблокирован на определенное время или бессрочно, в зависимости от настроек. Пример реального выражения:
failregex = \[ip=<HOST>;\] account - authentication failed for .* \(no such account\)$
\[ip=<HOST>;\] security - cmd=Auth; .* error=authentication failed for .*, invalid password;$
\[ip=<HOST>;\] security - cmd=AdminAuth; .* error=authentication failed for .*, invalid password;$
\[ip=<HOST>;\] security - cmd=Auth; .* error=authentication failed for .*, account lockout$
\[ip=<HOST>;\] account - authentication failed for .* \(account lockout\)$
;oip=<HOST>;.* security - cmd=Auth; .* protocol=soap; error=authentication failed for .* invalid password;$
\[oip=<HOST>;.* SoapEngine - handler exception: authentication failed for .*, account not found$
WARN .*ip=<HOST>;ua=ZimbraWebClient .* security - cmd=AdminAuth; .* error=authentication failed for .*;$
INFO .*ip=<HOST>;ua=zclient.*\] .* authentication failed for \[.*\], (invalid password|account not found|account lockout)+$
NOQUEUE: reject: RCPT from .*\[<HOST>\]: 550 5.1.1 .*: Recipient address rejected:
NOQUEUE: reject: RCPT from .*\[<HOST>\]: 554 5.7.1 .*: Relay access denied;
\(host .+\[<HOST>\] said: 454 4.7.1 .*: Relay access denied \(in reply to RCPT TO command\)\)$
Данные правила должны быть прописаны на всех серверах, принимающих соединения снаружи. И соответственно обновляться так же на всех в случае каких-то изменений.
Так же используется policyd, для ограничения количества сессий, ограничения попыток отправки сообщений и так далее. Данный сервис тоже настраивается на всех серверах.
Про amavis, clamav даже не говорю, это само собой разумеющиеся сервисы.
Ботнеты и что с ними делать
Как извечна борьба брони и патрона (пули) так и извечна борьба средств взлома и средств защиты информационных систем.
В последние годы, алгоритмы взлома почтовых систем вышли на качественно новый уровень (на мой взгляд конечно). И если раньше хватало ограничений в самой зимбре, и для "отстрела" диких спамеров хватало fail2ban, то сейчас этого уже мало.
Атаки стали более продолжительные и распределенные. К примеру одна сеть, насчитывающая несколько сотен (а то и тысяч) узлов, может долбиться месяцами. С одного ip-адреса попытка входа в один ящик может быть как раз в час, в несколько часов так и раз в день а то и неделю. Одновременно с этим в ту же учетную запись пытается авторизоваться другой узел, либо попытается через день. В общем отловить данные попытки стало очень сложно, и f2b уже не справляется.
Да и так как почтовая система распределенная, и правила защиты надо применять на нескольких узлах то сопровождение этого стало не так чтобы лёгким (есть ansible конечно, но не в данном случае).
Пораскинув мозгами, решили таки, попытаться придумать и реализовать систему, которая решит данную проблему - т.е. проблему частых и массовых блокировок учетных записей из-за процесса подборки паролей ботнетами.
Наколеночная система защиты
Проанализировав, в очередной раз, данные в эластике, заметили некоторую закономерность - основные атаки ведутся из-за рубежей нашего государства. Определить это стало возможным благодаря тому, что изначально в elasticsearh было добавлено определение гео-данных (GEOip) для выявления географических масштабов бедствия. Посредством ingest pipiline.
Алгоритм работы задуманной системы следующий:
- Из требуемых индексов в эластике выбираем данные о пользовательских сессиях за определенный период, пусть будет 4 минуты;
- Эти данные обрабатываются и отправляются в БД (PostgreSQL), с разбивкой по каждой учетной записи (учетки сохраняются в отдельную таблицу);
- После (параллельно) запускается процесс анализа данных на предмет обнаружения подозрительной активности (тревоги): -- количество сессий для одного аккаунта превышает заданное значение; -- код GeoIP не соответствует разрешенному (в нашем случае RU); -- попытки входа в одну учётку с разных адресов; -- попытки входа в несколько учетных записей с одного IP;
- Данные о "тревогах" заносятся в отдельную таблицу;
- На основе этих "тревог" другой процесс принимает решение о блокировании IP-адреса на конечных узлах
Каждому виду тревоги присвоен цифровой индекс, для удобства работы в БД:
- "Неразрешенный код страны"
- "Количество сессий больше заданного"
- "Вход в аккаунт более чем с одного IP"
- "Вход более чем в один аккаунт"
При обнаружении авторизационной сессии с адреса попадающего под блокировку, заносится запись в таблицу с данным IP, временем и типом тревоги с номером "1". При обнаружении повторной сессии с данного IP с данным типом тревоги но в другую учетную запись, создается запись для этого адреса но с типом тревоги равным "4". И так далее.
Тревоги с типами, подлежащими блокировке (либо еще какой реакции), в данном случае "4", заносятся в еще одну таблицу alarm_action. В которой отслеживается соотношение блокируемых адресов и серверов где этот адрес нужно заблокировать.
Для других типов тревог реакция может быть другой.
Для непосредственной блокировки адресов написан еще один сервис (агент). Данная штука лезет в БД (таблица alarm_action) и выбирает тревоги с нужным типом по которым еще не отмечено выполнения, т.е. новые записи. Записи выбираются для конкретного узла (на основе FQDN). Потом данный адрес блокируется при помощи системных средств (ipset), и данные о сём действии (временная метка) вносятся в ту же таблицу, в поле для соответствующего узла.
Так как у нас несколько серверов непосредственно торчащих в мир, то и блокировать супостатов требуется на каждом. Для этого, агенты системы защиты (о как звучит!) запущены на всех требуемых серверах.
Языком разработки единогласно мной, был принят GO - захотелось разобраться что это за язык, а вот и повод :)
Описывать код тут смысла не имеет, там все криво-косо но работает. А какой еще может быть код написанный не программистом на не знакомом языке? Код доступен тут, сомневаюсь что он кому-то поможет без реализации всех частей данной системы.
Выводы после нескольких лет эксплуатации
Изначально для блокировки использовали iptables. Но после того, как размер таблицы заблокированных перевалил за 10000 записей, нагрузка на процессоры выросла до 30% от общей только на работу iptables. И механизм блокировки был переделан на использование ipset, с ним нагрузки почти нет.
В идеале, конечно, отказаться от opensearch, но для этого на каждом узле надо ставить агента который будет обрабатывать все логи, и отправлять в БД уже готовые записи об аномалиях. Но пока это задвинули в долгий ящик, так как данные все равно отправляются и будут отправляться впредь в opensearch, к чему плодить сущности? Архитектура всего сервиса, тоже заслуживает быть пересмотренной и возможно измененной, но устраивает и так, во всяком случае на данный момент.
Система работает несколько лет, после ввода в эксплуатацию, по прошествии нескольких месяцев (пока росла база блокированных адресов) жалобы пользователей на блокировки практически прекратились, таким образом поставленная задача была выполнена. Система защиты комплексная, что-то отстреливает зимбра, что-то fail2ban, что-то данное изделие. И в результате пока победа за нами, но мы не обольщаемся на сей счёт.
Цель данной статьи показать, что даже на коленке можно, вполне успешно, противостоять угрозам.