Thursday, October 12, 2006

Executive Summary: This article details how to minimize hack attempts on open facing ports on a *nix machine, by reading the attackers IP addresses from a log file, and adding the address to hosts.deny

Chances are pretty good if you've got a port open to the internet you're seeing unwanted traffic on it. If you've got a service such as telnet or FTP or POP that transmits passwords unencrypted, you're significantly at risk for a breach from a packet being intercepted, but even if you're using a secure protocol such as SSH, you're vulnerable to a brute force attempt to break into your site. As an example, here is a sample of a log file showing a break-in attempt on one of my servers:

Oct 8 13:54:18 chapelle sshd(pam_unix)[4554]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=
Oct 8 13:54:22 chapelle sshd(pam_unix)[4557]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=
Oct 8 13:54:27 chapelle sshd(pam_unix)[4560]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=
Oct 8 13:54:31 chapelle sshd(pam_unix)[4563]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=
Oct 8 13:54:36 chapelle sshd(pam_unix)[4566]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=
Oct 8 13:54:40 chapelle sshd(pam_unix)[4569]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost= user=root
Oct 8 13:54:44 chapelle sshd(pam_unix)[4571]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost= user=root
Oct 8 13:54:49 chapelle sshd(pam_unix)[4573]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost= user=root
Oct 8 13:54:53 chapelle sshd(pam_unix)[4575]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=

On my linux box (Fedora Core 5), this is coming from /var/spool/messages What this shows is an attempt to login via SSH on the machine "chapelle." In some cases the login attempts were with no user name, in some cases, they were with username root.

The log files used to go on and on with these breakin attempts, as the users tried about 5000 common login names and passwords. And it could easily be more in the future. Although I use a secure username and password, and do not allow root to login remotely, I still am annoyed by the attempts. If you have users who have common user names, such as "john" or "jjones" or something like that, then these break-in attempts are more than just an annoyance for you. Don't think "I don't have anything worth breaking in for", because hackers certainly are not thinking that.

So what can you do? On a *nix based system, there is a file called hosts.deny -- on Fedora it's at /etc/hosts.deny If you add an IP address or hostname to this file, that IP address will no longer be able to connect. So, all you have to do is get the IP address of the attacking machine into that file, and you'll block them.

What follows is how I do it -- it's not particularly clever, but I think it's generic enough that anyone else faced with this problem can adapt my method to their problems. You could use this for any service that keeps a log file that shows offending IP addresses.

