#!/usr/local/bin/sh_root
# Sreen Tallam 07/14/09

# This script ensures that malicious dictonary login attempts
# are not left open for hackers. This will make sure that there
# are passwd lockouts set in place to aviod such scenaiors

#To turn on DEBUG uncomment the line below
# set -x
# set -e 

# create a dir for authlog files

AUTHDIR="/tmp/auth"

AUTHLOG="/var/log/authlog"
BLOCKED_IP_LIST="/var/log/blocked_ip"
MARKER_FILE="$AUTHDIR/marker_file"
SCRIPT_RUNNING_FILE="$AUTHDIR/ssh_script_running"
UNBLOCKED_IP_LIST="$AUTHDIR/unblocked_ip"

AUTHLOG_SNAP="$AUTHDIR/authlog_snap"
PASSWD_FAIL="$AUTHDIR/mal_passwd"
TEMP_BLOCKED_FILE="$AUTHDIR/temp_mal_file"
TEMP_FILE="$AUTHDIR/tempfile"
TEMP_FILE2="$AUTHDIR/tempfile2"
LOCKDIR="$AUTHDIR/lockdir"

IP4TABLES="/sbin/iptables"
IP6TABLES="/sbin/ip6tables"
DATE="/bin/date"

num_params=$#
cmd_param=$1
ip_addr_param=$2

# Sel logging variables, please refer netapp/include/sel.h for more info
# values under sel_component_t & sel_sev_t
SEL_PARAM="auth.notice"


# A rogue IP address is blocked for 15 mins, even though the count is 16
# the first decrement happens immediately when this script is run in Phase II
# without waiting for 1 min.
FREEZE_IP_TIME_MINS=16
FREEZE_IP_TIME_SECONDS=600
# Any IP address that had a FAILED passwd entry at least 5 times in the 
# last 10 mins is blocked
FAIL_PASSWD_LIMIT=5
# We look back 10 mins back into the /var/log/authlog to find out any dictionary
# sort of attacks
MAL_LOOKBACK_LIMIT=10
# We maintain a marker file, that has the last entry of /var/log/authlog 
# everytime this script is run. Since from the previous variable we lookback
# 10 mins in the past, we maintain 10 entries in the marker file. Once it
# overflows 10, we add the 11th entry into the file and delete the first entry 
# from the marker file, so to keep the count to 10
MAX_MARK_FILE_SIZE_LIMIT=10

MAX_SEL_LEN=46
SEL_LOGGER="/usr/local/bin/sel_logger"

# Create a lock to prevent other callers until script is complete.
mkdir $LOCKDIR

touch $BLOCKED_IP_LIST
touch $UNBLOCKED_IP_LIST
touch $SCRIPT_RUNNING_FILE
chmod 777 $AUTHDIR
chmod 666 $BLOCKED_IP_LIST
chmod 666 $UNBLOCKED_IP_LIST
chmod 4555 $IP4TABLES
chmod 4555 $IP6TABLES

# Phase I
# -------

# This function is used to look back 10 mins in the past of the recent
# /var/log/authlog activites. This routine, tries to get the starting line
# of the  authlog from which it needs to read through

