Активная защита FreeBSD на основе логов, sh и cron

Приветствую всех администраторов FreeBSD!

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

Задача возникла сама собой, при просмотре логов:
/var/log/exim/rejectlog
/var/log/auth.log
/var/log/apache22/httpd-error.log


в них постоянно попадала информация о неудачных попытках подобрать пароль к exim, к серверу и к веб-почте соответственно. Рано или поздно злоумышленники могут пароль подобрать, поэтому их нужно как-то остановить, например, добавив их IP-адрес в правила ipfw. А на веб-сервере еще и пытались найти несуществующие каталоги и файлы, относящиеся к администрированию, типа phpmyadmin, очевидно, чтобы проверить их на существующие уязвимости.

Google и статья lissyara о некоторых аспектах безопасности подсказали направление.

Решение в итоге выглядит просто: cron раз в минуту запускает sh-скрипт, который сканирует указанные логи и добавляет незадачливых нарушителей в правила ipfw в виде:
deny ip from 123.123.123.123 to me

Cкрипты от lissyara мне удалось доработать напильником:
  • в правила не добавляются пользователи доверенных подсетей
  • в правила не добавляются одни и те же IP
  • скрипт может обрабатывать один и тот-же лог файл без его копирования (основываясь на дате событий)
  • скрипт пишет свои логи
  • скрипт исправляет некоторые ошибки в логах


Для каждого лог-файла я сделал соответственно свой sh-скрипт:
mailsec.sh
nixsec.sh
websec.sh

т.к. у них немного разный подход к решению задачи, но общая концепция аналогична.

Скрипты стабильно отрабатывают свою электроэнергию, делая практически невозможным брутфорс и защищая от излишне любопытных. По накопленной статистике, за минуту у самых быстрых получалось сделать не более 100 попыток подбора. До ввода в эксплуатацию скриптов, попытки были круглосуточными и бесчисленными.

Думаю, что подобные скрипты должны стоять на каждом сервере, т.к. защиты никогда не бывает много.
Надеюсь, мои наработки помогут Вам в этом. Привожу код. Готов ответить не вопросы.



mailsec.sh

#!/bin/sh

# Настройки
trusted_net='192.168.'
debug_log="/var/log/mailsec.log"
cur_log_file="rejectlog"
cur_log_dir="/var/log/exim/"

#echo "Hi!" > /dev/ttyv0
# если два часа четыре минуты удаляем предыдущие правила
if [ `date +%H` -eq 02 ] && [ `date +%M` -eq 04 ]
    then {
    # пишем статистику использования накопленных правил
    echo "-= RULES STATS =-" >> ${debug_log}
    /sbin/ipfw show 3 >> ${debug_log}
    /sbin/ipfw delete 3 >/dev/null 2>&1
    echo "-= RESET RULES =-" >> ${debug_log}
    date >> ${debug_log}
    }
fi

# блокируем тех кто пробует логиниться на несуществующих пользователей
cat ${cur_log_dir}${cur_log_file} | grep "auth_login authenticator failed for" | sed 's/ (/--/' | sed 's/failed for--/failed for (/' | sort | awk '{print $1,$8}' | sed 's/\[/ /' | sed 's/\]:/  /' | uniq -c | sort |
{
 while read count_IP
 do
    count_deny=`echo ${count_IP} | awk '{print $1}'`
    logday=`echo ${count_IP} | awk '{print $2}' | cut -d - -f 3`
    logmonth=`echo ${count_IP} | awk '{print $2}' | cut -d - -f 2`
    logyear=`echo ${count_IP} | awk '{print $2}' | cut -d - -f 1`
    IP=`echo ${count_IP} | awk '{print $3}'`
    day=`date +%d`
    mon=`date +%m`
    yea=`date +%Y`
    ippresent=`/sbin/ipfw show | grep ${IP} >/dev/null 2>&1`
    if [ ${day} -eq ${logday} ] && [ ${mon} -eq ${logmonth} ] && [ ${yea} -eq ${logyear} ] && [ ! ${ippresent} ]
    then {
        if [ ${count_deny} -ge 6 ] && echo $IP | grep ${trusted_net} >/dev/null 2>&1
        then echo "MAILSEC: (bruteforce) IP address  = ${IP}        attempts count = ${count_deny}. Правило не применяется (доверенный IP)" >> ${debug_log}
        else {
           if [ ${count_deny} -ge 6 ]
               then {
               echo "MAILSEC: (bruteforce) IP address  = ${IP}        attempts count = ${count_deny}" >> ${debug_log}
               /sbin/ipfw add 3 deny ip from ${IP} to me >/dev/null 2>&1
               }
           fi
        }
        fi
    }
    fi
 done
}




