Script - Detect brute-force password guessing on a mail server

I've had a few people from around the world try to break into my mail server using a brute-force approach, trying various username-password combinations, sometimes several thousand of them, slowing my system down, filling my mail log with crap, and generally just annoying me. I figure there must be a kiddie-script out there to do this that these people are running, as some attacks send the exact same usernames in the exact same order... I've tried to ignore them, but they have been annoying.

After the latest one yesterday originating somewhere in China, I decided to do something about it. I wrote a script to detect when this happens and generate a report, including it in the daily periodic cron job so that when I go through the system report each morning, I can blacklist any IPs at my firewall. I figured I'd share it here in case somebody else finds it useful. :)


[cmd=]/usr/local/sbin/updateotherblocks[/cmd]:
Code:
/usr/bin/vi /usr/local/etc/IPBlocks/Others
/sbin/pfctl -t other-blocked -Tr -f /usr/local/etc/IPBlocks/Others

In /usr/local/etc/IPBlocks/Others, I have a list of IP addresses, one per line, that I want blacklisted. In my pf rule table, I have the following included:

Code:
# Block IPs that have tried to hack me
table <other-blocked> persist file "/usr/local/etc/IPBlocks/Others"
block in log quick on $ext_if from <other-blocked> to any


This effectively blocks any IP in the /usr/local/etc/IPBlocks/Others file at the firewall. While strictly a reactive measure, it does easily detect them and make it plain when they occur, allowing me to block the originating IP in a matter of a few seconds and ensuring that they won't be able to do so again from the same IP.


Log analysis script in the next post...
 
[cmd=]/usr/local/sbin/detectmailhack.pl[/cmd]:
Code:
#!/usr/bin/perl

###############################################################################
#                                                                             #
# Perl script to analyze a mail log file for failed logins in order to detect #
# brute-force attempts to guess passwords.  Outputs a table with the IP of    #
# the failed login, username attempted, and a count of each.                  #
#                                                                             #
# Parameter 1:                                                                #
#   Pass in the full path and name of a log file to use.                      #
#   If none is provided, the default is /var/log/maillog                      #
#                                                                             #
###############################################################################
#                                                                             #
# Ruler's Common-Sense License:                                               #
#                                                                             #
#   You may use this script however you want to, but I don't warrant it to    #
#   be good for anything in particular, though it happens to work well for    #
#   me.  (I hate putting BS like this in, but I hate more being sued.)  If    #
#   you use this script, you must keep this license and credit to me in it    #
#   in the form of this block, even if you modify it for your own use.  If    #
#   you want to send me money for it, fantastic!  Send me a private message   #
#   on the freebsd.org forums and I'll give you my PayPal address. :-)  Even  #
#   just a simple 'thank you' would be nice.  If not, that's fine too.  All   #
#   hate mail/spam is sent directly to /dev/null                              #
#                                                       - Jim, AKA Ruler2112  #
#                                                                             #
###############################################################################
#                                                                             #
# History:                                                                    #
#                                                                             #
#   2010-01-06 by Ruler2112       Wrote initial version.                      #
#   2010-01-07 by Ruler2112       Made it work better and support compressed  #
#                                 mail logs.                                  #
#   2010-01-07 by Ruler2112       Released on freebsd.org forums.             #
#                                                                             #
###############################################################################

use strict;

my $LogFile = shift;

###############################################################################
#                      Variable Declaration Section                           #
# Set these variables to customize this script's behavior.                    #
###############################################################################

# UserDelim
#   This specifies what the user in the mail log is immediately preceeded by.
#   I don't know if this differs for MTAs other than Postfix, but figured I'd
#   add it for ease of customizability.
my $UserDelim = "user=";

# IpDelim
#   What the IP address of the connecting client is immediately preceeded by
#   in the mail log.  Again, I'm not certain that this would ever be useful to
#   change, but it's easy enough to make customizable.
my $IpDelim = "ip=[";

# UserEnd
#   What designates the end of the user field in the mail log.  Some attempts
#   may have spaces in the name of the user, so a plain space probably isn't
#   a good choice.
my $UserEnd = ", ";

