PageRenderTime 62ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/hawk.pl

https://github.com/hackman/Hawk-IDS-IPS
Perl | 842 lines | 474 code | 177 blank | 191 comment | 144 complexity | c0dedc78d01768686d7c82230327239d MD5 | raw file
  1. #!/usr/bin/perl -T
  2. # 1H - Hawk IDS/IPS Copyright(c) 2010 1H Ltd
  3. # All rights Reserved
  4. # copyright@1h.com http://1h.com
  5. # This code is subject to the GPLv2 license.
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. use strict;
  16. use warnings;
  17. use DBD::Pg;
  18. use POSIX qw(setsid), qw(strftime), qw(WNOHANG);
  19. use lib '/home/1h/lib/perl/';
  20. use parse_config;
  21. $SIG{"CHLD"} = \&sigChld;
  22. $SIG{__DIE__} = sub { logger(@_); };
  23. $ENV{PATH} = ''; # remove unsecure path
  24. my $VERSION = '5.2.6';
  25. # input/output should be unbuffered. pass it as soon as you get it
  26. our $| = 1;
  27. my $debug = 0;
  28. $debug = 1 if (defined($ARGV[0]));
  29. # This will be our function that will print all logger requests to /var/log/$logfile
  30. sub logger {
  31. print HAWKLOG strftime('%b %d %H:%M:%S', localtime(time)) . ' ' . $_[0] . "\n" and return 1 or return 0;
  32. }
  33. # Compare the current attacker's ip address with the local ips (primary and localhost)
  34. sub is_local_ip {
  35. my %whitelists = %{$_[0]};
  36. my $current_ip = $_[1];
  37. # Return 1 if the attacker ip is our own ip
  38. return 1 if (defined($whitelists{$current_ip}));
  39. return 0;
  40. }
  41. # Check if hawk is already running
  42. sub is_hawk_running {
  43. my $pidfile = shift;
  44. # hawk is not running if the pid file is missing
  45. return 0 if (! -e $pidfile);
  46. # get the old pid
  47. open PIDFILE, '<', $pidfile or return 0;
  48. my $old_pid = <PIDFILE>;
  49. close PIDFILE;
  50. # if the pid format recorded in the file is incorrect answer as like hawk is running. this shoud never happen!
  51. return 1 if ($old_pid !~ /[0-9]+/);
  52. # hawk is running if the pid from the pidfile exists as dir in /proc
  53. return 1 if (-d "/proc/$old_pid");
  54. # hawk is not running
  55. return 0;
  56. }
  57. sub close_stdh {
  58. my $logfile = shift;
  59. # Close stdin ...
  60. open STDIN, '<', '/dev/null' or return 0;
  61. # ... and stdout
  62. open STDOUT, '>>', '/dev/null' or return 0;
  63. # Redirect stderr to our log file
  64. open STDERR, '>>', "$logfile" or return 0;
  65. return 1;
  66. }
  67. # write the program pid to the $pidfile
  68. sub write_pid {
  69. my $pidfile = shift;
  70. open PIDFILE, '>', $pidfile or return 0;
  71. print PIDFILE $$ or return 0;
  72. close PIDFILE;
  73. return 1;
  74. }
  75. # Clean the zombie childs!
  76. sub sigChld {
  77. while (waitpid(-1,WNOHANG) > 0) {
  78. logger("The child has been cleaned!") if ($debug);
  79. }
  80. }
  81. # Store each attacker attempt to the database if $_[0] is 0
  82. # Store the attacker's ip to the brootforce database if $_[0] 1
  83. # The brootforce table is later checked by the cron
  84. sub store_to_db {
  85. # $_[0] DB name
  86. # $_[1] DB user
  87. # $_[2] DB pass
  88. # $_[3] 0 insert into failed_log || 1 for insert into broots a.k.a 0 for log_me || 1 for broot_me || 2 inser into blacklist
  89. # $_[4] IP
  90. # $_[5] The service under attack - 0 = ftp, 1 = ssh, 2 = pop3, 3 = imap, 4 = webmail, 5 = cpanel | failed attempts if $_[3] == 2
  91. # $_[6] The user who is bruteforcing only if $_[3] == log_me
  92. my $conn = DBI->connect_cached($_[0], $_[1], $_[2], { PrintError => 1, AutoCommit => 1 }) or return 0;
  93. # Store each failed attempt to the failed_log table
  94. if ($_[3] == 0) {
  95. my $log_me = $conn->prepare('INSERT INTO failed_log ( ip, service, "user" ) VALUES ( ?, ?, ? ) ') or return 0;
  96. $log_me->execute($_[4], $_[5], $_[6]) or return 0;
  97. } elsif ($_[3] == 1) {
  98. my $broot_me = $conn->prepare('INSERT INTO broots ( ip, service ) VALUES ( ?, ? ) ') or return 0;
  99. $broot_me->execute($_[4], $_[5]) or return 0;
  100. } elsif ($_[3] == 2) {
  101. my $log_block = $conn->prepare('INSERT INTO blacklist ( date_add, ip, count, reason ) VALUES (now(), ?, ?, ?)') or return 0;
  102. $log_block->execute($_[4], $_[5], "Blocking IP $_[4] for having $_[5] $_[6] attempts") or return 0;
  103. }
  104. $conn->disconnect;
  105. # return 1 on success
  106. return 1;
  107. }
  108. sub get_attempts {
  109. my $new_count = shift;
  110. my $current_attacker_count = shift;
  111. # Return the current number of bruteforce attempts for that ip if no old records has been found
  112. return $new_count if (! defined($current_attacker_count));
  113. # Sum the number of current bruteforce attempts for that ip with the recorded number of bruteforce attempts
  114. return $new_count + $current_attacker_count;
  115. }
  116. # Compare the number of failed attampts to the $max_attempts variable
  117. sub check_broots {
  118. my $ip_failed_count = shift;
  119. my $max_attempts = shift; # max number of attempts(for $broot_time) before notify
  120. # Return 1 if $ip_failed_count > $max_attempts
  121. # On return 1 the attacker's ip will be recorded to the store_to_db(broots) table
  122. return 1 if ($ip_failed_count >= $max_attempts);
  123. # Do not block/store if the broot attempts for this ip are less than the $max_attempts
  124. return 0;
  125. }
  126. sub do_block {
  127. my $blocked_ip = shift;
  128. my $block_list = shift;
  129. $block_list =~ s/(\r|\n)//g;
  130. $blocked_ip = $1 if ($blocked_ip =~ /([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)/) or logger ("Illegal ip content at $blocked_ip") and return 0;
  131. if (system("/sbin/iptables -I in_hawk -s $blocked_ip -j DROP")) {
  132. logger("/sbin/iptables -I in_hawk -s $blocked_ip -j DROP FAILED: $!");
  133. return 0;
  134. }
  135. $block_list = $1 if ($block_list =~ /^(.*)$/);
  136. open BLOCKLIST, '+>>', $block_list or "Failed to open $block_list for append: $!" and return 0;
  137. print BLOCKLIST "iptables -I in_hawk -s $blocked_ip -j DROP\n" or "Failed to write to $block_list: $!" and return 0;
  138. close BLOCKLIST;
  139. return 1;
  140. }
  141. # Parse the pop3/imap logs
  142. sub dovecot_broot {
  143. # Dovecot POP3
  144. #Aug 30 03:01:57 tester dovecot: pop3-login: method=PLAIN, rip=87.118.135.130, lip=209.62.32.14 Disconnected (auth failed, 2 attempts)
  145. #Aug 30 03:11:00 tester dovecot: pop3-login: method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected Disconnected (auth failed, 3 attempts)
  146. #Aug 30 03:12:51 tester dovecot: pop3-login: user=<testuser>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14 Aborted login (auth failed, 1 attempts)
  147. #Aug 30 03:19:42 tester dovecot: pop3-login: Disconnected (auth failed, 1 attempts): user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14
  148. #Aug 30 03:20:06 tester dovecot: pop3-login: Disconnected (auth failed, 1 attempts): user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected
  149. #Aug 30 03:15:03 tester dovecot: pop3-login: user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14 Disconnected (auth failed, 1 attempts)
  150. #Aug 30 03:15:21 tester dovecot: pop3-login: user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected Disconnected (auth failed, 1 attempts)
  151. # Dovecot IMAP
  152. #Aug 30 03:11:59 tester dovecot: imap-login: method=PLAIN, rip=87.118.135.130, lip=209.62.32.14 Disconnected (auth failed, 3 attempts)
  153. #Aug 30 03:11:36 tester dovecot: imap-login: method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected Disconnected (auth failed, 2 attempts)
  154. #Aug 30 03:13:21 tester dovecot: imap-login: user=<testuser>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14 Aborted login (auth failed, 1 attempts)
  155. #Aug 30 03:15:37 tester dovecot: imap-login: user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected Disconnected (auth failed, 1 attempts)
  156. #Aug 30 03:20:26 tester dovecot: imap-login: Disconnected (auth failed, 1 attempts): user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14
  157. #Aug 30 03:20:40 tester dovecot: imap-login: Disconnected (auth failed, 1 attempts): user=<dqdo>, method=PLAIN, rip=87.118.135.130, lip=209.62.32.14, TLS: Disconnected
  158. my $current_service = 3; # The default service id is 3 -> imap
  159. $current_service = 2 if ($_ =~ /pop3-login:/); # Service is now 2 -> pop3
  160. # Extract the user, ip and number of failed attempts from the log
  161. my $user = 'multiple';
  162. $user = $1 if ($_ =~ /^.* user=<(.+)>,.*$/);
  163. my $ip = $1 if ($_ =~ /^.* rip=([0-9.]+),.*$/);
  164. my $attempts = $1 if ($_ =~ /^.* ([0-9]+) attempts\).*$/);
  165. chomp ($user, $ip, $attempts);
  166. logger("Returning User: $user IP: $ip Attempts $attempts") if ($debug);
  167. # return ip, number of failed attempts, service under attack, failed username
  168. # this is later stored to the failed_log table via store_to_db
  169. return ($ip, $attempts, $current_service, $user);
  170. }
  171. sub courier_broot {
  172. # cPanel
  173. # Aug 27 06:10:57 m670 imapd: LOGIN FAILED, user=wrelthkl, ip=[::ffff:87.118.135.130]
  174. # Aug 27 06:11:10 m670 pop3d: LOGIN FAILED, user=test, ip=[::ffff:87.118.135.130]
  175. # Aug 27 06:12:35 m670 pop3d-ssl: LOGIN FAILED, user=root:x:0:0:root:/root:/bin/bash, ip=[::ffff:87.118.135.130]
  176. # Aug 27 06:13:53 m670 imapd-ssl: LOGIN FAILED, user=root:x:0:0:root:/root:/bin/bash, ip=[::ffff:87.118.135.130]
  177. # Plesk
  178. # Mar 7 07:08:14 plesk pop3d: IMAP connect from @ [127.0.0.1]checkmailpasswd: FAILED: testing - short names not allowed from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  179. # Mar 7 07:08:39 plesk pop3d: IMAP connect from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  180. # Mar 7 07:09:01 plesk imapd: IMAP connect from @ [127.0.0.1]checkmailpasswd: FAILED: lala - short names not allowed from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  181. # Mar 7 07:09:28 plesk imapd: IMAP connect from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  182. # Mar 7 07:17:44 plesk pop3d-ssl: IMAP connect from @ [192.168.0.133]checkmailpasswd: FAILED: lalalal - short names not allowed from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  183. # Mar 7 07:18:28 plesk pop3d-ssl: IMAP connect from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  184. # Mar 7 07:20:33 plesk imapd-ssl: IMAP connect from @ [192.168.0.133]checkmailpasswd: FAILED: akakaka - short names not allowed from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  185. # Mar 7 07:20:53 plesk imapd-ssl: IMAP connect from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  186. chomp($_);
  187. my $current_service = 3; # The default service id is 3 -> imap
  188. $current_service = 2 if ($_ =~ /pop3d(-ssl)?:/); # Service is now 2 -> pop3
  189. my $user = 'unknown';
  190. my $ip = '';
  191. my $attempts = 1;
  192. # Get the user if available
  193. $user = $1 if ($_ =~ /user=(.*),/);
  194. $user = $1 if ($_ =~ /checkmailpasswd: FAILED: (.*) -/);
  195. # Parse the IP
  196. $ip = $1 if ($_ =~ /ip=\[(.*)\]/);
  197. $ip =~ s/.*://;
  198. # return ip, number of failed attempts, service under attack, failed username
  199. # this is later stored to the failed_log table via store_to_db
  200. return ($ip, $attempts, $current_service, $user);
  201. }
  202. sub ssh_broot {
  203. my $ip = '';
  204. my $user = '';
  205. my @sshd = split /\s+/, $_;
  206. if ($sshd[8] =~ /invalid/ ) {
  207. #May 16 03:27:24 serv01 sshd[25536]: Failed password for invalid user suport from ::ffff:85.14.6.2 port 52807 ssh2
  208. #May 19 22:54:19 serv01 sshd[21552]: Failed none for invalid user supprot from 194.204.32.101 port 20943 ssh2
  209. $sshd[12] =~ s/::ffff://;
  210. $ip = $sshd[12];
  211. $user = $sshd[10];
  212. logger("sshd: Incorrect V1 $user $ip") if ($debug);
  213. } elsif ($sshd[5] =~ /Invalid/) {
  214. #May 19 22:54:19 serv01 sshd[21552]: Invalid user supprot from 194.204.32.101
  215. $sshd[9] =~ s/::ffff://;
  216. $ip = $sshd[9];
  217. $user = $sshd[7];
  218. logger("sshd: Incorrect V2 $user $ip") if ($debug);
  219. } elsif ($sshd[5] =~ /pam_unix\(sshd:auth\)/ ) {
  220. #May 15 09:39:10 serv01 sshd[9474]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=194.204.32.101 user=root
  221. $sshd[13] =~ s/::ffff://;
  222. $sshd[13] =~ s/rhost=//;
  223. $ip = $sshd[13];
  224. #$user = $sshd[14];
  225. $user = $1 if ($sshd[14] =~ /user=(.*)/);
  226. logger("sshd: Incorrect PAM $user $ip") if ($debug);
  227. } elsif ($sshd[5] =~ /Bad/ ) {
  228. #May 15 09:33:45 serv01 sshd[29645]: Bad protocol version identification '0penssh-portable-com' from 194.204.32.101
  229. #my @sshd = split /\s+/, $_;
  230. $sshd[11] =~ s/::ffff://;
  231. $ip = $sshd[11];
  232. $user = 'none';
  233. logger("sshd: Grabber $user $ip") if ($debug);
  234. } elsif ($sshd[5] eq 'Failed' && $sshd[6] eq 'password' ) {
  235. #May 15 09:39:12 serv01 sshd[9474]: Failed password for root from 194.204.32.101 port 17326 ssh2
  236. #May 15 11:36:27 serv01 sshd[5448]: Failed password for support from ::ffff:67.15.243.7 port 47597 ssh2
  237. return undef if (! defined($sshd[10]));
  238. $sshd[10] =~ s/::ffff://;
  239. $ip = $sshd[10];
  240. $user = $sshd[8];
  241. logger("sshd: Incorrect V3 $user $ip") if ($debug);
  242. } else {
  243. logger("ssh_broot - unknown case. line: $_");
  244. # return undef if we do not know how to handle the current line. this should never happens.
  245. # if it happens we should create parser for $_
  246. return undef;
  247. }
  248. # return ip, number of failed attempts, service under attack, failed username
  249. # this is later stored to the failed_log table via store_to_db
  250. # service id 1 -> ssh
  251. return ($ip, 1, 1, $user);
  252. }
  253. sub pureftpd_broot {
  254. # May 16 03:06:43 serv01 pure-ftpd: (?@85.14.6.2) [WARNING] Authentication failed for user [mamam]
  255. # Mar 7 01:03:49 serv01 pure-ftpd: (?@68.4.142.211) [WARNING] Authentication failed for user [streetr1]
  256. my @ftp = split /\s+/, $_;
  257. $ftp[5] =~ s/\(.*\@(.*)\)/$1/; # get the IP
  258. $ftp[11] =~ s/\[(.*)\]/$1/; # get the username
  259. # return ip, number of failed attempts, service under attack, failed username
  260. # this is later stored to the failed_log table via store_to_db
  261. # service id 0 -> ftp
  262. return ($ftp[5], 1, 0, $ftp[11]);
  263. }
  264. sub proftpd_broot {
  265. #Aug 27 06:43:28 tester proftpd[4374]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER user: no such user found from ::ffff:87.118.135.130 [::ffff:87.118.135.130] to ::ffff:209.62.32.14:21
  266. #Aug 27 06:43:47 tester proftpd[4374]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER werethet: no such user found from ::ffff:87.118.135.130 [::ffff:87.118.135.130] to ::ffff:209.62.32.14:21
  267. #Aug 27 06:45:54 tester proftpd[7449]: tester (::ffff:127.0.0.1[::ffff:127.0.0.1]) - USER jivko (Login failed): Incorrect password.
  268. #Aug 27 06:46:31 tester proftpd[8655]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER jivko (Login failed): Incorrect password.
  269. # TODO
  270. my $user = $1 if ($_ =~ / - USER (\w+)/);
  271. my $ip = $1 if ($_ =~ /\(.*\[(.*)\]\)/);
  272. $ip =~ s/.*://g;
  273. logger("Returning: $ip, 1, 0, $user") if ($debug);
  274. return ($ip, 1, 0, $user);
  275. }
  276. sub cpanel_webmail_broot {
  277. #209.62.36.16 - webmail.1h216.com [07/17/2008:16:12:49 -0000] "GET / HTTP/1.1" FAILED LOGIN webmaild: user password hash is miss
  278. #201.245.82.85 - khaoib [07/17/2008:19:56:36 -0000] "POST / HTTP/1.1" FAILED LOGIN cpaneld: user name not provided or invalid user
  279. my @cpanel = split /\s+/, $_;
  280. my $service = 4; # Service type is webmail by default
  281. $service = 5 if ($cpanel[10] eq 'cpaneld:'); # Service type is cPanel if the log contains cpaneld:
  282. $cpanel[2] = 'unknown' if $cpanel[2] =~ /\[/;
  283. # return ip, number of failed attempts, service under attack, failed username
  284. # this is later stored to the failed_log table via store_to_db
  285. # service id 4 -> webmail
  286. # service id 5 -> cpanel
  287. return ($cpanel[0], 1, $service, $cpanel[2]);
  288. }
  289. sub da_broot {
  290. #87.118.135.130=attempts=7&date=1299076385&username=turba
  291. #87.118.135.130=attempts=2&date=1299076492&username=admin
  292. $_ =~ s/(\r|\n)//g;
  293. $_ =~ s/&/=/g; # Convert all & to = so we can easily parse them
  294. my @brute_log = split /=/, $_;
  295. logger("IP: $brute_log[0], Failed: $brute_log[2], SVC: 6, User: $brute_log[6]") if ($debug);
  296. # return ip, number of failed attempts, service under attack, failed username
  297. # this is later stored to the failed_log table via store_to_db
  298. return ($brute_log[0], $brute_log[2], 6, $brute_log[6]);
  299. }
  300. # This is the main function which calls all other functions
  301. # The entire logic is stored here
  302. sub main {
  303. my $conf = '/home/1h/etc/hawk.conf';
  304. my %config = parse_config($conf);
  305. # Hawk files
  306. my $logfile = $config{'logfile'}; # daemon logfile
  307. die "No logfile defined in the conf" if (! defined($logfile) || $logfile eq '');
  308. $logfile = $1 if ($logfile =~ /^(.*)$/);
  309. # open the hawk log so we can immediately start logging any errors or debugging prints
  310. open HAWKLOG, '>>', $logfile or die "DIE: Unable to open logfile $logfile: $!\n";
  311. logger("Hawk version $VERSION started!");
  312. my $pidfile = $config{'pidfile'}; # daemon pidfile
  313. $pidfile = $1 if ($pidfile =~ /^(.*)$/);
  314. # This is the system command that will monitor all log files
  315. # For our own convenience and so we can easily add new logs with new parsers the logs are defined in the conf
  316. # The logs should be space separated
  317. # If we need to monitor more logs just append them to the monitor_list conf var
  318. $config{'monitor_list'} = $1 if ($config{'monitor_list'} =~ /^(.*)$/);
  319. my $log_list = "/usr/bin/tail -s 1.00 -F --max-unchanged-stats=30 $config{'monitor_list'} |";
  320. # This is the lifetime of the broots hash
  321. # Each $broot_time all attacker's ips will be removed from the hash
  322. my $broot_time = $config{'broot_time'};
  323. my $start_time = time();
  324. my $hack_attempt = ();
  325. my $attacked_svcs = ();
  326. # What the name of the pid will be in ps auxwf :)
  327. $0 = $config{'daemon_name'};
  328. # make sure that hawk is not running before trying to create a new pid
  329. # THIS SHOULD BE FIXED!!!
  330. if (is_hawk_running($pidfile)) {
  331. logger("is_hawk_running() failed");
  332. exit 1;
  333. }
  334. # Get the local primary ip of the server so we do not block it later
  335. # This open a security loop hole in case of local bruteforce attempts
  336. # my $local_ip = get_ip();
  337. my $whitelislt = $config{'block_whitelist'};
  338. my %whitelists = map { $_ => '1' } split /,/, $whitelislt;
  339. my $set_limit = $config{'set_limit'};
  340. # me are daemon now :)
  341. defined(my $pid=fork) or die "DIE: Cannot fork process: $! \n";
  342. exit if $pid;
  343. setsid or die "DIE: Unable to setsid: $!\n";
  344. #umask 0;
  345. # close stdin and stdout
  346. # redirect stderr to the hawk log
  347. if (! close_stdh($logfile)) {
  348. logger("close_stdh() failed");
  349. exit 1;
  350. }
  351. # write the new pid to the hawk pid file
  352. if (! write_pid($pidfile)) {
  353. logger("write_pid() failed");
  354. exit 1;
  355. }
  356. # use tail to open all logs that should be monitored
  357. open LOGS, $log_list or die "open $log_list with tail failed: $!\n";
  358. # make the output of the opened logs unbuffered
  359. select((select(HAWKLOG), $| = 1)[0]);
  360. select((select(LOGS), $| = 1)[0]);
  361. select((select(STDIN), $| = 1)[0]);
  362. select((select(STDOUT), $| = 1)[0]);
  363. select((select(STDERR), $| = 1)[0]);
  364. # this should never ends.
  365. # this is the main infinity loop
  366. # read each line and parse it. if we do not know how to handle it go to the next line
  367. while (<LOGS>) {
  368. # parse each known line
  369. # if this is a real attack from non local ip the attacker's ip, the number of failed attempts, the bruteforced service and the failed user are stored to @block_results
  370. # $block_results[0] - attacker's ip address
  371. # $block_results[1] - number of failed attempts. NOTE: This is the CURRENT number of failed attempts for that IP. The total number is stored in $hack_attempt{svc}{$ip}
  372. # $block_results[2] - each service parser return it's own unique service id which is the id of the service which is under attack
  373. # $block_results[3] - the username that failed to authenticate to the given service
  374. my @block_results = undef;
  375. if (defined($config{'watch_ssh'}) && $config{'watch_ssh'}) {
  376. if ( $_ =~ /sshd\[[0-9].+\]:/) {
  377. next if ($_ !~ /Failed \w \w/ && $_ !~ /authentication failure/ && $_ !~ /Invalid user/i && $_ !~ /Bad protocol/); # This looks like sshd attack
  378. logger ("calling ssh_broot") if ($debug);
  379. @block_results = ssh_broot($_); # Pass it to the ssh_broot parser and get the attacker's results
  380. }
  381. }
  382. if (defined($config{'watch_cpanel'}) && $config{'watch_cpanel'}) {
  383. if ($_ =~ /FAILED LOGIN/ && ($_ =~ /webmaild:/ || $_ =~ /cpaneld:/)) { # This looks like cPanel/Webmail attack
  384. logger ("calling cpanel_webmail_broot") if ($debug);
  385. @block_results = cpanel_webmail_broot($_); # Pass it to the cpanel_webmail_broot parser and get the attacker's results
  386. }
  387. }
  388. if (defined($config{'watch_da'}) && $config{'watch_da'}) {
  389. # 87.118.135.130=attempts=7&date=1299076385&username=turba
  390. # 87.118.135.130=attempts=2&date=1299076492&username=admin
  391. # 'security.log' strings are skipped since when someone is logged out from the DA panel writes down this string:
  392. # - 87.118.135.130=attempts=1&date=1299076474&username=invalid username: check security.log
  393. if ($_ =~ /attempts.*date.*username/ && $_ !~ /security.log/) { # This looks like Direct admin attack
  394. logger ("calling da_broot") if ($debug);
  395. @block_results = da_broot($_); # Pass the line for parsing to da_broot
  396. }
  397. }
  398. if (defined($config{'watch_pureftpd'}) && $config{'watch_pureftpd'}) {
  399. if ($_ =~ /pure-ftpd:/ && $_ =~ /Authentication failed/) {
  400. logger ("calling pureftpd_broot") if ($debug);
  401. @block_results = pureftpd_broot($_);
  402. }
  403. }
  404. if (defined($config{'watch_proftpd'}) && $config{'watch_proftpd'}) {
  405. #Aug 27 06:43:28 tester proftpd[4374]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER user: no such user found from ::ffff:87.118.135.130 [::ffff:87.118.135.130] to ::ffff:209.62.32.14:21
  406. #Aug 27 06:43:47 tester proftpd[4374]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER werethet: no such user found from ::ffff:87.118.135.130 [::ffff:87.118.135.130] to ::ffff:209.62.32.14:21
  407. #Aug 27 06:45:54 tester proftpd[7449]: tester (::ffff:127.0.0.1[::ffff:127.0.0.1]) - USER jivko (Login failed): Incorrect password.
  408. #Aug 27 06:46:31 tester proftpd[8655]: tester (::ffff:87.118.135.130[::ffff:87.118.135.130]) - USER jivko (Login failed): Incorrect password.
  409. if ($_ =~ /proftpd\[[0-9]+\]:/ && $_ =~ /no such user|Incorrect password/) {
  410. logger ("calling proftpd_broot") if ($debug);
  411. @block_results = proftpd_broot($_);
  412. }
  413. }
  414. if (defined($config{'watch_dovecot'}) && $config{'watch_dovecot'}) {
  415. # Make sure to skip lines that say "Internal login failure". This is internal processing error inside the daemon itself and should not be considered as attack
  416. if ($_ =~ /pop3-login:|imap-login:/ && $_ =~ /auth failed/ && $_ !~ /Internal/) { # This looks like a pop3/imap attack.
  417. logger ("calling dovecot_broot") if ($debug);
  418. @block_results = dovecot_broot($_); # Pass the log line to the pop_imap_broot parser and get the attacker's details
  419. }
  420. }
  421. if (defined($config{'watch_courier'}) && $config{'watch_courier'}) {
  422. # cPanel
  423. # Aug 27 06:10:57 m670 imapd: LOGIN FAILED, user=wrelthkl, ip=[::ffff:87.118.135.130]
  424. # Aug 27 06:11:10 m670 pop3d: LOGIN FAILED, user=test, ip=[::ffff:87.118.135.130]
  425. # Aug 27 06:12:35 m670 pop3d-ssl: LOGIN FAILED, user=root:x:0:0:root:/root:/bin/bash, ip=[::ffff:87.118.135.130]
  426. # Aug 27 06:13:53 m670 imapd-ssl: LOGIN FAILED, user=root:x:0:0:root:/root:/bin/bash, ip=[::ffff:87.118.135.130]
  427. # Plesk
  428. # Mar 7 07:08:14 plesk pop3d: IMAP connect from @ [127.0.0.1]checkmailpasswd: FAILED: testing - short names not allowed from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  429. # Mar 7 07:08:39 plesk pop3d: IMAP connect from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  430. # Mar 7 07:09:01 plesk imapd: IMAP connect from @ [127.0.0.1]checkmailpasswd: FAILED: lala - short names not allowed from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  431. # Mar 7 07:09:28 plesk imapd: IMAP connect from @ [127.0.0.1]ERR: LOGIN FAILED, ip=[127.0.0.1]
  432. # Mar 7 07:17:44 plesk pop3d-ssl: IMAP connect from @ [192.168.0.133]checkmailpasswd: FAILED: lalalal - short names not allowed from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  433. # Mar 7 07:18:28 plesk pop3d-ssl: IMAP connect from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  434. # Mar 7 07:20:33 plesk imapd-ssl: IMAP connect from @ [192.168.0.133]checkmailpasswd: FAILED: akakaka - short names not allowed from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  435. # Mar 7 07:20:53 plesk imapd-ssl: IMAP connect from @ [192.168.0.133]ERR: LOGIN FAILED, ip=[192.168.0.133]
  436. if ($_ =~ /pop3d(-ssl)?:|imapd(-ssl?):/ && $_ =~ /FAILED/) {
  437. logger ("calling courier_broot") if ($debug);
  438. @block_results = courier_broot($_);
  439. }
  440. }
  441. next if (@block_results < 2); # Go ahead if the size of the block results is < 3
  442. next if (is_local_ip(\%whitelists, $block_results[0])); # Go ahead if this is a local ip
  443. # $block_results[0] - attacker's ip address
  444. # $block_results[1] - number of failed attempts. NOTE: This is the CURRENT number of failed attempts for that IP. The total number is stored in $hack_attempts{$svc}{$ip}
  445. # $block_results[2] - each service parser return it's own unique service id which is the id of the service which is under attack
  446. # $block_results[3] - the username that failed to authenticate to the given service
  447. my $curr_time = time();
  448. # Store this failed attempt to the database
  449. logger("Storing failed: 0, $block_results[0], $block_results[2], $block_results[3]") if ($debug);
  450. if (! store_to_db($config{"db"}, $config{"dbuser"}, $config{"dbpass"}, 0, $block_results[0], $block_results[2], $block_results[3])) {
  451. logger("store_to_db failed: 0, $block_results[0], $block_results[2], $block_results[3]!");
  452. }
  453. $hack_attempt->{$block_results[2]}->{$block_results[0]} = get_attempts($block_results[1], $hack_attempt->{$block_results[2]}->{$block_results[0]});
  454. logger("Failed attempts are $hack_attempt->{$block_results[2]}->{$block_results[0]}") if ($debug);
  455. if ($set_limit && check_broots($hack_attempt->{$block_results[2]}->{$block_results[0]}, $config{"block_count"})) {
  456. store_to_db($config{"db"}, $config{"dbuser"}, $config{"dbpass"}, 1, $block_results[0], $block_results[2]);
  457. if (! do_block($block_results[0], $config{'block_list'})) {
  458. logger("Failed to block $block_results[0] and store it to $config{'block_list'}") if ($debug);
  459. } else {
  460. logger("Successfully blocked $block_results[0] and stored to $config{'block_list'}") if ($debug);
  461. store_to_db($config{"db"}, $config{"dbuser"}, $config{"dbpass"}, 2, $block_results[0], $config{"block_count"}, "failed");
  462. }
  463. } elsif (check_broots($hack_attempt->{$block_results[2]}->{$block_results[0]}, $config{"broot_number"})) {
  464. #logger("store_to_db(broots): 1, ip, service code");
  465. store_to_db($config{"db"}, $config{"dbuser"}, $config{"dbpass"}, 1, $block_results[0], $block_results[2]);
  466. # Zero the number of failed attempts for this IP so we can prevent adding a new brute record on attempt_to_brute+1
  467. $hack_attempt->{$block_results[2]}->{$block_results[0]} = 0;
  468. # Push that particular bruteforce attempt to the $attacked_svcs array ref
  469. #push(@{$svc{'as'}}, @arr);
  470. push(@{$attacked_svcs->{$block_results[2]}}, [$curr_time, $block_results[0]]);
  471. while (my ($service, @attackers) = each %$attacked_svcs) {
  472. my %attacks = ();
  473. for (my $i = 0; $i < @{$attackers[0]}; $i++) {
  474. # This is really old attack and we do not count it now + we delete its records
  475. delete($attackers[0]->[$i]) and next if (($curr_time - $config{'broot_interval'}) > $attackers[0]->[$i]->[0]);
  476. # Remove the remaining elements for that IP if it is already blocked
  477. delete($attackers[0]->[$i]) and next if (defined($attacks{$attackers[0]->[$i]->[1]}[1]) && $attacks{$attackers[0]->[$i]->[1]}[1]);
  478. # Increase the number of broot attempts for this IP
  479. $attacks{$attackers[0]->[$i]->[1]}[0] = 0 if (! defined($attacks{$attackers[0]->[$i]->[1]}[0]));
  480. $attacks{$attackers[0]->[$i]->[1]}[0]++;
  481. #print "IP: $attackers[0]->[$i]->[1] Brutes: $attacks{$attackers[0]->[$i]->[1]}[0]\n";
  482. # Next as the bruteforce attempts are not enough for blocking
  483. next if ($attacks{$attackers[0]->[$i]->[1]}[0] < $config{'max_attempts'});
  484. if (! do_block($attackers[0]->[$i]->[1], $config{'block_list'})) {
  485. logger("Failed to block $attackers[0]->[$i]->[1] and store it to $config{'block_list'}") if ($debug);
  486. } else {
  487. logger("Successfully blocked $attackers[0]->[$i]->[1] and stored to $config{'block_list'}") if ($debug);
  488. $attacks{$attackers[0]->[$i]->[1]}[1] = store_to_db($config{"db"}, $config{"dbuser"}, $config{"dbpass"}, 2, $attackers[0]->[$i]->[1], $config{'max_attempts'}, "bruteforce");
  489. }
  490. }
  491. }
  492. } else {
  493. logger("Not enough minerals to block $block_results[0] for bruteforcing $block_results[2] attempts $hack_attempt->{$block_results[2]}->{$block_results[0]}") if ($debug);
  494. }
  495. # clean all %hack_attempt entries if the $broot_time from the conf passed
  496. if (($curr_time - $start_time) > $broot_time) {
  497. logger("Cleaning the faults hashes and resetting the timers") if ($debug);
  498. # clean the hack_attempt hash and reset the timer
  499. #delete @hack_attempt{keys \$hack_attempt};
  500. $hack_attempt = {};
  501. $start_time = time(); # set the start_time to now
  502. }
  503. }
  504. # We should never hit those unless we kill tail :)
  505. logger("Gone ... after the main loop");
  506. close LOGS;
  507. logger("Gone ... after we closed the logs");
  508. close STDIN;
  509. logger("Gone ... after we closed the stdin");
  510. close STDOUT;
  511. logger("Gone ... after we closed the stdout");
  512. close STDERR;
  513. logger("Gone ... after we closed the stderr");
  514. close HAWKLOG;
  515. exit 0;
  516. }
  517. main();
  518. =head1 NAME
  519. hawk.pl - SiteGround Commercial bruteforce monitoring detection and prevention daemon.
  520. =head1 SYNOPSIS
  521. /path/to/hawk.pl [debug]
  522. =head1 DESCRIPTION
  523. hawk.pl also known as [Hawk] is a bruteforce monitoring detection and prevention daemon.
  524. It monitors various CONFIGURABLE log files by using the GNU tail util.
  525. The output from the logs is monitored for predefined patterns and later passed to different parsers depending on the service which appears to be under attack.
  526. Currently [Hawk] is capable of detecting and blocking bruteforce attempts against the following services:
  527. - ftp - PureFTPD support only
  528. - ssh - OpenSSH support only
  529. - pop3 - Dovecot support only
  530. - imap - Dovecot support only
  531. - cPanel
  532. - cPanel webmail
  533. - more to come soon ... :)
  534. Each failed login attempt is stored to a local USER CONFIGURABLE PostgreSQL database inside the failed_log table which is later used by hawk-web.pl for data visualization and stats.
  535. In case of too many failed login attempts from a single IP address for certain predefined USER CONFIGURABLE amount of time the IP address is stored/logged to the same database but inside the broots table. The broots table is later parsed by the /root/hawk-blocker.sh which does the actual blocking of the IP via iptables.
  536. =head1 PROGRAM FLOW
  537. - main() - init the vital variables and go to the main daemon loop.
  538. - parse_config() - get the conf variables.
  539. - is_hawk_running() - make sure that hawk is not already running.
  540. - get_ip() - get the main ip of the server.
  541. - fork.
  542. - close_stdh() - close stdin and stdout, redirect stderr to the logs.
  543. - write_pid() - write the new [Hawk] pid to the pidfile.
  544. - open the logs for monitoring.
  545. - MONITOR THE LOGS
  546. - pop_imap_broot(), ssh_broot(), ftp_broot(), cpanel_webmail_broot() - In case of hack attempt match the control is passed to line parser for the given service.
  547. - is_local_ip() - Make sure that the IP of the attacker is not the local IP. We do not want to block localhosts.
  548. - get_attempts() - In case of bruteforce attempt we initialize or calculate the total number of failed attempts for that ip with this function.
  549. - store_to_db() - We also store this particular attempt to the failed_log table.
  550. - Check all attackers stored in %hack_attempt.
  551. - check_broots() - Compare the number of failed attempts for the current IP address with the max allowed failed attempts
  552. - store_to_db() - If the IP reached/exceeded the max allowed failed attempts the IP is stored to the broots table
  553. - Clear ALL IP addresses stored in %hack_attempt ONLY if $broot_time (USER CONFIGURABLE) seconds has elapsed and reset the timer
  554. - Start over to MONITOR LOGS
  555. =head1 IMPORTANT VARIABLES
  556. - $conf - Full path to the [Hawk] and hawk-web.pl configuration file
  557. - %config - Store all $k,$v from the conf file so we can easily refference them via the conf var name
  558. - $logfile - Full path to the hawk.pl log file
  559. - $pidfile - Full path to the hawk.pl pid file
  560. - $config{'monitor_list'} - Space separated list of log files that should be monitoried by hawk. All of them should be on a SINGLE line
  561. - $log_list - The system command that will be executed to monitor the commands
  562. - $broot_time - The amount of time in seconds that should elapse before clearing all hack attempts from the hash
  563. - $local_ip - Primary IP address of the server
  564. - @block_results - Temporary storage for the results returned by the service_name_parsers. If no results it should be undef.
  565. $block_results[0] - attacker's ip address
  566. $block_results[1] - number of failed attempts as returned by the parser. NOTE: This is the CURRENT number of failed attempts for that IP. The total number is stored in $hack_attempts{$svc}{$ip}
  567. $block_results[2] - each service parser return it's own unique service id which is the id of the service which is under attack
  568. $block_results[3] - the username that failed to authenticate to the given service
  569. =head1 FUNCTIONS
  570. =head2 get_ip() - Get the primary ip address of the server
  571. Input: NONE
  572. Returns: Main ip address of the server
  573. =head2 is_local_ip() - Compare the current attacker's ip address with the local server ip
  574. Input:
  575. $local_ip - the local ip address of the server previously obtained from get_ip()
  576. $current_ip - the ip attacker's address returned by the servive_name_parser
  577. Output:
  578. 0 if the IP address does not seem to be local
  579. 1 if the IP address appears to be local
  580. =head2 is_hawk_running() - Check if hawk is already running
  581. Input: $pidfile - The full system path to the pid file
  582. Output:
  583. 0 if the pid does not exists, the old pid left from previous hawk instances does not exist in proc
  584. 1 if hawk is already running or we have problem with the pid format left by previous/current hawk instance
  585. =head2 close_stdh() - Close STDIN, STDOUT and redirect STDERR to the log fil
  586. Input: $logfile - The full system path to the hawk.pl log file
  587. Output:
  588. 0 on failure
  589. 1 on success
  590. =head2 write_pid() - Write the new hawk pid to the pid file
  591. Input: $pidfile - The full system path to the hawk pid file
  592. Ouput:
  593. 0 on failure
  594. 1 on success
  595. =head2 sigChld() - Reaper of the dead childs
  596. Called only in case of SIG CHILD
  597. Input: None
  598. Output: None
  599. =head2 store_to_db() - Store the attacker's ip address to the failed_log or broots tables depending on the case
  600. Input:
  601. $_[0] - Where we should store this attempt
  602. - 0 means failed_log
  603. - 1 means broots
  604. $_[1] - The attacker's ip address that should be recorded to the DB
  605. $_[2] - The code of the service which is under attack
  606. $_[3] - The username that the attacker tried to use to login. Correctly defined only in case $_[0] is 0. Otherwise it is undef
  607. $_[4] - DB name
  608. $_[5] - DB user
  609. $_[6] - DB pass
  610. Output:
  611. 0 on failure - In such case we will retry to store the attacker later on the next loop :)
  612. 1 on success
  613. =head2 get_attempts() - Compute the number of failed attempts for the current attacker
  614. Input:
  615. $new_count - The number of failed attempts we just received from the service parser for that ip
  616. $current_attacker_count - The stored number of failed attempts for that ip. Undef if this is a new attacker
  617. Output:
  618. Total number of failed attempts (we just sum old+new or return new if old is undef)
  619. =head2 check_broots() - Compare the number of failed attempts for this attacker with the $max_attempts CONF variable
  620. Input:
  621. $ip_failed_count - Total number of failed attempts from this IP address
  622. $max_attempts - The conf variable
  623. Output:
  624. 0 if $ip_failed_count < $max_attempts
  625. 1 if $ip_failed_count >= $max_attempts -> This means store this IP to the broots db and later block it with iptables via the cron
  626. =head2 pop_imap_broot() ssh_broot() ftp_broot() cpanel_webmail_broot() - The logs output parsers for the supported services
  627. Input: $_ - The log line that looks like bruteforce attempt
  628. Output:
  629. $ip - The IP address of the attacker
  630. $num_failed - The number of failed attempts for that IP returned by the parser
  631. $service_id - The id/code of the service which is under attack
  632. 0 - FTP
  633. 1 - SSH
  634. 2 - POP3
  635. 3 - IMAP
  636. 4 - WebMai
  637. 5 - cPanel
  638. $username - The username that failed to authenticate from that IP
  639. =head2 main() - NO HELP AVAIL :)
  640. =head1 CONFIGURATION FILE and CONFIGURABLE parameters
  641. db - The name of the database where the data will be stored by the daemon
  642. dbuser - The name of the user which has the rights to connect and store info to the db
  643. dbpass - ...
  644. template_path - Path to the hawk templates. Used only by hawk-web.pl
  645. service_ids - service_name:id pairs. What is the ID of "this" service?
  646. service_names - id:service_name pairs. What is the name of "this" service id?
  647. logfile - The full system path to the hawk.pl log file
  648. monitor_list - The full space separated list of logfiles that should be monitored by [Hawk] via tail. Should be on a single line.
  649. broot_time - The max amount of time in seconds that should pass before we clear the stored attacker's from the hash
  650. max_attempts - The max number of failed attempts before we block the attacker's ip address
  651. daemon_name - The name of the hawk.pl daemon as it will appear in ps uaxwf
  652. =head1 SUPPORTED DATABASE ENGINES
  653. PostgreSQL only so far. We do not plan to release MySQL support as MySQL .... a duck :)
  654. =head1 REPORTING BUGS
  655. operations@1h.com
  656. =head1 COPYRIGHT
  657. FILL ME
  658. =head1 SEE ALSO
  659. hawk-web.pl, hawk-web.conf, hawk-block.sh, hawk.init
  660. =cut