Initial commit. Original version.
[blocksshd_00z.git] / blocksshd
blob07382b80dc265148cbd9f4ffba8d4edaedec949b
1 #!/usr/bin/perl -w
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/
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.
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.
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
25 use strict;
26 use warnings;
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;
36 use POSIX qw(setsid);
37 use vars qw($opt_d $opt_h $opt_v $opt_stop);
39 $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin';
41 my $version = "1.3";
43 our $cfg;
45 # This is where the configuration file is located
46 require '/etc/blocksshd.conf';
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
54 $cfg->{'whitelist_prepared'}=&loadwhitelist($cfg->{whitelist});
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);
63 if ($opt_d) {
64 if (-e $cfg->{pid_file})
65 { die "BlockSSHD is already running!\n" }
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: $!";
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;
83 if ($opt_stop) {
84 exithandler();
87 if ($opt_h) {
88 print_help();
89 exit;
92 if ($opt_v) {
93 print "BlockSSHD version $version\n";
94 exit;
97 openlog('blocksshd', 'pid', 'auth');
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;
105 # Watch signals
107 $SIG{'ALRM'} = \&unblock;
108 $SIG{'INT'} = \&exithandler;
109 $SIG{'QUIT'} = \&exithandler;
110 $SIG{'KILL'} = \&exithandler;
111 $SIG{'TERM'} = \&exithandler;
113 # Notify of startup
115 syslog('notice', "Starting BlockSSHD");
117 # Create iptables chain
119 setup();
121 # Clear existing rules
123 flush();
125 # Restore previously blocked rules
127 if($cfg->{restore_blocked} == 1) {
128 restore_blocked();
131 # The core process
133 my $ref=tie *FH, "File::Tail", (name=>$cfg->{logfile},
134 maxinterval=>$cfg->{logcheck},
135 interval=> 10,
136 errmode=> "return");
138 if ( $cfg->{unblock} == 1) {
139 alarm( ($cfg->{unblock_timeout} /2) );
142 while (<FH>) {
143 if( $alarmed ) {
144 $alarmed = 0;
145 next;
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
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);
174 } else {
175 block($IP);
181 closelog();
183 sub block {
184 # Confirm iptables table is created
185 setup();
187 my ($IP) = shift or die "Missing IP address!\n";
189 # check to see if IP address already blocked
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;
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;
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};
209 $timea{$IP} = time;
211 # increase the total number of failed attempts
212 $count{$IP}++;
214 if ($count{$IP} < $cfg->{max_attempts}+1) {
215 syslog('notice', "$IP was logged with a total count of $count{$IP} failed attempts");
217 if ($count{$IP} >= $cfg->{max_attempts}+1) {
218 syslog('notice', "IP $IP reached the maximum number of failed attempts!");
219 system_block($IP);
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");
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");
234 $timeb{$IP} = time;
235 # send mail if it is configured
236 if ($cfg->{send_email} eq '1') {
237 notify($IP);
239 if ($cfg->{restore_blocked} eq '1') {
240 log_ip($IP);
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}");
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);
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.");
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});
279 sub unblock {
280 # unblock old IPs based on timeout
281 $alarmed = 1;
283 if($cfg->{os} eq 'linux') {
284 open IPT, "$cfg->{iptables} -n -L $cfg->{chain} |";
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);
297 delete $timeb{$block_ip};
298 delete $timea{$block_ip};
299 delete $count{$block_ip};
304 close IPT;
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.");
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);
319 delete $timeb{$block_ip};
320 delete $timea{$block_ip};
321 delete $count{$block_ip};
326 close IPT;
328 } else {
329 die("No operating system specified in blocksshd.conf configuration file.");
332 alarm( ($cfg->{unblock_timeout}/2) );
335 sub loadwhitelist {
336 my $rwhiteList = shift @_; # $cfg->{whitelist}
337 my $sn = Net::Subnets->new;
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);
345 if (scalar(@bad_subnets) > 0) {
346 die "The whilelist holds invalid subnet entries: " .
347 join(', ', @bad_subnets) . "\n";
350 @subnets = grep(/$p_sn/, @subnets);
351 $sn->subnets( \@subnets );
354 return $sn;
357 sub trim {
358 my $str=shift @_;
359 $str =~ s/^[\s\r\n]+//;
360 $str =~ s/[\s\r\n]+$//;
361 return $str;
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;
376 close LOG;
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;
385 sub unlog_ip {
386 my $block_ip = shift or die("Can't get IP to unlog!\n");
387 my @file;
389 if( -e $cfg->{log_ips} && ((-s $cfg->{log_ips}) > 0)) {
391 tie @file, 'Tie::File', $cfg->{log_ips};
392 @file=grep { $_ ne $block_ip } @file;
393 untie @file;
395 syslog('notice',"Removed unblocked IP address ($block_ip) from log file $cfg->{log_ips}");
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($_);
408 else {
409 syslog('notice',"Invalid IP address ($_) found in $cfg->{log_ips}");
412 close (RLOG);
416 sub notify {
417 # send notification emails
418 my ($IP) = shift or die "Missing IP address!\n";
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'`;
425 system("echo \"$work->{hostname}: BlockSSHD blocking $IP\n\n $whois\" | $cfg->{mail} -s 'BlockSSHD blocking notification' $cfg->{email}");
428 sub exithandler {
429 if (-e $cfg->{pid_file})
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";
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";