# IpEnd
#   What comes immediately after the IP address in the mail log.
my $IpEnd = "]";

# CompressedCat
#   This is a cat utility that reads compressed files.  Typically known as 
#   'zcat' under FreeBSD.  This should be the full absolute path to and name 
#   of the file.
my $CompressedCat = "/usr/bin/zcat";

# RegularCat
#   This is the normal cat utiliy and again should contain the full absolute
#   path to and name of the file.
my $RegularCat = "/bin/cat";

###############################################################################
# !!! WARNING !!! WARNING !!! WARNING !!! WARNING !!! WARNING !!! WARNING !!! #
###############################################################################
#                    This is the Beginning of the Script                      #
#                                                                             #
# Do not change anything below this line unless you know what you're doing!   #
###############################################################################

my ($CatUtil);
my (@FailedIps, @FailedCount, $NumberIps, $TotalCount);
my (@FailedUsers, @FailedUserCount, $NumberUsers);

if($LogFile eq "")
  {
  $LogFile = "/var/log/maillog";
  }
if((-e $LogFile) and (-r $LogFile))
  {
  SetCatUtility();
  AnalyzeIps();
  AnalyzeUsers();
  }
else
  {
  print "$LogFile either does not exist or cannot be read!  Aborting...\n";
  }
exit;

sub SetCatUtility()
  {
  if((substr($LogFile, length($LogFile) - 3, 3) eq "bz2") or
     (substr($LogFile, length($LogFile) - 2, 2) eq "gz"))
    {
    $CatUtil = $CompressedCat;
    }
  else
    {
    $CatUtil = $RegularCat;
    }
  return;
  }

sub AnalyzeIps()
  {
  my ($counter, $workstr);
  BuildIpTable();
  $workstr = $NumberIps + 1;
  print "$TotalCount failed mail login attempts from $workstr sources\n\n";
  return;
  }

sub BuildIpTable()
  {
  my (@workary, @sortedarray);
  @FailedIps = ();
  @FailedCount = ();
  $NumberIps = -1;
  $TotalCount = 0;
  open(INFILE, "$CatUtil $LogFile |");
  while(<INFILE>)
    {
    if(index($_, "LOGIN FAILED") > 0)
      {
      ProcessIpRecord($_);
      }
    }
  close(INFILE);
  push @workary, [$FailedIps[$_], $FailedCount[$_]] foreach (0..$NumberIps);
  @sortedarray = sort { $a->[1] <=> $b->[1] } @workary;
  @sortedarray = reverse @sortedarray;
  @FailedIps = map { $$_[0] } @sortedarray;
  @FailedCount = map { $$_[1] } @sortedarray;
  return;
  }

sub ProcessIpRecord()
  {
  my $logline = shift;
  my ($workstr, $cutpos, $endpos, $leftover);
  my ($attempteduser, $attemptedip);
  chomp($logline);
  $cutpos = index($logline, $UserDelim);
  $endpos = index($logline, $UserEnd, $cutpos + 1);
  $attempteduser = substr($logline, $cutpos + length($UserDelim), $endpos - $cutpos - length($UserDelim));
  $cutpos = index($logline, $IpDelim);
  $endpos = index($logline, $IpEnd, $cutpos + 1);
  $attemptedip = substr($logline, $cutpos + length($IpDelim), $endpos - $cutpos - length($IpDelim));
  $workstr = FindIp($attemptedip);
  @FailedCount[$workstr]++;
  $TotalCount++;
  return;
  }

sub FindIp()
  {
  my $address = shift;
  my ($counter, $retval);
  $retval = -1;
  $counter = 0;
  while($counter <= $NumberIps)
    {
    if($address eq @FailedIps[$counter])
      {
      $retval = $counter;
      $counter = $NumberIps;
      }
    $counter++;
    }
  if($retval < 0)
    {
    $NumberIps++;
    $retval = $NumberIps;
    @FailedIps[$retval] = $address;
    @FailedCount[$retval] = 0;
    }
  return $retval;
  }

