#!/usr/bin/perl # bansshee # # Copyright 2006-2009 Wincent Colaiuta. All rights reserved. use strict; use warnings; use threads; use threads::shared; use Sys::Syslog qw(:DEFAULT setlogsock); use sigtrap qw(die untrapped normal-signals); # ensure that END block gets run (to clean up iptables) use Proc::Daemon; use File::Tail; # # Default settings: optional overrides may be placed in /etc/bansshee.conf # my $config_file = "/etc/bansshee.conf"; our $permitted_illegal_user = 5; # number of invalid user attempts permitted from a single IP address before it gets blocked our $permitted_incorrect_pass = 5; # number of incorrect pass attempts permitted from a single IP address before it gets blocked our $unban_wait = 3600; # minimum number of seconds an IP must wait before it gets removed from the blocklist (1 hour) our $grace_period = 3600; # number of seconds that must pass before prior invalid/incorrect attempt counts are reset (1 hour) our $unblocking_interval = 300; # number of seconds between checks of the blocklist for removing old IPs (5 minutes) # Platform specific settings, based on Red Hat Enterprise Linux ES release 3 (Taroon Update 7) our $logpath = '/var/log/secure'; # logfile to watch our $illegal_user_regex = 'sshd\[\d+\]: Failed password for illegal user (\S+) from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d+ ssh'; our $incorrect_pass_regex = 'sshd\[\d+\]: Failed password for (\S+) from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d+ ssh'; our $iptables = '/sbin/iptables'; # for manipulating the firewall our $iptables_create = '-N BANSSHEE'; # iptables parameters for creating the BANSSHEE chain our $iptables_add = '-A INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for adding the JUMP rule our $iptables_remove = '-D INPUT -p tcp --dport ssh -j BANSSHEE'; # iptables parameters for removing the JUMP rule our $iptables_flush = '-F BANSSHEE'; # iptables parameters for flushing the BANSSHEE chain our $iptables_delete = '-X BANSSHEE'; # iptables parameters for deleting the BANSSHEE chain our $id = '/usr/bin/id -u'; # for determing if running as root our $log_facility :shared = 'authpriv'; # Bansshee messages logged to /var/log/secure # # Storage # my %last_attempt = (); my %illegal_user_attempts = (); my %incorrect_pass_attempts = (); my %blocked_ips = (); my $iptables_configured = 0; # # Functions # sub log_message($$;@) : locked { my $log_level = shift; my $format_string = shift; my @arguments = @_; setlogsock('unix'); openlog('bansshee', 'pid', $log_facility); if (@arguments) { syslog($log_level, $format_string, @arguments); } else { syslog($log_level, $format_string); } closelog(); } sub prepare_iptables() { log_message('notice', 'Creating new BANSSHEE iptables chain.'); system("$iptables $iptables_create") == 0 or die; log_message('info', 'Adding JUMP rule (redirects all SSH traffic to BANSSHEE chain).'); system("$iptables $iptables_add") == 0 or die; $iptables_configured = 1; } sub cleanup_iptables() { log_message('info', 'Removing JUMP rule from INPUT chain.'); system("$iptables $iptables_remove") == 0 or die; log_message('info', 'Flushing BANSSHEE iptables chain.'); system("$iptables $iptables_flush") == 0 or die; log_message('notice', 'Deleting BANSSHEE iptables chain.'); system("$iptables $iptables_delete") == 0 or die; $iptables_configured = 0; } sub watch_blocklist_in_detached_thread() { while (1) { log_message('info', 'Performing periodic check of blocked IPs list.'); my @unblock; # build a list of IPs to unblock (so as to avoid altering the hash while enumerating over it) { lock %blocked_ips; my $cutoff_time = time() - $unban_wait; # blocked ips added before the cutoff get unblocked foreach my $ip (keys %blocked_ips) { if ($blocked_ips{$ip} < $cutoff_time) { push @unblock, $ip; } } # unlock %blocked_ips; } foreach my $ip (@unblock) { unblock_ip($ip); } sleep $unblocking_interval; } return; } sub block_ip($) { my $ip = shift; lock %blocked_ips; if (!defined($blocked_ips{$ip})) { log_message('warning', "Adding IP $ip to blocklist."); system("$iptables -I BANSSHEE -s $ip -j DROP") == 0 or die; $blocked_ips{$ip} = time(); # record time that IP was added to the blocklist } } sub unblock_ip($) { my $ip = shift; lock %blocked_ips; if (defined($blocked_ips{$ip})) { log_message('notice', "Removing IP $ip from blocklist."); system("$iptables -D BANSSHEE -s $ip -j DROP") == 0 or die; undef $blocked_ips{$ip}; } } sub update_blocked_ip_timestamp($) { my $ip = shift; lock %blocked_ips; if (defined($blocked_ips{$ip})) { $blocked_ips{$ip} = time(); } } sub illegal_user($$) { my $ip = shift; my $user = shift; if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time())) { $illegal_user_attempts{$ip} = 1; # reset counter } else { $illegal_user_attempts{$ip} += 1; } $last_attempt{$ip} = time(); log_message('warning', "Attempted connection with illegal user ($user) from IP $ip ($illegal_user_attempts{$ip} attempt(s) so far)."); if ($illegal_user_attempts{$ip} == $permitted_illegal_user) { block_ip($ip); } elsif ($illegal_user_attempts{$ip} > $permitted_illegal_user) { update_blocked_ip_timestamp($ip); } } sub incorrect_pass($$) { my $ip = shift; my $user = shift; if (defined($last_attempt{$ip}) && (($last_attempt{$ip} + $grace_period) < time())) { $incorrect_pass_attempts{$ip} = 1; # reset counter } else { $incorrect_pass_attempts{$ip} += 1; } $last_attempt{$ip} = time(); log_message('warning', "Failed password attempt for user ($user) from IP $ip ($incorrect_pass_attempts{$ip} attempt(s) so far)"); if ($incorrect_pass_attempts{$ip} == $permitted_incorrect_pass) { block_ip($ip); } elsif ($incorrect_pass_attempts{$ip} > $permitted_incorrect_pass) { update_blocked_ip_timestamp($ip); } } # # Main # log_message('notice', 'Bansshee startup.'); if (`$id` != "0") { log_message('err', 'Requires root privileges [exiting].'); die; } if (-f $config_file && -T $config_file) { log_message('info', "Reading config file $config_file."); do $config_file || log_message('err', "Error reading config file."); } log_message('notice', 'Daemonizing.'); Proc::Daemon::Init; prepare_iptables(); share(%blocked_ips); my $background_thread = async { watch_blocklist_in_detached_thread(); }; log_message('notice', "Tailing log: $logpath."); my $file = File::Tail->new(name=>$logpath, ignore_nonexistant=>1); while (defined(my $line=$file->read)) { if ($line =~ /$illegal_user_regex/) { illegal_user($2, $1); } elsif ($line =~ /$incorrect_pass_regex/) { incorrect_pass($2, $1); } else { # no match: skip this line } } log_message('notice', 'Banshee shutdown'); exit 0; END { if ($iptables_configured != 0) { cleanup_iptables(); } }