nixsec.sh

#!/bin/sh

#echo "Hi!" > /dev/ttyv0
# в два часа ночи удаляем предыдущие правила
if [ `date +%H` -eq 02 ] && [ `date +%M` -eq 00 ]
then {
     /sbin/ipfw delete 1 >/dev/null 2>&1
     echo "-= RESET RULES =-" >> /var/log/nixsec.log
     date >> /var/log/nixsec.log
}
fi


#
day=`date +%d`
month=`date +%m`
year=`date +%Y`
log_dir="/var/old_log/${year}/${month}"
# создаём папку для логов
mkdir -p ${log_dir}
log_file="${log_dir}/${day}_auth.log"

# переносим логи
cat /var/log/auth.log > /tmp/auth.log
cat /dev/null > /var/log/auth.log
cat /tmp/auth.log >> ${log_file}



# Вначале отлавливаем IP с которых пытаются залогинится
# под несуществующими пользователями
cat /tmp/auth.log | grep "Invalid user" | awk '{print $10}' | sort | uniq -c  | sort |
{
     while read count_IP
     do
     count_deny=`echo ${count_IP} | awk '{print $1}'`
     IP=`echo ${count_IP} | awk '{print $2}'`
     if [ ${count_deny} -ge 2 ]
     then {
          echo "NIXSEC: (invalid user) IP address  = ${IP}        deny count = ${count_deny}" >> /var/log/nixsec.log
          /sbin/ipfw add 1 deny ip from ${IP} to me #>/dev/null 2>&1
     }
fi
done
}



# Вылавливаем пытающихся ломать сервант
cat /tmp/auth.log | grep "Did not receive identification string" | awk '{print $12}' | sort | uniq -c  | sort |
{
 while read count_IP
do
count_deny=`echo ${count_IP} | awk '{print $1}'`
IP=`echo ${count_IP} | awk '{print $2}'`
if [ ${count_deny} -ge 2 ]
then {
echo "NIXSEC: (not ident) IP address  = ${IP}        deny count = ${count_deny}" >> /var/log/nixsec.log
/sbin/ipfw add 1 deny ip from ${IP} to me >/dev/null 2>&1
}
fi
done
}


# Вылавливаем пытающихся ломать сервант c несуществующих адресов
cat /tmp/auth.log | grep "but this does not map back to the address - POSSIBLE BREAK-IN ATTEMPT" | awk '{print $7}' | sort | uniq -c | sort |
{
while read count_IP
do
count_deny=`echo ${count_IP} | awk '{print $1}'`
IP=`echo ${count_IP} | awk '{print $2}'`
if [ ${count_deny} -ge 2 ]
then {
echo "NIXSEC: (break) IP address  = ${IP}        deny count = ${count_deny}" >> /var/log/nixsec.log
/sbin/ipfw add 1 deny ip from ${IP} to me >/dev/null 2>&1
}
fi
done
}


# Вылавливаем тех, кто лезет в root
cat /tmp/auth.log | grep "User root from" | awk '{print $9}' | sort | uniq -c  | sort |
{
while read count_IP
do
count_deny=`echo ${count_IP} | awk '{print $1}'`
IP=`echo ${count_IP} | awk '{print $2}'`
if [ ${count_deny} -ge 2 ]
then {
echo "NIXSEC: (root attempt) IP address  = ${IP}        deny count = ${count_deny}" >> /var/log/nixsec.log
/sbin/ipfw add 1 deny ip from ${IP} to me >/dev/null 2>&1
}
fi
done
}




