Abuse Scripts

I found a nice script at Calomel pf that watches web server logs and adds abusive hosts to a blacklist. I made a few minor adjustments to it.

Perl script

  1#!/usr/local/bin/perl -T
  2
  3use strict;
  4use warnings;
  5
  6## Calomel.org .:. https://calomel.org
  7##   name     : web_server_abuse_detection.pl
  8##   version  : 0.04
  9
 10# description: this script will watch the web server logs (like Apache or Nginx) and
 11#  count the number of http error codes an ip has triggered. At a user defined amount
 12#  of errors we can execute a action to block the ip using our firewall software.
 13
 14## which log file do you want to watch?
 15#my $log = "/var/log/h2o/louiskphoto.com.log /var/log/h2o/louiskowolowski.com.log /var/log/h2o/cryptomonkeys.com.log";
 16my $log = "/var/log/h2o/*.log";
 17
 18## how many errors can an ip address trigger before we block them?
 19my $errors_block = 10;
 20
 21## how many seconds before an unseen ip is considered old and removed from the hash?
 22my $expire_time = 7200;
 23
 24## how many error log lines before we trigger blocking abusive ips and clean up
 25## of old ips in the hash? make sure this value is greater than $errors_block above.
 26my $cleanup_time = 10;
 27
 28## do you want to debug the scripts output ? on=1 and off=0
 29my $debug_mode = 0;
 30
 31## clear the environment and set our path
 32$ENV{ENV} ="";
 33$ENV{PATH} = "/bin:/usr/bin:/usr/local/bin";
 34
 35## declare some internal variables and the hash of abusive ip addresses
 36my ( $ip, $errors, $time, $newtime, $newerrors );
 37my $trigger_count=1;
 38my %abusive_ips = ();
 39
 40## open the log file. we are using the system binary tail which is smart enough
 41## to follow rotating logs. We could have used File::Tail, but tail is easier.
 42open(LOG,"/usr/bin/tail -F $log |") || die "ERROR: could not open log file.\n";
 43
 44while(<LOG>) {
 45	## process the log line if it contains one of these error codes 
 46	if ($_ =~ m/( 301 | 302 | 303 | 307 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 444 | 494 | 495 | 496 | 497 | 500 | 501 | 502 | 503 | 504 | 507 )/) {
 47
 48		## Whitelisted ips. This is where you can whitelist ips that cause errors,
 49		## but you do NOT want them to be blocked. Googlebot at 66.249/16 is a good
 50		## example. We also whitelisted the private subnet 192.168/16 so web
 51		## developers inside the firewall can test and never be blocked. 
 52		## 64.41.200.100 ssllabs.com
 53		## 64.41.200.104 ssllabs.com
 54		if ($_ !~ m/^(64\.41\.200\.|66\.249\.|192\.168\.)/) {
 55
 56			## extract the ip address from the log line and get the current unix time
 57			$time = time();
 58			$ip = (split ' ')[0];
 59
 60			## if an ip address has never been seen before we need
 61			## to initialize the errors value to avoid warning messages.
 62			$abusive_ips{ $ip }{ 'errors' } = 0 if not defined $abusive_ips{ $ip }{ 'errors' };
 63
 64			## increment the error counter and update the time stamp.
 65			$abusive_ips{ $ip }{ 'errors' } = $abusive_ips{ $ip }->{ 'errors' } + 1;
 66			$abusive_ips{ $ip }{ 'time' } = $time;
 67
 68			## DEBUG: show detailed output
 69			if ( $debug_mode == 1 ) {
 70				$newerrors  = $abusive_ips{ $ip }->{ 'errors' };
 71				$newtime = $abusive_ips{ $ip }->{ 'time' };
 72				print "unix_time:  $newtime, errors:  $newerrors, ip:  $ip, cleanup_time: $trigger_count\n";
 73			}
 74
 75			## if an ip has triggered the $errors_block value we block them
 76			if ($abusive_ips{ $ip }->{ 'errors' } >= $errors_block ) {
 77
 78				## DEBUG: show detailed output
 79				if ( $debug_mode == 1 ) {
 80					print "ABUSIVE IP! unix_time:  $newtime, errors:  $newerrors, ip:  $ip, cleanup_time: $trigger_count\n";
 81				}
 82
 83				## Untaint the ip variable for use by the following external system() calls
 84				my $ip_ext = "$1" if ($ip =~ m/^([0-9\.]+)$/ or die "\nError: Illegal characters in ip\n\n" );
 85
 86				## USER EDIT: this is the system call you will set to block the abuser. You can add the command
 87				##  line you want to execute on the ip address of the abuser. For example, we are using logger to
 88				##  echo the line out to /var/log/messages and then we are adding the offending ip address to our
 89				##  FreeBSD Pf table which we have setup to block ips at Pf firewall.
 90				system("/usr/bin/logger", "$ip_ext", "is", "abusive,", "sent", "to", "BLOCKTEMP");
 91				system("/sbin/pfctl", "-t", "BLOCKTEMP", "-T", "add", "$ip_ext");
 92
 93				## after the ip is blocked it does need to be in the hash anymore
 94				delete($abusive_ips{ $ip });
 95			}
 96
 97			## increment the trigger counter which is used for the following clean up function. 
 98			$trigger_count++;
 99
100			## clean up function: when the trigger counter reaches the $cleanup_time we
101			## remove any old hash entries from the $abusive_ips hash
102			if ($trigger_count >= $cleanup_time) {
103				my $time_current =  time();
104
105				## DEBUG: show detailed output
106				if ( $debug_mode == 1 ) {
107					print "  Clean up... expire: $expire_time, pre-size of hash:  " . keys( %abusive_ips ) . ".\n";
108				}
109
110				## clean up ip addresses we have not seen in a long time
111				while (($ip, $time) = each(%abusive_ips)) {
112
113					## DEBUG: show detailed output
114					if ( $debug_mode == 1 ) {
115						my $total_time = $time_current - $abusive_ips{ $ip }->{ 'time' };
116						print "    ip: $ip, seconds_last_seen: $total_time, errors:  $abusive_ips{ $ip }->{ 'errors' }\n";
117					}
118
119					if ( ($time_current - $abusive_ips{ $ip }->{ 'time' } ) >= $expire_time) {
120						delete($abusive_ips{ $ip });
121					}
122				}
123
124				## DEBUG: show detailed output
125				if ( $debug_mode == 1 ) {
126					print "  Clean up... expire: $expire_time, post-size of hash:  " . keys( %abusive_ips ) . ".\n";
127				}
128
129				## reset the trigger counter
130				$trigger_count = 1;
131			}
132		}
133	}
134}
135#### EOF ####

