FreeBSD + IPFW: Блокируем SMTP Bruteforce

На предыдущей работе досталось в подчинение пара почтовых серваков с оооочень древними доменами, огромным количеством ящиков и не менее огромным количеством бесконечных брутфорсов. Не знаю что было причиной бесконечных атак на эти серваки, но то что он был в like list у всевозможных ботов — это уж точно! Почта была критичным сервисом, но ближайшие планы включали миграцию на MS Exchange, поэтому позиция руководства была примерно такой «Ну онож работает? Да! Вот и не трогаем пока … нам бы побыстрее мигронуть на эксчендж да вырубить их нафик!» Но один раз случился инцидент который заставил изменить позицию неприкосновения. Несмотря на рендомные пароли, один ящик был сбрутен, и как результат — отправка через наш сервак порядка 40.000 спам писем со ссылками на различные ресурсы интимного направления 🙂
Короче было дано задание «Чтото с этим сделать!»

А сделать можно все что угодно. Например наваять хитрый скриптик для анализа логов на предмет подбора пароля и банить ботов после N-ой неудачной попытки авторизации. Но было в прошлом пара случаев когда свои же сотрудники из удаленных офисов забывали пароли и как мартышки многократно пробовали различные варианты в надежде вспомнить заветную комбинацию. Такчто дополнительно было решено разделить ботов по географическому признаку с помощью GeoIP. Для зоны UA дать не полный бан, а некий сильно ограниченный по скорости pipe — какраз чтобы и брутфорсеру насолить и предусмотреть случай ложного срабатывания. Неплохо бы и Whitelist иметь 🙂 Также в случае блокировки обязательно уведомить админа по email — на всякий случай.
Поможет нам в решении этой задачи один из firewall-ов доступных в FreeBSD — IPFW в паре с dummynet модулем.
Итак, для включения поддержки pipe в ipfw нужна вот такая опция в /boot/loader.conf:

dummynet_load="YES"

Ботов будем добавлять в таблицы, в моем случае их 3, но вы можете уменьшить/увеличить их число просто немного изменив скрипты. Вот какие таблицы будут в моем примере:

table 13 - Для ботов которые абсолютно безнадежные, им будет самый "злой" pipe
table 17 - Для ботов где вероятность ложного срабатывания возможно, но не очень высока
table 23 - Для ботов из Украины, вероятность ложного срабатывания очень высока

Для создания этих таблиц, а также pipe-ов с разными настройками блокировки добавим в начало конфига фаервола вот что:

#!/bin/sh
fwcmd="/sbin/ipfw -q"
${fwcmd} -f flush

# Pipes for Bots #
## Config Pipes ##
${fwcmd} pipe 13 config bw 256bit/s delay 1333ms plr 0.5
${fwcmd} pipe 17 config bw 512bit/s delay 1000ms plr 0.4
${fwcmd} pipe 23 config bw 1024bit/s delay 888ms plr 0.33
## Tables with Bots to Pipes ##
${fwcmd} add pipe 13 ip from table\(13\) to me via em0 limit src-addr 10
${fwcmd} add pipe 17 ip from table\(17\) to me via em0 limit src-addr 17
${fwcmd} add pipe 23 ip from table\(23\) to me via em0 limit src-addr 25

#Setup loopback
${fwcmd} add allow all from any to any via lo0

...........

чтобы при ребуте не терять содержимое таблиц нужно предусмотреть их backup/restore. Для этого я написал скрипт ipfw_table.sh и поместил его в директорию /root/scripts/bruteforce. Вот листинг данного скрипта:

#!/usr/bin/env bash

BKP_DIR="/root/scripts/bruteforce"

[ -d "${BKP_DIR}" ] || mkdir -p "${BKP_DIR}"

#$1 - Action (BackUP/Restore)
#$2 - File

if [[ ${1} == "BackUP" ]]; then
  ipfw -q table ${2} list |cut -d' ' -f1 > "${BKP_DIR}"/"${2}_table.bkp"
elif [[ ${1} == "Restore" ]]; then
  ipfw -q table ${2} flush
  for IP in $(cat ${BKP_DIR}/$2_table.bkp)
   do
        #echo "${IP}"
        ipfw -q table ${2} add $IP
   done
else
   echo -e "Nothing to do ..."
   exit 1
fi

Скрипт блокировки будет его использовать чтобы сохранять содержимое таблиц в бекапные файлики, а для восстановления содержимого таблиц из бекапных файликов использую такие записи в /etc/rc.local:

# Restore 23 Table - UA Bots
/root/scripts/ipfw_table.sh Restore 23
# Restore 17 Table - RU|USA|TW Bots
/root/scripts/ipfw_table.sh Restore 17
# Restore 13 Table - Bad Bots
/root/scripts/ipfw_table.sh Restore 13

