Perl: w00tw00t-Einträge im Apache-Log automatisch für iptables-Firewallregeln verwenden
Im Apache-Errorlog finden sich bei mir immer wieder einige Einträge dieser Art:
„[Wed Apr 07 23:17:01 2010] [error] [client 88.191.61.110] client sent HTTP/1.1 request without hostname (see RFC2616 section 14.23): /w00tw00t.at.ISC.SANS.DFind:“.
Diese Einträge entstehen wohl, wenn Apache von einem Tool namens `dfind‘ gescannt wird. `dfind‘ sucht nach Schwachstellen in der verwendeten Server-Software.
Da solche Log-Einträge ziemlich überhand nahmen, schrieb ich ein kleines Perl-Script, dass ähnlich wie `tail‘ das Apache-Logfile beobachtet und bei einem Eintrag nach entsprechendem Muster („.*request without hostname.*w00tw00t.*“) iptables anweist, die Client-IP zu sperren – falls diese IP ohnehin nicht schon gesperrt ist. Das Script setzt sich selbst in den Hintergrund.
UPDATE: Ich habe das Script angepasst und erweitert. Um eigene Logformat-Einträge mit diesem Script durchsuchen zu lassen, kann über die Variablen $columnnumber und $ipdelimiter die Spalte identifiziert werden, in der sich die Client-IP befindet. Außerdem können jetzt mehrere Suchmuster nach verdächtigen Log-Einträgen angegeben werden.
Das Perl-Script verwendet die Pakete File::Tail, Net::Whois::IANA und Log::Log4perl, die unter Umständen nicht installiert sind.
Diese können eventuell vom Paketverwaltungssystem des Betriebssystems installiert werden, alternativ aber natürlich auch über `cpan':
cpan[1]> install File::Tail cpan[2]> install Net::Whois::IANA cpan[3]> install Log::Log4perl
Sollten die Paketinstallation über cpan fehlschlagen, kann versucht werden, mit `force install File::Tail‘ die Installation zu erzwingen.
Hier ist der Code des kleinen Scripts, ausgeführt mit Perl v5.16.1 (gentoo Linux):
#!/bin/env perl ######################################################################### use strict; use warnings; use File::Tail; use Net::Whois::IANA; use Log::Log4perl qw(:easy); use POSIX qw(setsid); ######################################################################### # Konfiguration my $bin_iptables = '/sbin/iptables'; my $bin_grep = '/bin/grep'; my $bin_gawk = '/bin/gawk'; my $working_dir = '/tmp'; my $logfile = '/chroot/apache/var/log/apache2/error_log'; my $columnnumber = 10; # apache2.2: 7 my @ipdelimiter = ( '^', ':\d*\]$' ); # apache2.2: ( '^', ':\]$' ) my @regexp_searches = ( '.*request without hostname.*w00tw00t.*', '.*Invalid method in request.*' ); my @ignore_ips = ( '64.4.11.37' ); my $log_conf = q( log4perl.rootLogger = DEBUG, LOG1 log4perl.appender.LOG1 = Log::Log4perl::Appender::File log4perl.appender.LOG1.filename = /var/log/w00tw00t.log log4perl.appender.LOG1.mode = append log4perl.appender.LOG1.layout = Log::Log4perl::Layout::PatternLayout log4perl.appender.LOG1.layout.ConversionPattern = %d %p %m %n ); ######################################################################### my ($line, $ip); my (@parts, @ipparts, @already_blocked); my ($iana) = new Net::Whois::IANA; Log::Log4perl->init(\$log_conf); ######################################################################### sub block_ip { my $ip = shift; my $regexp = shift; my $logger = get_logger(); if (system($bin_iptables." -I INPUT 1 -s ".$ip." -j DROP") != 0) { $logger->error("Failed executing command `".$bin_iptables." -I INPUT 1 -s ".$ip." -j DROP`"); } else { $iana->whois_query(-ip=>$ip); $logger->warn($ip." from ".$iana->country()." (".$iana->descr().") blocked (matched: '".$regexp."')"); push(@already_blocked, $ip); } } sub check_line { # Example line (apache 2.2): # [Wed Apr 07 23:17:01 2010] [error] [client 88.191.61.110] client sent HTTP/1.1 request \ # without hostname (see RFC2616 section 14.23): /w00tw00t.at.ISC.SANS.DFind:) # Example line (apache 2.4): # [Mon Nov 11 10:23:03.940219 2013] [core:error] [pid 19468:tid 139718641649424] \ # [client 80.86.84.72:36116] AH00135: Invalid method in request /horde/util/barcode.php\ # ?type=../../../../../../../../../../../var/log/psa-horde/psa-horde.log%00 HTTP/1.1, referer: 1 my $line = shift @_; my $logger = get_logger(); foreach my $regexp_search (@regexp_searches) { if ($line =~ m/$regexp_search/) { @parts = split(/\s+/, $line); if (defined($parts[$columnnumber])) { if ($parts[$columnnumber] =~ m/\d*\.\d*\.\d*\.\d*.*/) { ($ip) = $parts[$columnnumber] =~ m/$ipdelimiter[0](\d*\.\d*\.\d*\.\d*)$ipdelimiter[1]/; if ($ip) { if (!(grep $_ eq $ip, @ignore_ips)) { if (!(grep $_ eq $ip, @already_blocked)) { block_ip($ip, $regexp_search); } else { $logger->info($ip." already blocked (matched: '".$regexp_search."')"); } } else { $logger->info($ip." is in ignore list (matched: '".$regexp_search."')"); } } } } } } } sub get_already_blocked_ips { my @output = `$bin_iptables -vnL INPUT | $bin_grep DROP | $bin_gawk '{ print \$8 }'`; foreach my $line (@output) { if ($line =~ m/^\d*\.\d*\.\d*\.\d*$/) { push(@already_blocked, substr($line, 0, -1)); } } } sub daemonize { chdir($working_dir) or die("Can't chdir to $working_dir: $!"); open(STDIN, '/dev/null') or die("Can't read /dev/null: $!"); open(STDOUT, '>>/dev/null') or die("Can't write to /dev/null: $!"); open(STDERR, '>>/dev/null') or die("Can't write to /dev/null: $!"); defined(my $pid = fork()) or die("Can't fork: $!"); exit() if $pid; setsid() or die("Can't start a new session: $!"); umask(0); } ######################################################################### $| = 1; &daemonize(); while (1) { my $tailed_logfile = File::Tail->new(name => $logfile, tail => -1, maxinterval => 20); get_already_blocked_ips(); while(defined($line = $tailed_logfile->read)) { check_line($line); } sleep(30); }
Im Logfile kann man nun sehen, wann Einträge erfasst worden sind. In iptables sollten die entsprechenden IP-Adressen dann geblockt werden:
Man sollte allerdings hin und wieder die Firewall-Regeln resetten, da ansonsten auch Besucher ausgesperrt werden, die durch Zuweisung einer geblockten dynamischen IP-Adresse „fälschlicherweise“ betroffen sind.