rc script

The rc script (/usr/local/etc/rc.d/) looks like this:

 1#!/bin/sh
 2
 3# PROVIDE: webabuse
 4# BEFORE:  LOGIN
 5# KEYWORD:
 6
 7. /etc/rc.subr
 8
 9name=webabuse
10rcvar=`set_rcvar`
11command=/usr/local/bin/web_server_abuse_detection.pl
12command_interpreter=/usr/local/bin/perl
13webabuse_user=root
14start_cmd="/usr/sbin/daemon -u $webabuse_user $command"
15
16load_rc_config $name
17run_rc_command "$1"
18
19#### vi /usr/local/etc/rc.d/webabuse ####

Enable in rc.conf

You can enable it by adding this to /etc/rc.conf:

1sudo sysrc webabuse_enable=YES

Starting the service

and starting it with:

1service webabuse start

Sample output

Here is an example of a log message:

1Dec 22 19:39:12 mx root: 10.167.17.1 is abusive, sent to BLOCKTEMP

You may wish to whitelist certain IPs or IP blocks. For example, ssllabs.com (who hosts the web tool for testing a server’s SSL properties) does all manner of poking and prodding and, like a good firewall, pf blocks that right away.

Web version

Based on the web_server_abuse_detection.pl script, I made an ssh_server_abuse_detection.pl script that looks like this:

  1#!/usr/local/bin/perl -T
  2
  3use strict;
  4use warnings;
  5
  6## Calomel.org .:. https://calomel.org
  7##   name     : web_server_abuse_detection.pl
  8##   version  : 0.04
  9
 10# description: this script will watch the web server logs (like Apache or Nginx) and
 11#  count the number of http error codes an ip has triggered. At a user defined amount
 12#  of errors we can execute a action to block the ip using our firewall software.
 13
 14## which log file do you want to watch?
 15  my $log = "/var/log/auth.log";
 16
 17## how many errors can an ip address trigger before we block them?
 18  my $errors_block = 2;
 19
 20## how many seconds before an unseen ip is considered old and removed from the hash?
 21  my $expire_time = 7200;
 22
 23## how many error log lines before we trigger blocking abusive ips and clean up
 24## of old ips in the hash? make sure this value is greater than $errors_block above.
 25  my $cleanup_time = 10;
 26
 27## which table do we want to add IPs to when they misbehave?
 28  my $table = "sshguard";
 29
 30## do you want to debug the scripts output ? on=1 and off=0
 31  my $debug_mode = 0;
 32
 33## clear the environment and set our path
 34  $ENV{ENV} ="";
 35  $ENV{PATH} = "/bin:/usr/bin:/usr/local/bin";
 36
 37## declare some internal variables and the hash of abusive ip addresses
 38  my ( $ip, $errors, $time, $newtime, $newerrors );
 39  my $trigger_count=1;
 40  my %abusive_ips = ();
 41
 42## open the log file. we are using the system binary tail which is smart enough
 43## to follow rotating logs. We could have used File::Tail, but tail is easier.
 44  open(LOG,"/usr/bin/tail -F $log |") || die "ERROR: could not open log file.\n";
 45
 46  while(<LOG>) {
 47       ## process the log line if it contains one of these error codes 
 48	# Invalid user
 49       if ($_ =~ m/( Invalid\ user )/) {
 50
 51         ## Whitelisted ips. This is where you can whitelist ips that cause errors,
 52         ## but you do NOT want them to be blocked. Googlebot at 66.249/16 is a good
 53         ## example. We also whitelisted the private subnet 192.168/16 so web
 54         ## developers inside the firewall can test and never be blocked. 
 55         if ($_ !~ m/^(66.220.108.250)/) {
 56
 57         ## extract the ip address from the log line and get the current unix time
 58          $time = time();
 59          $ip = (split ' ')[9];
 60
 61         ## if an ip address has never been seen before we need
 62         ## to initialize the errors value to avoid warning messages.
 63          $abusive_ips{ $ip }{ 'errors' } = 0 if not defined $abusive_ips{ $ip }{ 'errors' };
 64
 65         ## increment the error counter and update the time stamp.
 66          $abusive_ips{ $ip }{ 'errors' } = $abusive_ips{ $ip }->{ 'errors' } + 1;
 67          $abusive_ips{ $ip }{ 'time' } = $time;
 68
 69         ## DEBUG: show detailed output
 70         if ( $debug_mode == 1 ) {
 71           $newerrors  = $abusive_ips{ $ip }->{ 'errors' };
 72           $newtime = $abusive_ips{ $ip }->{ 'time' };
 73           print "unix_time:  $newtime, errors:  $newerrors, ip:  $ip, cleanup_time: $trigger_count\n";
 74         }
 75
 76         ## if an ip has triggered the $errors_block value we block them
 77          if ($abusive_ips{ $ip }->{ 'errors' } >= $errors_block ) {
 78
 79             ## DEBUG: show detailed output
 80             if ( $debug_mode == 1 ) {
 81               print "ABUSIVE IP! unix_time:  $newtime, errors:  $newerrors, ip:  $ip, cleanup_time: $trigger_count\n";
 82             }
 83
 84             ## Untaint the ip variable for use by the following external system() calls
 85             my $ip_ext = "$1" if ($ip =~ m/^([0-9\.]+)$/ or die "\nError: Illegal characters in ip\n\n" );
 86
 87             ## USER EDIT: this is the system call you will set to block the abuser. You can add the command
 88             ##  line you want to execute on the ip address of the abuser. For example, we are using logger to
 89             ##  echo the line out to /var/log/messages and then we are adding the offending ip address to our
 90             ##  FreeBSD Pf table which we have setup to block ips at Pf firewall.
 91             system("/usr/bin/logger", "$ip_ext", "is", "abusive,", "sent", "to", "$table");
 92             system("/sbin/pfctl", "-t", "$table", "-T", "add", "$ip_ext");
 93
 94             ## after the ip is blocked it does need to be in the hash anymore
 95             delete($abusive_ips{ $ip });
 96          }
 97
 98         ## increment the trigger counter which is used for the following clean up function. 
 99          $trigger_count++;
100
101         ## clean up function: when the trigger counter reaches the $cleanup_time we
102         ## remove any old hash entries from the $abusive_ips hash
103          if ($trigger_count >= $cleanup_time) {
104             my $time_current =  time();
105
106             ## DEBUG: show detailed output
107             if ( $debug_mode == 1 ) {
108               print "  Clean up... expire: $expire_time, pre-size of hash:  " . keys( %abusive_ips ) . ".\n";
109             }
110
111              ## clean up ip addresses we have not seen in a long time
112               while (($ip, $time) = each(%abusive_ips)){
113
114               ## DEBUG: show detailed output
115               if ( $debug_mode == 1 ) {
116                 my $total_time = $time_current - $abusive_ips{ $ip }->{ 'time' };
117                 print "    ip: $ip, seconds_last_seen: $total_time, errors:  $abusive_ips{ $ip }->{ 'errors' }\n";
118               }
119
120                  if ( ($time_current - $abusive_ips{ $ip }->{ 'time' } ) >= $expire_time) {
121                       delete($abusive_ips{ $ip });
122                  }
123               }
124
125            ## DEBUG: show detailed output
126            if ( $debug_mode == 1 ) {
127               print "  Clean up... expire: $expire_time, post-size of hash:  " . keys( %abusive_ips ) . ".\n";
128             }
129
130             ## reset the trigger counter
131              $trigger_count = 1;
132          }
133         }
134       }
135  }
136#### EOF ####

The rc script (/usr/local/etc/rc.d/) looks like this:

 1#!/bin/sh
 2
 3# PROVIDE: sshabuse
 4# BEFORE:  LOGIN
 5# KEYWORD:
 6
 7. /etc/rc.subr
 8
 9name=sshabuse
10rcvar=`set_rcvar`
11command=/usr/local/bin/ssh_server_abuse_detection.pl
12command_interpreter=/usr/local/bin/perl
13sshabuse_user=root
14start_cmd="/usr/sbin/daemon -u $sshabuse_user $command"
15
16load_rc_config $name
17run_rc_command "$1"
18
19#### vi /usr/local/etc/rc.d/sshabuse ####

You can enable it by adding this to /etc/rc.conf:

1sudo sysrc sshabuse_enable=YES

and starting it with:

1sudo service sshabuse start

At some point, you may want to know which hosts are in your block list or block temp. Here is another script from calomel.org that will list abusive hosts. I’ve tweaked it to account for additional pf tables.

 1#!/usr/local/bin/bash
 2#
 3## Calomel.org  show_abusive_hosts.sh
 4## Purpose: Display ips and hostnames in the abusive hosts tables
 5#
 6echo "May prompt for sudo password"
 7total_blacklist=`sudo pfctl -t BLOCKPERM -T show | wc -l`
 8total_blacklist=`sudo pfctl -t BLOCKTEMP -T show | wc -l`
 9total_blacklist=`sudo pfctl -t BLACKLIST -T show | wc -l`
