The Palner Group, Inc.

Kamailio, Asterisk, VoIP, and IT Consulting

Automatically Block Failed SIP Peer Registrations

Previously we posted a little script for quickly checking your asterisk log for failed peer registrations. Building on that script, and with the use of iptables and cron, you can easily (and automatically) block flooding traffic from your system. Iptables, a linux command line program to filter IP traffic, provides high level packet filtering before the traffic can be used to corrupt a program. Cron, the linux time scheduler, enables you to automatically run commands at scheduled time periods.

Set up IP Tables

We will not be discussing the intricacies of iptables in this post. There are excellent tutorials on iptables, and with most things linux, help is only a google away. To help identify the traffic blocked as asterisk related, a new chain will be created appropriately called… asterisk.

Here’s how to add the new chain:

iptables -N asterisk
iptables -A INPUT -j asterisk
iptables -A FORWARD -j asterisk

This will help identify hosts blocked for failed registrations.

Asterisk’s Log for Failed Registrations

In most cases of a sip flood attack, the host attempts registration to Asterisk. These hosts are identified in the Asterisk log (/var/log/messages) as “No matching peer found.” The following perl script scans /var/log/messages for these patterns, strips the IP address, and puts the IP address into an array.

After the file has been read, the IP addresses are counted (each count is a failed attempt), compared against the existing blocked hosts, and new occurrences are blocked. With this script we are blocking any host after the 4th failed attempt.

Here’s the script (last updated 05 SEP 2010):

#!/usr/bin/perl -w
use strict;
use warnings;
my (@failhost);
my %currblocked;
my %addblocked;
my $action;

open (MYINPUTFILE, "/var/log/asterisk/messages") or die "\n", $!, "Does log file file exist\?\n\n";

while (<MYINPUTFILE>) {
	my ($line) = $_;
	chomp($line);
	if ($line =~ m/\' failed for \'(.*?)\' - No matching peer found/) {
		push(@failhost,$1);
	}
	if ($line =~ m/\' failed for \'(.*?)\' – Wrong password/) {
		push(@failhost,$1);
	}
}

my $blockedhosts = `/sbin/iptables -n -L asterisk`;

while ($blockedhosts =~ /(.*)/g) {
	my ($line2) = $1;
	chomp($line2);
	if ($line2 =~ m/(\d+\.\d+\.\d+\.\d+)(\s+)/) {
		$currblocked{ $1 } = 'blocked';
	}
}

while (my ($key, $value) = each(%currblocked)){
	print $key . "\n";
}

if (@failhost) {
	&count_unique(@failhost);
	while (my ($ip, $count) = each(%addblocked)) {
		if (exists $currblocked{ $ip }) {
			print "$ip already blocked\n";
		} else {
			$action = `/sbin/iptables -I asterisk -s $ip -j DROP`;
			print "$ip blocked. $count attempts.\n";
		}
	}
} else {
	print "no failed registrations.\n";
}

sub count_unique {
    my @array = @_;
    my %count;
    map { $count{$_}++ } @array;
    map {($addblocked{ $_ } = ${count{$_}})} sort keys(%count);
}

Schedule the script with cron

The final step is to schedule your script to run every X minutes in cron. We’ve chosen to run our script every 2 minutes, but you can change this to 1 minute or any other time period you choose. Just remember… you can receive thousands of attempts within 2 minutes.

If you have named your script check-failed-regs.pl and placed it in your /usr/local/bin directory, your cron statement would look like this:

*/2 * * * * perl /usr/local/bin/check-failed-regs.pl &> /dev/null

Questions? Comments? We love feedback. Or, contact us for more information.

