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
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 # Support for repeated (but valid) syslog lines by Abel `00z' Camarillo <00z@the00z.org> - http://the00z.org
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful, but
18 # WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
20 # Public License for more details.
22 # You should have received a copy of the GNU General Public License along
23 # with this program; if not, write to the Free Software Foundation, Inc.,
24 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
33 use Net
::DNS
::Resolver
;
38 use vars
qw($opt_d $opt_h $opt_v $opt_stop);
40 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin';
46 # This is where the configuration file is located
47 require '/etc/blocksshd.conf';
50 ipv4 => '(?:\d{1,3}\.){3}\d{1,3}', # regexp to match ipv4 address
51 fqdn => '[\da-z\-.]+\.[a-z]{2,4}', # regexp to match fqdn
52 hostname => hostname, # get hostname
55 $cfg->{'whitelist_prepared'}=&loadwhitelist($cfg->{whitelist});
57 Getopt::Long::Configure('bundling');
59 ("start" => \$opt_d, "daemon" => \$opt_d, "d" => \$opt_d,
60 "h" => \$opt_h, "help" => \$opt_h,
61 "v" => \$opt_v, "version" => \$opt_v,
62 "stop" => \$opt_stop);
65 if (-e $cfg->{pid_file})
66 { die "BlockSSHD is already running!\n" }
69 chdir '/' || die "Can't change directory to /: $!";
71 open(STDIN, "+>/dev/null");
72 open(STDOUT, "+>&STDIN");
73 open(STDERR, "+>&STDIN");
74 defined(my $pid = fork) || die "Can't fork: $!";
76 setsid || die "Can't start a new session: $!";
79 open (PID,">$cfg->{pid_file}") || die ("Cannot open BlockSSHD PID file $cfg->{pid_file}!\n");
94 print "BlockSSHD version $version\n";
98 openlog('blocksshd', 'pid', 'auth');
100 my $alarmed = 0; # ALRM state
101 my %count = (); # hash used to store total number of failed tries
102 my %timea = (); # hash used to store last time when IP was active
103 my %timeb = (); # hash used to store time when IP was blocked
104 my $res = Net::DNS::Resolver->new;
108 $SIG{'ALRM'} = \&unblock;
109 $SIG{'INT'} = \&exithandler;
110 $SIG{'QUIT'} = \&exithandler;
111 $SIG{'KILL'} = \&exithandler;
112 $SIG{'TERM'} = \&exithandler;
116 syslog('notice', "Starting BlockSSHD");
118 # Create iptables chain
122 # Clear existing rules
126 # Restore previously blocked rules
128 if($cfg->{restore_blocked} == 1) {
134 my $ref=tie *FH, "File::Tail", (name=>$cfg->{logfile},
135 maxinterval=>$cfg->{logcheck},
139 if ( $cfg->{unblock} == 1) {
140 alarm( ($cfg->{unblock_timeout} /2) );
144 # Used because sometimes when the same message appears a lot of times syslog
145 # simply replace the line next to the first message with a :
149 # <month> <day> <hour> <host> sshd[<pid>] Failed password for <USER> from <ip>
150 # <month> <day> <hour> <host> last message repeated 2 times.
152 # this var stores a hash ref that contain an array ref to the IPs that the
153 # previous line blocked
163 # if the previous line was a bad line (it blocked some ips)
164 if ($prev_line->{'ip'})
166 # `block' it again N times
167 if (my ($times) = /last message repeated ([[:digit:]]+) times/i)
169 map { block($_) } @{$prev_line->{'ip'}} while ($times--);
171 # and this line is not bad. (you cannot nest repeated messages, that will be
173 $prev_line = {ip => undef};
177 # `bad' lines definition:
179 /.*Failed (password) .* from ($work->{ipv4}|$work->{fqdn}) port [0-9]+/i ||
180 /.*(Invalid|Illegal) user .* from ($work->{ipv4}|$work->{fqdn})$/i ||
181 /.*Failed .* for (invalid|illegal) user * from ($work->{ipv4}|$work->{fqdn}) port [0-9]+ .*/i ||
182 /.*Failed .* for (invalid|illegal) user .* from ($work->{ipv4}|$work->{fqdn})/i ||
183 /.*(Postponed) .* for .* from ($work->{ipv4}|$work->{fqdn}) port [0-9]+ .*/i ||
184 /.*Did not receive (identification) string from ($work->{ipv4}|$work->{fqdn})$/i ||
185 /.*Bad protocol version (identification) .* from ($work->{ipv4}|$work->{fqdn})$/i ||
186 /.* login attempt for (nonexistent) user from ($work->{ipv4}|$work->{fqdn})$/i ||
187 /.* bad (password) attempt for '.*' from ($work->{ipv4}|$work->{fqdn}):[0-9]+/i ||
188 /.*unknown (user) .* from ($work->{ipv4}|$work->{fqdn}).*/i ||
189 /.*User .* (from) ($work->{ipv4}|$work->{fqdn}) not allowed because.*/i ||
190 /.*USER.*no such (user) found from ($work->{ipv4}|$work->{fqdn}).*/i
197 if ( $IP =~ /$work->{fqdn}/i)
199 foreach my $type (qw(AAAA A))
201 my $query = $res->search($IP, $type);
205 foreach my $rr ($query->answer)
208 push @ips, $rr->address;
210 $prev_line = {ip
=> \
@ips};
215 $prev_line = {ip
=> [$IP]};
219 $prev_line = {ip
=> undef};
226 # Confirm iptables table is created
229 my ($IP) = shift or die "Missing IP address!\n";
231 # check to see if IP address already blocked
233 if($cfg->{os
} eq 'linux') {
234 my ($exists) = system("$cfg->{iptables} -n -L $cfg->{chain} | grep -q '$IP'");
239 elsif($cfg->{os
} eq 'bsd') {
240 my ($exists) = system("$cfg->{pfctl} -t $cfg->{chain} -T show| grep -q '$IP'");
246 # Reset IP count if timeout exceeded
247 if ($timea{$IP} && ($timea{$IP} < time - $cfg->{timeout
})) {
248 syslog
('notice', "Resetting $IP count, since it wasn't active for more than $cfg->{timeout} seconds");
253 # increase the total number of failed attempts
256 if ($count{$IP} < $cfg->{max_attempts
}+1) {
257 syslog
('notice', "$IP was logged with a total count of $count{$IP} failed attempts");
259 if ($count{$IP} >= $cfg->{max_attempts
}+1) {
260 syslog
('notice', "IP $IP reached the maximum number of failed attempts!");
266 my $IP=shift or die("Can't find IP to block.\n");
267 if (ref($cfg->{'whitelist_prepared'}->check(\
$IP)) ne 'SCALAR') {
268 if($cfg->{os
} eq 'linux') {
269 syslog
('notice', "Blocking $IP in iptables table $cfg->{chain}.");
270 system("$cfg->{iptables} -I $cfg->{chain} -p tcp --dport 22 -s $IP -j DROP") == 0 || syslog
('notice', "Couldn't add $IP to firewall");
272 if($cfg->{os
} eq 'bsd') {
273 syslog
('notice', "Blocking $IP in pf table $cfg->{chain}.");
274 system("$cfg->{pfctl} -t $cfg->{chain} -T add $IP") == 0 || syslog
('notice', "Couldn't add $IP to firewall");
277 # send mail if it is configured
278 if ($cfg->{send_email
} eq '1') {
281 if ($cfg->{restore_blocked
} eq '1') {
288 # Check and setup iptables table if missing
289 if($cfg->{os
} eq 'linux') {
290 system("$cfg->{iptables} -L $cfg->{chain} | grep -qs '$cfg->{chain}'") == 0 ||
291 system("$cfg->{iptables} -N $cfg->{chain}");
293 # Create IP log file if restore block function is on
294 if($cfg->{restore_blocked
} == 1) {
295 if( !-e
$cfg->{log_ips
} ) {
296 open CLOG
,">$cfg->{log_ips}" || syslog
('notice',"Can't create $cfg->{log_ips}\n");
303 # Flush any existing firewall rules
304 syslog
('notice', "Flushing existing rules in $cfg->{chain}.");
305 if($cfg->{os
} eq 'linux') {
306 system("$cfg->{iptables} -F $cfg->{chain}") == 0 || syslog
('notice', "Unable to flush existing firewalls rules from $cfg->{chain}");
307 } elsif($cfg->{os
} eq 'bsd') {
308 system("$cfg->{pfctl} -t $cfg->{chain} -T flush") == 0 || syslog
('notice', "Unable to flush existing firewalls rules from $cfg->{chain}");
310 syslog
('notice',"No operating system specified in blocksshd.conf configuration file.");
312 # If blocking restore is turned off then clear contents of block
314 if($cfg->{restore_blocked
} == 0) {
315 if( -e
$cfg->{log_ips
} && !-z
$cfg->{log_ips
} ) {
316 unlink($cfg->{log_ips
});
322 # unblock old IPs based on timeout
325 if($cfg->{os
} eq 'linux') {
326 open IPT
, "$cfg->{iptables} -n -L $cfg->{chain} |";
330 next if ($_ !~ /^DROP/);
331 my ($target, $prot, $opt, $source, $dest, $prot2, $dport) = split(' ', $_);
332 while ( my ($block_ip, $block_time) = each(%timeb) ) {
333 if (($block_ip == $source) && ($block_time < time - $cfg->{unblock_timeout
})) {
334 syslog
('notice', "Unblocking IP address $block_ip.");
335 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.");
336 if( -e
$cfg->{log_ips
} && ((-s
$cfg->{log_ips
}) > 0)) {
339 delete $timeb{$block_ip};
340 delete $timea{$block_ip};
341 delete $count{$block_ip};
348 } elsif($cfg->{os
} eq 'bsd') {
349 open IPT
, "$cfg->{pfctl} -t $cfg->{chain} -T show|" || syslog
('error',"Can't open $cfg->{pfctl} for reading.");
354 while ( my ($block_ip, $block_time) = each(%timeb) ) {
355 if (($block_ip == $source) && ($block_time < time - $cfg->{unblock_timeout
})) {
356 syslog
('notice', "Unblocking IP address $block_ip.");
357 system("$cfg->{pfctl} -t $cfg->{chain} -T delete $block_ip") == 0 || syslog
('notice', "Couldn't unblock $block_ip from firewall.");
358 if( $cfg->{restore_blocked
} == 1) {
361 delete $timeb{$block_ip};
362 delete $timea{$block_ip};
363 delete $count{$block_ip};
371 die("No operating system specified in blocksshd.conf configuration file.");
374 alarm( ($cfg->{unblock_timeout
}/2) );
378 my $rwhiteList = shift @_; # $cfg->{whitelist}
379 my $sn = Net
::Subnets
->new;
381 if (ref($rwhiteList) eq 'ARRAY') {
382 my @subnets = map { chomp $_; &trim
($_); } @
{$rwhiteList};
383 @subnets = grep(!/^#|^$/, @subnets);
384 my $p_sn='^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$';
385 my @bad_subnets = grep(!/$p_sn/, @subnets);
387 if (scalar(@bad_subnets) > 0) {
388 die "The whilelist holds invalid subnet entries: " .
389 join(', ', @bad_subnets) . "\n";
392 @subnets = grep(/$p_sn/, @subnets);
393 $sn->subnets( \
@subnets );
401 $str =~ s/^[\s\r\n]+//;
402 $str =~ s/[\s\r\n]+$//;
407 my $IP = shift or syslog
('notice',"Can't get ip to log!\n");
409 if( -e
$cfg->{log_ips
} && ((-s
$cfg->{log_ips
}) > 0)) {
410 open LOG
,"<$cfg->{log_ips}" || syslog
('notice',"Can't open $cfg->{log_ips}\n");
421 open LOG
,">>$cfg->{log_ips}" || syslog
('notice',"Can't open $cfg->{log_ips}\n");
428 my $block_ip = shift or die("Can't get IP to unlog!\n");
431 if( -e
$cfg->{log_ips
} && ((-s
$cfg->{log_ips
}) > 0)) {
433 tie
@file, 'Tie::File', $cfg->{log_ips
};
434 @file=grep { $_ ne $block_ip } @file;
437 syslog
('notice',"Removed unblocked IP address ($block_ip) from log file $cfg->{log_ips}");
441 sub restore_blocked
{
442 if( -e
$cfg->{log_ips
} && ((-s
$cfg->{log_ips
}) > 0)) {
443 open RLOG
,"<$cfg->{log_ips}" || syslog
('notice',"Can't open $cfg->{log_ips}\n");
446 if(/$work->{ipv4}|$work->{fqdn}/i) {
447 syslog
('notice',"Blocking IP $_ - previously blocked and saved in $cfg->{log_ips}");
451 syslog
('notice',"Invalid IP address ($_) found in $cfg->{log_ips}");
459 # send notification emails
460 my ($IP) = shift or die "Missing IP address!\n";
462 syslog
('notice', "Sending notification email to $cfg->{email}");
464 if($cfg->{email_whois_lookup
} == 1) {
465 $whois = `$cfg->{whois} $IP|$cfg->{sed} -e 's/\"/\\"/g'`;
467 system("echo \"$work->{hostname}: BlockSSHD blocking $IP\n\n $whois\" | $cfg->{mail} -s 'BlockSSHD blocking notification' $cfg->{email}");
471 if (-e
$cfg->{pid_file
})
473 my $pid=`/bin/cat $cfg->{pid_file}`;
474 system("/bin/kill -9 $pid");
475 unlink($cfg->{pid_file
});
476 die "BlockSSHD exiting\n";
478 die "BlockSSHD is not running!\n";
483 print "BlockSSHD command line options\n";
484 print "-d | --daemon | --start Start BlockSSHD in daemon mode\n";
485 print "--stop Stop BlockSSHD\n";
486 print "-h | --help Print this help text\n";
487 print "-v | --version Display version\n";