#!/usr/bin/perl -w # httprot.pl, "request for comments" version # run as: tail -f /usr/local/apache/logs/access_log | ./ httprot.pl # Configuration starts here # for "combined" apache log format, unauthorized request my $AUTH_REQ='^([^ ]+) ([^ ]+) ([^ ]+) \[([^\]]+)\] "([^"]+)" 401'; # host ident user date request authrequired my $threshold=5; # allow $threshold unauthorized requests my $interval=5; # within $interval seconds without alerting my $timeout=15*60; # block attacker for $timeout seconds my $syslog_facility='auth'; # when alerting, use these my $syslog_priority='alert'; # facility and priority # Configuration finishes here use strict; use Date::Parse; use Date::Format; use Sys::Syslog; my %hit=(); my %sum=(); my %blocked=(); my ($host,$date,$time,$request); openlog('httprot','pid',$syslog_facility); $SIG{INFO}=\&print_hit; $SIG{ALRM}=\&cleanup; alarm $interval; # main loop while(<>) { chomp; next unless /$AUTH_REQ/; $host=$1; $date=$4; $time=str2time($date); $request=$5; # $hit{$time}{$host} is number of bad requests from $host # within second number $time (unixtime) if(defined($hit{$time})) { $hit{$time}{$host}++; } else { $hit{$time}{$host}=1; } # $sum{$host} is number of bad requests from $host # within last $interval sec (roughly) if(defined($sum{$host})) { $sum{$host}++; } else { $sum{$host}=1; } &block if(!defined($blocked{$host}) && ($sum{$host}>=$threshold)); } # for testing purposes: hit ctrl-t to see current stats sub print_hit { my $rest=alarm(0); # trying to reduce time leak syslog($syslog_priority,"Current state:"); my ($time,$host); my %list; foreach $time (keys(%hit)) { print time2str('%x %T',$time),":\n"; %list=%{$hit{$time}}; foreach $host (keys(%list)) { syslog($syslog_priority,"$host: $hit{$time}->{$host}"); } } syslog($syslog_priority,"Summary:"); foreach $host (keys(%sum)) { syslog($syslog_priority,"$host: $sum{$host}, blocked until " . time2str('%x %T',$blocked{$host})) if defined($blocked{$host}); } alarm($rest); # but will leak up to 1 second per SIGINFO } # free memory - throw away expired stats every $interval seconds sub cleanup { my ($h,$s); my %hosts; my $now=time(); my $limit=$now-$interval; foreach $s (keys(%hit)) { if($s<$limit) { # entries older than $limit have expired %hosts=%{$hit{$s}}; foreach $h (keys(%hosts)) { # correct sum when expiring stats $sum{$h}-=$hit{$s}{$h}; delete $sum{$h} unless $sum{$h}>0; # free memory } delete $hit{$s}; # again, free memory } } foreach $h (keys(%blocked)) { unblock($h) unless $blocked{$h}>$now; } alarm $interval; # restart timer } # site dependent action; this example is harmless sub block { $blocked{$host}=time()+$timeout; syslog($syslog_priority, "%d unauthorized requests from %s, last(%s): %s", $sum{$host},$host,$date,$request); system("/sbin/ipfw table 1 add $host/32"); } sub unblock { my $host=shift; delete $blocked{$host}; syslog($syslog_priority,"host %s unblocked",$host); system("/sbin/ipfw table 1 delete $host/32"); }