48 Comments

  1. Nice script. But in my case, only searching “No matching peer found” is not enough. It should also test for “Wrong password”. See my log here at the end.

    Jian

    [2010-02-15 23:27:30] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:31] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
    [2010-02-15 23:27:32] NOTICE[2369] chan_sip.c: Registration from '"1001" ' failed for '82.90.77.205' - Wrong password
  2. Hi Jian. First, We’d recommend that you not have extensions as sip usernames. That being said, you could modify the script as follows:

    #!/usr/bin/perl -w
    use strict;
    use warnings;
    my (@failhost);
    my %currblocked;
    my %addblocked;
    my $action;
    
    open (MYINPUTFILE, "/var/log/asterisk/messages") or die "\n", $!, "Does log file file exist\?\n\n";
    
    while (<MYINPUTFILE>) {
    	my ($line) = $_;
    	chomp($line);
    	if ($line =~ m/\' failed for \'(.*?)\' - No matching peer found/) {
    		push(@failhost,$1);
    	}
    	if ($line =~ m/\' failed for \'(.*?)\' – Wrong password/) {
    		push(@failhost,$1);
    	}
    }
    
    my $blockedhosts = `iptables -n -L asterisk`;
    
    while ($blockedhosts =~ /(.*)/g) {
    	my ($line2) = $1;
    	chomp($line2);
    	if ($line2 =~ m/(\d+\.\d+\.\d+\.\d+)(\s+)/) {
    		$currblocked{ $1 } = 'blocked';
    	}
    }
    
    if (@failhost) {
    	&count_unique(@failhost);
    	while (my ($ip, $count) = each(%addblocked)) {
    		if (exists $currblocked{ $ip }) {
    			print "$ip already blocked\n";
    		} else {
    			if ($count >= 4) {
    				$action = `iptables -I asterisk -s $ip -j DROP`;
    				print "$ip blocked. Failed attempts: $count\n";
    			} else {
    				print "$ip NOT blocked. Only $count failed attempt(s).\n";
    			}
    		}
    	}
    } else {
    	print "no failed registrations.\n";
    }
    
    sub count_unique {
        my @array = @_;
        my %count;
        map { $count{$_}++ } @array;
        map {($addblocked{ $_ } = ${count{$_}})} sort keys(%count);
    }
  3. Hi,

    This is a very useful script.

    It works perfectly when I log in as root using SSH and run it manually. It only inserts the same rules once.

    When I run it as a cron job I had to change line 39 in the last example to $action = `/sbin/iptables -I asterisk -s $ip -j DROP`; otherwise it would not insert any rules.

    Now every time it runs as a cron job it keeps re-inserting the same rules.
    Is there any simple way around this?

    Brian.

  4. Brian,

    Thanks… you’re right and the script is updated.

  5. Hi,

    Thank you, it works perfectly.
    Keep up the good work.

    Brian.

  6. Thanks, glad that you pointed me to this during the VUC teleconference. Works nicely. Had to change the name of the log file from “messages” to “full”.

    Very Nice…

  7. I had to change line 17 in the last example to “- Wrong password/)

    Grato, Thanks

  8. Thanks Sergio. I’ve corrected the code.

  9. Thanks for the great script…

    heads-up that on my asterisk 1.8 installation, the IP address in the log file now includes the source port. Therefore either change line 14 to …

    if ($line =~ m/\’ failed for \'(.*?):.*\’ – No matching peer found/) {

    or insert 3 new lines before 14 …
    if ($line =~ m/\’ failed for \'(.*?):.*\’ – No matching peer found/) {
    push(@failhost,$1);
    }

    Thanks again

  10. Actually, there is a vulnerability in this approach. It kinda relies on blocking the offending IP before it cracks a password. Once a password is cracked, it can be exploited from another IP address.

    Therefore I suggest an upgrade thus.

    1. Create a few honeypot sip extensions, say username=1, password=1, username=100, password=100
    2. Point these at a honeypot context…
    3. … which says
    exten => _.,1,Set(ip=${CHANNEL(recvip)})
    exten => _.,n,Log(WARNING,”please block ${ip}”)
    exten => _.,n,Answer()
    exten => _.,n,Hangup()
    4. In the perl script, also grep for this alternative string
    if ($line =~ m/\’please block (.*?) {
    push(@failhost,$1);
    }

    (please double check my perl fragment as I’m not a perl programmer)

  11. Colin,

    That’s a good idea. This script does not anticipate the cracking of a password within 2 minutes and thus could be a vulnerability.

  12. Thanks a lot!! the script is very useful.

  13. I added some code to eliminate internal client IP addresses. My Perl is not too good BTW… Thanks for the script!

    if (@failhost) {
            &amp;count_unique(@failhost);
            while (my ($ip, $count) = each(%addblocked)) {
                    if (exists $currblocked{ $ip }) {
                            print "$ip already blocked\n";
                    } else {
                            if (substr($ip,0,3) == '10.') {
                                    print "$ip is in local subnet.\n";
                            } else {
                            $action = `/sbin/iptables -I asterisk -s $ip -j DROP`;
                            print "$ip blocked. $count attempts.\n";
                            }
                    }
            }
    } else {
            print "no failed registrations.\n";
    }
  14. Hi,

    Thank you for the script. I have the following question.

    Since I am not familiar with a perl, i would like to ask the community if the following code will work:

    while () {
    	my ($line) = $_;
    	chomp($line);
    	
    	if ($line =~ m/\' failed for \'X.X.X.X.\' - No matching peer found/) {
    		goto SKIP;
    		
    	elsif ($line =~ m/\' failed for \'(.*?)\' - No matching peer found/) {
    		push(@failhost,$1);
    	}
    }
    

    SKIP:
    my $blockedhosts = `/sbin/iptables -n -L asterisk`;

    Where is X.X.X.X is a static IP address, that I want to except from banning. Since, I am receiving an unsuccesful registration messages from this IP the script is blocking it. But I would like to allow it to send such messages.

    Is there any way to make an exception?

    Thank You in advance.

  15. Instead you could use…

    if (@failhost) {
    	&count_unique(@failhost);
    	while (my ($ip, $count) = each(%addblocked)) {
    		if (exists $currblocked{ $ip }) {
    			print "$ip already blocked\n";
    		} else {
    			unless ($ip eq "X.X.X.X") {
    				$action = `/sbin/iptables -I asterisk -s $ip -j DROP`;
    				print "$ip blocked. $count attempts.\n";
    			}
    		}
    	}
    } else {
    	print "no failed registrations.\n";
    }
  16. Awesome script, thanks!

    small issue, and I’m not sure if it’s an easy fix or not, I’m using the 2nd version that also scans for “Wrong password”, and it turns out, for whatever reason, if that’s included it actually doesn’t work for finding the
    “No matching peer found”. To get around this, I’m just running 2 different scripts, but it’d be nice if I could just use the 1…

    Thanks again!

  17. Also, I can’t figure out where to set the number of failed attempts to allow…

    This is supposed to be running every minute, but if I look at my logs, I see MANY “No matching peer found” spanning over 3 to 5 minutes… I’m not sure why it’s only half working yet…

  18. Can you give a little more detail as to what your issues are?

  19. @Vasily869

    It might be safer to (also) whitelist your IP addresses in your iptables configuration.

    Here is a fragment of my iptables …

    ACCEPT all — X.X.X.X anywhere
    ACCEPT all — Y.Y.Y.Y anywhere
    asterisk udp — anywhere anywhere udp dpt:sip

    So any traffic from my whitelisted hosts is accepted before entering the asterisk chain, and the asterisk chain is only invoked for SIP traffic.

    The way the iptables instructions at the top of this article are phrased, there is a real danger of locking yourself out of your own machine.

  20. @TeamForrest:

    Thank You for the code. It works now.

    @ Colin:

    Thanks for your comment.
    I alreday did it. :)))

    But without script changing, your solution doesn’t work. IPTables ban my ip even if it in a white list.

  21. @TeamForrest

    Wow, thanks for the response, I was waiting for an email saying there was a reply (I’m new to this comment business).

    I think I may have worked out some of the issue, I’ll have to continue testing. Part of my issue was I *think* that the ‘asterisk’ chain came after everything else, I’ve just added it to the beginning by doing: iptables -I INPUT 1 -j asterisk

    Mainly I’m just having a hard time verifying that it’s working… I see that the iptables rules are there, but the script is running on the same box that asterisk is, not sure how much that changes things…

  22. I also added this extra filter in addition to previously whitelisting my own network and domains.

    # catch: [Aug 27 19:44:53] NOTICE[18826] chan_sip.c: Registration from '"1002" ' failed for '183.110.185.220' - Wrong password
    
            if ($line =~ m/ Registration from '"[^"]*" ]*&gt;' failed for '([^']*)' - Wrong password/ ) {
                    push(@failhost,$1);
            }
    

    This might be handy too.

  23. Being a total newbie it would be really nice if some kind soul would add all these mods and make one script as its a bit daunting not knowing where to add all these mods to the original script.

    Thanks

  24. @Simon / @asteriskmad :

    The wrong password mod was added in a comment on 21 APR, but we’ve gone ahead and updated the main code to reflect that addition (so it’s in one script).

    We did not add the ip’s of what not to block… that’s more of a personal choice. In most of our deployments if we have an internal attack or a rogue system, it’s ok for that to be blocked.

  25. If is posible to add to this script, check for not mutch ACL like this :

    if ($line =~ m/\' failed for \'(.*?)\' – Device does not match ACL/) {
    		push(@failhost,$1);
    	}
    
  26. Absolutely. You would just add that after the others… you can add all of them.

  27. Just FYI, there is an application called Fail2Ban which is designed to do just this sort of thing (analyse log files, and ban IPs). You could use it to defend against SSH brute force attacks and various other vectors simultaneously.

  28. Correct. This is for those that do not wish to run fail2ban or wish to customize completely to their needs.

  29. Thanks for this script. My company hosts an Asterisk server that has been getting these messages recently.

    I’ve modified the script to run as a daemon process. This will allow it to check the log file every second for new entries, and add them immediately if matched.

    Here is the modified script. You need the Perl modules “Proc::Daemon” and “Proc::PID::File” installed. I also rewrote certain parts in my own perl style. 😉

    #!/usr/bin/perl
    
    use Proc::Daemon;
    use Proc::PID::File;
    use IO::Handle;
    
    # Init as a service
    Proc::Daemon::Init();
    if (Proc::PID::File-&gt;running()) {
      exit(0);
    }
    
    # Global vars
    my %currblocked;
    my %addblocked;
    my $action;
    
    # Get currently blocked IPs
    open (IPTABLES, "/sbin/iptables -n -L asterisk |");
    while () {
      chomp;
      if (/(\d+\.\d+\.\d+\.\d+)(\s+)/) {
        $currblocked{$1} = 'blocked';
      }
    }
    close(IPTABLES);
    
    # Open log file
    open (ASTLOG, "/var/log/asterisk/messages") or die "\n$! Cannot open Asterisk log file\n";
    
    # Main loop
    for (;;) {
      my @failhost;
      while () {
        chomp; my $line = $_;
        if ($line =~ /\' failed for \'(.*?)\' - No matching peer found/) {
          push(@failhost,$1);
        }
        if ($line =~ /\' failed for \'(.*?)\' - Wrong password/) {
          push(@failhost,$1);
        }
      }
    
      if (@failhost) {
        &amp;count_unique(@failhost);
        while (my ($ip, $count) = each(%addblocked)) {
          if (not exists $currblocked{$ip}) {
            system("/sbin/iptables","-I","asterisk","-s",$ip,"-j","DROP");
          }
        }
      }
    
      # Sleep for a second, then check file again
      sleep 1;
    
      # Clear the EOF error, so we can continue from the same point in the log
      ASTLOG-&gt;clearerr();
    }
    
    # Subroutines
    sub count_unique {
      my @array = @_;
      my %count;
      map { $count{$_}++ } @array;
      map { ($addblocked{ $_ } = ${count{$_}}) } sort keys(%count);
    }
    
  30. Looking over the code above, I notice that there are certain sections that are removed because they were mistaken for html code.

    Basically, where you see a “while ()”, there should be a filehandle in the brackets, surrounded by less than/greater than symbols.

    The first while () should read the IPTABLES filehandle.
    The second while () should read the ASTLOG filehandle.

    Sorry for that. I should have realised the comment wouldn’t have allowed that part of the code.

  31. Just an FYI that all versions of PBX in a Flash come with a fairly elaborate implementation of IPtables with Fail2Ban. Those of you wanting to deploy on other platforms might want to set up a VM of PBX in a Flash and take a look at our /etc/sysconfig/iptables and /etc/fail2ban/jail.conf setups.

  32. I love this script and I’m using it. I did just get bitten in the a$$ because it ended up blocking one of my phones (which was my fault). It would be nice if there was a way to whitelist blocks of IP addresses (192.168.xxx.xxx, for example :).

    I’m fumbling around with the Perl code, but it’s not exactly my bag…

  33. My problem with this method is that it can take a minute (if you are running the script every minute) before it scans the log file. While the attack is going on it’s likely that Asterisk will be swamped with Registration attempts so much that any audio in progress starts to break up. Also if using VoIP for inbound/outbound calls, the attack saturates the bandwidth on the Internet connection.

    I have Asterisk deployments that get hit 4 or 5 times a day, if it was causing a loss of service for 1 or 2 minutes each time, this would be unacceptable for my customers.

    I am currently using OSSEC to detect and block these attacks and it generally takes two seconds from start of attack to iptables blocking further attempts, even then it’s normal to see 200+ attempts in the Asterisk log just in those two seconds.

  34. Paul,

    If you’re getting hit that much, I’d recommend a Kamailio box in front of your asterisk boxes.

    —fred

  35. Thanks again for this script, it’s great!

    I noticed yesterday a lot of incoming traffic from an odd IP, nothing outgoing though. I checked the logs and noticed it was an IP that had been added to ‘the list’.

    Long story short it was sending me a sip flood. At the time I noticed it, the IP had already been added to the chain for about 2 hours and it was still going. One idea I decided to try was to add a rule to REJECT instead of DROP. At the time I implemented that the flood had been going for 4 hours. After that, it only continued for 1 hour and was done. It’s hard to be sure if the rejection was the cause or not, just thought I’d start a conversation about whether to drop or reject.

    I understand that ‘normally’ you’d just want to drop packets instead of respond to them, but in this case… I’m not so sure a sudden rejection wouldn’t be better.

    Thoughts?

  36. A small addition to the daemonised version of the code I posted.

    Replace the line

      my @failhost;

    with

    my @failhost, %addblocked;

    This will stop repeated additions of previous IPs to the block list.

    Also, for those who want a whitelist, you don’t have to add it to the code. You can do it all via iptables. Just run the following commands:

    iptables -N whitelist
    iptables -I INPUT -j whitelist

    Now, if you want to whitelist, for example, the IP range 192.168.0.0/16, you can run the following command:

    iptables -A whitelist -s 192.168.0.0/16 -j ACCEPT

    Repeat the command for any other IPs or subnets you want to whitelist. Note though, if you’re blocking anything else via the linux firewall, the newly whitelisted IPs will now have unblocked access.

  37. I have to agree with Paul – this is just reinventing fal2ban or ossec, but less effectively.

    A SIP request is approximately 200 chars long – with a 2mbs links, this means 1,000 requests per second. Tools like sipvicious WILL send that many requests.

    So the cront every minute is NOT an option.

    Additionnally, fail2ban uses gamin (if installed), that is very effective ressource-wise. Even with this fast detection, you can see a dozen of requests before blocking occurs – I can not imagine if I have to wait for a minute !!!

    Why do you want to reinvent the wheel – while the existing one is functional & free ?

  38. Let’s not let this get into a crazy religious war along the lines of debian vs. centos vs. whatever. Clearly there are 100 different ways to go about something. Why do I not use fail2ban? Too many reasons, and in the end run, it’s immaterial why. We simply don’t use it and like other, and also free, products better (such as blockhosts). This script was first written in April. Second, since it’s something we wrote its extremely quick to modify and customize… such as whitelisting, etc. Currently I have ours automatically archiving/rotating the log, sending out a tweet and then sending a kill using sipvicious to the attacker.

    Again, many different ways to handle this and this is one of them. If you like fail2ban, you should use fail2ban. Most of the time, on heavier use systems we will handle this with Kamailio instead.

    Thanks for your feedback, and we encourage you to use the solution that makes you feel most comfortable.

    — fred

  39. I really love this script.
    It is quite easy to customize, but I dont know much about perl programming and how to run it as a Demon.
    Can someone please post a full Version of the demonized Script of Deek?
    That would be really great.
    Since Oct 30 we had an increase in attacks of about 10000%. Did anyone else experience something like this?

    Thanks a lot
    – Johannes

  40. Hi !
    We too had experience an increase of attachs. A lot of other * users too. You can refer to : http://marc.info/?t=128846380700002&r=1&w=2

    – Nicolas

  41. Why does the code check /var/log/asterisk/messages? I thought the file was /var/log/asterisk/full. I don’t see /var/log/asterisk/messages in my server, but the script runs well. Am I missing something here?

  42. We dont want to use extensions as sip usernames, How can i use another way ..

    Kindly update with examples

    here is my example .

    User extension : 152012
    username : 152012
    password : *************************

    i like to use username different from extension ..

    User extension : 152012
    username : Millernethanbagger or 567812349012563489
    password : *************************

  43. The code checks /var/log/asterisk/messages as this is the default log for asterisk. Sounds like you’re running freepbx or trixbox which will log to /var/log/asterisk/full. Just change the code to reflect your log.

  44. You shouldn’t use extensions as usernames. That’s not something we recommend.

    If you’re running asterisk, you will change this in sip.conf. If you’re running freepbx check the forums for help on renaming sip usernames.

  45. To create iptables chain “asterisk”, it is better to use INSERT:”-I”, instead of APPEND:”-A”.
    Someone may already have rules to ACCEPT sip/iax, “-A” will not block traffic in such case.

    iptables -N asterisk
    iptables -I INPUT -j asterisk
    iptables -I FORWARD -j asterisk

  46. Thanks for the very useful script. Fail2ban seemed much too complicated to use. This script worked perfectly for me.

    I have been receiving 10s if not 100s of registration attempts a second when the attack begins. Rather than decreasing the cron job frequency, I have added rate limiting which uses the state module:
    http://pbxinaflash.com/forum/showthread.php?t=5018

    Now, no more than a few failed attempts until your script kicks in the next cron job.

  47. I think fail2ban is more usefull , if someone needs help there are thousand of people who can help, this script is just for some people if someone asks you something instead of you help you remove the comment.
    That means fai2ban is for everyone this script not.

  48. Great that you think fail2ban is more useful. You should run it. This script is for those that do not wish to run fail2ban.

Comments are closed.