MYCSS

2012-10-10

Як захистити себе від спамерів шо атакують поштовий сервер Postfix - Fail2ban, Policyd2

Як захистити себе від спамерів що атакують поштовий сервер Postfix.

Визначаємо активних  користувачів що відсилають пошту понад дозволенного ліміту за певний час, блокуємо доступ з IP адреси до сервера на певний час. Також блокуємо користувачів що ввели забагато неправильних паролів: authentication failed та супер активних користувачів що хочуть надіслати дуже швидко і багато : Connection rate limit exceeded.



Компоненти:

Policyd2

/usr/ports/mail/policyd2
policyd2-2.0.12   -  Policyd v2 is a multi-platform policy server for popular MTA ...


/usr/local/etc/cluebringer.conf:
[server]

# Protocols to load
protocols=<<EOT
Postfix
#Bizanga
EOT

# Modules to load
modules=<<EOT
Core
AccessControl
#CheckHelo
#CheckSPF
#Greylisting
Quotas
EOT


[database]
DSN=DBI:Pg:database=policyd;host=localhost
Username=somedbuser

Password=somedbuserpwd

# Access Control module
[AccessControl]
enable=1

# Greylisting module
[Greylisting]
enable=0

# CheckHelo module
[CheckHelo]
enable=0

# CheckSPF module
[CheckSPF]
enable=0

# Quotas module
[Quotas]
enable=1


Postfix & Policyd2


postfix/main.cf:
smtpd_recipient_restrictions =
   check_policy_service inet:127.0.0.1:10031,
   permit_mynetworks,
   reject_non_fqdn_sender,
   reject_sender_login_mismatch,
   permit_sasl_authenticated,
   reject_unauth_destination
 

smtpd_end_of_data_restrictions = check_policy_service inet:127.0.0.1:10031

anvil_rate_time_unit = 240s
smtpd_client_connection_rate_limit = 10
smtpd_client_event_limit_exceptions = $mynetworks



далі налаштування policyd2 тільки через Web interface.
cluebringer-2.0.12/INSTALL:
5. Install the webui/*  into your apache directory, check out   includes/config.php  and adjust the MySQL server details.




Таким чином усі користувачі що знаходяться у таблиці Member "newuser" попадають під правила Policy List: "newusers to not internal"
А модуль QUOTAS та POLICY: "newusers to not internal" підраховує скільки відіслано повідомлень за кількістю (MessageCount) , або скільки повідомлень за розміром (MessageCumulativeSize). Якщо ліміт перевищено, то виконується дія за POLICY: "newusers to not internal" - REJECT "LIMIT AT QUOTAUSE" - відкинути, і це буде записано до postfix лог файлу.


Fail2ban

/usr/ports/security/py-fail2ban
py27-fail2ban-0.8.6 - Scans log files and bans IP that makes too many password ...



/usr/local/etc/fail2ban/filter.d/postfix-sasl.conf:
failregex = (?i): warning: [-._\w]+\[<HOST>\]: SASL (?:LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5) authentication failed(: [ A-Za-z0-9+/]*={0,2})?\s*$

/usr/local/etc/fail2ban/filter.d/postfix-conlim.conf:
failregex = Connection rate limit exceeded: .* from (.*)\[<HOST>\] for service smtp

/usr/local/etc/fail2ban/filter.d/postfix-overlim.conf:
failregex = reject: RCPT from (.*)\[<HOST>\]: .*: LIMIT AT QUOTAUSE;


/usr/local/etc/fail2ban/jail.conf:
[postfix-sasl]
enabled  = true
filter   = postfix-sasl
action   = pf
logpath  = /var/log/maillog
bantime  = 360000
maxretry = 4
 

[postfix-conlim]
enabled  = true
filter   = postfix-conlim
action   = pf
logpath  = /var/log/maillog
bantime  = 360000

maxretry = 4
 

[postfix-overlim]
enabled  = true
filter   = postfix-overlim
action   = pf
logpath  = /var/log/maillog
bantime  = 360000

maxretry = 4


Описую як треба передати інформацію до таблиці файровола pf, також завершення усіх поточних з'єднань за визначеною адресою та портами SMTP, SMTPS сервера.
Додатково для спрощення аналізу - оновлю файл з переліком  блокованих адрес - fail2ban.txt.

/usr/local/etc/fail2ban/action.d/pf.conf:


actionban = /sbin/pfctl -t fail2ban -T add <ip>/32
            /usr/sbin/tcpdrop -la | /usr/bin/egrep ' (25|465|110|995) <ip>' |/bin/sh

           /sbin/pfctl -t fail2ban -T show -q > /var/db/filter/fail2ban.txt

actionunban = /sbin/pfctl -t fail2ban -T delete <ip>/32

pf.conf:
# block banned SMTP's  IP
table <fail2ban> persist file "/var/db/filter/fail2ban.txt"
block in on $ext_if proto tcp from <fail2ban> to any port {25, 465, 110, 995}



Надалі до таблиці policyd2 - Member "newuser", додаємо користувачів за котрими треба "наглядати" у ручному або автоматичному режимі.

Автоматичний режим додавання користувачів до "нагляду"

Базується на аналізі лог файлу на предмет підрахувань спроб авторизації користувачів з різних країн та різних інтернет провайдерів під одним обліковим записом.

У мене в наявності є рішення з описаним класом на PHP, а також спрощену версію з використанням шел скрипту.
Ось скрипт що додає до бази даних спроби авторизації користувачів та їх адреси та код інтернет провайдеру що видав цю адресу.
store-postfix-sasl.sh:
#!/bin/sh

echo "DELETE FROM xm_sasl_user_activity;"| psql policyd2 somedbuser
( zcat /var/log/maillog.0.bz2 ; cat /var/log/maillog ) | grep "sasl_username=" | cut -d " " -f 1,3,4,10,8  | \
    while  read str; do \
    #echo $str;
    date=`echo $str | sed "s/^\(.*\) client.*/\1/g"`
    ip=`echo $str | sed "s/.*\[\(.*\)\].*/\1/g"`
    user=`echo $str | sed "s/.*\=\(.*\)/\1/g"`
            #sed "s/.*\[\(.*\)\].*/\1/g"
            if [ -n "$ip" ]; then
             isp=`whois $ip | grep netname |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ */,"", $2);print $2;}'`
             if [ -z $isp ]; then
                 isp=`whois $ip | grep '\-num:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ */,"", $2);print $2;}'`
             fi
             if [ -z $isp ]; then
                isp=`whois $ip | grep -i 'netname:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ */,"", $2);print $2;}'`
             fi
             if [ "$isp" == "N/A" ]; then
                isp=`whois $ip | grep -i 'ownerid:' |head -n 1| /usr/bin/awk '{ FS = ":"; gsub(/^ */,"", $2);print $2;}'`
             fi
             year=`date "+%Y"`
             echo $year $date
