Frustrated by brute force attacks on your server from botnets with ever-changing IP addresses?

Me, too.

I finally got around to assembling a simple Linux active firewall, which I call “slaf”. This PHP script monitors the CentOS secure log and records failed FTP login attempts into a MySQL database. When the failed attempts get to be too numerous from a single IP address, the script adds the offending IP address to an ipset blacklist that is part of the iptables ruleset.

There are other ways to accomplish this, but I found this approach to be easy to code and reliable. It also performs very well, both in terms of how fast it processes the secure log, and how efficiently ipset handles a long list of blocked IP addresses.

The “slaf” utility is comprised of four basic parts:

  1. A php script we’ll call slaf.php. It turns failed FTP login log records into MySQL records for tracking the number of failed logins, including when they happened, and also does the needed queries and record keeping for tracking which IPs we’ve already blocked.
  2. The MySQL database, which we’ll call slaf.
  3. An ipset list called blacklist that we’ll add to our iptables firewall rules.
  4. A cron job that runs slaf.php on whatever schedule suits you.

Here’s slaf.php. It’s fairly heavily commented, so hopefully it’s clear what is happening in each section of code.

= 1 && $failedstringpos >= 1);
	DebugOutput($buffer);
	if($failedftplogin && $maxlogdate <= $mysqldate) {
		// if this is a failed login, and the date is newer than anything we've previously recorded, record it in the database log_entries table
		$tempipstr=substr(substr($buffer,$failedstringpos),strpos(substr($buffer,$failedstringpos),"[")+1);
		$failedip=substr($tempipstr,0,strpos($tempipstr,"]"));
		$serviceid=substr($buffer,33,strpos($buffer,"]")-33);
		DebugOutput("MySQL Date: ".$mysqldate);
		DebugOutput("Failed IP: ".$failedip);
		DebugOutput("Service ID: ".$serviceid);
		$sql = "insert into log_entries (service,serviceid,logdate,ip,entrydate) values ('proftpd','$serviceid','$mysqldate','$failedip',now());";
		DebugOutput($sql);
		mysql_query($sql);
	}
	else
		DebugOutput("Skipping log record");
}

// now lets query the database to find out which ip addresses are acting offensively, haven't been blocked before, and then block the offenders who cross our threshhold (more than $attemptslimit in $dayslimit days or less)
$result=mysql_query("SELECT COUNT(*) AS ipcount,ip,MIN(logdate) AS minlogdate,MAX(logdate) AS maxlogdate,DATEDIFF(MAX(logdate),MIN(logdate)) AS timelapse FROM log_entries where not ip in (select ip from drop_log) GROUP BY ip ORDER BY ipcount DESC");
while ($dbrow=mysql_fetch_assoc($result)) {
	if($dbrow["ipcount"] >= $attemptslimit and $dbrow["timelapse"] <= $dayslimit) {
		$dropcommand="ipset add blacklist ".$dbrow["ip"];
		// record the ip address in our log of dropped ips
		$sql = "insert into drop_log (ip,mindate,maxdate,dropcommand,entrydate) values ('".$dbrow["ip"]."','".$dbrow["minlogdate"]."','".$dbrow["maxlogdate"]."','$dropcommand',now());";
		mysql_query($sql);
		DebugOutput($dropcommand);
		// execute the shell command to add the ip address to the ipset blacklist
		DebugOutput(shell_exec($dropcommand));
		DebugOutput($sql);
	}
}

?>

Here’s what you’ll need to create the MySQL database I’m calling slaf:

/*
SQLyog Community v10.12 
MySQL - 5.1.73-log : Database - slaf
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`slaf` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `slaf`;

/*Table structure for table `drop_log` */

DROP TABLE IF EXISTS `drop_log`;

CREATE TABLE `drop_log` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `ip` varchar(50) NOT NULL,
  `mindate` datetime NOT NULL,
  `maxdate` datetime NOT NULL,
  `dropcommand` varchar(255) NOT NULL,
  `entrydate` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ip` (`ip`)
) ENGINE=MyISAM AUTO_INCREMENT=56 DEFAULT CHARSET=utf8;


DROP TABLE IF EXISTS `log_entries`;

CREATE TABLE `log_entries` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `service` varchar(50) NOT NULL,
  `serviceid` int(11) NOT NULL,
  `logdate` datetime NOT NULL,
  `ip` varchar(50) NOT NULL,
  `entrydate` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `service-unique` (`service`,`serviceid`,`logdate`),
  KEY `logdate` (`logdate`),
  KEY `ip` (`ip`)
) ENGINE=MyISAM AUTO_INCREMENT=3788 DEFAULT CHARSET=utf8;


/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

Don’t forget to create a database user and password, and put those values in the PHP script.

Next up, if you don’t already have it, install ipset on your server. For CentOS, that command is:

# yum install ipset

After ipset is installed, create the ipset list we’re calling blacklist:

# ipset create blacklist hash:ip hashsize 4096

Then, add the ipset blacklist as a rule to our iptables firewall:

# iptables -I INPUT  -m set --match-set blacklist src -j REJECT

You’re ready to run slaf.php manually:

# php /whatever-path-you-used/slaf.php

You can examine the ipset blacklist to see what IP addresses have been blocked:

# ipset list

You can also review the records in the log_entries and drop_log tables in the slaf MySQL database.

Once slaf is tuned to your liking, add a cron job to run it on a schedule that suits you. If your server can handle it, running it every few minutes will stop a lot of brute force attempts while they are in process. If you aren’t comfortable running it that often, you may miss attacks while they are in progress, but your server will still benefit from blocking IPs that are obviously compromised.

This utility should be fairly easy to adapt to other log files, including Apache access logs. Making it work for other services is really just a matter of adapting the log file parsing: Find the conditions that look like illicit attempts to access server resources, log the IP in the database, and then apply some logic via the database query for how much of that nonsense you are willing to tolerate.

There are, of course, other ways to do this. Here’s a helpful article on how to stop FTP hacking using proftpd’s configuration file and various shell tools.

If you are familiar with iptables but have not used ipset, I highly recommend you read up on ipset. It’s straightforward, and a great tool for blocking long lists of IP addresses without taxing your server.