#!/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;
}