#            echo $user
#            echo $ip
#            echo $isp
             echo "INSERT INTO sasl_user_activity (sasl_username,ip,netname,tstamp) VALUES ('${user}','${ip}','${isp}',to_timestamp('${year} ${date
             psql -q imp nobody
            fi
    done



Databse policyd2 table sasl_user_activity:
>psql policyd2 somedbuser
policyd2=> \d sasl_user_activity
                                         Table "public.sasl_user_activity"
    Column     |            Type             |                                Modifiers
---------------+-----------------------------+-----------------------------
 user_id       | integer                     | not null default nextval('sasl_user_activity_user_id_seq'::regclass)
 sasl_username | character varying(30)       | not null
 ip            | inet                        | not null
 netname       | character varying(70)       |
 tstamp        | timestamp without time zone | not null default now()
Indexes:
    "sasl_user_activity_pkey" PRIMARY KEY, btree (user_id)



Class PHP SASL_Activity.php:

<?php
require_once 'Net/Whois.php';

class SASL_Activity {
    protected $db;

    public function __construct($db,$debug) {
        $this->db = $db;
        $this->whois = new Net_Whois;
        $this->pattern1 = '/origin:  *(.*)/i';
        $this->pattern3 = '/netname: *(.*)/';
        $this->pattern2 = '/ownerid: *(.*)/i';
        $this->pattern4 = '/netname: *(.*)/i';
        $this->pattern5 = '/(.*) \(NET-.*\)/i';
        $this->debug = $debug;
    }

    public function clear() {
        $res=$this->db->query("DELETE FROM sasl_user_activity");
        $result=$res->fetch();
        return $result[0];
    }

    public function check($user,$ip) {
        $res = $this->db->query("SELECT count(*) FROM sasl_user_activity WHERE sasl_username=? AND ip=?", array($user,$ip));
        return ('0' == $res->fetchColumn(0));
    }