lookback_in_time()
{
	# Delete the temp files before start
	rm -f 	$AUTHLOG_SNAP \
			$PASSWD_FAIL \
			$TEMP_FILE_LIST \
			$TEMP_BLOCKED_FILE \
			$TEMP_FILE2 \
			$TEMP_FILE 

	# Create a snapshot of the current AUTHLOG
	grep sshd $AUTHLOG > $AUTHLOG_SNAP

	# Last line # in authlog, at this moment
	last_line_num=`cat $AUTHLOG_SNAP | wc -l`
	# Last line present in authlog
	last_line_authlog=`tail -n 1 $AUTHLOG_SNAP | sed -e 's|  *|_|'`

	# Total number of entries present in the marker file
	if [ -e $MARKER_FILE ]
	then
		total_num_lines_mark=`cat $MARKER_FILE | wc -l`
	else
		total_num_lines_mark=0
	fi


	# If there are no entires in the $AUTHLOG_SNAP then, you can exit, as there
	# is nothing to process. Else start processing to find out the start line #
	# from which we need to read from in $AUTHLOG_SNAP
	if [ $last_line_num -gt 0 ]
	then
		prev_line_num=$(tail -n 1 $MARKER_FILE | awk '{ print $3 }')
		echo "Line #:" $last_line_num "<::> String:"$last_line_authlog"<::> Record time:" `$DATE` >> $MARKER_FILE
		# If there are more entries in the $MARKER_FILE 
		# than the $MAX_MARK_FILE_SIZE_LIMIT, then fetch the start line # 
		# or else set the $start_line_num to 0, also delte the top of the
		# line entry from the $MARKER_FILE so the count is back to 
		# MAX_MARK_FILE_SIZE_LIMIT always.
		if [ $total_num_lines_mark -ge $MAX_MARK_FILE_SIZE_LIMIT ]
		then
				# Get the starting line# to read from $AUTHLOG_SNAP
				head -n 1 $MARKER_FILE > $TEMP_FILE
				start_line_num=`awk '{print $3}' $TEMP_FILE`
				
				# Two step calculation to get the string from the $MARKER_FILE
				awk -F "String:" '{print $2}' $TEMP_FILE > $TEMP_FILE2
				start_line_string=`awk -F "<::>" '{print $1}' $TEMP_FILE2`
				
				# Now get the string at $start_line_num from $AUTHLOG_SNAP	
				search_line_string=`head -n $start_line_num $AUTHLOG_SNAP \
				| tail -n 1 | sed -e 's|  *|_|'` 
				# Check to see if the string in the marker file matches
				# to the string in the $AUTHLOG_SNAP
				if [ "$start_line_string" != "$search_line_string" ]
				then
					start_line_num=0
				fi
				# Delete the top entry in the log file.		
				sed 1d $MARKER_FILE > $TEMP_FILE
				mv -f $TEMP_FILE $MARKER_FILE
		else 
			# Since there are less than $MAX_MARK_FILE_SIZE_LIMIT entries
			# in the $MARKER_FILE, read the whole $AUTHLOG_SNAP
			start_line_num=0
			fi
	else
		# Nothing to be done, delete the files created.
		rm -f $AUTHLOG_SNAP
		rm -f $MARKER_FILE
		
		# Either it is the intial boot where there are no entries within 
		# the $AUTHLOG_SNAP or the logs have rotated, in that case as well
		# the $AUTHLOG_SNAP will be empty. So before exit, lets run through
		# the update_or_unblock_ipadds to make sure that we have updated the
		# $BLOCKED_IP list 	
		update_or_unblock_ipadds
		
		# remove lock 
		rmdir $LOCKDIR
		exit 0		
	fi
}


# Phase II
# --------

# This function, works through the last 10 mins of the /var/log/authlog
# to find all the IP addresses that have failed with wrong passwds for more
# than 5 times. These failed IP addresses are stored under $PASSWD_FAIL

find_the_malicious_ipadds()
{
	# Collect all the fail passwd attempts under /tmp/auth/mal_passwd
	# Example failed passwds:
	#
	# Unknown user, Unknown passwd
	# Jul 23 22:17:26 291565 sshd[1961]: Failed password for invalid user 
    # tallam from 172.22.132.205 port 51035 ssh2 
	#
	# Known user, Unknown passwd
	# Jul 23 22:17:44 291565 sshd[1963]: Failed password for 
	# root from 172.22.132.205 port 51036 ssh2

	cat $AUTHLOG_SNAP | awk -v startline=$start_line_num -v regex1="^.*sshd.*: Failed password for .* from .* port .* ssh2" -v regex2="^.*sshd.*: Failed password for .* from " 'NR>startline && $0 ~ regex1 {sub(regex2,"");print $1}' > $PASSWD_FAIL

	# Store only the failed passwd that are more than $FAIL_PASSWD_LIMIT
	# in $PASSWD_FAIL
	sort $PASSWD_FAIL | uniq -c > $TEMP_FILE
	awk -v passwd_limit="$FAIL_PASSWD_LIMIT" '{if ($1 >= passwd_limit) print $2}' $TEMP_FILE > $PASSWD_FAIL

	# Store the contents into a temporary variable
	if [ -s $PASSWD_FAIL ]
	then
		mal_ipadd_track=`cat $PASSWD_FAIL`
	else
		mal_ipadd_track=""	
	fi
	# echo $mal_ipadd_track
}

# Phase IIa
# ---------

# Find only /new/ failed attempts and log them to the SEL
find_new_failed_logins()
{
	sed $((prev_line_num+1))',$s/^.*sshd.*: Failed password for .* from \(.*\) port .*/\1/p; d' $AUTHLOG_SNAP \
		| while read ip ; do
			sel_string=$(echo "Failed SSH password from $ip" | cut -c-$MAX_SEL_LEN)
			$SEL_LOGGER $SEL_PARAM "$sel_string"
		done
}

# Phase III
# ---------

