# HOWTO: Break bruteforce connections

## xHemi

As of late I have been getting a lot of unwanted bruteforce ssh connections to my server and I needed a fast and simple remedy to the problem. This is my 'quick and dirty for n00bs'   :Wink:  .

First I added a new chain to iptables

```

...

iptables -I INPUT -j BANISH

iptables -I FORWARD -j BANISH

iptables -I OUTPUT -j BANISH

iptables -N BANISH

...

```

The BANISH rule chain will just do one thing, to check the source of the IP packet against a blacklist, /etc/banished. If a packet is mapped to that then it will be dropped. Remember that we are doing this to IP packets which effectively breaks any type of connection.

```

..

BL_CL=$(cat /etc/banished| xargs)

for i in $BL_CL;do

    iptables -A BANISH -s $i -j DROP

done

..

```

Then I created a script to analyze my system log file

```

#!/bin/bash

BLIST="/etc/banished"

LOG="/var/log/messages"

RUN_FIREWALL="/etc/init.d/firewall.sh"

MF=$(tail -n 700 $LOG | grep -E "sshd.*Failed[[:space:]]password"|sed -e "s#.*from[[:space:]]\(.*\)[[:space:]]port.*#\1#" | uniq)

for i in $MF;do

  echo "FOUND: $i"

  if [ "$(tail -n 700 $LOG | grep "$i" | wc -l)" -gt 50 ];then

    echo "INTRUDER: $i"

    if [ -z "$(grep $i $BLIST)" ]; then

      echo "$i" >> $BLIST

      echo "ADDED: $i TO: $BLIST"

      CHANGE=1

    else

      echo "PRESENT: $i IN: $BLIST"

    fi

  fi

done

if [ -n "$CHANGE" ];then $RUN_FIREWALL;fi

```

What the script does is to check the last 700 entries in the system log, 700 was enough for me and a tradeoff between speed/efficiency, if anyone during those 700 lines is found to have made 50 consecutive failed login attempts then they are recognized as an intruder. If an intruder has been detected then the script adds that IP to the /etc/banished file and reruns the firewall script.

The last thing to do is to make a cronjob which runs the check every 30 minutes.

```

...

30   *   *      *   *      /usr/local/sbin/banish_morons

...

```

And of course activate the new crontab! 

For me this is effective enough and its somewhat scalable since it lets you create other scripts which can also add or remove IP addresses to the /etc/banished file. I hope this helps someone, happy gentooing everybody.  :Wink: 

----------

## jlh

I had the same problem, but solved it a bit differently.  Those attacks make tons of connections in a row, so banning them as quickly as possible is better, safer and does not flood the log files.  Your method can make bans happen up to 30 minutes later (15 min. average), when the host most probably already stopped the attack, because SSH's "max connections per host" limit will be hit long before that.  And for those that wonder, these connections come from random locations, probably from comprimised systems, so it's of no use to keep bans for a long time.  My script lifts them after like 10 minutes and it's unusual for the same IP to ever try again.  Banning policy is quite heavy: A ban happens anytime someone tries to log in with a user name that has a levenshtein distance > 2 from any allowed user name (allowing for two typos in real names).

I'm in a hurry now, but if someone is interested in my solution, please ask and I could put it somewhere (perl script).

----------

## xHemi

Great feedback jlh! It would be nice of you to post the perl script.

----------

## user118696

 *jlh wrote:*   

> A ban happens anytime someone tries to log in with a user name that has a levenshtein distance > 2 from any allowed user name (allowing for two typos in real names).

 

 *jlh wrote:*   

> I'm in a hurry now, but if someone is interested in my solution, please ask and I could put it somewhere (perl script).

 

I'm interested in your solution. That levenshtein distance... never heard of it before. Can you post your scripts here.

----------

## Aurora

There are a couple of scripts that accomplish the same thing...   :Smile: 

http://blinkeye.ch/mediawiki/index.php/SSH_Blocking

and...

http://www.fail2ban.org/wiki/index.php/Main_Page

----------

## mudrii

I use DenyHosts 

http://gentoo-wiki.com/HOWTO_Protect_SSHD_with_DenyHosts

----------

## jlh

Yes, there are many such solutions around, but some people just like to write their own.   :Very Happy:   Here's mine.  I thought this would be a good time to clean it up a little, I hope I did not introduce bugs.  Comments welcome.

As far as I can tell, these are the dependencies:dev-perl/Text-LevenshteinXSnet-firewall/iptablessys-apps/util-linux

WARNING: You have to edit the settings at the top of the file!  Especially for the list of users allowed to log in!  If you don't, this will just ban everyone that tries!

/usr/local/sbin/badguy

