Brute-force script blocking conversion

I am in the process of locking down one of my Linux servers through dynamic iptables usage. Currently I have my sshd covered from brute-force attacking via a special block.pl script (link available below), but now I found that script kiddies are now attempting to get through via my ftp port (currently running ProFTPD).

Block.pl script (the one for sshd) can be found here: http://shellscripts.org/projects/s/sshblock/version_1.2/block.pl

Though I am using I am using ProFTPD and I know I could modify the proftpd.conf to give a more subtle log file via LogFormat, I would rather like to have a converted script (like the one I use for the sshd) checking the /var/log/messages file as well.

I have absolutely no Perl scripting/programming knowledge, thus the question is being asked here...

Need to convert the following string:

May 17 00:59:37 servername proftpd[32673]: server.domain.com (192.168.1.15[192.168.1.15]) - no such user 'blah'

The perl script already has a way to look at sshd attack/attempts in the following manner:

if (/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) {


Anyone have a guess on what I need to have to read the /var/log/messages file then look for the "no such user" string given, then extract the IP address from the [ ] field and place it into a variable that can be added to the iptables rule in the rest of the script?

Thanks.

-- Michael
LVL 29
Michael WorshamInfrastructure / Solutions ArchitectAsked:
Who is Participating?
 
mjcoyneConnect With a Mentor Commented:
There will always be text between proftpd\[\d+\] and the beginning of the IP address -- why make it optional?  Also, It seemed more valuable to me to capture both IP addresses -- I don't know they'll always be the same.

But, Perl_Diver's other diagnosis is correct -- it's the parenthesis around the IP addesses that screwed things up; in particular the last one (because the line doesn't end with ...nn.nnn.nn.nnn] - no such user... but rather ends with ...nn.nnn.nn.nnn]) - no such user... so the final parenthesis is a must.  The opening parenthesis in the line is covered by the .* (which means any number of any characters), but there is a subtle flaw here -- as written, the regex will return:

$1 contains: proftpd[32673], $2 contains: 2.168.1.15, $3 contains: 192.168.1.15

when run against "May 17 00:59:37 servername proftpd[32673]: server.domain.com (192.168.1.15[192.168.1.15]) - no such user 'blah'".  Notice that $2 is incorrect -- it took the least number of digits that matched "any number of any characters followed by any number of digits followed by a period", considering the '1' and '9' of $2 to be part of the .* part, rather than part of the [0-9]+\. part.  The fix is to use the opening parenthesis as a delimiter, so it then becomes "any number of any characters followed by a parenthesis followed by any number of digits followed by a period...".  Thus, for two different reasons, we want to explicitly require the parenthesis:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) || (/(proftpd\[\d+\]).*\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]\) - no such user/)) {

Sorry -- I've been writing these from a machine without Perl installed, and thus had no opportunity to test them...  I'm back at my machine now, so I can be a bit more accurate...

BTW, now that I see a bit more of what your log looks like, you might want to use some of the other info in the proftpd line to further identify it, and make a better log entry.  If you want to do this, change the regex line to:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) || (/(proftpd\[\d+\]).*\(([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]\) - no such user \'(.*)\'/)) {

and change the print DEBUG line to:

    if ($4) {
        my $time = localtime;
        print  DEBUG "$1: Unknown user $4 from $2\[$3\] detected on $time\n";     # add this line for debug
    }

This way, we're taking advantage of $4 (which only exists in the proftpd matches) to limit the entries written to the debug log to only those involving proftpd, rather than sshd and proftpd.  Additionally, since we now have a $4, we might as well use it -- I made it capture the attempted user name.  I also added a date stamp (which will be the time the script writes to the log, not necessarily the time the user attcked, but it'll be close, I imagine) to add a bit more info to the log.

Given "May 19 01:58:13 games proftpd[4153]: games.murpe.com (61.134.40.163[61.134.40.163]) - no such user 'Administrator'" as an example, it would enter "proftpd[4153]: Unknown user Administrator from 61.134.40.163[61.134.40.163] detected at Fri May 19 07:55:40 2006" in the log.
0
 
mjcoyneCommented:
Maybe something like:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) || (/(proftpd[\d+]).*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)] - no such user/)) {

The original statement has three capturing parenthesis: Failed password for or Invalid user (captured in $1), the username (shown by \w+, and contained within $2), and the IP address (defined by [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ and captured by $3).

The new line will still look for that, but also react to ...proftpd[32673]: server.domain.com (192.168.1.15[192.168.1.15]) - no such user...  capturing "proftpd['series of numbers']" as $1, and the IP address twice, in $2 and $3.  The script relies on the IP address to be in $3 (see the line my $ip=$3;), so I figured that's the major requirement in the addition -- that the IP address to be blocked resides in $3.

Your log won't be perfect, but you'll be able to tell the entry was generated from proftpd, and the IP address that was blocked.
0
 
Perl_DiverCommented:
my @no_users = ();
open(LOG,'</var/logs/messages') or die "can't open var/logs/messages: $!";
while (my $line = <LOG>) {
   next unless index($line,'no such user') > -1;
   push (@no_users,$1) if ($line =~ /\(\d+\.\d+\.\d+\.\d+\[(\d+\.\d+\.\d+\.\d+)\]\)/);
}
close(LOG);
foreach my $lines (@no_users) {
   do something useful
}

0
Keep up with what's happening at Experts Exchange!

Sign up to receive Decoded, a new monthly digest with product updates, feature release info, continuing education opportunities, and more.

 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
mjcoyne: I tried your version first in place of the code already in the block.pl file. When I tried to execute it, I was given this message...

Unmatched ) in regex; marked by <-- HERE in m/(proftpd[\d+]).*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) <-- HERE ] - no such user/ at ./block.pl line 12.