Теперь основной скрипт, он анализирует логи postfix на наличие $PATTERN, пропускает через GeoIP и в зависимости от страны — добавляет бота в ту или иную таблицу. Также высылает админу на email $ADMIN сообщение в котором сообщает о блокировке и в тело письма последние строчки из лога для заблокированного IP. В общем листинг скрипта лучше тысячи слов:

#!/usr/bin/env bash

# <= Config => #
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/root/bin"
ADMIN="admin@some.domain.ua"
PATTERN="authentication failed: authentication failure"
MAILLOG="/var/log/maillog"
WORKDIR="/root/scripts/bruteforce"
LOGDIR="/var/log/bruteforce/smtp"
WHITELIST="${WORKDIR}/whitelist.txt"
IPFW_TABLE_DUMP="${WORKDIR}/ipfw_table.sh"
# <= => #

# <= Functions => #
function notifyme() {
        DATE_SUB=`date +%d\ %b\ %Y\ -\ %R`
        SUBJECT=`echo -e "[${DATE_SUB}] [mail.some.domain.ua] -= Host ${bot} Blocked for bruteforce SMTP =-"`
        cat ${LOGFILE} | mail -s "${SUBJECT}" ${ADMIN}
}

## GeoIP over http
#function geoip() {
#        curl http://www.geoiptool.com/en/?IP=$1 2>/dev/null |grep '<div>Country:'|sed -e 's/^.*Country:\ //' -e 's/\<\/div\>\"\;//'
#}

## GeoIP in system
function geoip() {
        geoiplookup $1 |sed -e 's/^.*Edition\:\ //' -e 's/\,.*$//'
}

function block() {
        case $1 in
          UA)
                echo -e "Added $2 as $1 Host to table 23"
                ipfw -q table 23 add $2
                ${IPFW_TABLE_DUMP} BackUP 23
                ;;
          RU)
                echo -e "Added $2 as $1 Host to table 17"
                ipfw -q table 17 add $2
                ${IPFW_TABLE_DUMP} BackUP 17
                ;;
          *)
                echo -e "Added $2 as $1 Host to table 13"
                ipfw -q table 13 add $2
                ${IPFW_TABLE_DUMP} BackUP 13
                ;;
        esac
}
# <= => #

# <= Main => #

BOTS=`grep "${PATTERN}" ${MAILLOG} |cut -d'[' -f3 |sed 's/\]://' |sort |cut -d' ' -f1 |uniq -c |awk '{if($1>23)print $2}'`
DATE=`date +%F`
LOGFILE="${DATE}_${bot}_log"

cd ${LOGDIR}

for bot in ${BOTS}
 do
   # if logfile exist - nothing to do ... #
   LOGFILE="${DATE}_${bot}_log"
   if [ -e "${LOGFILE}" ]; then
     continue
   else
     # Variables #
     ALL_FAILS=`grep "${PATTERN}" ${MAILLOG} |grep ${bot} |wc -l`
     RPT_MSG=`grep "${PATTERN}" ${MAILLOG} |grep ${bot} |head -25`
     COUNTRY=`geoip ${bot}`
     # if $bot in $WHITELIST then only notify admin! #
     for WHITE_IP in `grep -v ^# ${WHITELIST}`
       do
        if [[ ${bot} == ${WHITE_IP} ]]; then
           ACTION="Host ${bot} from Whitelist! Check logfile ${MAILLOG} manually!"
        else
           ACTION=`block ${COUNTRY} ${bot}`
        fi
       done
     # Build Report #
     echo -e "Host IP: ${bot}" > ${LOGFILE}
     echo -e "Country: ${COUNTRY}" >> ${LOGFILE}
     echo -e "All fails: ${ALL_FAILS}" >> ${LOGFILE}
     echo -e "Action: ${ACTION}" >> ${LOGFILE}
     echo -e "Example 25 lines:\n${RPT_MSG}\n......" >> ${LOGFILE}
     notifyme
   fi
 done
# <= => #

Ну какбы все!
По результатам могу сказать что после пары месяцев работы данного скрипта в таблицах (восновном в 13-й) накопилось около 200 ботов. После этого бурный рост таблиц прекратился, и улов стал насчитывать единицы в неделю. Настроенный ранее мониторинг на данных серверах показал что упала нагрузка как на сервис MTA так и на систему вцелом. Конечно это не мега оптимизация — но всеравно приятно. А главное более безопасно!

TODO:
1. Дописать некий garbage collector — авто чистку старых записей из таблиц
2. Дописать нотификейшин на abuse регистратору домена и сетевому администратору бота

Поскольку я уже не админю те серваки, данные хотелки канули в небытиё.
Возможно комуто пригодится.

Enjoy!

  1. Комментов пока нет

  1. Трэкбэков пока нет.

Why ask?