Category: Networking
Author/Contact Info
Description: This code makes use of the /etc/hosts.deny file to block brute force attacks using ssh (however it can easily be modified for other tcp wrapped services such as FTP). The code writes a line to the /etc/hosts.deny file when the number of failed logins exceeds a stated criteria (in this case 9) within a set time (in this case 1 minute). This is accomplished by first getting the last 100 lines of the /var/log/secure file and looking for failed logins.
#!/usr/local/bin/perl

use strict;
use warnings;

    my %possibleDenyIPs = ();
    my %alreadyDeniedIPs = ();
    my $sleepSeconds = 3;
    my $minutes = 1;          # failed logins within this number of mi
+nutes
    my $failCriteria = 9;     # number of failed logins within $minute
+s minutes
    my @tailLines = ();

while(1)
{

   my $currentTime = pastTime();
   # First we tail the last 100 lines of /var/log/secure and then grep
+ those lines for failed password.
   @tailLines = `tail -n 100 /var/log/secure | grep -i "failed passwor
+d"`;
    my $count = @tailLines;

    # only need to check if the number of lines with failed password i
+s greater than $failCriteria
    if($count > $failCriteria )
    {
       # use those lines to get the IPs with failed passwords.
        my $pastTime = pastTime($minutes);
        %possibleDenyIPs = getPossibleDenyIPs($currentTime, $pastTime,
+ @tailLines);

       # Now  we read the existing hosts.deny file for IP address alre
+ady in the file.
       # We do NOT want duplicate entries in the /etc/hosts.deny file.
        %alreadyDeniedIPs = getAlreadyDeniedIPs();

       # Finally we write the IP to /etc/hosts.deny, excluding any IP 
+already in the file.
        writeHostsDenyFile($failCriteria, %possibleDenyIPs);
       #*** **********************************************************
+*******************************
    }
    
    # clear all the lists
    @tailLines = ();
    %possibleDenyIPs=();
    %alreadyDeniedIPs=();
    
    # run every $sleepSeconds
    sleep($sleepSeconds);
}


######################################################################
+###############################################

sub getPossibleDenyIPs
{
     my $currentTime = shift;
     my $pastTime = shift;
     my @tailLines = @_;
     my %possibleDenyIPs = ();

      for(my $index=0; $index<@tailLines; $index++)
      {
          if($tailLines[$index]=~/failed password/io)
          {
                chomp($tailLines[$index]);
            print "\nLine$index***$tailLines[$index]***\n";

                my @line = split /:/,$tailLines[$index];
                #showArray(@line);
                my $seconds = (split /\s/, $line[2])[0];

                # Needs to be a numeric date versus an alpha-numeric d
+ate.
                # Necessary so that comparisons between line date time
+ and past time work.
                my $lineTime = lineDateTime($line[0]);
                my $lineDateTime = $lineTime . $line[1] . $seconds;

                print "lineDateTime: ***$lineDateTime***\n";
                print "currentTime:  ***$currentTime***\n";
                print "pastDateTime: ***$pastTime***\n";
                #<STDIN>;                                      # crite
+ria: (1)   must be failed login attempt
                if($lineDateTime gt $pastTime)                 #      
+     (2)  login is within time limit
                {                                              # i.e, 
+lineDateTime after pastTime

                    #*************************************************
+***********************************
                    #my $IP = old_getIP($line[3]);
                    my $IP = getIP($tailLines[$index]);
                    print "Possible deny IP:***$IP***\n";
                    #*************************************************
+************************************
                    $possibleDenyIPs{$IP}++;              # These are 
+POSSIBLE deny IP's
                                                          # use the IP
+ as a key to the hash and keep track of failed login attempts
              }
          }
      }

      return %possibleDenyIPs;
}


sub getIP
{
      my $line = $_[0];

      if($line =~/(\d{1,3}\.){3}\d{1,3}/)
      {
            # $&:  Perl internal variable contains the pattern that wa
+s matched
            return $&;
      }
}