websec.sh

#!/bin/sh

# Настройки
trusted_nets='192.168. 127.0. 10.0.'
debug_log="/var/log/websec.log"
cur_log_file="httpd-error.log"
cur_log_dir="/var/log/apache22"
old_log_dir="/var/log/apache22/old"


#echo "Hi!" > /dev/ttyv0
# в два часа две минуты удаляем предыдущие правила
if [ `date +%H` -eq 02 ] && [ `date +%M` -eq 02 ]
    then {
    echo "-= RULES STATS =-" >> ${debug_log}
    /sbin/ipfw show 2 >> ${debug_log}
    /sbin/ipfw delete 2 >/dev/null 2>&1
    echo "-= RESET RULES =-" >> ${debug_log}
    date >> ${debug_log}
    }
fi

# создаем папку для архивных логов
day=`date +%d`
month=`date +%m`
year=`date +%Y`
log_dir="${old_log_dir}/${year}/${month}"
# создаЈм папку для логов
mkdir -p ${log_dir}
log_file="${log_dir}/${day}_${cur_log_file}"

# копируем логи
cat ${cur_log_dir}/${cur_log_file} > /tmp/${cur_log_file}
# очищаем исходный логфайл
cat /dev/null > ${cur_log_dir}/${cur_log_file}
# обновляем архивный логфайл
cat /tmp/${cur_log_file} >> ${log_file}



# блокируем тех кто ищет несуществующие файлы
cat /tmp/${cur_log_file} | grep "File does not exist" | sed 'y/]/ /' | awk '{print $8}' | sort | uniq -c | sort |
{
 while read count_IP
 do
    count_deny=`echo ${count_IP} | awk '{print $1}'`
    IP=`echo ${count_IP} | awk '{print $2}'`

    if [ ${count_deny} -ge 5 ] && echo $IP | grep 192.168. >/dev/null 2>&1
    then echo "WEBSEC: (files) IP address  = ${IP}        attempts count = ${count_deny}. Правило не применяется (доверенный IP)" >> ${debug_log}
    else {

        if [ ${count_deny} -ge 5 ]
            then {
            echo "WEBSEC: (files) IP address  = ${IP}        attempts count = ${count_deny}" >> ${debug_log}
            /sbin/ipfw add 2 deny ip from ${IP} to me >/dev/null 2>&1
            }
        fi
    }
    fi
 done
}



# Вылавливаем спецов по составлению неправильных URI
cat /tmp/${cur_log_file} | grep "Invalid URI in request" | sed 'y/]/ /' | awk '{print $8}' | sort | uniq -c | sort |
{
 while read count_IP
 do
    count_deny=`echo ${count_IP} | awk '{print $1}'`
    IP=`echo ${count_IP} | awk '{print $2}'`
    if [ ${count_deny} -ge 2 ]
        then {
        echo "WEBSEC: (URI) IP address  = ${IP}        attempts count = ${count_deny}" >> ${debug_log}
        /sbin/ipfw add 2 deny ip from ${IP} to me >/dev/null 2>&1
        }
    fi
 done
}



# Вылавливаем спецов по нарушению RFC
cat /tmp/${cur_log_file} | grep "request without hostname" | sed 'y/]/ /' | awk '{print $8}' | sort | uniq -c | sort |
{
 while read count_IP
 do
    count_deny=`echo ${count_IP} | awk '{print $1}'`
    IP=`echo ${count_IP} | awk '{print $2}'`
    if [ ${count_deny} -ge 2 ]
        then {
        echo "WEBSEC: (RFC) IP address  = ${IP}        attempts count = ${count_deny}" >> ${debug_log}
        /sbin/ipfw add 2 deny ip from ${IP} to me >/dev/null 2>&1
        }
    fi
 done
}