# This functions checks if an ip address has been unblocked recently
# and thus can be used to prevent from blocking again during this time
# Return 1 if the address is not supposed to be blocked
# Return 0 if the address can be blocked again since it hasn't been
# blocked recently
address_unblocked()
{
    ret_val=0
    if [ -e $UNBLOCKED_IP_LIST ]; then

        now=$($DATE +%s);
        while read line
        do        
            ip=$(echo $line | awk -F"<::>" '{print $1}');
            if [ $1 = $ip ]; then
                dt=$(echo $line | awk -F"<::>" '{print $2}');
                # Check if this date is in the last 10 minutes from $now
                # If so then this address is not supposed to be blocked again
                if [ $(expr $now - $dt) -lt $FREEZE_IP_TIME_SECONDS ]; then
                    ret_val=1;
                fi
            fi
        done <$UNBLOCKED_IP_LIST
    fi
    return $ret_val
}

# This function now gets all the IP addresses from $PASSWD_FAIL and
# blocks those addresses in the firewall, if they have not been blocked
# already. It adds an entry in the $BLOCKED_IP_LIST, logs a message of 
# an address being blocked under /var/log/authlog. Further, adds this entry 
# into the SEL records.

block_malicious_ipadds()
{
	# Store the final list of filtered address into a "Watch list"
	# these addresse under this list will be restricted in connection for
	# the next $FREEZE_IP_TIME_MINS
 	
	# Ex: mal_ipadd	- 10.56.92.161

	for mal_ipadd in $mal_ipadd_track
	do

		address_unblocked $mal_ipadd
		if [[ $? -eq 1 ]]; then
			continue;
		fi
		ret=`grep $mal_ipadd $BLOCKED_IP_LIST | awk '{print $1}'`
		if [ -z "$ret" ] 
		then 
			echo $mal_ipadd "<::>" `$DATE` "<::> time left" \
				$FREEZE_IP_TIME_MINS >> $BLOCKED_IP_LIST
			ip_type=`echo "$mal_ipadd" | awk '/:/ {print "IP6"}'`
			# Block the malicious IP address from the IPTABLES/IP6TABLES
			# Decide if we need to add a entry with IP6TABLES/IPTABLES
			# within the firewall
			if [ -n "$ip_type" ]
			then 
				$IP6TABLES -A blocked_ip -p ALL -s $mal_ipadd -j DROP
				logger -p auth.info - Blocked IPv6 $mal_ipadd in firewall
				$SEL_LOGGER	$SEL_PARAM "Block $mal_ipadd"
			else 
				$IP4TABLES -A blocked_ip -p ALL -s $mal_ipadd -j DROP
				logger -p auth.info - Blocked IPv4 $mal_ipadd in firewall
				$SEL_LOGGER	$SEL_PARAM "Block $mal_ipadd"					
			fi
		fi
	done


	# Remove all the files that are needed from now on
	rm -f 	$AUTHLOG_SNAP \
			$PASSWD_FAIL \
			$TEMP_BLOCKED_FILE \
			$TEMP_FILE2 \
			$TEMP_FILE 

}
# Phase IV
# ---------

# Once the adding of the IP addresses is completed, decrement the counter
# of the "time_left" in each of the entry in the $BLOCKED_IP_LIST,
# if the "time_left" is zero then remove the entry from $BLOCKED_IP_LIST
# release it from the firewall, log an entry in /var/log/authlog and as
# well as mark a record entry in the SEL records

update_or_unblock_ipadds()
{

	# Store the default contents of IFS in a temp variable
	OLDIFS=$IFS
	# Set the input field seperator to a newline
	IFS="
	"
	# Decrement the counter for the malicious IP, to get them of the
	# freeze period. If the time for a particular IP has come to 0
	# remove that from the list and as well as remove the blocking rule
	# for the Firewall IPTABLES rules.

	# Ex for line: "10.56.92.161 <::> Jul 21 16:15:01 (none) <::> time left 15"
	
	# Create a temp file
	> $TEMP_BLOCKED_FILE
 
	for line in `cat $BLOCKED_IP_LIST`
	do
    	str=`echo "$line" | awk '{print $12}'`
		store_val=`expr $str - 1`
		# Timer expired, remove the IP rule from firewall for this IP address
		if [ $store_val -eq 0 ]	
		then
			mal_ipadd=`echo "$line" | awk '{print $1}'`
			ip_type=`echo "$mal_ipadd" | awk '/:/ {print "IP6"}'`	
		
			# Decide if we need to delete a entry with IP6TABLES/IPTABLES
			# within the firewall
			if [ -n "$ip_type" ]
			then
				$IP6TABLES -D blocked_ip -p ALL -s $mal_ipadd -j DROP
				logger -p auth.info - Unblock IPv6 $mal_ipadd in firewall
				mal_ipadd_temp=`echo $mal_ipadd | sed 's/://g'`
				$SEL_LOGGER $SEL_PARAM "Unblock $mal_ipadd"
			else
				$IP4TABLES -D blocked_ip -p ALL -s $mal_ipadd -j DROP
				# Store the result of the previous command return status
				ret=$?
				logger -p auth.info - Unblock IPv4 $mal_ipadd in firewall
				$SEL_LOGGER $SEL_PARAM "Unblock $mal_ipadd"
			fi
		# Or else, decrement the time of the IP address and keep in the 
		# malicious list and as well as have it in the firewall
		else
			str=`echo "$line" | \
				sed 's/time left '$str'/time left '$store_val'/g'`
			echo $str >> $TEMP_BLOCKED_FILE		
		fi	
	done

	# Restore IFS back to the default value
	IFS=$OLDIFS
	# Update the malicious list with updated time values, if there are any
	if [ -e $TEMP_BLOCKED_FILE ]
	then
		cat $TEMP_BLOCKED_FILE > $BLOCKED_IP_LIST
	fi
}