```
#!/usr/bin/perl -w

# written by jlh at gmx dot ch (2006 - 2007)

# public domain

use strict;

use warnings;

use Text::LevenshteinXS qw(distance);

my %opts = (

   # log file to watch

   log_file => '/var/log/messages',

   # patterns that match a login attempt.  the first capture is the user

   # name, the second capture is the host/IP.  Note that for some

   # systems/messages, you'll get an IP here, for others a host name.

   patterns => [

      qr/sshd\[\d+\]: Invalid user (\S+) from (\S+)/,

      qr/sshd\[\d+\]: User (\S+) from (\S+) not allowed because not listed in AllowUsers/,

   ],

   # here are sample lines from my logs, yours might differ:

   # "May  3 23:24:40 bender sshd[20244]: Invalid user admin from 71.4.52.81"

   # "May  5 15:33:47 bender sshd[21196]: User root from sahrit.org.zw not allowed because not listed in AllowUsers"

   # interval in seconds for checking the log file

   poll_interval => 5,

   # list of user names that are allowed to log in!  WARNING: be sure to

   # put here all the users that should be allowed to log in!

   user_list => [ qw(jlh gin) ],

   # max levenshtein distance permitted to allowed user names (number of

   # typos allowed), see http://en.wikipedia.org/wiki/Levenshtein_distance

   max_lsd => 2,

   # how long to ban, in seconds

   ban_time => 600,

   # hosts/IPs to never ban (no DNS resolution is done, merely string

   # matching to whatever the log file gives us).  you might want to put

   # your hostname in here (mine is bender), to avoid annoying bans

   never_ban => [

      qr/^bender$/,

      qr/^127\.|^localhost$/,           # localhost 127/8 block

      qr/^10\./,                        # private 10/8 block

      qr/^172\.(?:1[6-9]|2\d|3[01])\./, # private 172.16/12 block

      qr/^192\.168\./,                  # private 192.168/16 block

   ],

);

# TODO: when /var/log/messages gets replaced (log rotation), this program will

# stop working.

sub ip_ban {

   my $ip = shift;

   system("iptables -A INPUT -s '$ip' -j DROP");

}

sub ip_unban {

   my $ip = shift;

   system("iptables -D INPUT -s '$ip' -j DROP");

}

sub log {

   my $s = shift;

   # using this form so we don't have to quote anything (it's not going

   # through a shell)

   system('logger', 'badguy:', $s);

}

# %ban_list contains all currently banned IPs/hosts, indexed by IP/host and the

# values are hash refs containing:

#   user    => username that was attempted,

#   release => UNIX time of when to release this ban

my %ban_list;

sub ban_new {

   my ($ip, $user, $ban_time) = @_;

   $ban_list{$ip} = { 'user' => $user, 'release' => $ban_time + time };

   &ip_ban($ip);

   &log("$ip banned for login attempt as '$user'");

}

# remove bans that are over

sub ban_purge {

   my $now = time;

   for my $ip (keys %ban_list) {

      if ($now >= $ban_list{$ip}{release}) {

         &ip_unban($ip);

         &log("ban on $ip removed");

         delete $ban_list{$ip};

      }

   }

}

sub ban_remove_all {

   for my $ip (keys %ban_list) {

      &ip_unban($ip);

      &log("ban on $ip removed (cleaning up)");

   }

   %ban_list = ( );

}

sub check_login {

   my ($ip, $user) = @_;

   return if exists $ban_list{$ip};

   # check for mistyped real users

   for my $u (@{$opts{user_list}}) {

      return if &distance(lc $u, lc $user) <= $opts{max_lsd};

   }

   # check for things to never ban

   for (@{$opts{never_ban}}) {

      return if $ip =~ $_;

   }

   &ban_new($ip, $user, $opts{ban_time});

}

sub handle_line {

   my $line = shift;

   for my $p (@{$opts{patterns}}) {

      next unless $line =~ $p;

      &check_login($2, $1); # $2 = ip, $1 = user

      last;

   }

}

sub signal_handler {

   my $sig = shift;

   &ban_remove_all;

   &log("received SIG$sig, exiting");

   exit 0;

}

$SIG{TERM} = \&signal_handler;

$SIG{INT} = \&signal_handler;

&log('started');

# here we open the log file, seek to the end and try to read new lines at

# regular intervals (similar to 'tail -f')

open FH, '<', $opts{log_file};

seek FH, 0, 2;

while(1) {

   seek FH, 0, 1; # this makes sure EOF flag is reset

   while (<FH>) {

      &handle_line($_);

   }

   &ban_purge;

   sleep $opts{poll_interval};

}
```

This file is for making it a service that you can set to be started at boot time.

/etc/init.d/badguy

```
#!/sbin/runscript

# written by jlh at gmx dot ch (2007)

# public domain

exe=/usr/local/sbin/badguy

pidfile=/var/run/badguy.pid

depend() {

   # this service doesn't make sense without sshd running, but

   # we won't make it a dependency.  (order is unimportant)

        use logger

}

start() {        

        ebegin "Starting bad guy (SSH banner)"  

        start-stop-daemon --start --background --pidfile "$pidfile" --make-pidfile --exec "$exe"

        eend $?

}

stop() {

        ebegin "Stopping bad guy (SSH banner)"

        start-stop-daemon --stop --pidfile "$pidfile" --exec "$exe" --name badguy

        eend $?

}
```

----------

