Skip to content

Instantly share code, notes, and snippets.

@ZonkerHarris
Created September 2, 2010 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ZonkerHarris/563011 to your computer and use it in GitHub Desktop.
Save ZonkerHarris/563011 to your computer and use it in GitHub Desktop.
#! /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