#!/usr/bin/perl ################# # # sshit.pl v0.3 # # Perl script for blocking ip addresses after several # failed ssh/ftp login attempts within a specified time. # # Author: Andreas Pettersson # Email: andpet telia.com # Web: http://anp.ath.cx/sshit/ # # THIS IS STILL A BETA VERSION # USE WITH CARE # ################# # # How it works: # # The script must be fed with tail pipe from /var/log/auth.log. # When a failed login attempt occurs the ip is looked up in the hash %list. # If it is not found it is added, and the counter (n) set to 1. # If it is found the counter n is incremented, # and if it reaches the threshold (3 by default) # within specified time the ip is blocked using ipfw. # # A janitor process is running in the background. It has 2 tasks: # # 1. Reset counters (n) after a specified time (1 minute by default) # 2. Release blocked ip's after a specified time (5 minutes by default) # # # Some notes: # # The script makes no difference between failed root # logins or illegal users. # # The time to block an ip can be set at your needs, # but to get rid of ssh brute force robots a time of # 10 seconds might be enough. They usually don't retry # connecting and move on to the next victim instead. # # This script is written on FreeBSD 5. # ################## # # What it needs: # # Perl module IPC::Shareable # Proc::PID::File # Install it (as root) by launching CPAN shell: # # perl -MCPAN -e 'shell' # # and then run: # # install IPC::Shareable # install Proc::PID::File # # (IPFW) It also needs some free ipfw rule numbers. # By default 2100 to 2199 is used. # (IPFW2 & PF) It needs a table and corresponding rule to deny the connections # ################# # # How to use it: # # Edit the configure file, which located at /usr/local/etc/sshit.conf # # To have it running in background, # add this line to /etc/syslog.conf: # # auth.info;authpriv.info |exec /path/to/sshit.pl [path_of_config_file] # # Do /etc/rc.d/syslogd restart and make some # failing ssh logins and watch /var/log/auth.log. # ################# # # To be done: # # Fix blocker so it reuses cleaned up ipfw rule numbers. # The $ipfw_rulenr increases until it reaches $IPFW_RULE_END # and then it jumps back to $IPFW_RULE_START. It would be # nicer if it could reuse rule numbers that has been cleaned up. # # Support for iptables and hosts.allow. # # The following log entry is not handled, but I'm not sure # it needs special care since the failed logins are still counted: # reverse mapping checking getaddrinfo for hostname.domain.com # failed - POSSIBLE BREAKIN ATTEMPT! # ################# # # Bugs: # # If the script aborts with active blocks, they won't be cleaned. # If the script starts with active blocks, they will be overwritten.. # # Log entries like this won't be counted: # last message repeated x times # # Sometimes there's new sshit.pl processes spawned for no apparent # reason. I haven't investigated yet, but it's harmless, I think :) # # I suppose there might be some variations of auth.log entries # that don't match my regexp: # /failed .*from (\d+\.\d+\.\d+\.\d+)/i # # Please let me know if you stumble across any. # ################# # # Credits and references: # # http://pleac.sourceforge.net/pleac_perl/processmanagementetc.html # # Matthew Dillon's SSHLOCKOUT # http://leaf.dragonflybsd.org/mailarchive/users/2005-03/msg00008.html # ################# # # Version history: # # v0.3 (2005-08-13) # - More generic regex pattern for matching failed # FTP login attempts (FTP LOGIN FAILED FROM x.x.x.x) # old: if (/Failed .* for .* from (\d+\.\d+\.\d+\.\d+) /) { # new: if (/failed .*from (\d+\.\d+\.\d+\.\d+) /i) { # - Added ipfw blocking of port 21 for FTP attempts. # # # v0.2 (2005-07-23) # - Minor cosmetic changes to code. # - Added syslog.conf setting to documentation. # - Added ipfw rule numbers to syslog entries # # # v0.1 (2005-07-22) # - Initial release. # ### DEFAULT SETTINGS $FIREWALL_TYPE = "pf"; # We use pf as firewall on default $MAX_COUNT = 3; # Number of failed login attempts within time before we block $WITHIN_TIME = 60; # Time in seconds in which all failed login attempts must occur $RESET_IP = 300; # Time in seconds to block ip in firewall $IPFW_CMD = "/sbin/ipfw"; $IPFW_RULE_START = 2100; # Make sure you don't have any important rules here already $IPFW_RULE_END = 2199; $IPFW2_CMD = "/sbin/ipfw"; $IPFW2_TABLE_NO = 0; $PFCTL_CMD = "/sbin/pfctl"; $PF_TABLE = "badhosts"; use Sys::Syslog qw(:DEFAULT setlogsock); use IPC::Shareable; use Proc::PID::File; # Parse configure file $conffile = $ARGV[0] || "/usr/local/etc/sshit.conf"; if(open ( FILE, "< $conffile" )) { while($str = ) { if(! $str =~ /^#/ ) { ($name, $value) = split(/\t /, $str); if ($name =~ /^FIREWALL_TYPE$/i ) { if($value =~ /^(pf|ipfw|ipfw2)$/) { $FIREWALL_TYPE = $value; } else { die("Incorrect firewall type!"); } } elsif ($name =~ /^MAX_COUNT$/i ) { $MAX_COUNT = $value; } elsif ($name =~ /^WITHIN_TIME$/i ) { $WITHIN_TIME = $value; } elsif ($name =~ /^RESET_IP$/i) { $RESET_IP = $value; } elsif ($name =~ /^IPFW_CMD$/i) { $IPFW_CMD = $value; } elsif ($name =~ /^IPFW_RULE_START$/i ) { $IPFW_RULE_START = $value; } elsif ($name =~ /^IPFW_RULE_END$/i ) { $IPFW_RULE_END = $value; } elsif ($name =~ /^IPFW2_CMD$/i ) { $IPFW2_CMD = $value; } elsif ($name =~ /^IPFW2_TABLE_NO$/i ) { $IPFW2_TABLE_NO = $value; } elsif ($name =~ /^PFCTL_CMD$/i ) { $PFCTL_CMD = $value; } elsif ($name =~ /^PF_TABLE$/i ) { $PF_TABLE = $value; } } } close(FILE); } my %options = ( create => 1, exclusive => 0, mode => 0644, destroy => 0, ); $handle = tie %list, 'IPC::Shareable', 'sshi', { %options }; $SIG{INT} = sub { die "$$ dying\n" }; setlogsock('unix'); openlog("sshit.pl", 'ndelay', 'LOG_AUTH'); ################################### ## Here is the janitor (child) ## ################################### unless ($child = fork) { # This section becomes the child fork die "cannot fork: $!" unless defined $child; if(! Proc::PID::File->running()) { while (1) { sleep(1); foreach $ip (keys %list) { if ($list{$ip}{n} < $MAX_COUNT) { # delete all ip's that hasn't reached $MAX_COUNT within time if (time() - $list{$ip}{time} > $WITHIN_TIME) { ##print "janitor deleted $ip (did not reach $MAX_COUNT attempts within $WITHIN_TIME seconds)\n"; syslog(LOG_ERR, "janitor deleted $ip (did not reach $MAX_COUNT attempts within $WITHIN_TIME seconds)\n"); delete($list{$ip}); } } else { # remove block for ip's that has reached block time if (time() - $list{$ip}{time} > $RESET_IP) { if($FIREWALL_TYPE =~ /^ipfw$/i) { system("$IPFW_CMD delete $list{$ip}{rulenr}"); } elsif ($FIREWALL_TYPE =~ /^ipfw2$/i) { system("$IPFW2_CMD table $IPFW_TABLE_NO delete $ip"); } elsif ($FIREWALL_TYPE =~ /^pf$/i) { system("$PFCTL_CMD -t $PF_TABLE -Tdelete $ip"); } ##print "janitor removed block rule $list{$ip}{rulenr} for $ip (reset time of $RESET_IP seconds reached)\n"; syslog(LOG_ERR, "janitor removed block rule $list{$ip}{rulenr} for $ip (reset time of $RESET_IP seconds reached)\n"); delete($list{$ip}); } } } } } exit(0); } ################################### ## Here is the main (parent) ## ################################### $ipfw_rulenr = $IPFW_RULE_START; while (<>) { chomp; if (/failed .*from (\d+\.\d+\.\d+\.\d+|[\da-fA-F:]+)/i ) { # IPv4 & IPv6 $ip = $1; if ($list{$ip}{name}) { if ($list{$ip}{n} >= $MAX_COUNT) { syslog(LOG_ERR, "block for $ip not working!"); } else { $list{$ip}{n}++; ##print "$ip found $list{$ip}{n} times\n"; if (($list{$ip}{n} >= $MAX_COUNT) && (time() - $list{$ip}{time} <= $WITHIN_TIME)) { # Let's block it :) # Time should be measured from the trigging # login attempt, and not the 1st occurance $list{$ip}{time} = time(); if($FIREWALL_TYPE =~ /^ipfw$/i) { # Assign a rule number and do the actual block $list{$ip}{rulenr} = $ipfw_rulenr; system("$IPFW_CMD add $ipfw_rulenr deny tcp from $ip to me 21,22 > /dev/null"); ##print "BLOCKING $ip, rule $ipfw_rulenr\n"; syslog(LOG_ERR, "BLOCKING $ip, rule $ipfw_rulenr\n"); $ipfw_rulenr++; if ($ipfw_rulenr > $IPFW_RULE_END) { $ipfw_rulenr = $IPFW_RULE_START; } } elsif ($FIREWALL_TYPE =~ /^ipfw2$/i) { system("$IPFW2_CMD table $IPFW2_TABLE_NO add $ip"); syslog(LOG_ERR, "BLOCKING $ip with ipfw2\n"); } elsif ($FIREWALL_TYPE =~ /^pf$/i) { system("$PFCTL_CMD -t $PF_TABLE -Tadd $ip"); syslog(LOG_ERR, "BLOCKING $ip with pf\n"); } } } } else { # Add new entry in hash $list{$ip}{name} = $ip; # ip address to watch $list{$ip}{n} = 1; # first occurance $list{$ip}{time} = time(); # time of first occurance ##print "keeping an eye on $ip...\n"; } } }