Возможно специалисты по sh-скриптам смогут помочь оптимизировать код.
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 54

    +26
    Для этого есть fail2ban
      +2
      А можно комментарии практикующих пользователей услышать по этой программе?
        +1
        Я использую на всех серверах-в зависимости от попадания в логах каких-от записей, Ip блочатся на фаерволле. Использую для антибрутфорса ssh и asterisk. 5 неудачных попыток и в бан. Правда если попыток много за короткое время, то пока fail2ban родит и заблочит, бот успеет еще раз 30-40 поломится. Сообщения о бане присылает на почту вместе с whois информацией о Ip, с которого шла атака.
          0
          Изучение принципов работы fail2ban и настройка заняли у меня полдня, а потом нужные правила писал за 10 минут каждое — регексы там очень удобны и упрощены до предела всякими словами-патернами.
            0
            я пробовал пользоваться. На слабом ВПСе забирал слишком много ресурсов, но в принципе работал хорошо. Сейчас ограничился защитой на уровне iptables — разрешено 3 подключения к порту в 120 секунд. Для меня этого вполне достаточно.
            0
            Рекомендую SshGuard www.sshguard.net
            Смотрит в прямом эфире, кто пытается авторизоваться и блочит в зависимости от настроек
            Я, например, блочу в случае 3 ошибок в течение 1 секунды
              +1
              А если раз в секунду, но с 10к разных адресов?
                0
                Логи говорят, что такого не происходит )
                Долбятся по нескольку раз с 1 ip
            +1
            А ipfw потом шевелиться будет? Записей-то можно получить огого после такого.
              +4
              ipfw table 1 add xxx
              использовать надо. Тогда точно тормозов не будет.
                +4
                Праивльнее, конечно же, заносить адреса в таблицы ipfw.
                  0
                  Про таблицы я не подумал, т.к. не так хорошо знаю ipfw, но на моем сервере он шевелился даже при 3-4 тысячах правил без затруднений. Хотя и нагрузки на нем не те, чтобы я мог об этом Вам что-то сказать.
                  • UFO just landed and posted this here
                  +4
                  Посмотрите на /usr/ports/security/bruteblock в портах, похоже что он делает практически то же самое.
                  Кстати, для добавления deny правил в ipfw грамотнее использовать таблицы, что bruteblock и делает.
                  Пользуюсь им сам уже много лет, никаких нареканий нет.
                    0
                    А что лучше, он или fail2ban? Я вот как раз решаю что использовать.
                      +1
                      Не знаю, не пользовался fail2ban.
                      Попался когда-то под руку bruteblock, он настраивается легко и гибко. Неприхотлив, из зависимостей кажется перл только. Если сильно долбят снаружи то нагрузка пойдёт на ipfw в основном, так что типичную нагрузку эта связка должна держать. Вроде ничего больше и не нужно…
                      Альтернативы даже не смотрел, но за bruteblock могу сказать по своему реальному опыту — работает железно и никогда не подводил.
                        +1
                        Вот блин, сейчас посмотрел, он оказывается на чистом С написан, так что там даже и перл не нужен :) Чёрт побери, мне это нравится…
                    +3
                    trusted_net='192.168.'

                    А я бы и локальной сетке не доверял, если уж полноценно подходить к защите корпоративной информации. Ладно там раз-другой неудачно кто-то пароль ввел, но если начнется брут из локалки, сознательный или кто-нибудь чего-нибудь подцепит — ваш скрипт ему мешать не будет, как я понял из беглого его просмотра.
                      0
                      Я ей тоже не доверяю, но делаю допущение, т.к. все компы защищены корпоративным DrWeb. Кроме того, не хочется лишний раз беспокоить директора, который вдруг не увидит свою почту.
                      Это очень слабое место, но при использовании sh, мне кажется, иначе не поступить. Разве только начинать писать конкурента bruteblock.

                      Спасибо за подсказку, я изучу bruteblock.

                      Пока писал, пришло в голову: можно для доверенных подсетей сделать другой порог срабатывания. Т.е. дать им немного больше попыток ошибиться, чем внешним пользователям.
                      0
                      Коллеги, а вот я столкнулся с такой ситуацией, когда в логи попадает имя хоста с пробелом, это полностью парализует скрипт, т.к. пробел — есть разделитесь для {print $n}. Кто-нибудь в курсе, как такое поведение злоумышленника отработано в предложенных программах?
                      • bruteblock
                      • fail2ban
                      • SshGuard


                      Пример:
                      2011-10-12 16:06:42 auth_login authenticator failed for host104-147-static.34-88-b.business.telecomitalia.it (windows) [88.34.147.104]: 535 Incorrect authentication data
                        +1
                        The Full DNS name for internal domain can contain letters (a-z, A-Z), numbers (0-9), and hyphens, but not spaces.

                        # dig -x 88.34.147.104 +short
                        host104-147-static.34-88-b.business.telecomitalia.it.
                        # host 88.34.147.104
                        104.147.34.88.in-addr.arpa domain name pointer host104-147-static.34-88-b.business.telecomitalia.it.

                        скорей всего Exim так пишет лог
                        0
                        Я делал похожие вещи для своего сервера. Но потом сервер начал тупить. Решилось командой EXIT в конце скрипта.

                        и еще, это не самый производительный вариант парсить логи.
                        сегодня ваш лог весить 40Мб а завтра 1Гб.

                        Особенно использование «cat /tmp/auth.log | grep „Did not receive identification string“ |»

                        тут достаточно использовать один grep.

                        Запустите два разных скрипта с cat и без cat так time ./script.sh. Очень заметно на больших логах.
                          0
                          Вопрос в рациональности анализа логов остается, можно сэкономить кучу нервов и сил если:
                          1)ССШ зафаерволить (открыть только с определенных адресов)
                          2) аналогично произвести с админкой веба ограничив доступ с определенных адресов(если такая имеется) .htaccess

                          В любом случае скрипт это хорошо, но опять же это будет хорошо работать при низкой интенсивности брутфорса.
                            0
                            У меня всегда недопонимание, когда я слышу о таком подходе. А если с телефона надо зайти? А если админ в отпуске (в другом городе, стране)?
                              0
                              VPN для этого и придумали. В конце концов в отпуске должен быть заместитель, который может решить текущие задачи ;)
                                0
                                А если VPN тоже будут брутфорсить? )
                                  0
                                  это менее вероятно. Например OpenVPN попробуйте)
                                    0
                                    По своему опыту: увы, vpn не со всеми домашними провайдерами работает. Это касается как и OpenVPN, так и pptp/l2tp.
                                    Я так недавно не смог подключиться к офисному pptp из отеля в отпуске.
                                      +1
                                      pptp требует умного ната, т.к используются протокол GRE, в большинстве сошо-роутерах может одновременно только 1 сессия pptp существовать.
                                      С опенвпн все просто — ставишь его на порт 443(если ваш провайдер не зверь и он не режет/проксируется https то все будет ок.
                                        0
                                        Спасибо. Годный совет, кстати. Надо попробовать.
                                0
                                  0
                                  Вариант, но это опять же костыли. Еще есть такой момент что если для кнока использовать UDP порты то количество попыток «достучатся» заметно увеличивается)
                              0
                              #!/bin/sh

                              у вас во фряхе баш дефолтным шеллом?

                              Ну и фраза «подобрать пароль к exim», извиние, порвала в клочья.
                                0
                                #!/bin/sh — вполне нормально для FreeBSD. Никакой это не bash.
                                  0
                                  ooops, не доглядел. Действительно, скрипт сишелловый. А сначала показалось почему-то, что на баше :)
                                    +1
                                    Я использую эту строку, ибо mc в таком случае подсвечивает синтаксис.
                                0
                                ИМХО, описанное решение — это слегка overkill для большинства систем. Наверняка среди россыпи фрёвых фаерволов есть такой, который позволяет ограничивать количество подключений к порту каким-то стандартным правилом, и этого будет вполне достаточно.
                                  0
                                  да, на PF можно настроить ограничения по кол-ву соединений в минуту(если неошибаюсь) + также можно заносить провинившихся в отдельную таблицу, но опять же придется использовать крон чтобы список применялся, что не всегда уместно делать.
                                    0
                                    >но опять же придется использовать крон чтобы список применялся,
                                    Не надо там такого извращения, в таблице и на правило. Вот чтобы очищать таблицу от устаревших правил да, надо дополнительно ставить либо pfguard, либо руками.
                                  0
                                  Здесь еще не упоминалась замечательная програмка sshit, которая очень легко конфигурится, и которую использую на домашнем сервере уже не первый год.
                                    0
                                    pipe pipe pipe… Блин меня убивает такой код… Научись наконец awk и правильным использованием пайпов :)
                                      0
                                      А Вы не могли бы привести свой пример замены awk для конкретного случая, например для websec.sh в блоке «Вылавливаем спецов по нарушению RFC»?
                                        0
                                        Этого должно хватить :)

                                        fgrep "request without hostname" /tmp/${cur_log_file} |awk 'sub("]", " ");{print $8}' | sort | uniq -c
                                      0
                                      Меня смущает cat всего файла, да еще и каждую минуту!

                                      Я бы использовал tail -f logfile | script.sh &
                                        0
                                        Поэтому разрастающиеся логи ротируются каждую минуту в отдельную папочку с текущей датой. Это в самом начале скрипта:
                                        # копируем логи
                                        cat ${cur_log_dir}/${cur_log_file} > /tmp/${cur_log_file}
                                        # очищаем исходный логфайл
                                        cat /dev/null > ${cur_log_dir}/${cur_log_file}
                                        # обновляем архивный логфайл
                                        cat /tmp/${cur_log_file} >> ${log_file}


                                        tail же выдаст только последнюю часть файла, а не все события за период. В этом случае смысл скрипта пропадает.
                                          0
                                          А вообще тут я бы рекомендовал использовать fifo и работать c данными уже из него.
                                            0
                                            Ок, попробую ответить развернуто.

                                            У Вас на каждый тип проверки перечитывается файл (cat | grep «type»)

                                            При использовании tail -f |
                                            наш скрипт обрабатывает лог построчно:

                                            #!/bin/sh

                                            while read mon day time domain pname msg; do
                                            тут можно разбирать, что в наших переменных
                                            например $msg — текст сообщения, $time — время и тд, думаю Вы поняли смысл
                                            done

                                            Такой подход значительно экономнее и нагляднее. К тому же, он актуальнее, работа начинается как только появилась новая строка в логе.

                                            И такая архитектура избавит Вас от необходимости как-то дополнительно подготавливать (ротировать) файлы
                                              0
                                              Что ж, все понятно. Осталось придумать, как следить, чтобы сервис был постоянно запущен. Кажется, с этим будут сложности, также как и при запуске после ребута.
                                                +1
                                                О, совсем нет.
                                                script.sh & и процес работает в фоне. А while read настолько прост, что никогда не падает сам ;)
                                                  0
                                                  Стоит обратить внимание. Спасибо.
                                          0
                                          Сделал примерно похожий скрипт по анализу логов и бану через ipfw
                                          Если его запускать «руками» работает отлично
                                          При запуске через крон отказывается работать.
                                          Долгие копания привели к тому, что я выяснил, что при запуске через крон grep возвращает ноль, а руками — нормальную цифру
                                          Ни кто не знает в чем может быть проблема?
                                          Команда вот такая

                                          /bin/ps aux | /usr/bin/grep httpd | /usr/bin/wc -l

                                          Это я по количеству процессов httpd определяю активность на сайтах

                                          не работает именно /usr/bin/grep и только при запуске через крон
                                            0
                                            Ларчик открывается просто:
                                            — делаете файл sh, куда пишите свой скрипт
                                            — пишите в крон строку на его запуск
                                              0
                                              А может надо в кавычки взять греп-строку?
                                                0
                                                скрипт /root/apachecount.sh
                                                Содержимое:
                                                /bin/ps aux | /usr/bin/grep httpd | /usr/bin/wc -l > /root/apachecount.txt
                                                запуск через /etc/crontab от имени рута
                                                Через крон пишет всегда 1
                                                Руками пишет всегда нормальную цифру
                                                httpd пробовал писать и с двойными кавычками и с одинарными и без ковычек

                                            Only users with full accounts can post comments. Log in, please.