update_unblocked_list()
{
    local last_line_num=$(wc -l < $UNBLOCKED_IP_LIST);
    # We allow maximum 10 entries and then need to make some space
    if [ $last_line_num -ge 10 ]; then
        #make space for some new entries
        tail -n 7 $UNBLOCKED_IP_LIST > $TEMP_UNBLOCKED_IP_LIST
        cat $TEMP_UNBLOCKED_IP_LIST > $UNBLOCKED_IP_LIST
    fi
    # Add the ip address passed in $1 to the unblocked ip list
    echo $1"<::>"$($DATE +%s) >> $UNBLOCKED_IP_LIST
    echo "$1 address was unblocked."
}


update_or_unblock_ipadds_cli()
{

	# Store the default contents of IFS in a temp variable
	OLDIFS=$IFS
	# Set the input field seperator to a newline
	IFS="
	"
	# If the user unblocked an IP address, remove that address
	# from the list and remove the blocking rule
	# for the Firewall IPTABLES rules.

	# Create a temp file
	> $TEMP_BLOCKED_FILE
 
	for line in `cat $BLOCKED_IP_LIST`
	do
    	str=`echo "$line" | awk '{print $12}'`
		mal_ipadd=`echo "$line" | awk '{print $1}'`
		
		# If IP address is being cleared from CLI, clear it from list
		# and unblock from firewall.
		
		if [ $mal_ipadd = $ip_addr_param ] || [ $ip_addr_param = all ]; 
		then
			ip_type=`echo "$mal_ipadd" | awk '/:/ {print "IP6"}'`	
		
			# Decide if we need to delete a entry with IP6TABLES/IPTABLES
			# within the firewall
			if [ -n "$ip_type" ]
			then
				$IP6TABLES -D blocked_ip -p ALL -s $mal_ipadd -j DROP
				logger -p auth.info - Unblock IPv6 $mal_ipadd in firewall
				update_unblocked_list $mal_ipadd
				mal_ipadd_temp=`echo $mal_ipadd | sed 's/://g'`
				$SEL_LOGGER $SEL_PARAM "Unblock $mal_ipadd"
			else
				$IP4TABLES -D blocked_ip -p ALL -s $mal_ipadd -j DROP
				# Store the result of the previous command return status
				ret=$?
				logger -p auth.info - Unblock IPv4 $mal_ipadd in firewall
				$SEL_LOGGER $SEL_PARAM "Unblock $mal_ipadd"
				update_unblocked_list $mal_ipadd
			fi
		# Or else, decrement the time of the IP address and keep in the 
		# malicious list and as well as have it in the firewall
		else
			str=`echo "$line" | \
				sed 's/time left '$str'/time left '$store_val'/g'`
			echo $str >> $TEMP_BLOCKED_FILE		
		fi	
	done

	# Restore IFS back to the default value
	IFS=$OLDIFS
	# Update the malicious list with updated time values, if there are any
	if [ -e $TEMP_BLOCKED_FILE ]
	then
		cat $TEMP_BLOCKED_FILE > $BLOCKED_IP_LIST
	fi
}


main ()
{

   if [ $num_params -eq 0 ] ; then
		# Called from timer thread. No parameters required
		lookback_in_time
		find_the_malicious_ipadds
		find_new_failed_logins
		block_malicious_ipadds		
		update_or_unblock_ipadds
	else
		# Called from CLI. Either clear a specific IP or all IPs, based on inputs.
		if [ $num_params -eq 2 ] ; then
			update_or_unblock_ipadds_cli
		fi
	fi
	
    # Remove lock file
    rmdir $LOCKDIR
}

#Start of the script.
main 

exit 0