    public function get_isp($ip) {
        $res = $this->db->query("SELECT netname FROM sasl_user_activity WHERE ip=? LIMIT 1", array($ip));
        $isp = $res->fetchColumn(0);
            if ( $isp == '') {
                $data1 = $this->whois->query($ip);
                if (preg_match($this->pattern1,$data1,$matches1)) {
                    $isp=trim($matches1[1]);
                }else if (preg_match($this->pattern2,$data1,$matches1)) {
                    $isp=trim($matches1[1]);
                }else if (preg_match($this->pattern3,$data1,$matches1)) {
                    $isp=trim($matches1[1]);
                }else if (preg_match($this->pattern4,$data1,$matches1)) {
                    $isp=trim($matches1[1]);
                }else if (preg_match($this->pattern5,$data1,$matches1)) {
                    $isp=trim($matches1[1]);
                }else{
                    $isp="***UNDEFINED";
                }
            }else{
                if ($this->debug) echo "ISP detected early: $isp\t";
            }
        return $isp;
 

    public function store ($date,$user,$ip) {
        if ($this->check($user,$ip)) {
            $ispa=$this->get_isp($ip);
            if ($this->debug)   echo "ISP: $ispa";
            if ($this->db->query("INSERT INTO sasl_user_activity (sasl_username,ip,netname,tstamp) VALUES (?,?,?,?)",array(${user},${ip},${ispa},${date}))) {
                if ($this->debug)       echo "\tInsterted OK\n";
            }
        }else{
            if ($this->db->query("UPDATE sasl_user_activity SET (tstamp) = (?) WHERE sasl_username=? AND ip=?",array(${date},${user},${ip}))) {
                if ($this->debug) echo "\tUpdated OK\n";
            }
        }
    }

    public function get_activity(){
        $res = $this->db->query("SELECT foo.sasl_username, COUNT(foo.sasl_username) AS differ_isp FROM (SELECT Distinct  sasl_username, netname  FROM  xm_sasl_user_activity GROUP BY netname, sasl_username) AS foo GROUP BY sasl_username ORDER by differ_isp DESC");
        return $res;
    }

    public function get_hyper_activity($limit=0){
        $res = $this->db->query("SELECT * FROM (SELECT foo.sasl_username, COUNT(foo.sasl_username) AS differ_isp FROM (SELECT Distinct  sasl_username, netname  FROM  xm_sasl_user_activity GROUP BY netname, sasl_username) AS foo GROUP BY sasl_username) AS activity
        return $res->fetchAll(Zend_Db::FETCH_ASSOC);
    }


    private function __clone()
    {}
}


sasl-act.php:
date_default_timezone_set('UTC');
set_time_limit(60 * 60 * 24);
error_reporting( E_ALL ^ E_NOTICE);
require_once 'SASL_Activity.php';

$debug=0;
$dirMailLog = '/var/log/';
$fileMailLog = 'maillog.0.bz2';
$log_file=$dirMailLog . $fileMailLog;
 

$useractivity = new SASL_Activity($db,$debug);

//Oct  3 02:10:14 mail postfix/smtpd[49400]: 1B33DB44909: client=unknown[137.243.223.131], sasl_method=LOGIN, sasl_username=noman
//for parse log of postfix
$pattern = '/(\w{3}[^a-zA-Z]+)+ mail postfix.* client=.*?\[([0-9.]+)+\],.*sasl_username=(.*)/';

$fh = bzopen($log_file,'r') or die($php_errormsg);

$i = 1;
$file_prev_day=strtotime('-1 day', filemtime($log_file));
$file_year=date ("Y", $file_prev_day);
if ($debug) echo "$log_file year:$file_year from:". date('Y-m-d',$file_prev_day). PHP_EOL;
//echo "Purge table:".$useractivity->clear()."\n";

while (!feof($fh)) {

    // read each line and trim off leading/trailing whitespace
    if ($s = bzread($fh,16384)) { //fgets
        // match the line to the pattern
        if (preg_match($pattern,$s,$matches)) {
            list($whole_match,$date,$remote_host,$user) = $matches;
           
if ($debug) echo "\n##:".$i."  $file_year $date  $remote_host $user";
            if ($debug)  echo "\nUser: $user\tIP: $remote_host";
            $date="$file_year ".$date;
            if ($debug) echo "\nStore: ";
            $useractivity->store($date,$user,$remote_host);
       } else {
            // complain if the line didn't match the pattern
            error_log("Can't parse line $i: $s");
      }
    }
    $i++;
}
bzclose($fh) or die($php_errormsg);

add_newusers_activity($members,$useractivity);
 

//add hyper acivity users to tables newusers poilicy
    $hyperact=$useractivity->get_hyper_activity(10);
    foreach($hyperact as &$val) {
        if ($debug)  echo $val[sasl_username].PHP_EOL;
        $user=$val[sasl_username];
        // here some methods for addMember($user.') to policyd2 tables with members of newusers poilicy
    }


 







1 коментар:

Анонім сказав...

Набагато простіше та ефективніше буде встановити ASSP (http://www.howtoforge.com/installing-assp-anti-spam-smtp-proxy-on-ubuntu-server-10.04-debian-5.0) на тому ж сервері ;)

Коли забув ти рідну мову, біднієш духом ти щодня...
When you forgot your native language you would become a poor at spirit every day ...

Д.Білоус / D.Bilous
Рабів до раю не пускають. Будь вільним!

ipv6 ready