#!/usr/bin/perl -w use strict; ## Scott Wiersdorf ## Created: Tue Jan 3 09:25:34 MST 2006 ## $Id: rascals,v 1.12 2007/01/02 17:28:48 scott Exp $ ## rascals - tarpit via hosts.allow for dictionary attacks ## usage: rascals [selection options] [action options] log our $VERSION = '1.10'; use Time::Local qw(timelocal); use Getopt::Long; my %opt = ( report => '', exclude => [], service => 'sshd', period => '5m', limit => 5, pattern => '^Failed password for (?:root|(?:[Ii]nvalid|[Ii]llegal) user \S+) from (\S+)', expire => '2h', rascals => '/var/log/rascals', ); unless( GetOptions( \%opt, 'help|h', 'report=s', 'version', 'exclude:s', \@{$opt{exclude}}, 'service|s=s', 'period|p=s', 'limit|l=i', 'pattern=s', 'expire|e=s', 'rascals|r=s', ) ) { usage(); } usage() if $opt{help}; die "This is rascals version $VERSION\n" if $opt{version}; $opt{pattern} = qr($opt{pattern})io; my %Moy = ( Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5, Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11 ); my $time = time; my ($c_month, $c_year) = (localtime($time))[4,5]; my $threshold = $time - to_seconds($opt{period}); my $expires = $time + to_seconds($opt{expire}); my %exclude_hosts = (); @exclude_hosts{@{$opt{exclude}}} = (1) x @{$opt{exclude}}; my %hosts = (); my $line = 0; my $last_month = 0; while( <> ) { chomp; ## look for lines like this: ## Jan 2 23:32:33 deep2 sshd[5482]: Invalid user staff from 210.240.17.26 ## this is fast my($mon,$day,$hr,$min,$sec,$srv,$msg) = $_ =~ /^(\w+)\s+ ## month (\d+)\s+ ## day (\d+):(\d+):(\d+)\s+ ## hh:mm:ss \S+\s+ ## skip hostname ([\w\.-]+)\[\d+\]:\s+ ## service (skip [pid]:) (.+)/x ## message or next; SPAN_YEAR_CHECK: { ## first line: look at the month if ( $line++ == 0 ) { if( $Moy{$mon} > $c_month ) { ## a log that spans last year $c_year -= 1; ## decrement the year } } ## subsequent lines: look for the new year else { if( $Moy{$mon} < $last_month ) { $c_year += 1; } } $last_month = $Moy{$mon}; } ## look for this service next unless $srv eq $opt{service}; ## look for message pattern next unless my($host) = $msg =~ $opt{pattern}; next if $exclude_hosts{$host}; ## do time conversion my $line_t = timelocal($sec,$min,$hr,$day,$Moy{$mon},$c_year); ## reject lines too old next if $line_t < $threshold; ## count hosts $hosts{$host}++; } ## write out to the expiration log my $tmp_rascals = $opt{rascals} . ".$$.tmp." . int(rand(999999)); die "$tmp_rascals exists! Exiting\n" if -e $tmp_rascals; open TMP, ">", $tmp_rascals or die "Could not write to temp file: $!\n"; ## preen hosts under the limit my @hosts = (); for my $host ( keys %hosts ) { next if $hosts{$host} < $opt{limit}; push @hosts, $host; } ## add these hosts to the new rascals file if( scalar @hosts ) { print TMP "## expires: $expires (" . scalar localtime($expires) . ")\n"; print TMP $_ . "\n" for @hosts; print TMP "\n"; if( $opt{report} =~ /^(?:new|all)/i ) { print STDERR "New rascals (expire " . scalar localtime($expires) . ")\n"; print STDERR $_ . "\n" for @hosts; print STDERR "\n"; } } ## remove expired rascals if( open LOG, "<", $opt{rascals} ) { my $skip = 0; while( ) { if( my ($expire) = $_ =~ /^\#\# expires: (\d+)/ ) { $skip = $time > $expire; if( $skip ) { if( $opt{report} =~ /^(?:old|all)/i ) { print STDERR "Expired rascals (" . scalar localtime($expire) . "):\n"; next; } } } ## normal line if( $skip ) { if( $opt{report} =~ /^(?:old|all)/i ) { print STDERR; } next; } print TMP; } close LOG; } close TMP; rename $tmp_rascals, $opt{rascals}; exit; sub to_seconds { my $ts = shift; my ($tm,$unit) = $ts =~ /^(\d+)([dhms]?)/; $unit ||= 'm'; if( $unit eq 'd' ) { $tm *= ( 60 * 60 * 24 ); } elsif( $unit eq 'h' ) { $tm *= ( 60 * 60 ); } elsif( $unit eq 'm' ) { $tm *= 60; } return $tm; } sub usage { die <<'_USAGE_'; rascals [selection options] [action options] auth.log 'rascals' scans recent lines in auth.log for lines matching: (date) (hostname) (service)[pid]: Invalid user (user) from (remote host) and adds 'remote host' to /var/log/rascals for denial from /etc/hosts.allow. You'll need to add this one line to the top of your /etc/hosts.allow file: sshd : /var/log/rascals : deny selection options: --log=logfile which logfile to look for rascals --exclude=host exclude this host when looking for rascals. May be specified multiple times --service=srv service as found in auth.log (default = sshd) --period=time length of time to query log in minutes (default = 5m) --limit=num maximum allowed 'invalid user' errors per host (default = 5) --pattern=message experts only: specify a different log message pattern action options: --expire=time remove entries older than 'time' (default = 2h) --rascals=filename write to this file (default = /var/log/rascals) See 'perldoc rascals' for complete documentation. _USAGE_ } =head1 NAME rascals - find and blacklist rascally twerps =head1 SYNOPSIS rascals --exclude=231.58.192.122 --period=1h --limit=30 --expire=1d auth.log =head1 DESCRIPTION B reads F looking for ssh dictionary attackers ("rascals") and adds the offending host to a blacklist. This list may be used by the "tcp wrappers" subsystem (via F) to block or tarpit attacking hosts. B automatically removes older entries in the F blacklist each time it runs, based on the expiration date when the entries were added. B is designed to run with no maintenance: install and forget. B may be run from the command-line as root, or from a root crontab (see L and L, especially the B below). Nearly all of the default behavior of B can be changed (see L below). To make use of the F blacklist using tcp wrappers, add the following line to the top of your F file: sshd : /var/log/rascals : deny See L below for some other interesting F options. =head1 INSTALLATION For the impatient, here is the checklist of things that must be done for B to maintain its blacklist automatically: =over 4 =item * install B Use the installer provided, or just copy B to your favorite system executable directory (e.g., F). Make sure B is executable: chmod 0755 /usr/local/sbin/rascals =item * add F entry Something like this in F: sshd : /var/log/rascals : deny =item * add F entry Here's a sample crontab entry to run B every half-hour (make sure the path to F matches your installation): */30 * * * * root /usr/local/sbin/rascals /var/log/auth.log The author runs B every 2 minutes, with a period of 2m so no evil-doers miss an opportunity to be trounced: */2 * * * * root /usr/local/sbin/rascals --period=2m /var/log/auth.log =back That's it--no fuss, no muss. =head1 OPTIONS In a nutshell, B works like this: Rascals goes through the last 5 minutes ("period") of the given log looking for 5 entries ("limit") matching sshd ("service") requests for "Illegal user" ("pattern") and adds the offending IP to the rascals log ("rascals") for a period of 2 hours ("expire"). All options are configurable: you can change the period (how much of the log to examine), the limit (how many times an offense must occur before they're blacklisted), the service (defaults to sshd, but could be any service, such as 'sm-mta' or 'ipop3d'), the pattern (what an offense looks like in the log), and the expiration (how long the offending IP will be on the blacklist). This section describes in some detail your options. =head2 General Options =over 4 =item B Shows a brief help message and exits. =item B Writes a brief report to STDERR when done. This options is useful if you want to get a little reporting from cron or just want to see what happened. If 'new' is specified, new rascals added to the blacklist will be returned (if no output appears, no new entries were added to the blacklist at this time). If 'old' is specified, expiring rascals will be listed (if no output appears, no expiring rascals were removed from the blacklist at this time). 'all' prints both new additions and expirations. If no report type is specified, B runs silently. =item B Shows the current version of B and exits. =back =head2 Selection Options These options determine how B finds hosts that are attacking your server. Please be aware that B only understands log files in standard I format: Jan 10 12:02:38 myhostname someservice[8888]: message from someservice This includes auth.log, maillog, messages, and other common logs written to by B. =over 4 =item B Which hosts to exclude when looking for rascals. This option may be specified multiple times for each host to exclude. E.g.,: rascals --exclude=123.45.67.89 --exclude=132.41.33.12 The B option is a safety valve: put your own IP address here, as well as any addresses from valid clients who might regularly mistype their own password. Default: (empty) =item B Which service to look for in the specified log file when looking for rascals. Examples of 'services' include sshd, sendmail, proftpd, telnet, ipop3d, popper, sm-mta, etc. B can find attacks from any type of service (as long as it can be found in a log file), but tcp wrappers does not wrap all services. Check your host configuration to determine which services can be protected by tcp wrappers. Only exact string matches will work here. To match multiple services you'll need to run B multiple times. This may change in a future release. Default: sshd =item B How far back in F to look for rascals (i.e., entries older than this will be skipped). See L