"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 }