sub AnalyzeUsers()
  {
  my ($counter, $counter2, $spacing, $workstr);
  for($counter = 0; $counter <= $NumberIps; $counter++)
    {
    BuildUserTable(@FailedIps[$counter]);
    $workstr = @FailedIps[$counter] . ":";
    $spacing = 40 - length($workstr) - length(@FailedCount[$counter]);
    $workstr = $workstr . (" " x $spacing);
    $workstr = $workstr . @FailedCount[$counter];
    print "$workstr\n";
    for($counter2 = 0; $counter2 <= $NumberUsers; $counter2++)
      {
      $workstr = (" " x 10) . @FailedUsers[$counter2];
      $spacing = 30 - length($workstr) - length(@FailedUserCount[$counter2]);
      $workstr = $workstr . (" " x $spacing) . @FailedUserCount[$counter2];
      print "$workstr\n";
      }
    }
  return;
  }

sub BuildUserTable()
  {
  my $address = shift;
  my (@workary, @sortedarray);
  $NumberUsers = -1;
  @FailedUsers = ();
  @FailedUserCount = ();
  open(INFILE, "$CatUtil $LogFile |");
  while(<INFILE>)
    {
    if((index($_, "LOGIN FAILED") > 0) and (index($_, "$address") > 0))
      {
      ProcessUserRecord($_);
      }
    }
  close(INFILE);
  push @workary, [$FailedUsers[$_], $FailedUserCount[$_]] foreach (0..$NumberUsers);
  @sortedarray = sort { $a->[0] cmp $b->[0] } @workary;
  @sortedarray = sort { $a->[1] <=> $b->[1] } @workary;
  @sortedarray = reverse @sortedarray;
  @FailedUsers = map { $$_[0] } @sortedarray;
  @FailedUserCount = map { $$_[1] } @sortedarray;
  return;
  }

sub ProcessUserRecord()
  {
  my $logline = shift;
  my ($workstr, $cutpos, $endpos, $leftover);
  my ($attempteduser, $attemptedip);
  chomp($logline);
  $cutpos = index($logline, $UserDelim);
  $endpos = index($logline, $UserEnd, $cutpos + 1);
  $attempteduser = substr($logline, $cutpos + length($UserDelim), $endpos - $cutpos - length($UserDelim));
  $cutpos = index($logline, $IpDelim);
  $endpos = index($logline, $IpEnd, $cutpos + 1);
  $attemptedip = substr($logline, $cutpos + length($IpDelim), $endpos - $cutpos - length($IpDelim));
  $workstr = FindUser($attempteduser);
  @FailedUserCount[$workstr]++;
  return;
  }

sub FindUser()
  {
  my $user = shift;
  my ($counter, $retval);
  $retval = -1;
  $counter = 0;
  while($counter <= $NumberUsers)
    {
    if($user eq @FailedUsers[$counter])
      {
      $retval = $counter;
      $counter = $NumberUsers;
      }
    $counter++;
    }
  if($retval < 0)
    {
    $NumberUsers++;
    $retval = $NumberUsers;
    @FailedUsers[$retval] = $user;
    @FailedUserCount[$retval] = 0;
    }
  return $retval;
  }
 
LOL - I saw your post and thought there was already something built to do this, which bummed me out. Then saw it only worked with SSH and was happy again. Doesn't look like bruteforceblocker works with mail, only SSH, which I don't have on the box. (I only administer the server from a PC in my office hard-wired to a dedicated NIC.) I also don't want to automatically block an IP based on the number of failed attempts (I thought of it early on, but then discarded the idea) for the simple reason that the vast majority of my co-workers are rather stupid and routinely forget their passwords, so it'd block everybody in the store from connecting to retrieve/send e-mail. :OOO ;)
 
bruteforceblocker works with anything, you only need to slightly modify its source (i.e. add the corresponding regexps to the appropriate place) and configure syslog correctly...
 
danger@ said:
bruteforceblocker works with anything, you only need to slightly modify its source (i.e. add the corresponding regexps to the appropriate place) and configure syslog correctly...

Damn... never mind this script then. :(

I'm still going to use it for what I wrote it for, namely to run from the periodic daily cron job to add the report to the daily system e-mail. It's done and does exactly what I want, plus I really don't want the IPs blocked automatically. It's here if anybody wants to use it.
 
Back
Top