Any ideas?

-- Michael
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
Perl_Diver: I see the idea behind the script, but the extraction of the ip address inbetween the '[ ]' in the log file is crucial as this is what will be added to the iptables rule. As I said originally, I have no Perl experience so I will be pretty much need to be told exactly what goes where.

The iptables layout: iptables -I INPUT -s <ipaddress> -j DROP

Thanks for the snippet, though. It does give me some insight on how Perl works.

-- Michael
0
 
Perl_DiverCommented:
/quote/
the extraction of the ip address in between the '[ ]' is crucial
/endquote/

thats what is being done here:

push (@no_users,$1) if ($line =~ /\(\d+\.\d+\.\d+\.\d+\[(\d+\.\d+\.\d+\.\d+)\]\)/);

but how that gets incorporated into your existing script is hard to say. Your existing script has two conditions

if (/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) {

and:

elsif (/sshd.*Accepted (password|publickey) for (\w+) from (::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) {

do those still have to be checked? Are you adding a third condition to the file parsing?:

if (index($_,'no such user') > -1 && /\(\d+\.\d+\.\d+\.\d+\[(\d+\.\d+\.\d+\.\d+)\]\)/)
0
 
mjcoyneCommented:
Oops -- I was working without an editor...

It should be:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) || (/(proftpd\[\d+\]).*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\] - no such user/)) {

There were the correct number of parenthesis, it's just that the square brackets (from, for example, [32673], or [192.168.1.15]) were throwing the regular expression off -- it interprets these brackets as defining a character class.  Escape them (by adding a slash in front of them, like \[) and it'll interpret them as literal characters...

Sorry...
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
Perl Diver: Modifying the block.pl would be plus to handle both sshd and proftpd for invalid users, but its not a necessity. The original sshd block.pl works just fine as is. I was just hoping to add in the proftpd check as script kiddies have found ftp exploits can do just as much damage as a sshd one and having one script always running in the background makes it a little easier on the system (and on me) for checking the logs, etc.

mjcoyne: Testing the new code now. Two Quick questions: is there a way to add in a sort of mock debug statement to see what $1, $2 and $3 are showing? If I were to copy the block.pl to say ftp-block.pl, would the proftpd part you have replace the sshd check or would something else need to be modified to allow it to run (i.e. less a '()', etc)?

-- M
0
 
mjcoyneCommented:
Sure -- how about something like:

use Data::Dumper;
open (DEBUG, ">>debug.log") || die "Cannot open debug.log: $!\n";     # add this line for debug

...etc...

print STDOUT "block.pl: $1 $2 from $3 \n";
print DEBUG "\$1 contains: $1, \$2 contains: $2, \$3 contains: $3\n";     # add this line for debug

You can just comment them out when you want them inactive.

As to splitting the script in two, it would have to be re-worked a bit because it's actually an if-else loop, and if you change the "if" part to include only the FTP entries, you'll need to also change the "else" part, but it wouldn't be too hard.

I have my SSH server blocked in a similar manner, but I use sshblack (see http://www.pettingers.org/code/sshblack.html).  I run an SSH server exclusively, so I have my FTP port blocked at the router (and by IPtables, 'cause all my unbused ports are blocked here anyway).  Why do you need both an SSH and an FTP server?

Have you considered a door-knocking approach (see http://www.soloport.com/iptables.html)?  A pretty clever solution for a low traffic, somewhat exclusive server, but it'd be difficult to make workable with hundreds of users...
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
I run an online game hosting server for MUDs (text-based games, sort of like Zork but multiplayer based). I have a Linksys RV082 business-class firewall/router (not one of those basic NAT translators). Then I have a SuSe Linux server (running VMWare Server) in the background running the hosting environment under another SuSe Linux.

I have http, ssh/sftp, ftp, telnet and each of the game accounts ports open routed through the hardware firewall to the ports on the virtual SuSe image as it does virtual hosting for each of the game sites. The virtual image is running iptables along with portsentry (for port scan attacks) and the block.pl for sshd brute-force attacks. Just lately I noticed that the script kiddies are trying to brute-force the ftp port. I have considered just keeping the ssh/sftp and disabling basic ftp, but it would be nice just to foil the script kiddies instead.

-- M
0
 
mjcoyneCommented:
Ah, Zork -- I first played it on a Commodore VIC-20...:).
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
I have the script w/ modifications in place. I am going to allow it to run overnight. So far, most of the ftp attacks have originated from China, Russia and other 3rd world countries. Did have one early this morning from Australia, but it didn't last very long (might have been my e-mail to the originating ISPs abuse site ;-) ).

As for my site, you're welcome to visit it: www.murpe.com (MultiUser RolePlay Entertainment). Primarly I am a solutions provider for SMBs in the Southeast Georgia region, but I do game development in addition to the game hosting side. In my spare time, I am working on designing a Java-based game engine that allows you to actively modify java files on the fly while inside the virtual game world and recompile them in a real-time fashion w/o restarting the game.

-- M
0
 
Perl_DiverCommented:
personally I think you should write an entirely seperate regexp to check for the proftpd lines, the regexp that mjcoyne wrote looks like it  will work but it's getting long and complicated and probably should have comments added to it. At least you would know what bits are doing what with the comments in case you ever needed to modify it again.
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
Update on the script:

I let the modified block.pl script w/ the lines added to it run for overnight:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) ||
(/(proftpd\[\d+\]).*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\] - no such user/)) {
print STDOUT "block.pl: $1 $2 from $3 \n";
print DEBUG "\$1 contains: $1, \$2 contains: $2, \$3 contains: $3\n";     # add this line for debug

The debug.log file did see and reposnd corrected to a ssh attack, revealing the details below:

$1 contains: Invalid user, $2 contains: fluffy, $3 contains: 218.57.13.166
$1 contains: Invalid user, $2 contains: admin, $3 contains: 218.57.13.166
$1 contains: Invalid user, $2 contains: carol, $3 contains: 62.245.114.254

As of a few minutes ago, the server got attacked again, but its seems the proftpd part in the script isn't picking up the offender's IP address as the debug.log didn't record it -- so I had to manually enter it in.

The /var/log/messages shows the following:

May 19 01:58:13 games proftpd[4153]: games.murpe.com (61.134.40.163[61.134.40.163]) - FTP session opened.
May 19 01:58:13 games proftpd[4153]: games.murpe.com (61.134.40.163[61.134.40.163]) - no such user 'Administrator'
May 19 01:58:13 games proftpd[4153]: games.murpe.com (61.134.40.163[61.134.40.163]) - USER Administrator: no such user found from 61.134.40.163 [61.134.40.163] to 192.168.1.16:21
May 19 01:58:15 games proftpd[4153]: games.murpe.com (61.134.40.163[61.134.40.163]) - Maximum login attempts (3) exceeded

Any more ideas?

-- M
0
 
Perl_DiverConnect With a Mentor Commented:
looks like there is a problem in the final regexp mycoyne posted that you used, the actual parenthesis around the IP addresses are missing in the regexp:

-->(192.168.1.15[192.168.1.15])<--

change this:

if ((/sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) || (/(proftpd\[\d+\]).*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\] - no such user/)) {

to:

if ( /sshd.*(Failed password for|Invalid user) (\w+) from (?:::ffff:)?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/  ||
     /(proftpd\[\d+\])(.*?)\([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\]\) - no such user/ ) {

I put parenthesis around (.*?) just so you will still have three capturing groups and the IP will be in $3.
0
 
Michael WorshamInfrastructure / Solutions ArchitectAuthor Commented:
Modifications worked perfectly to the block.pl code. Added to the system and already had 10 sshd and 15 proftpd brute force attempts -- IP address added successfully to the iptables rules and dropped from connection. Can't ask for anything better. :-)

Thanks all for the help.

-- Michael
0
Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.

All Courses

From novice to tech pro — start learning today.