OpenWrt Forum Archive

Topic: fail2ban alternative, lightweight ssh brute force banning script here

The content of this topic has been archived on 20 Apr 2018. There are no obvious gaps in this topic, but there may still be some posts missing at the end.

I wanted to install something that would auto-block IPs from people trying to brute-force ssh passwords.  fail2ban and similar scripts aren't available since they are written in Python which is a bit heavyweight for OpenWRT.  logtrigger looks nice but I don't see a package for it and I don't feel like cross compiling it and installing it.  So I wrote a short shell script which more or less does the job, and though I'd share it.

It's lightweight, runs out of cron, and it's all self contained in one file including documentation.  It's written in busybox ash and should work completely out of the box with no additional packages required.

The list of banned hosts can optionally persistent across reboots, you can permanently whitelist (or blacklist) hosts or networks, and the default expiration time ("lease time") is easily set.

#!/bin/sh
#
# dropBrute.sh by robzr
#
# minimalist OpenWRT/dropbear ssh brute force attack banning script
#
# Installation steps:
#
# 1) Optionally edit the variables in the header of this script to customise
#    for your environment
#
# 2) Insert a reference for this rule in your firewall script before you
#    accept ssh, something like:
#
#    iptables -N dropBrute
#    iptables -I input_rule -i br-wan -p tcp --dport 22 -j dropBrute
#    iptables -I input_rule -i br-wan -p tcp --dport 22 -m state --state NEW -m limit --limit 6/min --limit-burst 6 -j ACCEPT
#
# 3) Run the script periodically out of cron:
#
#    echo '*/10 * * * * /usr/sbin/dropBrute.sh 2>&1 >> /tmp/dropBrute.log' >> /etc/crontabs/root
#
# 4) If cron is not enabled, you'll also need to run the following:
#
#    /etc/init.d/cron enable && /etc/init.d/cron start
#
#
# To whitelist hosts or networks, simply add a manual entry to the lease
# file with a leasetime of -1.  This can be done with the following syntax:
#
#    echo -1 192.168.1.0/24 >> /tmp/dropBrute.leases
#
# A static, or non-expiring blacklist of a host or network can also be
# added, just use a lease time of 0.  This can be done with the following syntax:
#
#    echo 0 1.2.3.0/24 >> /tmp/dropBrute.leases

# How many bad attempts before banning. Only the log entries from the 
# current day are checked.
allowedAttempts=10

# How long IPs are banned for after the current day ends.
# default is 7 days
secondsToBan=$((7*60*60*24))

# the "lease" file - defaults to /tmp which does not persist across reboots
leaseFile=/tmp/dropBrute.leases

# This is the iptables chain that drop commands will go into.
# you will need to put a reference in your firewall rules for this
iptChain=dropBrute

# the IP Tables drop rule
iptDropRule='-j DROP'

# the IP Tables whitelist rule
iptWhiteRule='-j RETURN'

# You can put default leasefile entries in the following space.
# Syntax is simply "leasetime _space_ IP_or_network".  A leasetime of -1 is a 
# whitelist entry, and a leastime of 0 is a permanent blacklist entry.
[ -f $leaseFile ] || cat <<__EOF__>>$leaseFile
-1 192.168.1.0/24
__EOF__

# End of user customizable variables (unless you know better :) )

ipt='/usr/sbin/iptables'

[ `date +'%s'` -lt 1320000000 ] && echo System date not set, aborting. && exit -1
$ipt -N $iptChain >&/dev/null

today=`date +'%b %d'`
now=`date +'%s'`
nowPlus=$((now + secondsToBan))

echo Running dropBrute on `date` \($now\)

# find new badIPs
for badIP in `logread|egrep "^$today"|fgrep dropbear|egrep 'login attempt for nonexistent user'\|'bad password attempt for'|sed 's/^.*from //'|sed 's/:.*$//'|sort -u` ; do
  found=`logread|egrep "^$today"|fgrep dropbear|egrep 'login attempt for nonexistent user'\|'bad password attempt for'|sed 's/^.*from //'|sed 's/:.*$//'|fgrep $badIP|wc -l`
  if [ $found -gt $allowedAttempts ] ; then
    if [ `egrep \ $badIP\$ $leaseFile|wc -l` -gt 0 ] ; then
       [ `egrep \ $badIP\$ $leaseFile|cut -f1 -d\ ` -gt 0 ] && sed -i 's/^.* '$badIP\$/$nowPlus\ $badIP\/ $leaseFile
    else
       echo $nowPlus $badIP >> $leaseFile
    fi
  fi
done

# now parse the leaseFile
while read lease ; do
  leaseTime=`echo $lease|cut -f1 -d\ `
  leaseIP=`echo $lease|cut -f2 -d\ `
  if [ $leaseTime -lt 0 ] ; then
    if [ `$ipt -S $leaseChain|egrep \ $leaseIP/32\ \|\ $leaseIP\ |fgrep -- "$iptWhiteRule"| wc -l` -lt 1 ] ; then
      echo Adding new whitelist rule for $leaseIP
      $ipt -I $iptChain -s $leaseIP $iptWhiteRule
    fi
  elif [ $leaseTime -ge 1 -a $now -gt $leaseTime ] ; then
    echo Expiring lease for $leaseIP
    $ipt -D $iptChain -s $leaseIP $iptDropRule
    sed -i /$leaseIP/d $leaseFile
  elif [ $leaseTime -ge 0 -a `$ipt -S $leaseChain|egrep \ $leaseIP/32\ \|\ $leaseIP\ |wc -l` -lt 1 ] ; then
    echo Adding new rule for $leaseIP
    $ipt -A $iptChain -s $leaseIP $iptDropRule
  fi
done < $leaseFile

(Last edited by robzr on 6 Nov 2011, 21:45)

wiremeup wrote:

try http://configserver.com/free/csf/install.txt

may or may not help, heard it's light on resources and good for blocking brute force attacks

1.8mb of Perl scripts, not to mention Perl itself, is not considered lightweight by OpenWRT standards smile

Rob

I would suggest the logwatch package from the x-wrt project here. It allows you to define scan patterns for various log sources and define triggers for them. It is small and written in C.

@robzr
This looks exactly what I am looking for - "lightweight fail2ban". Could you elaborate a little on the installation step 2. I kinda understand iptables rules (and can read man pages), but some confirmation from the expert would be helpful...
I put these lines in /etc/firewall.user; so I assume they will run with each firewall (re-)start and that's what's needed.
You have '-i br-wan' in the rule. Does it have to match anything? or it is just the label of a rule?

Thank you

ymhee_bcex wrote:

@robzr
This looks exactly what I am looking for - "lightweight fail2ban". Could you elaborate a little on the installation step 2. I kinda understand iptables rules (and can read man pages), but some confirmation from the expert would be helpful...
I put these lines in /etc/firewall.user; so I assume they will run with each firewall (re-)start and that's what's needed.
You have '-i br-wan' in the rule. Does it have to match anything? or it is just the label of a rule?

Thank you

Hi, you're right, /etc/firewall.user is run each time the network interfaces are brought up/down or the firewall is restarted (manually, via /etc/init.d/firewall restart).

The "-i br-wan" means to match packets incoming on the interface br-wan.  Generally in OpenWRT, the br-wan is an bridge interface that is on the "WAN" (Internet usually) side of your firewall.  So the reason that's in there is so you are only checking packets coming from the "dangerous" side of your firewall (assuming you trust your LAN users; if you're using OpenWRT, thats probably a safe assumption).

Here's the rundown of what those rules will do:

iptables -N dropBrute

Creates a new user-defined chain, called dropBrute

iptables -I input_rule -i br-wan -p tcp --dport 22 -j dropBrute

Any packets coming in on the wan interface will traverse the dropBrute chain.  This is important, because the dropBrute script will insert DROP rules into this chain to drop SSH requests if they are coming from banned hosts, or ACCEPT rules if it's from a whitelist host.  So it's the first thing you want to traverse.

iptables -I input_rule -i br-wan -p tcp --dport 22 -m state --state NEW -m limit --limit 6/min --limit-burst 6 -j ACCEPT

Once it's passed the black/white list and doesn't match anything in there, control will return back to the input_rule chain, and the next rule is to allow in a limited number of SSH requests.  You can do without the "-m limit --limit 6/min --limit-burst 6" if you like, it might be considered a little redundant alongside the dropBrute script.

regards,

Rob

robzr,

I am still a bit nervous about br-wan. I don't see this anywhere else. I see br-lan, and my WAN interface is called eth0.1. Moreover, I added a line for my smartphone (0 a.b.c.d) and then ran dropBrute.sh (I even got an output "Adding new rule for a.b.c.d") - and still had no problem logging in from that IP. So, something is not working properly.

I remember there is a way to find the appropriate names from 'uci show' command - if I only knew what to look for!

I also have a doubt about the parsing part of the code - but (a) the iptable part is obviously more important; (b) I can troubleshoot it myself; and (c) I can add entries manually, as long as they actually get effect.

I appreciate your time helping me out with that. Hopefully, it will make your program better in the end smile

ok, once I put eth0.1 instead of br-wan, seems to be working... I think you can get the name of the interface by running

uci get network.wan.ifname

I think I see the problem.
I have a firewall rule (through LuCI UI) that opens port 22 on the firewall. Everything that is added afterwards is just not getting invoked after that rule. Whether it's through your script, or through UI, it's added *after* generic "ACCEPT" rule. Whatever firewall does, it matches this ACCEPT rule (for any source IP), and allows traffic to the router. I am not sure how to get this DENY rule in dropBrute chain *before* my generic ACCEPT rule.

I hope I could explain clearly... At this time dropBrute chain is just not getting triggered sad

Many thanks for this! I've just implemented a IDS in a DMZ outside my router and now have a method to limit SSH break-in attempts. I changed the iptables rules to match the WAN interface name as listed by 'ifconfig' and I've already got a 'hit' listed! :-)

Thanks for the feedback.  You motivated me to blow the dust off this script (not to mention my router) and get a fresh Attitude Adjustment install setup, and incorporate a few fixes and new features into the script.  If you have any suggestions or bugs you've found, please let me know.

Rob

Hi rob,

got visited by Chinese Hackers, too

Did you make any progress in porting your script to attitude adjustment?

I'm really interested to implement it on my OpenWRT router, too.

Many thanks and best regards

WRTHacker wrote:

Hi rob,

got visited by Chinese Hackers, too

Did you make any progress in porting your script to attitude adjustment?

I'm really interested to implement it on my OpenWRT router, too.

Many thanks and best regards

Me too smile

There was some issues with the lease logic, case sensitive grep, date format etc. Made some changes so it works for me in barrier breaker.

#!/bin/sh
#
# dropBrute.sh by robzr
#
# minimalist OpenWRT/dropbear ssh brute force attack banning script
#
# Installation steps:
#
# 1) Optionally edit the variables in the header of this script to customise
#    for your environment
#
# 2) Insert a reference for this rule in your firewall script before you
#    accept ssh, something like:
#
#    iptables -N logndrop
#    iptables -N dropBrute
#    iptables -A logndrop -j LOG
#    iptables -A logndrop -j DROP
#    iptables -A input_rule -p tcp --dport 22 -i eth1 -m state --state NEW -m recent --set
#    iptables -A input_rule -p tcp --dport 22 -i eth1 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j logndrop
#    iptables -A input_rule -p tcp --dport 22 -j dropBrute
#
#    This will block the brute force attack until the IP is banned by the script
#
# 3) Run the script periodically out of cron:
#
#    echo '*/10 * * * * /usr/sbin/dropBrute.sh 2>&1 >> /tmp/dropBrute.log' >> /etc/crontabs/root
#
# 4) If cron is not enabled, you'll also need to run the following:
#
#    /etc/init.d/cron enable && /etc/init.d/cron start
#
#
# To whitelist hosts or networks, simply add a manual entry to the lease
# file with a leasetime of -1.  This can be done with the following syntax:
#
#    echo -1 192.168.1.0/24 >> /tmp/dropBrute.leases
#
# A static, or non-expiring blacklist of a host or network can also be
# added, just use a lease time of 0.  This can be done with the following syntax:
#
#    echo 0 1.2.3.0/24 >> /tmp/dropBrute.leases

# How many bad attempts before banning. Only the log entries from the 
# current day are checked.
allowedAttempts=10

# How long IPs are banned for
# default is 1 day
secondsToBan=$((1*60*60*24))

# the "lease" file - defaults to /tmp which does not persist across reboots
leaseFile=/tmp/dropBrute.leases

# This is the iptables chain that drop commands will go into.
# you will need to put a reference in your firewall rules for this
iptChain=dropBrute

# the IP Tables drop rule
iptDropRule='-j DROP'

# the IP Tables whitelist rule
iptWhiteRule='-j RETURN'

# You can put default leasefile entries in the following space.
# Syntax is simply "leasetime _space_ IP_or_network".  A leasetime of -1 is a 
# whitelist entry, and a leastime of 0 is a permanent blacklist entry.
[ -f $leaseFile ] || cat <<__EOF__>>$leaseFile
-1 192.168.1.0/24
__EOF__

# End of user customizable variables (unless you know better :) )

ipt='/usr/sbin/iptables'

[ `date +'%s'` -lt 1320000000 ] && echo System date not set, aborting. && exit -1
$ipt -N $iptChain >&/dev/null

now=`date +'%s'`
nowPlus=$((now + secondsToBan))

echo Running dropBrute on `date` \($now\)

# find new badIPs
for badIP in `logread | fgrep dropbear | egrep -i 'login attempt for nonexistent user'\|'bad password attempt for'| sed 's/^.*from //' | sed 's/:.*$//' | sort -u` ; do
  found=`logread | fgrep dropbear | egrep -i 'login attempt for nonexistent user'\|'bad password attempt for'|sed 's/^.*from //'|sed 's/:.*$//' | fgrep $badIP | wc -l`
  if [ $found -gt $allowedAttempts ] ; then
    # if there is not a lease, add it
    if [ $(egrep -c $badIP$ $leaseFile) -eq 0 ] ; then
       echo $nowPlus $badIP >> $leaseFile
    fi
  fi
done

# now parse the leaseFile
cat $leaseFile | while read lease ; do
  leaseTime=`echo $lease|cut -f1 -d\ `
  leaseIP=`echo $lease|cut -f2 -d\ `
  # when used with the iptables example on the top a return rule is not needed
  if [ $leaseTime -lt 0 ] ; then
    if [ `$ipt -S $leaseChain|egrep \ $leaseIP/32\ \|\ $leaseIP\ |fgrep -- "$iptWhiteRule"| wc -l` -lt 1 ] ; then
      #echo Adding new whitelist rule for $leaseIP
      #$ipt -I $iptChain -s $leaseIP $iptWhiteRule
      true
    fi
  elif [ $leaseTime -ge 1 -a $now -gt $leaseTime ] ; then
    echo Expiring lease for $leaseIP
    $ipt -D $iptChain -s $leaseIP $iptDropRule
    $ipt -D $iptChain -s $leaseIP -j LOG --log-prefix "dropBrute "
    sed -i /$leaseIP/d $leaseFile
  elif [ $leaseTime -ge 0 -a `$ipt -S $leaseChain|egrep \ $leaseIP/32\ \|\ $leaseIP\ |wc -l` -lt 1 ] ; then
    echo Adding new rule for $leaseIP
    $ipt -I $iptChain -s $leaseIP $iptDropRule
    $ipt -I $iptChain -s $leaseIP -j LOG --log-prefix "dropBrute "
  fi
done < $leaseFile

(Last edited by arokh on 16 Jun 2014, 12:40)

@robzr or if not around anmore, could someone please extend the script so it would ban bad auth brute force trys too, because my logs are full of them:

Tue Oct 21 00:04:34 2014 authpriv.info dropbear[23553]: Child connection from 202.109.143.35:3646
Tue Oct 21 00:05:15 2014 authpriv.info dropbear[23553]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:05:18 2014 authpriv.info dropbear[24190]: Child connection from 202.109.143.35:2613
Tue Oct 21 00:05:58 2014 authpriv.info dropbear[24190]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:06:10 2014 authpriv.info dropbear[24943]: Child connection from 202.109.143.35:2416
Tue Oct 21 00:06:44 2014 authpriv.info dropbear[24943]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:06:53 2014 authpriv.info dropbear[25580]: Child connection from 202.109.143.35:1096
Tue Oct 21 00:07:26 2014 authpriv.info dropbear[25580]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:07:44 2014 authpriv.info dropbear[26113]: Child connection from 202.109.143.35:4782
Tue Oct 21 00:08:12 2014 authpriv.info dropbear[26463]: Child connection from 222.186.34.122:4166
Tue Oct 21 00:08:19 2014 authpriv.info dropbear[26463]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:08:20 2014 authpriv.info dropbear[26537]: Child connection from 222.186.34.122:4716
Tue Oct 21 00:08:22 2014 authpriv.info dropbear[26113]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:08:29 2014 authpriv.info dropbear[26537]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:08:29 2014 authpriv.info dropbear[26703]: Child connection from 222.186.34.122:4716
Tue Oct 21 00:08:35 2014 authpriv.info dropbear[26703]: Exit before auth (user 'root', 5 fails): Error reading: Connection reset by peer
Tue Oct 21 00:08:36 2014 authpriv.info dropbear[26709]: Child connection from 202.109.143.35:4746

It's mostly because my dropbear is set to just accept key file auth. But brutdrop doesnt ban the connection trys of those.

Is it possible to make this working for port 443 instead of 22?
I have changed 22->443 twice in iptables section but after few successful tries to access it's still no new hosts are listed in /tmp/dropBrute.leases.

(Last edited by jcb on 28 Mar 2015, 17:48)

I've stolen this form Mikrotik forums and translated to iptables/ipset:

# cat /etc/firewall.user 
# This file is interpreted as shell script.
# Put your custom iptables rules here, they will
# be executed with each firewall (re-)start.

ipset -exist restore << __EOF__
create ssh_stage1 hash:ip family inet hashsize 1024 maxelem 65536 timeout 60
create ssh_stage2 hash:ip family inet hashsize 1024 maxelem 65536 timeout 60
create ssh_stage3 hash:ip family inet hashsize 1024 maxelem 65536 timeout 60
create ssh_blacklist hash:ip family inet hashsize 1024 maxelem 65536 timeout 864000
create ssh_whitelist hash:net family inet hashsize 1024 maxelem 65536
__EOF__

iptables-restore -n << __EOF__
*filter
-A input_rule -p tcp -m conntrack --ctstate NEW -m set --match-set ssh_whitelist src -m tcp --dport 22 -m comment --comment "permit SSH whitelist unconditionally" -j ACCEPT
-A input_rule -p tcp -m tcp --dport 22 -m set --match-set ssh_blacklist src -m comment --comment "drop ssh bruteforcers" -j DROP
-A input_rule -p tcp -m conntrack --ctstate NEW -m set --match-set ssh_stage3 src -m tcp --dport 22 -m comment --comment "blacklist timeout: 10 days" -j SET --add-set ssh_blacklist src --timeout 864000
-A input_rule -p tcp -m conntrack --ctstate NEW -m set --match-set ssh_stage2 src -m tcp --dport 22 -m comment --comment "stage3 timeout: 60 sec" -j SET --add-set ssh_stage3 src --timeout 60
-A input_rule -p tcp -m conntrack --ctstate NEW -m set --match-set ssh_stage1 src -m tcp --dport 22 -m comment --comment "stage2 timeout: 60 sec" -j SET --add-set ssh_stage2 src --timeout 60
-A input_rule -p tcp -m conntrack --ctstate NEW -m tcp --dport 22 -m comment --comment "stage1 timeout: 60 sec" -j SET --add-set ssh_stage1 src --timeout 60
COMMIT
__EOF__

For that to work you need to install ipset and kmod-ipt-ipset packages. No cronjobs needed. Enjoy!

(Last edited by sbv on 3 Sep 2015, 10:52)

sbv wrote:

I've stolen this form Mikrotik forums and translated to iptables/ipset...

Hi, thanks for sharing, that is a clever snippet.  IPset is an elegant way of dealing with a blacklist/whitelist, but keep in mind the logic here is effectively an escalating limiter for ssh connections, there's no distinction made between a valid connection and an invalid one.

I've been working on an updated version of dropBrute to incorporate some of the changes arokh made, make sure it works out of the box with Barrier Breaker / Chaos Calmer, use uci for config, make it easier to be persistent across firewall restarts/reboots, intelligently insert itself into the firewall if the user desires, in short just make it simpler & better, while maintaining as few dependencies as possible.  I'm welcome to suggestions and contributions! 

The git repo is at https://github.com/robzr/dropBrute if anyones interested, as soon as I have a working draft it'll go up there.

regards,

Rob

Is there any way to include blocking of this type of intruder in the old dropBrute.sh script?

Wed Jan 20 17:07:39 2016 daemon.info : 14[NET] received invalid IKE header from 84.111.42.104 - ignored

I tried something myself in Arokh's script: I changed this line:

`logread | fgrep dropbear | egrep -i 'login attempt for nonexistent user'\|'bad password attempt for'| sed 's/^.*from //' | sed 's/:.*$//' | sort -u`

into

`logread | egrep -i 'login attempt for nonexistent user'\|'bad password attempt for'\|'received invalid IKE header from'| sed 's/^.*from //' | sed 's/:.*$//' | sort -u`

but that does not remove the " - ignored"-part of the error message.

I am a complete newbie w.r.t. sed, so any help is apprecated smile

I'd recommend you upgrade to bearDropper, it's improved in many ways, but if you can't run Chaos Calmer, I can help you with this.  What OpenWRT version are you running?  Post the output of "sed -h" and I'll see if I can give you a better command.

Rob

Hi Rob,

Yes please!
I have replaced dropBrute by bearDropper, what's next? smile

I'm running Arokh's trunk build r48259 on a WDR4900.

(Last edited by bouwew on 23 Jan 2016, 09:50)

Great, in your config file (/etc/uci/bearDropper) put in the following line:

option  regexLogString          '^[a-zA-Z ]* [0-9: ]* (authpriv.warn dropbear|daemon.info .*invalid IKE header)'

Let me know how it works, and for bearDropper related stuff please post in https://forum.openwrt.org/viewtopic.php?id=62084

I might change the config to use a list instead of an option, so people can more easily add search regex's, too.

regards,

Rob

The discussion might have continued from here.