├── CHANGELOG ├── CREDITS ├── INSTALL ├── README ├── README.md ├── bruteforceblocker.conf └── bruteforceblocker.pl /CHANGELOG: -------------------------------------------------------------------------------- 1 | BruteForceBlocker v1.2.6 - Nov 3 2018 2 | - add new regexps to match more failure log entries of recent OpenSSH versions 3 | - resolve reverse DNS of blocked IPs 4 | 5 | BruteForceBlocker v1.2.5 - Feb 11 2018 6 | - add a new regexp to match another failure log entry 7 | - contributed by Yasuhiro KIMURA 8 | 9 | BruteForceBlocker v1.2.4 - Sep 2 2017 10 | - add a new regexp to match failure log entries of recent OpenSSH versions 11 | - contributed by Max Khon 12 | 13 | BruteForceBlocker v1.2.3 - Mar 6 2006 14 | - fixed regexp to match fqdn 15 | - remove configuration directive of location of pf.conf, instead add 16 | new IPs from project site via pfctl command 17 | 18 | BruteForceBlocker v1.2.2 - Mar 4 2006 19 | - script now resolves reverse DNS records so it can handle IP logged 20 | as hostname, when used UseDNS yes in sshd_config 21 | - it is possible to configure location of pf.conf file 22 | 23 | BruteForceBlocker v1.2.1 - Jan 1 2006 24 | - fixed bug in download URL of remote IPs from project website 25 | Spotted by: myself after hard new year's eve party :) 26 | - increased mincount default conf value to 2 27 | - Happy new year 2006! 28 | 29 | BruteForceBlocker v1.2 - Nov 12 2005 30 | - code cleanup 31 | - configuration was moved to separate file 32 | - script uses Sys::Syslog perl module to log it's output 33 | - script catches ipv6 addresses 34 | - script is able to send mail when blocking new IP 35 | - script is able to report blocked IP to the project site so IP can be 36 | easily shared with other people (done via LWP::UserAgent module) 37 | - script is able to download the blacklist from project site and 38 | use it. It is possible to download only those IP which were active 39 | in last x days and/or were reported from x source machines 40 | - we have recorded time of IP addition in the table file 41 | - new regexps 42 | 43 | BruteForceBlocker v1.1 - Sep 04 2005 44 | - script is now aware of File::Tail module and uses syslog pipes to 45 | collect data. This makes BruteForceBlocker much faster 46 | - script does not run in background and does not need rc script 47 | - added some new regexps to catch more data 48 | 49 | BruteForceBlocker v1.0 - Apr 12 2005 50 | - First public release 51 | - script uses File::Tail perl module to collect data 52 | - script runs daemonized in background and is started from rc script 53 | 54 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | Thanks to: 2 | 3 | - Jonas Davidsson and Matt Pearce for good ideas 4 | - J.R. Oldroyd for great ideas, he is also one of the reasons why 5 | BruteForceBlocker v1.2 was released 6 | - Branislav Gerzo for some perl coding hints and help with code cleanup 7 | - Kan Sasaki who sent me a patch for BruteForceBlocker which allows to 8 | resolve reverse DNS to an IP address 9 | - Max Khon for regexp to match recent OpenSSH failure log entries 10 | - Yasuhiro KIMURA for regexp to match another failure log entry 11 | - Balazs Mateffy for new regexps to match more failure log entr and 12 | an idea to resolve reverse DNS records for blocked IPs 13 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | 2 | INSTALLATION 3 | 4 | o) You will need to add a new table to the pf config file. You can do so 5 | by adding lines to the pf.conf similar to these: 6 | 7 | table persist file "/var/db/ssh-bruteforce" 8 | block in log quick proto tcp from to any port ssh 9 | 10 | o) You will also need to add new line into your /etc/syslog.conf so it 11 | will look like this: 12 | 13 | auth.info;authpriv.info | exec /usr/local/sbin/bruteforceblocker 14 | 15 | - note that you should keep the original line, otherwise you will lose 16 | your sshd's logs - e.g. they will not be logged. 17 | 18 | o) You should also consider starting syslogd with -c option. 19 | 20 | More information can be found at BruteForceBlocker Project site located 21 | at http://danger.rulez.sk/projects/bruteforceblocker. 22 | 23 | CONFIGURATION 24 | 25 | All configuration is now done in separate file, usually located at 26 | /usr/local/etc/bruteforceblocker.conf. If not, you have to edit main 27 | script and put there full path to the configuration file. 28 | 29 | Most of values in this config should be sufficient for your 30 | requirements, but there's no problem to tweak them. 31 | 32 | o) email - email address where will be send mails when a new IP will be 33 | blocked. To disable emailing, set to undef, or '' 34 | o) table - name of the pf table in pf.conf 35 | o) tablefile - location of table on filesystem, specified in pf.conf 36 | o) max_attempts - maximum number of failed tries before blocking IP 37 | o) timeout - number of seconds of inactivity needed before resetting IP 38 | counter 39 | o) report - if the reporting should be enabled or not. Reporting is 40 | good to share your catched attackers with other people 41 | o) debug - enable/disable debugging 42 | o) use_remote - set to 1 if you want BruteForceBlocker to download 43 | blacklist of IPs catched by other users and load them 44 | to pf table 45 | o) mindays - use only those IPs from blacklist which were reported in 46 | last x days 47 | o) mincount - use only those IPs which were reported from at least x 48 | different source machines 49 | o) mail - location of mail binary used to send mails 50 | o) pfctl - location of pfctl binary 51 | o) whitelist - list of IPs that will never get blocked 52 | 53 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | BruteForceBlocker v1.2.6 2 | 3 | BruteForceBlocker is a perl script, that works along with pf - OpenBSD's 4 | firewall (which is also available on FreeBSD and NetBSD) and its main 5 | purpose is to block SSH bruteforce attacks via firewall. 6 | 7 | When this script is running, it checks sshd logs from syslog and looks 8 | for failed login attempts - mostly some annoying script attacks, and 9 | counts number of such attempts. 10 | 11 | When given IP reaches configured limit of fails, script puts this IP to 12 | the pf's table and blocks any further traffic from the given IP. 13 | 14 | Furthermore, the blocked IP is reported to the project site which 15 | enables users to share a list of abusive IPs. The list is publicly 16 | available at http://danger.rulez.sk/projects/bruteforceblocker/blist.php 17 | 18 | If you are bored of those automated auth tries, you will be happy with 19 | this script. BruteForceBlocker is easy to use, simple, and effective. 20 | 21 | For installation instructions see INSTALL file. 22 | 23 | WWW: http://danger.rulez.sk/index.php/bruteforceblocker/ 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /bruteforceblocker.conf: -------------------------------------------------------------------------------- 1 | # vim: syntax=perl 2 | 3 | $cfg = { 4 | email => 'root', # undef or '' to disable mailing 5 | table => 'bruteforce', # name of pf table 6 | tablefile => '/var/db/ssh-bruteforce', # file where table persist 7 | max_attempts => 3, # number of max allowed fails 8 | timeout => 3600, # number of seconds after resetting of ip 9 | report => 1, # report blocked IPs to project site? 10 | debug => 0, # to enable, set to 1 11 | use_remote => 1, # get blacklist from project site? 12 | mindays => 365, # use IPs from project blacklist that were reported in last mindays days 13 | mincount => 2, # use IPs that were reported at least from mincount source boxes 14 | 15 | mail => '/usr/bin/mail', # location of mail binary 16 | pfctl => '/sbin/pfctl', # location of pfctl binary 17 | 18 | # whitelist - list of IPs that will never be blocked 19 | whitelist => [qw{ 20 | 127.0.0.1 21 | 172.16.0.2 22 | 10.10.1.4 23 | }], 24 | }; 25 | 26 | #leave 1; here! 27 | 1; 28 | -------------------------------------------------------------------------------- /bruteforceblocker.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # BruteForceBlocker by Daniel Gerzo 4 | 5 | use strict; 6 | use warnings; 7 | 8 | use Sys::Syslog; 9 | use Sys::Hostname; 10 | use LWP::UserAgent; 11 | use Net::DNS::Resolver; 12 | 13 | $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin'; 14 | our $cfg; 15 | 16 | # this is where configuration file is located 17 | require '/usr/local/etc/bruteforceblocker.conf'; 18 | 19 | my $work = { 20 | version => '1.2.6', 21 | ipv4 => '(?:\d{1,3}\.){3}\d{1,3}', # regexp to match ipv4 address 22 | ipv6 => '[\da-fA-F:]+', # regexp to match ipv6 address 23 | fqdn => '[\da-z\-.]+\.[a-z]{2,4}', # regexp to match fqdn 24 | hostname => hostname, # get hostname from Sys::Hostname module 25 | projectsite => 'http://danger.rulez.sk/projects/bruteforceblocker', 26 | }; 27 | 28 | openlog('BruteForceBlocker', 'pid', 'auth'); 29 | 30 | if ($cfg->{use_remote}) { 31 | # load IPs from existing table to the @localIPs array 32 | open(TABLE, $cfg->{tablefile}) or syslog("notice", "Couldn't open $cfg->{tablefile} for reading"); 33 | while () { 34 | push(@{$work->{localIPs}}, $1) if /^($work->{ipv4}|$work->{ipv6})/; 35 | } 36 | close(TABLE) or syslog("notice", "Couldn't close $cfg->{tablefile}"); 37 | 38 | syslog('notice', 'downloading blacklist from the project site') if $cfg->{debug}; 39 | 40 | # download the list from project site and load IPs to @remoteIPs array 41 | if ( my $content = download("$work->{projectsite}/blist.php?mindays=$cfg->{mindays}&mincount=$cfg->{mincount}") ) { 42 | while( $content =~ /^($work->{ipv4}|$work->{ipv6})/gm ) { 43 | push(@{$work->{remoteIPs}}, $1); 44 | } 45 | } else { 46 | syslog('notice', "Unable to download IP blacklist from the project site") if $cfg->{debug}; 47 | } 48 | 49 | # get IPs that we don't have in local pf table 50 | my %seen = (); 51 | @seen{@{$work->{localIPs}}} = () if exists $work->{localIPs}; 52 | 53 | foreach my $IP (@{$work->{remoteIPs}}) { 54 | push(@{$work->{newa}}, $IP) unless exists $seen{$IP}; 55 | } 56 | 57 | # add them to the table 58 | $work->{timea} = scalar(localtime); 59 | foreach my $IP (@{$work->{newa}}) { 60 | if (!grep { /$IP/ } @{$cfg->{whitelist}}) { 61 | $work->{pool} = $IP . '/32' if ($IP =~ /\./); # block whole ipv4 pool 62 | $work->{pool} = $IP . '/128' if ($IP =~ /\:/); # block while ipv6 pool 63 | syslog('notice', "adding $IP to the $cfg->{table} table and firewall") if $cfg->{debug}; 64 | system("echo '$work->{pool}\t\t# added from project site at $work->{timea}' >> $cfg->{tablefile}") == 0 || 65 | syslog('notice', "Couldn't add $work->{pool} from project site to $cfg->{tablefile}"); 66 | system("$cfg->{pfctl} -t $cfg->{table} -T add $work->{pool}") == 0 || 67 | syslog('notice', "Couldn't add $work->{pool} to firewall"); 68 | } 69 | } 70 | syslog('notice', 'blacklist synchronized with the project site') if $cfg->{debug}; 71 | } 72 | 73 | my %count = (); # hash used to store total number of failed tries 74 | my %timea = (); # hash used to store last time when IP was active 75 | my $res = Net::DNS::Resolver->new; 76 | 77 | # the core process 78 | 79 | while (<>) { 80 | if (/.*Failed password.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}) port.*/i || 81 | /.*Failed keyboard.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}) port.*/i || 82 | /.*Invalid user.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn})$/i || 83 | /.*Did not receive identification string from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn})$/i || 84 | /.*Bad protocol version identification .* from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn})$/i || 85 | /.*User.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}) not allowed because.*/i || 86 | /.*error: maximum authentication attempts exceeded for.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}).*/i || 87 | /.*error: PAM: authentication error for.*from ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}).*/i || 88 | /.*fatal: Unable to negotiate with ($work->{ipv4}|$work->{ipv6}|$work->{fqdn}).*/i) { 89 | 90 | my $IP = $1; 91 | if ($IP =~ /$work->{fqdn}/i) { 92 | foreach my $type (qw(AAAA A)) { 93 | my $query = $res->search($IP, $type); 94 | if ($query) { 95 | foreach my $rr ($query->answer) { 96 | block($rr->address); 97 | } 98 | } 99 | } 100 | } else { 101 | block($IP); 102 | } 103 | } 104 | } 105 | 106 | closelog(); 107 | 108 | sub download { 109 | my $url = shift or die "Need url!\n"; 110 | # create useragent 111 | my $ua = LWP::UserAgent->new( 112 | agent => "BruteForceBlocker v$work->{version}", 113 | timeout => 10 114 | ); 115 | # send request 116 | my $res = $ua->get($url); 117 | 118 | # check the outcome 119 | if ($res->is_success) { 120 | return $res->content; 121 | } else { 122 | syslog('notice', "Error: " . $res->status_line); 123 | } 124 | } 125 | 126 | sub block { 127 | my ($IP) = shift or die "Need IP!\n"; 128 | 129 | my $query = $res->search($IP, "PTR"); 130 | 131 | while ($query && ($query->answer)[0]->type eq "CNAME") { 132 | $query = $res->search(($query->answer)[0]->cname, "PTR"); 133 | } 134 | 135 | my $RDNS = ($query && ($query->answer)[0]->type eq "PTR") ? ($query->answer)[0]->ptrdname : "not resolved"; 136 | 137 | if ($timea{$IP} && ($timea{$IP} < time - $cfg->{timeout})) { 138 | syslog('notice', "resetting $IP ($RDNS) count, since it wasn't active for more than $cfg->{timeout} seconds") if $cfg->{debug}; 139 | delete $count{$IP}; 140 | } 141 | $timea{$IP} = time; 142 | 143 | # increase the total number of failed attempts 144 | $count{$IP}++; 145 | 146 | if ($cfg->{debug} && ($count{$IP} < $cfg->{max_attempts}+1)) { 147 | syslog('notice', "$IP ($RDNS) was logged with total count of $count{$IP} failed attempts"); 148 | } 149 | 150 | if ($count{$IP} == $cfg->{max_attempts}+1) { 151 | syslog('notice', "IP $IP ($RDNS) reached maximum number of failed attempts!") if $cfg->{debug}; 152 | if (!grep { /$IP/ } @{$cfg->{whitelist}}) { 153 | $work->{pool} = $IP . '/32' if ($IP =~ /\./); # block whole ipv4 pool 154 | $work->{pool} = $IP . '/128' if ($IP =~ /\:/); # block while ipv6 pool 155 | $work->{timea} = scalar(localtime); 156 | 157 | syslog('notice', "blocking $work->{pool} in pf table $cfg->{table}."); 158 | system("$cfg->{pfctl} -t $cfg->{table} -T add $work->{pool}") == 0 || 159 | syslog('notice', "Couldn't add $cfg->{pool} to firewall"); 160 | system("$cfg->{pfctl} -k $IP") == 0 || 161 | syslog('notice', "Couldn't kill all states for $IP"); 162 | system("echo '$work->{pool}\t\t# $work->{timea} ($RDNS)' >> $cfg->{tablefile}") == 0 || 163 | syslog('notice', "Could't write $work->{pool} to $cfg->{table}'s table file"); 164 | 165 | # send mail if it is configured 166 | if ($cfg->{email} && $cfg->{email} ne '') { 167 | syslog('notice', "sending email to $cfg->{email}") if $cfg->{debug}; 168 | open(MAIL, "| $cfg->{mail} -s '$work->{hostname}: BruteForceBlocker blocking $work->{pool} ($RDNS)' $cfg->{email}"); 169 | print (MAIL "BruteForceBlocker blocking $work->{pool} ($RDNS) in pf table $cfg->{table}\n"); 170 | close(MAIL); 171 | } 172 | ; 173 | 174 | # report blocked IP if it is enabled 175 | if ($cfg->{report}) { 176 | syslog('notice', "Reporting $IP to BruteForceBlocker project site") if $cfg->{debug}; 177 | download("$work->{projectsite}/report.php?ip=$IP"); 178 | } 179 | } else { 180 | syslog('notice', "...but $IP is whitelisted, so we will not block it!") if $cfg->{debug}; 181 | } 182 | } 183 | } 184 | --------------------------------------------------------------------------------