Created
September 2, 2010 21:46
-
-
Save ZonkerHarris/563011 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/perl | |
# | |
# sshattackblock.pl | |
# by Zonker Harris 23 AUG 2010 (v1.0.1 2 SEP 2010) | |
# | |
# Designed to be run manually, to test certain parts of the full attacker script. | |
# There is normally no output, unless you use the undocumented debug flag | |
# | |
# Still to do; | |
# - we tank the same address in many runs, if it still shows in the log slice | |
# (check the logs, next if the ADDR is already there?) | |
# - we also want to try sending some log info in email, or syslog to a DB File... | |
# (maybe Runbook can tail that log for clues? last run timestamp, status, addrs tanked?) | |
use warnings ; | |
use strict ; | |
use DB_File ; | |
our (%bipdb, $runstamp, $badIP) ; | |
### Preset some variables | |
# The timestamp when the script is run... | |
our $timestamp = time; | |
# | |
# How many failures in this time span means this is likely an attacker? | |
# (We don't want to lock out every valid user who forgot their password...) | |
# We need to exceed the $FailCount... | |
our $FailCount = "6"; | |
# 10 minutes = 600 seconds. 1 hour = 3600 seconds. 12 hours = 43,200 | |
# 1 day = 86,400. 4 days = 345,600 seconds. 1 week = 604,800 seconds | |
our $AgeSeconds = "14400"; | |
our $AgeTime = ( $AgeSeconds / 60 ); | |
our $OldStamp = ( $timestamp - $AgeSeconds ); | |
our $LogDepth = "75"; | |
our $logfile = "/var/log/messages" ; | |
### Check for extra data on the command line, and print instructions. | |
# (there shouldn't be extra arguments... Except the hidden "-d" debug flag!) | |
my $DEBUG = "0"; | |
if(scalar( @ARGV) ) | |
{ | |
my $userinput = $ARGV[0]; | |
if ($userinput eq "-d") | |
{ $DEBUG = "1"; } | |
else | |
{ show_usage(); | |
exit; } | |
} | |
## Initializing or pre-setting the rest of our variables... | |
our $GetRulesCmd = "/usr/sbin/iptables -L INPUT --line-numbers --numeric | grep DROP | grep tcp"; | |
our $LogCmd = "tail -" . $LogDepth . " " . $logfile . " | grep ailed"; | |
our (@CurrentRules, @LogQuery, @AlertLog ); | |
our ($attacker, $attackStatus, $attackerID, $count, $status, $oldTimestamp, $oldAttacker, $ruleNumber, $AttackerIP); | |
our %BlockList = (); | |
our %AttackCount = (); | |
our %RuleNumbers = (); | |
our %DeleteList = (); | |
# how do we set entries into the Input list (we need the preface for the command) | |
our $iptableset = "/usr/sbin/iptables -I INPUT -p tcp -s"; | |
# how do we remove entries from the Input list (we need the preface for the command) | |
our $iptabledelete = "/usr/sbin/iptables -D INPUT"; | |
# Our host IP address... we need to specify this as the destination in IPTABLES | |
our $hostip = "192.168.117.35"; | |
#### Here is the meat of the script... | |
Check_the_log (); | |
if ( $LogQuery[0] ) | |
{ | |
Parse_for_attackers(); | |
my $AttackerCount = scalar keys %BlockList ; | |
unless ( $AttackerCount = 0 ) | |
{ | |
Grab_the_current_rules(); | |
Remove_old_Rules (); | |
Block_the_attackers( %BlockList ); | |
} | |
} | |
else | |
{ | |
if ($DEBUG == 1) | |
{ print "* No attackers seen.\n"; } | |
} | |
$status = 0; | |
print "\n"; | |
exit; | |
##### Subroutines are below... | |
sub Check_the_log | |
{ | |
# Tail the messages log file for a certain number of lines... | |
# Grep the result for "ailed" to look for only failure messages... | |
# Add them to an attacker array, for line-at-a-time reading... | |
@LogQuery = `$LogCmd`; | |
if ($DEBUG == 1) | |
{ print "---- Messages Log Data (out of " . $LogDepth . " lines) -------\n"; | |
#my $logLineCount = @LogQuery; | |
#print " There are $logLineCount Elements in the log query results.\n"; | |
print @LogQuery; | |
} | |
} | |
sub Parse_for_attackers | |
{ | |
# First, check if there were interesting lines in the log file to parse... | |
if ( @LogQuery eq "" ) | |
{ | |
if ($DEBUG == 1) | |
{ print " * no interesting lines in the log file\n"; } | |
} | |
else | |
{ | |
# Pull each IP address from the attacker array ($LogQuery)... | |
# If it is a safety address, do nothing, but set a flag and send an alert | |
# If it isn't a safety address, check if it's in the local attacker counter array | |
# If it's new, set it's count to "1" | |
# If it's already listed, increment it's hit count... | |
# For each attacker in the attacker counter array | |
# If they are currently marked "active" in the attacker DB file, do nothing | |
# Otherwise, add them to the DB File, and mark them "active" (timestamp, IP, count, flag) | |
# | |
# Which address should we NOT block with our filters | |
# list the IP's for "trusted hosts" that should not be blocked automatically | |
my @safety = qw( 192.168.117.1 192.168.117.22 192.168.117.53 ); | |
my %Blocked = (); | |
#link "badIPdb" ; | |
tie %bipdb, "DB_File", "attackerIPdb", O_RDWR|O_CREAT, 0666, $DB_HASH | |
or die "Cannot open file 'attackerIPdb': $!\n"; | |
# We also need to scan the IPTABLES rules for DROP filters, just a list of unique IPs... | |
# When we determine the attacker IP, it may be a persistant log because we banned him last time | |
# It's possible to add the same address many time over... but we don't want to. | |
@CurrentRules = `$GetRulesCmd`; | |
my $BlockedIP = ""; | |
foreach my $rule (@CurrentRules) | |
{ | |
#get the rule number and the IP of the attacker... we rely on Search being 'greedy'... | |
if ($rule =~ /^(\d{1,2} )/) | |
{ $ruleNumber = $1; } | |
if ($rule =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/) | |
{ $BlockedIP = $1 . "." . $2 . "." . $3 . "." . $4; } | |
$Blocked{$BlockedIP} = $rule | |
} | |
if ($DEBUG == 1) | |
{ print "---- Parsing log results for attackers -------"; } | |
# We need to break $LogQuery down into individual lines... | |
LOGQUERY: foreach my $Line (@LogQuery) | |
{ | |
$attackStatus = 0 ; | |
my $AttackCount = 0 ; | |
# Look for an IP address, aggregate from $line into $attacker | |
if ($Line =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/) | |
{ $attacker = $1 . "." . $2 . "." . $3 . "." . $4; } | |
# Did we find an IP address? If not, skip to the next log line... | |
else | |
{ | |
next LOGQUERY ; | |
} | |
if ($attacker =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) | |
{ | |
# $attacker is a valid IP address... | |
unless ( exists $BlockList{$attacker} ) | |
{ | |
# This is no rule blocking attacker... | |
if (($DEBUG == 1) && ($attacker ne "")) | |
{ print "\nAttacker IP address is $attacker"; } | |
} | |
else | |
{ | |
# We already have a rule in place for this address... | |
if ($DEBUG == 1) | |
{ print "\n $attacker is already in IPTABLES, old log entry"; } | |
next LOGQUERY; | |
} | |
if ( exists $AttackCount{$attacker} ) | |
{ | |
# We've seen this attacker more than once in this run, increment the count | |
$count = $AttackCount{$attacker} ; | |
$count++ ; | |
$AttackCount{$attacker} = $count ; | |
#if ($DEBUG == 1) | |
#{ print " Attacker $attacker, count = $count\n"; } | |
} | |
else | |
{ | |
# This is the first time we have seen this attack in this pass... | |
#$AttackCount = 1 ; | |
#$AttackCount{$attacker} = 1 ; | |
#if ($DEBUG == 1) | |
#print " Attacker $attacker, count = $AttackCount"; | |
} | |
# Build the key for the DB-file, by concatenating the timestamp and the IP | |
$attackerID = $timestamp . ":" . $attacker ; | |
# This next case simply prints Marching Ants for repeat attackers... | |
if (($DEBUG == 1) & ($attacker ne "")) | |
{ print "."; } | |
# See if the attacker is one of our Trusted (@Safety) hosts... | |
TRUSTED: foreach my $trusted (@safety) | |
{ | |
if ($attacker eq $trusted) | |
{ | |
# Exempt the Main Street AMS collector address (204.147.180.199)from the list... | |
if ($attacker eq "172.12.117.4") | |
{ | |
# Add debug indication that we exempted him... | |
if ($DEBUG == 1) | |
{ print " Never mind, it is the Main Street collector..."; } | |
$attackStatus = 1; | |
next LOGQUERY ; | |
} | |
else | |
{ | |
if ($DEBUG == 1) | |
{ print "Attack is from a host in the Safety list!"; } | |
$attackStatus = 2; | |
# One of our trusted addresses is attacking us? | |
# append new allert line to $AlertLog | |
next LOGQUERY ; | |
} | |
} | |
} | |
## If we get here, the attacker is NOT a Trusted host! | |
# Is the Attacker in the DB File? | |
# YES: increment his count; status = "1"; update the timestamp; | |
# NO: set his count to "1"; status = "1"; update the timestamp; | |
my $count = "1"; | |
my $status = "1"; | |
if ( exists $bipdb{$attackerID} ) | |
{ | |
# YES: increment his count; leave the status = "1"; | |
$attackStatus = 2; | |
($count, $status) = split /:/, $bipdb{$attackerID} ; | |
$count++; | |
$bipdb{$attackerID} = join (":", ($count, $status)) ; | |
if ( $count > $FailCount ) | |
{ | |
# Add $Attacker IP into the $BlockList | |
if ($DEBUG == 1) | |
{ print " Added to the BlockList... "; } | |
$BlockList{$attacker} = $count ; | |
} | |
} | |
else | |
{ | |
# NO: set his count to "1"; status = "1"; | |
if ($DEBUG == 1) | |
{ print " Added to DB file... "; } | |
$attackStatus = 2; | |
$bipdb{$attackerID} = join (":", ($count, $status)) ; | |
} | |
} | |
else | |
{ | |
if (($DEBUG == 1) && ($attacker ne "")) | |
{ print "\nA line had no IP address, not an attacker."; } | |
} | |
} | |
# At this point, we know the attack is valid, is NOT a Safety host, and is ALREADY in the blocklist | |
if (($DEBUG == 1) && ($attacker ne "")) | |
{ | |
$attacker = ""; | |
print "\n-------- Print the DB File ----------\n"; | |
foreach $attackerID (keys %bipdb) | |
{ | |
($count, $status) = split /:/, $bipdb{$attackerID} ; | |
printf("%-34s %-7s %-2s\n", $attackerID ,$count, $status); | |
} | |
print "-------- Print the BlockList ----------\n"; | |
#my $AttackerCount = scalar keys %BlockList ; | |
#if (($DEBUG == 1) | |
#{ print "There are $AttackerCount entries in the Block List.\n"; } | |
foreach my $attacker (keys %BlockList) | |
{ | |
print "$attacker\n"; | |
} | |
} | |
untie %bipdb; | |
} | |
} | |
sub Grab_the_current_rules | |
{ | |
# List the current IPTABLES ruleset for the INPUT filter | |
# We want to be able to search the list for just the DROPped TCP lines... | |
# We want them with line numbers, so we can remove them later... | |
# Fetch the current list of denied addresses in the Input list... | |
@CurrentRules = `$GetRulesCmd`; | |
if ($DEBUG == 1) | |
{ print "----------- Filter Rules ----------\n"; | |
my $rulesCount = @CurrentRules; | |
print " There are $rulesCount Elements in the filter rules query results.\n"; | |
print @CurrentRules; | |
} | |
foreach my $rule (@CurrentRules) | |
{ | |
#get the rule number and the IP of the attacker... we rely on Search being 'greedy'... | |
if ($rule =~ /^(\d{1,2} )/) | |
{ $ruleNumber = $1; } | |
if ($rule =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/) | |
{ $AttackerIP = $1 . "." . $2 . "." . $3 . "." . $4; } | |
if ($DEBUG == 1) | |
{ print "rule $ruleNumber, attacker IP is $AttackerIP\n"; } | |
$RuleNumbers{$ruleNumber} = $AttackerIP; | |
} | |
my $rulesCount = scalar keys %RuleNumbers; | |
if ($DEBUG == 1) | |
{ print " There are $rulesCount Elements in the filter rules query results.\n"; } | |
} | |
sub Remove_old_Rules | |
{ | |
# DB File Status field: 2 = Rule added to IPTABLES, 1 = Active attack, but no rule, | |
# but 0 = the rule has been removed (noting that they were attackers before) | |
# Read the DB File, find records with status 2, then look for the matching rule number | |
tie %bipdb, "DB_File", "attackerIPdb", O_RDWR|O_CREAT, 0666, $DB_HASH | |
or die "Cannot open file 'attackerIPdb': $!\n"; | |
if ($DEBUG == 1) | |
{ print "------- Removing Attackers older than $AgeTime minutes ------\n"; } | |
CHECKLIST: foreach $attackerID (keys %bipdb) | |
{ | |
(my $attackTimestamp, my $oldAttacker) = split /:/, $attackerID; | |
($count, $status) = split /:/, $bipdb{$attackerID} ; | |
#if ($DEBUG == 1) | |
#{ print "AttackerID: $attackerID / ATT time: $attackTimestamp / Old time: $OldStamp / IP: $oldAttacker / Count: $count / Status: $status\n"; } | |
# | |
next CHECKLIST if ($attackTimestamp =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/); | |
# Now a test, is the DB file "attack timestamp" older than the "aged-out threshold timestamp" here... | |
# next line if it's younger (greater than) than old timestamp; | |
next CHECKLIST if ( $attackTimestamp > $OldStamp ); | |
#if ($DEBUG == 1) | |
#{ print "old attacker: $oldAttacker / attack timestamp: $attackTimestamp / old timestamp: $timestamp\n"; } | |
# Here, we skip the record, unless we know there was a rule set... | |
next CHECKLIST if ($status < 2); | |
{ | |
#It's older than our old-record timeout, and there WAS a rule set in IPTABLES... | |
my $AttackerCount = scalar keys %RuleNumbers ; | |
#if ($DEBUG == 1) | |
#{ print "status = 2, old attacker: $oldAttacker, number of rules in the RuleNumbers hash is $AttackerCount\n"; } | |
# Skip this if we have no DENY rules in the IPTABLES list... (an unlikely event) | |
unless ( $AttackerCount == 0 ) | |
{ | |
foreach my $rule (keys %RuleNumbers) | |
{ | |
my $priorAttacker = ""; | |
$priorAttacker = ( $RuleNumbers{$rule} ); | |
# Check if the IP in this rule matches the IP from this loop of the DB file... | |
if ( $priorAttacker eq $oldAttacker ) | |
{ | |
if ($DEBUG == 1) | |
{ print "Old attacker $oldAttacker found in rule $rule, added the rule to DeleteList.\n"; } | |
$DeleteList{$rule} = $oldAttacker; | |
} | |
} | |
} | |
} | |
} | |
# Now, sort the $DeleteList keys in descending order (remove the rules highest to lowest) | |
if ($DEBUG == 1) | |
{ print "------- Processing the DeleteList ------\n"; } | |
foreach my $rule (sort high_to_low keys %DeleteList) | |
{ | |
my $attackerIP = $DeleteList{$rule} ; | |
if ($DEBUG == 1) | |
{ print "Removing rule $rule, was $attackerIP\n"; } | |
#Remove the rule! | |
# our $iptabledelete = "sudo /usr/sbin/iptables -D INPUT"; | |
my $deleteCmd = `$iptabledelete $rule 2>&1`; | |
my $cmdStatus = $?; # 0 if ok | |
if ($cmdStatus == 0) | |
{ | |
if ($DEBUG == 1) | |
{ print "rule successfully removed\n"; } | |
# Set the status for this lin of the DB file to 0, since we are remooving this rule. | |
$status = "0"; | |
$attackerID = $timestamp . ":" . $attackerIP ; | |
$bipdb{$attackerID} = join (":", ($count, $status)); | |
} | |
else | |
{ | |
if ($DEBUG == 1) | |
{ print "rule could NOT be removed, returned status $cmdStatus \n"; } | |
} | |
} | |
untie %bipdb; | |
} | |
sub Block_the_attackers | |
{ | |
# For each line in the attacker counter array | |
# Add a filter rule to block them | |
# | |
my @BlockList = @_; | |
if ($DEBUG == 1) | |
{ print "---- Blocking attackers verified in this pass -------\n"; } | |
tie %bipdb, "DB_File", "attackerIPdb", O_RDWR|O_CREAT, 0666, $DB_HASH | |
or die "Cannot open file 'attackerIPdb': $!\n"; | |
foreach $attacker (keys %BlockList) | |
{ | |
# sudo /usr/sbin/iptables -I INPUT -p tcp -s $userinput -d 205.248.105.205 --dport 22 -j DROP 2>&1 | |
# our $iptableset = "sudo /usr/sbin/iptables -I INPUT -p tcp -s"; | |
my $blockCmd = `$iptableset $attacker -d $hostip --dport 22 -j DROP 2>&1`; | |
my $cmdStatus = $?; # 0 if ok | |
if ($cmdStatus == 0) | |
{ | |
# Filter DROP rule has been successfully added | |
if ($DEBUG == 1) | |
{ print "Added attacker $attacker into IPTABLES\n"; } | |
# Update the DB file Status field to reflect a successful rule-add... | |
$attackerID = $timestamp . ":" . $attacker ; | |
($count, $status) = split /:/, $bipdb{$attackerID}; | |
$status=2; | |
$bipdb{$attackerID} = join (":", ($count, $status)); | |
} | |
else | |
{ | |
# Filter rule could NOT be successfully added... | |
if ($DEBUG == 1) | |
{ print "Rule was NOT added for $attacker. The command returned $cmdStatus \n"; } | |
} | |
} | |
untie %bipdb; | |
@CurrentRules = `$GetRulesCmd`; | |
if ($DEBUG == 1) | |
{ print "----------- Final Filter Rules ----------\n"; | |
my $rulesCount = @CurrentRules; | |
print " The following $rulesCount rules are now active...\n"; | |
print @CurrentRules; | |
} | |
# Yes, this last bracket below DOES belong there... | |
} | |
sub high_to_low | |
{ | |
my ($ruleA, $ruleB) = ($a, $b); | |
$ruleB <=> $ruleA; | |
} | |
sub show_usage | |
{ | |
# Show the command usage banner... | |
print "Usage: attackblock.pl\n\nThis script does not accept any added arguments.\n"; | |
print "This script is meant to be invoked manually, or run by cron.\n\n"; | |
print "The script looks in the last " . $LogDepth . " lines of the logs for signs of\n"; | |
print "failed SSH logins, and will add an attackers IP to the IPTABLES\n"; | |
print "after " . $FailCount . " recent failures.\n\n"; | |
print "The script also logs to a local DB file. When the script runs,\n"; | |
print "it also looks at the DB file to find any addreses older than\n"; | |
print $AgeTime . " minutes, and it will remove those addresses from the INPUT\n"; | |
print "filter of the IPTABLES list, and then from the DB file.\n\n"; | |
} | |
### eof |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment