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
Comments