"SfR Fresh" - the SfR Freeware/Shareware Archive

Member "blocksshd-1.3/blocksshd" of archive blocksshd-1.3.tar.gz:


As a special service "SfR Fresh" has tried to format the requested source page into HTML format using source code syntax highlighting with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. That can be also achieved for any archive member file by clicking within an archive contents listing on the first character of the file(path) respectively on the according byte size field.
    1 #!/usr/bin/perl -w
    2 
    3 # This is BlockSSHD which protects computers from SSH brute force attacks by
    4 # dynamically blocking IP addresses using iptables based on log entries.
    5 # BlockSSHD is modified from BruteForceBlocker v1.2.3 by Daniel Gerzo
    6  
    7 # Copyright (C) 2006, James Turnbull
    8 # Support for pf and whois added by Anton - valqk@webreality.org - http://www.webreality.org
    9 # Support for subnets in the whitelist added by Lester Hightower - hightowe@10east.com - http://www.10east.com/
   10 
   11 # This program is free software; you can redistribute it and/or modify
   12 # it under the terms of the GNU General Public License as published by
   13 # the Free Software Foundation; either version 2 of the License, or
   14 # (at your option) any later version.
   15 
   16 # This program is distributed in the hope that it will be useful, but
   17 # WITHOUT ANY WARRANTY; without even the implied warranty of
   18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
   19 # Public License for more details.
   20 
   21 # You should have received a copy of the GNU General Public License along
   22 # with this program; if not, write to the Free Software Foundation, Inc.,
   23 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
   24 
   25 use strict;
   26 use warnings;
   27 
   28 use Sys::Syslog;
   29 use Sys::Hostname;
   30 use Tie::File;
   31 use File::Tail;
   32 use Net::DNS::Resolver;
   33 use Net::Subnets; 
   34 use Getopt::Long;
   35 
   36 use POSIX qw(setsid);
   37 use vars qw($opt_d $opt_h $opt_v $opt_stop);
   38 
   39 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin';
   40 
   41 my $version = "1.3";
   42 
   43 our $cfg;
   44 
   45 # This is where the configuration file is located
   46 require '/etc/blocksshd.conf';
   47 
   48 my $work = {
   49         ipv4            => '(?:\d{1,3}\.){3}\d{1,3}',   # regexp to match ipv4 address
   50         fqdn            => '[\da-z\-.]+\.[a-z]{2,4}',   # regexp to match fqdn
   51         hostname        => hostname,                    # get hostname
   52 };
   53 
   54 $cfg->{'whitelist_prepared'}=&loadwhitelist($cfg->{whitelist});
   55 
   56 Getopt::Long::Configure('bundling');
   57 GetOptions
   58         ("start" => \$opt_d, "daemon"  => \$opt_d, "d" => \$opt_d,
   59          "h" => \$opt_h, "help"        => \$opt_h,
   60          "v" => \$opt_v, "version"     => \$opt_v,
   61          "stop" => \$opt_stop);
   62 
   63 if ($opt_d) {
   64          if (-e $cfg->{pid_file})
   65             { die "BlockSSHD is already running!\n" }
   66 
   67          # Fork daemon   
   68          chdir '/' || die "Can't change directory to /: $!";
   69          umask 0;        
   70          open(STDIN,  "+>/dev/null");
   71          open(STDOUT, "+>&STDIN");
   72          open(STDERR, "+>&STDIN");
   73          defined(my $pid = fork) || die "Can't fork: $!";
   74          exit if $pid;
   75          setsid || die "Can't start a new session: $!";
   76 
   77          # Record PID
   78          open (PID,">$cfg->{pid_file}") || die ("Cannot open BlockSSHD PID file $cfg->{pid_file}!\n");
   79          print PID "$$\n";
   80          close PID;
   81 }
   82 
   83 if ($opt_stop) {
   84         exithandler();
   85 }
   86 
   87 if ($opt_h) {
   88         print_help();
   89         exit;
   90 }
   91 
   92 if ($opt_v) {
   93         print "BlockSSHD version $version\n";
   94         exit;
   95 }
   96 
   97 openlog('blocksshd', 'pid', 'auth');
   98 
   99 my $alarmed = 0; # ALRM state
  100 my %count = (); # hash used to store total number of failed tries
  101 my %timea = (); # hash used to store last time when IP was active
  102 my %timeb = (); # hash used to store time when IP was blocked
  103 my $res   = Net::DNS::Resolver->new;
  104 
  105 # Watch signals
  106 
  107 $SIG{'ALRM'} = \&unblock;
  108 $SIG{'INT'}  = \&exithandler;
  109 $SIG{'QUIT'} = \&exithandler;
  110 $SIG{'KILL'} = \&exithandler;
  111 $SIG{'TERM'} = \&exithandler;
  112 
  113 # Notify of startup
  114 
  115 syslog('notice', "Starting BlockSSHD");
  116 
  117 # Create iptables chain
  118 
  119 setup();
  120 
  121 # Clear existing rules
  122 
  123 flush();
  124 
  125 # Restore previously blocked rules
  126 
  127 if($cfg->{restore_blocked} == 1) {
  128     restore_blocked();
  129 }
  130 
  131 # The core process
  132 
  133 my $ref=tie *FH, "File::Tail", (name=>$cfg->{logfile},
  134                         maxinterval=>$cfg->{logcheck},
  135                         interval=> 10,
  136                         errmode=> "return");
  137 
  138 if ( $cfg->{unblock} == 1) {
  139 alarm( ($cfg->{unblock_timeout} /2) );
  140 }
  141 
  142 while (<FH>) {
  143     if( $alarmed ) {
  144         $alarmed = 0;
  145         next;
  146       }   
  147 
  148     if (
  149         /.*Failed (password) .* from ($work->{ipv4}|$work->{fqdn}) port [0-9]+/i ||
  150         /.*(Invalid|Illegal) user .* from ($work->{ipv4}|$work->{fqdn})$/i ||
  151         /.*Failed .* for (invalid|illegal) user * from ($work->{ipv4}|$work->{fqdn}) port [0-9]+ .*/i ||
  152         /.*Failed .* for (invalid|illegal) user .* from ($work->{ipv4}|$work->{fqdn})/i ||
  153         /.*(Postponed) .* for .* from ($work->{ipv4}|$work->{fqdn}) port [0-9]+ .*/i ||
  154         /.*Did not receive (identification) string from ($work->{ipv4}|$work->{fqdn})$/i ||
  155         /.*Bad protocol version (identification) .* from ($work->{ipv4}|$work->{fqdn})$/i ||
  156         /.* login attempt for (nonexistent) user from ($work->{ipv4}|$work->{fqdn})$/i ||
  157         /.* bad (password) attempt for '.*' from ($work->{ipv4}|$work->{fqdn}):[0-9]+/i ||
  158         /.*unknown (user) .* from ($work->{ipv4}|$work->{fqdn}).*/i ||
  159         /.*User .* (from) ($work->{ipv4}|$work->{fqdn}) not allowed because.*/i ||
  160         /.*USER.*no such (user) found from ($work->{ipv4}|$work->{fqdn}).*/i
  161        ) {
  162         if($1 || $2) {
  163          my $IP=$1 unless $2;
  164          $IP=$2 if $2;
  165               if ( $IP =~ /$work->{fqdn}/i) {
  166                 foreach my $type (qw(AAAA A)) {
  167                     my $query = $res->search($IP, $type);
  168                     if ($query) {
  169                         foreach my $rr ($query->answer) {
  170                             block($rr->address);
  171                         }
  172                     }
  173                 }
  174             } else {
  175                 block($IP);
  176               }
  177         }
  178     }
  179 }
  180 
  181 closelog();
  182 
  183 sub block {
  184     # Confirm iptables table is created
  185     setup();
  186  
  187     my ($IP) = shift or die "Missing IP address!\n";
  188     
  189     # check to see if IP address already blocked
  190     
  191     if($cfg->{os} eq 'linux') {
  192     my ($exists) = system("$cfg->{iptables} -n -L $cfg->{chain} | grep -q '$IP'");
  193     if ($exists == 0) {
  194        return;
  195        }
  196     }
  197     elsif($cfg->{os} eq 'bsd') {
  198     my ($exists) = system("$cfg->{pfctl} -t $cfg->{chain} -T show| grep -q '$IP'"); 
  199     if ($exists == 0) {
  200        return;
  201        }
  202     }
  203     
  204     # Reset IP count if timeout exceeded 
  205     if ($timea{$IP} && ($timea{$IP} < time - $cfg->{timeout})) {
  206         syslog('notice', "Resetting $IP count, since it wasn't active for more than $cfg->{timeout} seconds");
  207         delete $count{$IP};
  208     }
  209     $timea{$IP} = time;
  210 
  211     # increase the total number of failed attempts
  212     $count{$IP}++;
  213 
  214     if ($count{$IP} < $cfg->{max_attempts}+1) {
  215         syslog('notice', "$IP was logged with a total count of $count{$IP} failed attempts");
  216     }
  217     if ($count{$IP} >= $cfg->{max_attempts}+1) {
  218         syslog('notice', "IP $IP reached the maximum number of failed attempts!");
  219         system_block($IP);
  220     }
  221 }
  222 
  223 sub system_block {
  224     my $IP=shift or die("Can't find IP to block.\n");
  225     if (ref($cfg->{'whitelist_prepared'}->check(\$IP)) ne 'SCALAR') {
  226         if($cfg->{os} eq 'linux') {
  227             syslog('notice', "Blocking $IP in iptables table $cfg->{chain}.");
  228             system("$cfg->{iptables} -I $cfg->{chain} -p tcp --dport 22 -s $IP -j DROP") == 0 || syslog('notice', "Couldn't add $IP to firewall");
  229         }
  230         if($cfg->{os} eq 'bsd') {
  231             syslog('notice', "Blocking $IP in pf table $cfg->{chain}.");
  232             system("$cfg->{pfctl} -t $cfg->{chain} -T add $IP") == 0 || syslog('notice', "Couldn't add $IP to firewall");
  233         }
  234         $timeb{$IP} = time;
  235         # send mail if it is configured
  236         if ($cfg->{send_email} eq '1') {
  237             notify($IP);
  238         }
  239         if ($cfg->{restore_blocked} eq '1') {
  240             log_ip($IP);
  241         }
  242     }
  243 }
  244 
  245 sub setup {
  246          # Check and setup iptables table if missing
  247            if($cfg->{os} eq 'linux') {
  248                 system("$cfg->{iptables} -L $cfg->{chain} | grep -qs '$cfg->{chain}'") == 0 ||
  249                 system("$cfg->{iptables} -N $cfg->{chain}");
  250            } 
  251          # Create IP log file if restore block function is on
  252            if($cfg->{restore_blocked} == 1) { 
  253                 if( !-e $cfg->{log_ips} ) {
  254                   open CLOG,">$cfg->{log_ips}" || syslog('notice',"Can't create $cfg->{log_ips}\n");
  255                   close(CLOG);
  256                 }
  257            }
  258 }
  259 
  260 sub flush {
  261          # Flush any existing firewall rules
  262            syslog('notice', "Flushing existing rules in $cfg->{chain}.");
  263            if($cfg->{os} eq 'linux') {
  264                 system("$cfg->{iptables} -F $cfg->{chain}") == 0 || syslog('notice', "Unable to flush existing firewalls rules from $cfg->{chain}");
  265            } elsif($cfg->{os} eq 'bsd') {
  266                 system("$cfg->{pfctl} -t $cfg->{chain} -T flush") == 0 || syslog('notice', "Unable to flush existing firewalls rules from $cfg->{chain}");
  267            } else {
  268                 syslog('notice',"No operating system specified in blocksshd.conf configuration file.");
  269             }
  270          # If blocking restore is turned off then clear contents of block
  271          # file
  272            if($cfg->{restore_blocked} == 0) {
  273                 if( -e $cfg->{log_ips} && !-z $cfg->{log_ips} ) {
  274                     unlink($cfg->{log_ips});
  275                 }           
  276            } 
  277 }
  278 
  279 sub unblock {
  280             # unblock old IPs based on timeout
  281             $alarmed = 1;
  282 
  283             if($cfg->{os} eq 'linux') {
  284                 open IPT, "$cfg->{iptables} -n -L $cfg->{chain} |";
  285 
  286                      while(<IPT>) {
  287                      chomp;
  288                      next if ($_ !~ /^DROP/);
  289                      my ($target, $prot, $opt, $source, $dest, $prot2, $dport) = split(' ', $_);
  290                         while ( my ($block_ip, $block_time) = each(%timeb) ) {
  291                               if (($block_ip == $source) && ($block_time < time - $cfg->{unblock_timeout})) {
  292                               syslog('notice', "Unblocking IP address $block_ip.");
  293                               system("$cfg->{iptables} -D $cfg->{chain} -p tcp --dport 22 -s $block_ip -j DROP ") == 0 || syslog('notice', "Couldn't unblock $block_ip from firewall.");
  294                                if( -e $cfg->{log_ips} && ((-s $cfg->{log_ips}) > 0)) {
  295                                  unlog_ip($block_ip);
  296                                }
  297                               delete $timeb{$block_ip};
  298                               delete $timea{$block_ip};
  299                               delete $count{$block_ip};
  300                               }
  301                          }
  302                      }
  303                      
  304                 close IPT;
  305  
  306             } elsif($cfg->{os} eq 'bsd') {
  307                 open IPT, "$cfg->{pfctl} -t $cfg->{chain} -T show|" || syslog('error',"Can't open $cfg->{pfctl} for reading.");
  308             
  309                      while(<IPT>) {
  310                      s/^\s+//;
  311                      my $source=$_; 
  312                         while ( my ($block_ip, $block_time) = each(%timeb) ) {
  313                               if (($block_ip == $source) && ($block_time < time - $cfg->{unblock_timeout})) {
  314                                  syslog('notice', "Unblocking IP address $block_ip.");
  315                                  system("$cfg->{pfctl} -t $cfg->{chain} -T delete $block_ip") == 0 || syslog('notice', "Couldn't unblock $block_ip from firewall.");
  316                               if( $cfg->{restore_blocked} == 1) {
  317                                  unlog_ip($block_ip);
  318                                }
  319                               delete $timeb{$block_ip};
  320                               delete $timea{$block_ip};
  321                               delete $count{$block_ip};
  322                               }
  323                         }
  324                      }
  325                  
  326                 close IPT;
  327 
  328              } else {
  329                 die("No operating system specified in blocksshd.conf configuration file.");
  330              }
  331                    
  332             alarm( ($cfg->{unblock_timeout}/2) );
  333 }
  334 
  335 sub loadwhitelist {
  336                   my $rwhiteList = shift @_;	# $cfg->{whitelist}
  337                   my $sn = Net::Subnets->new;
  338 
  339                   if (ref($rwhiteList) eq 'ARRAY') {
  340                      my @subnets = map { chomp $_; &trim($_); } @{$rwhiteList};
  341                      @subnets = grep(!/^#|^$/, @subnets);
  342                      my $p_sn='^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$';
  343                      my @bad_subnets = grep(!/$p_sn/, @subnets);
  344 
  345                   if (scalar(@bad_subnets) > 0) {
  346                      die "The whilelist holds invalid subnet entries: " .
  347                      join(', ', @bad_subnets) . "\n";
  348                   }
  349 
  350                   @subnets = grep(/$p_sn/, @subnets);
  351                   $sn->subnets( \@subnets );
  352                   }
  353 
  354                   return $sn;
  355 }
  356 
  357 sub trim {
  358    my $str=shift @_;
  359    $str =~ s/^[\s\r\n]+//;
  360    $str =~ s/[\s\r\n]+$//;
  361    return $str;
  362 }
  363 
  364 sub log_ip {
  365     my $IP = shift or syslog('notice',"Can't get ip to log!\n");
  366     my $inlist=0;
  367     if( -e $cfg->{log_ips} && ((-s $cfg->{log_ips}) > 0)) {
  368         open LOG,"<$cfg->{log_ips}" || syslog('notice',"Can't open $cfg->{log_ips}\n");
  369         while(<LOG>) {
  370             chomp;
  371             if($_ == $IP) {
  372                 $inlist=1;
  373                 last;
  374             }
  375         }
  376         close LOG;
  377     }
  378     if($inlist == 0) {
  379         open LOG,">>$cfg->{log_ips}" || syslog('notice',"Can't open $cfg->{log_ips}\n");
  380         print LOG "$IP\n";
  381         close LOG;
  382     }
  383 }
  384 
  385 sub unlog_ip {
  386     my $block_ip = shift or die("Can't get IP to unlog!\n");
  387     my @file;
  388 
  389      if( -e $cfg->{log_ips} && ((-s $cfg->{log_ips}) > 0)) {
  390     
  391         tie @file, 'Tie::File', $cfg->{log_ips};
  392         @file=grep { $_ ne $block_ip } @file;
  393         untie @file;
  394     
  395         syslog('notice',"Removed unblocked IP address ($block_ip) from log file $cfg->{log_ips}");
  396      }  
  397 }
  398 
  399 sub restore_blocked {
  400     if( -e $cfg->{log_ips} && ((-s $cfg->{log_ips}) > 0)) {
  401         open RLOG,"<$cfg->{log_ips}" || syslog('notice',"Can't open $cfg->{log_ips}\n");
  402         while(<RLOG>) {
  403             chomp;
  404             if(/$work->{ipv4}|$work->{fqdn}/i) {
  405                 syslog('notice',"Blocking IP $_ - previously blocked and saved in $cfg->{log_ips}");
  406                 system_block($_);
  407             }
  408              else {
  409                 syslog('notice',"Invalid IP address ($_) found in $cfg->{log_ips}");
  410             }
  411         }
  412         close (RLOG);
  413     }
  414 }
  415 
  416 sub notify {
  417     # send notification emails
  418     my ($IP) = shift or die "Missing IP address!\n";       
  419    
  420     syslog('notice', "Sending notification email to $cfg->{email}");
  421     my $whois = '';
  422       if($cfg->{email_whois_lookup} == 1) {
  423            $whois = `$cfg->{whois} $IP|$cfg->{sed} -e 's/\"/\\"/g'`;      
  424       }
  425     system("echo \"$work->{hostname}: BlockSSHD blocking $IP\n\n $whois\" | $cfg->{mail} -s 'BlockSSHD blocking notification' $cfg->{email}"); 
  426 }
  427 
  428 sub exithandler {
  429     if (-e $cfg->{pid_file})
  430     {
  431         my $pid=`/bin/cat $cfg->{pid_file}`;
  432         system("/bin/kill -9 $pid");
  433         unlink($cfg->{pid_file});
  434         die "BlockSSHD exiting\n";
  435     } else {
  436         die "BlockSSHD is not running!\n";
  437     }
  438 }
  439 
  440 sub print_help {
  441     print "BlockSSHD command line options\n";
  442     print "-d | --daemon | --start  Start BlockSSHD in daemon mode\n";
  443     print "--stop                   Stop BlockSSHD\n";
  444     print "-h | --help              Print this help text\n";
  445     print "-v | --version           Display version\n";
  446 }