On a *nix box, sshd (the program used to run ssh) is set up to log all important messages to /var/log/messages. This log file usually contains alot of other messages too, so it's important to filter out some of the noise before trying to process the file. In order to do that, I run the following command:
(I made it tiny so it will be one one line. You're gonna cut and paste it anyway)

cat /var/log/messages | grep "`date "+%b %e %H"`" | grep authentication | grep failure | grep sshd > /usr/local/hourlyssh.log

I'll break down what this does step by step

cat /var/log/messages --> prints out the entire messages file
| grep "`date "+%b %e %H"`" --> only include lines from this hour
| grep authentication --> only include lines with the word "authentication
| grep failure --> only include lines with the word "failure"
| grep sshd only --> include lines with the word "sshd"
> /usr/local/hourlyssh.log --> overwrite the hourlyssh log with the new results

After running that command, you've got only the lines you want in a file. Now you just have to parse the file to get out the IP addresses and add them to /etc/hosts.deny You could easily do this in a shell script, or in PERL or really any language you want. I'm doing it in Java because I believe that's the lowest common denominator.

Save the following as ~root/

import java.util.Hashtable;
import java.util.StringTokenizer;
import java.util.Enumeration;

public class DenyAddress {

public static final String HOSTSDENY_PATH = "/etc/hosts.deny";
public static final String HOURLYLOG_PATH = "/usr/local/hourlyssh.log";

public static void main(String argv[]) {
try {
File fml = new File(HOURLYLOG_PATH);
String line = null;
Hashtable toBeBanned = new Hashtable();
BufferedReader in = new BufferedReader(new FileReader(fml));
This assumes your log format looks like this:

Aug 2 06:01:27 myserver sshd(pam_unix)[7402]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=

If it doesn't you'll have to parse the ip out of the log yourself.

while ((line = in.readLine()) != null) {
StringTokenizer st = new StringTokenizer(line, " ");
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (token.startsWith("rhost")) {
String ip = token.substring(6);
if (toBeBanned.get(ip) == null) {
toBeBanned.put(ip, 0);
else {
//keep track of how many failed attempts per ip address
Integer i = (Integer) toBeBanned.get(ip);
toBeBanned.put(ip, i + 1);
//we're finished with reading toBeBanned
Enumeration en = toBeBanned.keys();
while (en.hasMoreElements()) {
String ip = (String) en.nextElement();
Integer val = (Integer) toBeBanned.get(ip);
//only ban people with 3 or more failed login attempts
if (val <= 3) {



else {

//do nothing;



//now read from hosts.deny to make sure we don't add an address that's already banned

fml = new File("/etc/hosts.deny");

in = new BufferedReader(new FileReader(fml));

while ((line = in.readLine()) != null) {

if (line.startsWith("#")) {



else {

StringTokenizer st = new StringTokenizer(line, ":");

while (st.hasMoreTokens()) {

if (st.countTokens() == 2) {


String nip = st.nextToken().trim();






//append onto the existing file;

FileWriter fw = new FileWriter(fml, true);

PrintWriter pw = new PrintWriter(fw);

Enumeration en2 = toBeBanned.keys();

StringBuffer bannedString = new StringBuffer();

while (en2.hasMoreElements()) {

String bip = (String) en2.nextElement();

pw.println("ALL: " + bip);

bannedString.append(" " + bip);





if (bannedString.length() > 0) {
//write banned addresses to syslog
//you can omit this line if you don't need extra confirmation
Runtime.getRuntime().exec("logger \"banning " + bannedString.toString() + "\"");
catch (Exception e) {
e.printStackTrace(); //run this a few times manually to see if it works for you


So now I've got the code that does what I want -- now I just have to rol it all together.

I make a shell script called denyaddresses and put it in ~root

echo `date`
cat /var/log/messages | grep "`date "+%b %e %H"`" | grep authentication | grep failure | grep sshd > /usr/local/hourlyssh.log
cd /root
java DenyAddress
echo " ";
echo " ";

compile in /root by doing

$JAVA_HOME/bin/javac -classpath .

then as root do a crontab -e and add the following line:

*/5 * * * * ~root/denyaddresses >> ~root/deny.log

Note that this must be run as root in order to edit the hosts.deny file

I've tried to make this relatively generic. Hopefully you'll be able to adapt this to your own needs. If not, post here, and I'm sure the community can help you out.


JimmytheGeek said...

java?!?!? I'll work this up in bash or perl and compare.


Dan Fishman said...

Addendum: 3 years later, I had to clean out the log files -- it filled up my little FW disk...3400 banned IP addresses. Probably don't need to print every 5 minutes about the addresses I've already banned over and over and over again :)

Dan Fishman said...

Addendum: Since Jimmy never got around to it, here's Chat GPT's version in bash


# Log file that contains login attempts

# Temporary file for parsing

# Extract relevant lines with failed attempts, reverse to handle newest entries first
grep "Failed password" "$LOGFILE" | tac | while read -r line
# Extract IP and timestamp
IP=$(echo "$line" | awk '{print $(NF-3)}')
TIMESTAMP=$(echo "$line" | cut -d" " -f1-3)

# Convert the timestamp to seconds from epoch for easier comparison
TIMESTAMP_EPOCH=$(date --date="$TIMESTAMP" +%s)

# Check if this IP has already been processed
if grep -q "$IP" "$TMPFILE"; then

# Check how many attempts within the last 5 minutes
COUNT=$(grep "$IP" "$LOGFILE" | while read -r line2
TIMESTAMP2=$(echo "$line2" | cut -d" " -f1-3)
TIMESTAMP2_EPOCH=$(date --date="$TIMESTAMP2" +%s)

# If the time difference is less than or equal to 5 minutes
if [ $((TIMESTAMP_EPOCH - TIMESTAMP2_EPOCH)) -le 300 ]; then
echo "$line2"
done | wc -l)

# If there are 5 or more attempts, add to hosts.deny
if [ "$COUNT" -ge 5 ]; then
echo "ALL: $IP" >> /etc/hosts.deny

# Log this IP as processed
echo "$IP" >> "$TMPFILE"

# Remove temporary file