10echo " "
11echo -n "BLOCKPERM"; echo -n " ("; echo -n $total_blockperm; echo ")"
12for i in $( sudo pfctl -t BLOCKPERM -T show ) ; do
13	echo -n " "; echo -n $i; echo -n -e "\t" ; echo -n "  "; host $i | awk '{print $5}'
14done
15echo " "
16echo -n "BLOCKTEMP"; echo -n " ("; echo -n $total_blocktemp; echo ")"
17for i in $( sudo pfctl -t BLOCKTEMP -T show ) ; do
18	echo -n " "; echo -n $i; echo -n -e "\t" ; echo -n "  "; host $i | awk '{print $5}'
19done
20echo " "
21echo -n "BLACKLIST"; echo -n " ("; echo -n $total_blacklist; echo ")"
22for i in $( sudo pfctl -t BLACKLIST -T show ) ; do
23	echo -n " "; echo -n $i; echo -n -e "\t" ; echo -n "  "; host $i | awk '{print $5}'
24done

Output looks like this (I terminated it because the list gets long):

 1[louisk@mail louisk 13 ]$ ./show_abusive_hosts.sh
 2May prompt for sudo password
 3
 4BLOCKPERM ()
 5
 6BLOCKTEMP ()
 7
 8BLACKLIST (294)
 9 1.34.22.137	  1-34-22-137.HINET-IP.hinet.net.
