Viewing file:
httpd-guardian.pl (15.96 KB) -rwxr-xr-xSelect action/file-type:

(
+) |

(
+) |

(
+) |
Code (
+) |
Session (
+) |

(
+) |
SDB (
+) |

(
+) |

(
+) |

(
+) |

(
+) |

(
+) |
#!/usr/bin/perl -w
#
# httpd-guardian - detect DoS attacks by monitoring requests
# Apache Security, http://www.apachesecurity.net
# Copyright (C) 2005 Ivan Ristic <ivanr@webkreator.com>
#
# $Id: httpd-guardian,v 1.6 2005/12/04 11:30:35 ivanr Exp $
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# This script is designed to monitor all web server requests through
# the piped logging mechanism. It keeps track of the number of requests
# sent from each IP address. Request speed is calculated at 1 minute and
# 5 minute intervals. Once a threshold is reached, httpd-guardian can
# either emit a warning or execute a script to block the IP address.
#
# Error message will be sent to stderr, which means they will end up
# in the Apache error log.
#
# Usage (in httpd.conf)
# ---------------------
#
# Without mod_security, Apache 1.x:
#
# LogFormat "%V %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %{UNIQUE_ID}e \"-\" %T 0 \"%{modsec_message}i\" 0" guardian
# CustomLog "|/path/to/httpd-guardian" guardian
#
# or without mod_security, Apache 2.x:
#
# LogFormat "%V %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %{UNIQUE_ID}e \"-\" %T %D \"%{modsec_message}i\" 0" guardian
# CustomLog "|/path/to/httpd-guardian" guardian
#
# or with mod_security (better):
#
# SecGuardianLog "|/path/to/httpd-guardian"
#
# NOTE: In order for this script to be effective it must be able to
# see all requests coming to the web server. This will not happen
# if you are using per-virtual host logging. In such cases either
# use the ModSecurity 1.9 SecGuardianLog directive (which was designed
# for this very purpose).
#
#
# Usage (with Spread)
# -------------------
#
# 1) First you need to make sure you have Spread running on the machine
# where you intend to run httpd-guardian on.
#
# 2) Then uncomment line "use Spread;" in this script, and change
# $USE_SPREAD to "1".
#
# 3) The default port for Spread is 3333. Change it if you want to
# and then start httpd-guardian. We will be looking for messages
# in the Spread group called "httpd-guardian".
# TODO Add support to ignore certain log entries based on a
# regex applied script_name.
#
# TODO Warn about session hijacking.
#
# TODO Track ip addresses, sessions, and individual users.
#
# TODO Detect status code anomalies.
#
# TODO Track accesses to specific pages.
#
# TODO Open proxy detection.
#
# TODO Check IP addresses with blacklists (e.g.
# http://www.spamhaus.org/XBL/).
#
# TODO Is there a point to keep per-vhost state?
#
# TODO Enhance the script to tail a log file - useful for test
# runs, in preparation for deployment.
#
# TODO Can we track connections as Apache creates and destroys them?
#
# TODO Command-line option to support multiple log formats. E.g. common,
# combined, vcombined, guardian.
#
# TODO Command-line option not to save state
#
use strict;
use Time::Local;
# SPREAD UNCOMMENT
# use Spread;
# -- Configuration----------------------------------------------------------
my $USE_SPREAD = 0;
my $SPREAD_CLIENT_NAME = "httpd-guardian";
my $SPREAD_DAEMON = "3333";
my $SPREAD_GROUP_NAME = "httpd-guardian";
my $SPREAD_TIMEOUT = 1;
# If defined, execute this command when a threshold is reached
# block the IP address for one hour.
# $PROTECT_EXEC = "/sbin/blacklist block %s 3600";
# $PROTECT_EXEC = "/sbin/samtool -block -ip %s -dur 3600 snortsam.example.com";
#my $PROTECT_EXEC;
# For testing only:
my $PROTECT_EXEC = "/usr/bin/logger Possible DoS Attack from %s";
# Max. speed allowed, in requests per
# second, measured over an 1-minute period
#my $THRESHOLD_1MIN = 2; # 120 requests in a minute
# For testing only:
my $THRESHOLD_1MIN = 0.01;
# Max. speed allowed, in requests per
# second, measured over a 5-minute period
my $THRESHOLD_5MIN = 1; # 360 requests in 5 minutes
# If defined, httpd-guardian will make a copy
# of the data it receives from Apache
# $COPY_LOG = "";
my $COPY_LOG;
# Remove IP address data after a 10-minute inactivity
my $STALE_INTERVAL = 400;
# Where to save state (at this point only useful
# for monitoring what the script does)
my $SAVE_STATE_FILE = "/tmp/httpd-guardian.state";
# How often to save state (in seconds).
my $SAVE_STATE_INTERVAL = 10;
my $DEBUG = 0;
# -----------------------------------------------------------------
my %months = (
"Jan" => 0,
"Feb" => 1,
"Mar" => 2,
"Apr" => 3,
"May" => 4,
"Jun" => 5,
"Jul" => 6,
"Aug" => 7,
"Sep" => 8,
"Oct" => 9,
"Nov" => 10,
"Dec" => 11
);
# -- log parsing regular expression
# 127.0.0.1 192.168.2.11 - - [05/Jul/2005:16:56:54 +0100]
# "GET /favicon.ico HTTP/1.1" 404 285 "-"
# "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4"
# - "-" 0 0 "-" 0
my $logline_regex = "";
# hostname
$logline_regex .= "^(\\S+)";
# remote host, remote username, local username
$logline_regex .= "\\ (\\S+)\\ (\\S+)\\ (\\S+)";
# date, time, and gmt offset
$logline_regex .= "\\ \\[([^:]+):(\\d+:\\d+:\\d+)\\ ([^\\]]+)\\]";
# request method + request uri + protocol (as one field)
$logline_regex .= "\\ \"(.*)\"";
# status, bytes out
$logline_regex .= "\\ (\\d+)\\ (\\S+)";
# referer, user_agent
$logline_regex .= "\\ \"(.*)\"\\ \"(.*)\"";
# uniqueid, session, duration, duration_msec
$logline_regex .= "\\ (\\S+)\\ \"(.*)\"\\ (\\d+)\\ (\\d+)";
# modsec_message, modsec_rating
$logline_regex .= "\\ \"(.*)\"\\ (\\d+)";
# the rest (always keep this part of the regex)
$logline_regex .= "(.*)\$";
my $therequest_regex = "(\\S+)\\ (.*?)\\ (\\S+)";
# use strict
my %ipaddresses = ();
my %request;
my $current_time;
my $last_state_save;
sub parse_logline {
$_ = shift;
my %request = ();
$request{"invalid"} = 0;
my @parsed_logline = /$logline_regex/x;
if (@parsed_logline == 0) {
return (0,0);
}
(
$request{"hostname"},
$request{"remote_ip"},
$request{"remote_username"},
$request{"username"},
$request{"date"},
$request{"time"},
$request{"gmt_offset"},
$request{"the_request"},
$request{"status"},
$request{"bytes_out"},
$request{"referer"},
$request{"user_agent"},
$request{"unique_id"},
$request{"session_id"},
$request{"duration"},
$request{"duration_msec"},
$request{"modsec_message"},
$request{"modsec_rating"},
$request{"the_rest"}
) = @parsed_logline;
if ($DEBUG == 2) {
print "\n";
print "hostname = " . $request{"hostname"} . "\n";
print "remote_ip = " . $request{"remote_ip"} . "\n";
print "remote_username = " . $request{"remote_username"} . "\n";
print "username = " . $request{"username"} . "\n";
print "date = " . $request{"date"} . "\n";
print "time = " . $request{"time"} . "\n";
print "gmt_offset = " . $request{"gmt_offset"} . "\n";
print "the_request = " . $request{"the_request"} . "\n";
print "status = " . $request{"status"} . "\n";
print "bytes_out = " . $request{"bytes_out"} . "\n";
print "referer = " . $request{"referer"} . "\n";
print "user_agent = " . $request{"user_agent"} . "\n";
print "unique_id = " . $request{"unique_id"} . "\n";
print "session_id = " . $request{"session_id"} . "\n";
print "duration = " . $request{"duration"} . "\n";
print "duration_msec = " . $request{"duration_msec"} . "\n";
print "modsec_message = " . $request{"modsec_message"} . "\n";
print "modsec_rating = " . $request{"modsec_rating"} . "\n";
print "\n\n";
}
# parse the request line
$_ = $request{"the_request"};
my @parsed_therequest = /$therequest_regex/x;
if (@parsed_therequest == 0) {
$request{"invalid"} = "1";
$request{"request_method"} = "";
$request{"request_uri"} = "";
$request{"protocol"} = "";
} else {
(
$request{"request_method"},
$request{"request_uri"},
$request{"protocol"}
) = @parsed_therequest;
}
if ($request{"bytes_out"} eq "-") {
$request{"bytes_out"} = 0;
}
# print "date=" . $request{"date"} . "\n";
(
$request{"time_mday"},
$request{"time_mon"},
$request{"time_year"}
) = ( $request{"date"} =~ m/^(\d+)\/(\S+)\/(\d+)/x );
# print "time=" . $request{"time"} . "\n";
(
$request{"time_hour"},
$request{"time_min"},
$request{"time_sec"}
) = ( $request{"time"} =~ m/(\d+):(\d+):(\d+)/x );
$request{"time_mon"} = $months{$request{"time_mon"}};
$request{"time_epoch"} = timelocal(
$request{"time_sec"},
$request{"time_min"},
$request{"time_hour"},
$request{"time_mday"},
$request{"time_mon"},
$request{"time_year"}
);
# print %request;
my $offset = index($request{"request_uri"}, "?");
if ($offset != -1) {
$request{"script_name"} = substr($request{"request_uri"}, 0, $offset);
$request{"query_string"} = substr($request{"request_uri"}, $offset + 1);
} else {
$request{"script_name"} = $request{"request_uri"};
$request{"query_string"} = "";
}
$request{"request_uri"} =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
$request{"query_string"} =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
return %request;
}
sub update_ip_address() {
my $ipd = $ipaddresses{$request{"remote_ip"}};
if (defined($$ipd{"counter"})) {
$$ipd{"counter"} = $$ipd{"counter"} + 1;
if ($DEBUG) {
print STDERR "httpd-guardian: Incrementing counter for " . $request{"remote_ip"} . " (" . $$ipd{"counter"} . ")\n";
}
my($exec) = 0;
# check the 1 min counter
if ($current_time - $$ipd{"time_1min"} > 60) {
# check the counters
my $speed = ($$ipd{"counter"} - $$ipd{"counter_1min"}) / ($current_time - $$ipd{"time_1min"});
if ($speed > $THRESHOLD_1MIN) {
print STDERR "httpd-guardian: IP address " . $ipaddresses{$request{"remote_ip"}} . " reached the 1 min threshold (speed = $speed req/sec, threshold = $THRESHOLD_1MIN req/sec)\n";
$exec = 1;
}
# reset the 1 min counter
$$ipd{"time_1min"} = $current_time;
$$ipd{"counter_1min"} = $$ipd{"counter"};
}
# check the 5 min counter
if ($current_time - $$ipd{"time_5min"} > 360) {
# check the counters
my $speed = ($$ipd{"counter"} - $$ipd{"counter_5min"}) / ($current_time - $$ipd{"time_5min"});
if ($speed > $THRESHOLD_5MIN) {
print STDERR "httpd-guardian: IP address " . $request{"remote_ip"} . " reached the 5 min threshold (speed = $speed req/sec, threshold = $THRESHOLD_5MIN req/sec)\n";
$exec = 1;
}
# reset the 5 min counter
$$ipd{"time_5min"} = $current_time;
$$ipd{"counter_5min"} = $$ipd{"counter"};
}
if (($exec == 1)&&(defined($PROTECT_EXEC))) {
my $cmd = sprintf($PROTECT_EXEC, $request{"remote_ip"});
print STDERR "httpd-guardian: Executing: $cmd\n";
system($cmd);
}
} else {
# start tracking this email address
my %ipd = ();
$ipd{"counter"} = 1;
$ipd{"counter_1min"} = 1;
$ipd{"time_1min"} = $current_time;
$ipd{"counter_5min"} = 1;
$ipd{"time_5min"} = $current_time;
$ipaddresses{$request{"remote_ip"}} = \%ipd;
}
}
sub process_log_line {
update_ip_address();
}
sub remove_stale_data {
while(my($key, $value) = each(%ipaddresses)) {
if ($current_time - $$value{"time_1min"} > $STALE_INTERVAL) {
if ($DEBUG) {
print STDERR "httpd-guardian: Removing key $key\n";
}
delete($ipaddresses{$key});
}
}
}
sub save_state {
if (!defined($SAVE_STATE_FILE)) {
return;
}
if (!defined($last_state_save)) {
$last_state_save = 0;
}
if ($current_time - $last_state_save > $SAVE_STATE_INTERVAL) {
open(FILE, ">$SAVE_STATE_FILE") || die("Failed to save state to $SAVE_STATE_FILE");
print FILE "# $current_time\n";
print FILE "# IP Address\x09Counter\x09\x091min (time)\x095min (time)\n";
while(my($key, $value) = each(%ipaddresses)) {
print FILE ("$key" . "\x09" . $$value{"counter"} . "\x09\x09" . $$value{"counter_1min"} . " (" . $$value{"time_1min"} . ")\x09" . $$value{"counter_5min"} . " (" . $$value{"time_5min"} . ")\n");
}
close(FILE);
$last_state_save = $current_time;
}
}
# load state from $SAVE_STATE_FILE, store the data into $ipaddresses
sub load_state {
return unless ( defined $SAVE_STATE_FILE );
return unless ( -e $SAVE_STATE_FILE && -r $SAVE_STATE_FILE );
open my $fd, "<", $SAVE_STATE_FILE
or die "cannot open state file for reading : $SAVE_STATE_FILE : $!";
while (<$fd>) {
s/^\s+//;
next if /^#/;
#--------------------------------------------------
# # 1133599679
# # IP Address Counter 1min (time) 5min (time)
# 211.19.48.12 396 396 (1133599679) 395 (1133599379)
#--------------------------------------------------
my ($addr, $counter, $time1, $time5) = split /\t+/, $_; # TAB
my ($counter_1min, $time_1min) = split /\s+/, $time1;
my ($counter_5min, $time_5min) = split /\s+/, $time5;
$ipaddresses{$addr} = {
counter => $counter,
counter_1min => $counter_1min,
time_1min => chop_brace($time_1min),
counter_5min => $counter_5min,
time_5min => chop_brace($time_5min),
}
}
close $fd;
}
# return strings between braces
sub chop_brace {
my $str = shift;
$str =~ /\((.*)\)/;
return $1;
}
sub process_line {
my $line = shift(@_);
if (defined($COPY_LOG)) {
print COPY_LOG_FD $line;
}
if ($DEBUG) {
print STDERR "httpd-guardian: Received: $line";
}
%request = parse_logline($line);
if (!defined($request{0})) {
# TODO verify IP address is in correct format
# extract the time from the log line, to allow the
# script to be used for batch processing too
$current_time = $request{"time_epoch"};
remove_stale_data();
process_log_line();
save_state();
} else {
print STDERR "Failed to parse line: " . $line;
}
}
# -----------------------------------
load_state();
if (defined($COPY_LOG)) {
open(COPY_LOG_FD, ">>$COPY_LOG") || die("Failed to open $COPY_LOG for writing");
# enable autoflush on the file descriptor
$| = 1, select $_ for select COPY_LOG_FD;
}
if ($USE_SPREAD) {
my($sperrno);
my %args;
$args{"spread_name"} = $SPREAD_DAEMON;
$args{"private_name"} = $SPREAD_CLIENT_NAME;
my($mbox, $privategroup) = Spread::connect(\%args);
if (!defined($mbox)) {
die "Failed to connect to Spread daemon: $sperrno\n";
}
Spread::join($mbox, $SPREAD_GROUP_NAME);
for(;;) {
my($st, $s, $g, $mt, $e, $msg);
while(($st, $s, $g, $mt, $e, $msg) = Spread::receive($mbox, $SPREAD_TIMEOUT)) {
if ((defined($st))&&($st == 2)&&(defined($msg))) {
process_line($msg . "\n");
}
}
}
} else {
while(<STDIN>) {
process_line($_);
}
}
if (defined($COPY_LOG)) {
close(COPY_LOG_FD);
}