sub getAlreadyDeniedIPs
{
      my $hostsDeny = "//etc//hosts.deny";
      my %deniedIPs = ();

      # We read the existing hosts.deny file for IP addresses already 
+in the file.
      # We do not want duplicate entries in the /etc/hosts.deny file.
      if(-e $hostsDeny)     # if the file exists
      {
          unless(open(INDENY, "<$hostsDeny"))     # readfile
          {
              warn "Can't create\\open file $hostsDeny : $!\n";
              die;
          }
          while(<INDENY>)   # get the IP's already in hosts.deny
          {
              my $line = $_;
              #print "line:***$line***\n";
              if($line =~/^(\#)/io) {next;}       # skip comment lines
+ in /etc/hosts.deny, line starts with #.
              else
              {
                  my $IP = getIP($line);    # ************************
+***************
                  print "Already denied IP:***$IP***\n";
                  if($IP) { $deniedIPs{$IP}++; }  # use the IP as a ke
+y to the hash and set value to TRUE (1 or greater)
              }
          }
          close(INDENY);
      }
      else
     { print "$hostsDeny does not exist\n";}

      return %deniedIPs;
}


sub writeHostsDenyFile
{
    my $failCriteria = shift;
    my %possibleDenyIPs = @_;
    my $hostsDeny = "//etc//hosts.deny";
    my $newDenyIPsFile = "//var//log//newDenyIPs";

    if(-e $hostsDeny)     # if the file /etc/hosts.deny already exists
+ append to it
    {
        if( open(INDENY, ">>$hostsDeny") )   { }                      
+# append to file  /etc/hosts.deny
        else                                   { warn "Can't create\\o
+pen file $hostsDeny: $!\n";}
    }
    else                 # if the file does not exist, create and writ
+e to it
    {
        if( open(INDENY, ">$hostsDeny") )   { }                       
+ # create and write to file /etc/hosts.deny
        else                                  { warn "Can't create\\op
+en file $hostsDeny: $!\n";}
    }

    # this file is strictly not necessary since the /etc/hosts.deny fi
+le will also contain the denied IP's
    if(-e $newDenyIPsFile)     # if the file /var/log/newDenyIPs alrea
+dy exists
    {
        if( open(INF, ">>$newDenyIPsFile") )   { }                    
+ # append to file  /var/log/newDenyIPs
        else                                     { warn "Can't create\
+\open file $newDenyIPsFile: $!\n";}
    }
    else
    {
      if( open(INF, ">>$newDenyIPsFile") )   { }                      
+ # create and write to file  /var/log/newDenyIPs
      else                                   { warn "Can't create\\ope
+n file $newDenyIPsFile: $!\n";}
    }

    my $comment = commentDateTime();
    foreach my $key (sort keys %possibleDenyIPs)
    {
        # If the IP has already been denied,
        #     i.e., it is already in /etc/hosts.deny, do nothing.
        if($alreadyDeniedIPs{$key}) {next;}
        else
        {
            # If the IP has $failCriteria or more failed login attempt
+s,
            #    write a new rule in the /etc/hosts.deny file for the 
+IP.
            if( $possibleDenyIPs{$key} > $failCriteria)
            {
                    print INDENY "sshd : $key    $comment\n";
                    print INF "sshd : $key  $comment failed logins: $p
+ossibleDenyIPs{$key}\n";
            }
        }
    }
    close(INDENY);
    close(INF);
}


sub pastTime        # used to get current time and past time for compa
+risons
{
    my $secondsPast = 0;
    if($_[0])  { $secondsPast = $_[0]*60; }
    my $Time = time() - $secondsPast;
    my ($sec, $min, $hour, $monthday, $month, $year, $wday, $yday, $is
+dst) = localtime ($Time);
    #my $thisday = qw(Sun Mon Tue Wed Thur Fri Sat)[(localtime)[6]];
    #$mon = qw(Jan Feb Mar Apr May June July Aug Sept Oct Nov Dec)[(lo
+caltime)[4]];
    # month: Jan=0, Feb=1, Mar=3, etc.
    $year = $year + 1900;
    $month++;
    if($monthday < 10)  {$monthday = '0' . $monthday};
    if($hour < 10)      {$hour = '0' . $hour};
    if($min < 10)       {$min = '0' . $min};
    if($sec < 10)       {$sec = '0' . $sec};
    if($month <10)      {$month = '0' . $month};
    #my $timeString = "$mon ". "$monthday " . "$hour:$min:$sec";
    my $timeString = $month . $monthday . "$hour$min$sec";
    return   $timeString;
}


sub lineDateTime
{
    # date will be like May 19 10:07:25
    my $dateTimeString = shift;

    # split on space
    my ($month, $monthday, $time) = split /\s/, $dateTimeString;

    # Need 2 digit date for comparisons like 7 10 and 7 8 to work prop
+erly
    if($monthday && $monthday < 10)   # the date
    {
        $monthday = '0' . $monthday;
    }

    # split on colon and then join
    # want time to be formatted as hhmmss versus hh:mm:ss
    my @timeString = split /:/, $time;
    for(my $index=0; $index<@timeString; $index++)
    {
         if($timeString[$index] < 10)
         {
            $timeString[$index] = '0' . $timeString[$index];
         }
    }
    $time = join "", @timeString);

    # want numeric month
    if($month =~ /^(jan)/io) {$month = '01';}
    if($month =~ /^(feb)/io) {$month = '02';}
    if($month =~ /^(mar)/io) {$month = '03';}
    if($month =~ /^(apr)/io) {$month = '04';}
    if($month =~ /^(may)/io) {$month = '05';}
    if($month =~ /^(jun)/io) {$month = '06';}
    if($month =~ /^(jul)/io) {$month = '07';}
    if($month =~ /^(aug)/io) {$month = '08';}
    if($month =~ /^(sep)/io) {$month = '09';}
    if($month =~ /^(oct)/io) {$month = '10';}
    if($month =~ /^(nov)/io) {$month = '11';}
    if($month =~ /^(dec)/io) {$month = '12';}

    $dateTimeString = join "", $month, $monthday, $time;
    return $dateTimeString;
}


sub commentDateTime
{
  my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = l
+ocaltime();
  #my $thisday = qw(Sun Mon Tue Wed Thur Fri Sat)[(localtime)[6]];
  $mon = qw(Jan Feb Mar Apr May June July Aug Sept Oct Nov Dec)[(local
+time)[4]];
  # month: Jan=0, Feb=1, Mar=3, etc.
  #**************mon++; if($mon < 10) {$mon = '0' . $mon};
  $year = $year + 1900;
  #$mon++;
  #print "comment month = $mon\n";
  if($mday < 10)  {$mday = '0' . $mday};
  if($hour < 10)  {$hour = '0' . $hour};
  if($min < 10)   {$min = '0' . $min};
  if($sec < 10)   {$sec = '0' . $sec};
  #if($mon < 10)    {$mon = '0' . $mon};
  my $commentDateTime = "#  $year" . "$mon". "$mday " . "$hour:$min:$s
+ec";

  return   $commentDateTime;
}