10 1.34.37.220	  1-34-37-220.HINET-IP.hinet.net.
11 1.34.118.204	  1-34-118-204.HINET-IP.hinet.net.
12 1.34.120.110	  1-34-120-110.HINET-IP.hinet.net.
13 1.34.186.220	  1-34-186-220.HINET-IP.hinet.net.
14 5.14.166.230	  5-14-166-230.residential.rdsnet.ro.
15 5.40.247.175	  5.40.247.175.static.user.ono.com.
16 5.140.218.7	  3(NXDOMAIN)
17 14.55.82.153	  3(NXDOMAIN)
18 14.162.1.22	  static.vnpt.vn.
19 14.162.2.86	  static.vnpt.vn.
20 14.162.224.99	  static.vnpt.vn.
21 14.164.64.57	  static.vnpt.vn.
22 14.169.41.21	  static.vnpt.vn.
23 14.175.228.185	  static.vnpt.vn.
24 14.181.87.119	  static.vnpt.vn.
25 14.189.184.169	  static.vnpt.vn.
26 23.31.62.17	  23-31-62-17-static.hfc.comcastbusiness.net.
27^C 27.64.74.16	  localhost.
28 27.105.106.193	  27-105-106-193-adsl-TPE.static.so-net.net.tw.
29 27.152.7.38	  ^C
30[louisk@mail louisk 14 ]$

If you’re looking for a script to modify your pf tables, this will do the trick. Its not terribly pretty, but it works.

 1#!/bin/sh
 2#
 3echo "May prompt for sudo password"
 4
 5TABLE=$1
 6ACTION=$2
 7IP=$3
 8if [ -z ${TABLE} -o -z ${ACTION} -o -z ${IP} ] ; then
 9	echo "Syntax: $0 <table_name> <action> <ip>"
10	echo "action could be: add delete
11	exit 1
12fi
13
14#CMD_PFX='echo "Would Do: "'
15echo "${ACTION} ${IP} to/from ${TABLE}"
16${CMD_PFX} sudo /sbin/pfctl -t ${TABLE} -T ${ACTION} ${IP}

Lastly, you will want a cron entry to clean out your pf tables. I find that most abusive hosts are not permanent infrastructure, but hosts that get compromised and added to bot nets. Because of this, I don’t leave things in the BLOCKTEMP for very long. sshguard is different, I leave them in for a while longer. Here are the crontab entries:

1#### Clear entries older than 3600 secs from the table "blacklist"
20	*	*	*	*	root	pfctl -q -t BLOCKTEMP -T expire 3600 > /dev/null
30	5	1	*	*	root	pfctl -q -t sshguard -T expire 2000000 > /dev/null

Footnotes and References

Copyright

Comments