A script for strict packet filter updates

Against the Wall

comment Module

To cancel locks after a certain time, the iptables comment module must also be in place, either integrated directly into the kernel or downloaded as a kernel module. Without this feature, the script works, but it releases locked areas quite quickly once the logfile no longer documents the access attempts. The duration of the lock is thus automatically based on the interval of any logrotate scripts. In the comments, note the time of the last lock. The script then automatically removes obsolete records after a configurable wait.

Script Structure

The script [8] has three functions: (1) setting up the system to use the rule chains; (2) removing the rules and setup without leaving any leftovers; and (3) executing the filter functions to detect IP addresses, prefixes, and AS numbers. Before the individual functions, the script defines the helper functions that assist iptables in creating and deleting rules (Listing 1).

Listing 1

updateFirewall.sh

001 #!/bin/bash
002
003 # Configuration
004 BLOCK_IP_THRESHOLD=1
005 BLOCK_PREFIX_THRESHOLD=3
006 BLOCK_ASN_THRESHOLD=3
007
008 # Automatically remove old rules after X seconds
009 UNBLOCK_TIME=$((60 * 60 * 24 * 30)) # Unblock after 30 days
010
011 # IPs that should never be locked (can also be a prefix)
012 LAST_RESORT_IPS='
013 127.0.0.1
014 '
015
016 FILTER='
017 cat /var/log/secure | awk '\''$5~/^ssh/ && $6~/^Failed/ && $7~/^keyboard-interactive/ && $9~/[^invalid]/ {print $11}'\''
018 cat /var/log/secure | awk '\''$5~/^ssh/ && $6~/^Failed/ && $7~/^keyboard-interactive/ && $9~/[invalid]/ {print $13}'\''
019 '
020
021 # Path to the client script at the University of Bonn
022 BART_CLIENT='./request.py'
023
024 ##########################################################
025 ##### Nothing has to be changed after this line #####
026 ##########################################################
027
028 IPTABLES=`which iptables`
029
030 # Test to see whether iptables supports comments; otherwise, no auto-unblock
031 UNBLOCK_AUTO=0
032
033 OUT=`cat /proc/net/ip_tables_matches | grep 'comment'`
034 if [ "$OUT" = "comment" ]; then
035   UNBLOCK_AUTO=1
036 fi
037
038 function chain_exists
039 {
040   TABLENAME="-t filter"
041   CHAINNAME=$1
042   if [ "$2" != "" ]; then
043     TABLENAME="-t $1"
044     CHAINNAME=$2
045   fi
046   if [ "$CHAINNAME" != "" ]; then
047     ${IPTABLES} ${TABLENAME} -nL ${CHAINNAME} 2> /dev/null 1> /dev/null
048     RET=$?
049     return ${RET}
050   fi
051 }
052
053 function delete_rule
054 {
055   # Find correct rule parameters
056   RULE=`echo ${@} | awk 'BEGIN{FS=" -j"}; {print $1}'`
057   TARGET=`echo ${@} | awk 'BEGIN{FS=" -j"}; {print "-j"$2}'`
058
059   ${IPTABLES} -S | grep --regexp="$RULE" | grep --regexp="$TARGET" | cut -d " " -f 2- | xargs -L1 ${IPTABLES} -D 2> /dev/null
060 }
061
062 function insert_rule
063 {
064   # echo "insert: $@"
065   delete_rule $@
066
067   if [ "${UNBLOCK_AUTO}" -ne 1 ]; then
068     ${IPTABLES} -I $@
069   else
070     TS=$(date +%s)
071     ${IPTABLES} -I $@ -m comment --comment "Created: $TS"
072   fi
073 }
074
075 function add_rule
076 {
077   # echo "add: $@"
078   delete_rule $@
079
080   if [ "${UNBLOCK_AUTO}" -ne 1 ]; then
081     ${IPTABLES} -A $@
082   else
083     TS=$(date +%s)
084     ${IPTABLES} -A $@ -m comment --comment "Created: $TS"
085   fi
086 }
087
088 if [ "$1" = "setup" ]; then
089   # Deactivate comments to keep these rules permanently
090   UNBLOCK_AUTO=0
091
092   echo "Initialize iptables"
093   # Create rule chains
094   chain_exists LAST_RESORT || ${IPTABLES} -N LAST_RESORT
095   ${IPTABLES} -F LAST_RESORT
096   add_rule LAST_RESORT -j RETURN
097   for IP in "${LAST_RESORT_IPS}"; do
098     insert_rule LAST_RESORT -s ${IP} -j ACCEPT
099   done
100
101   chain_exists BLOCK_IP || ${IPTABLES} -N BLOCK_IP
102   ${IPTABLES} -F BLOCK_IP
103   add_rule BLOCK_IP -j RETURN
104
105   chain_exists BLOCK_PREFIX || ${IPTABLES} -N BLOCK_PREFIX
106   ${IPTABLES} -F BLOCK_PREFIX
107   add_rule BLOCK_PREFIX -j RETURN
108
109   chain_exists BLOCK_ASN || ${IPTABLES} -N BLOCK_ASN
110   ${IPTABLES} -F BLOCK_ASN
111   add_rule BLOCK_ASN -j RETURN
112
113
114   # Create rules in the INPUT chain
115   insert_rule INPUT -j BLOCK_IP
116   insert_rule INPUT -j BLOCK_PREFIX
117   insert_rule INPUT -j BLOCK_ASN
118   insert_rule INPUT -j LAST_RESORT
119   echo "Done"
120   exit
121 elif [ "$1" = "teardown" ]; then
122   echo "Remove all rules and chains"
123   delete_rule INPUT -j BLOCK_IP
124   delete_rule INPUT -j BLOCK_PREFIX
125   delete_rule INPUT -j BLOCK_ASN
126   delete_rule INPUT -j LAST_RESORT
127
128   ${IPTABLES} -F LAST_RESORT 2> /dev/null
129   ${IPTABLES} -X LAST_RESORT 2> /dev/null
130
131   ${IPTABLES} -F BLOCK_IP 2> /dev/null
132   ${IPTABLES} -X BLOCK_IP 2> /dev/null
133
134   ${IPTABLES} -F BLOCK_PREFIX 2> /dev/null
135   ${IPTABLES} -X BLOCK_PREFIX 2> /dev/null
136
137   ${IPTABLES} -F BLOCK_ASN 2> /dev/null
138   ${IPTABLES} -X BLOCK_ASN 2> /dev/null
139   echo "Done"
140   exit
141 fi
142
143 # Test whether setup is already performed
144 chain_exists BLOCK_IP || echo "Please start '$0 setup'"; exit
145
146 IPS=""
147 OLD_IFS=${iFS}
148 IFS="
149 "
150 for RULE in ${FILTER}; do
151   # Apply the filter rule
152   eval FOUND_IPS=\`${RULE}\`
153   IPS="${IPS}
154 ${FOUND_IPS}"
155 done
156
157 # Filter by IP address, incidence count, and use of the IP threshold
158 IPS=`echo "${IPS}" | grep -E '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -n | uniq -c | awk '$1 >= '${BLOCK_IP_THRESHOLD}' {print $2}'`
159
160 # echo "${IPS}"
161 for IP in ${IPS}; do
162   insert_rule BLOCK_IP -s ${IP} -j DROP
163 done
164
165
166
167 # Finding and blocking the prefix
168 QUERY_RESPONSE=`echo "begin
169 prefix
170 $IPS
171 end" | netcat whois.cymru.com 43`
172
173 PREFIXES=`echo "${QUERY_RESPONSE}" | awk 'BEGIN{FS="|"}; {print $1" "$3}' | grep -v '^$' | sort -n | uniq -c | awk '$1 >= '${BLOCK_PREFIX_THRESHOLD}' {print $2" "$3}'`
174
175 # echo "${PREFIXES}"
176 for P in $( echo "${PREFIXES}" | awk '{print $2}' ); do
177   DEL_IPS=`echo "${QUERY_RESPONSE}" | grep "${P}" | awk 'BEGIN{FS="|"}; {print $2}' | sort -n | uniq | awk '{print $1}'`
178   for IP in ${DEL_IPS}; do
179     # echo "Delete single check for IP: ${IP}"
180     delete_rule BLOCK_IP -s ${IP} -j DROP
181   done
182   insert_rule BLOCK_PREFIX -s ${P} -j DROP
183 done
184
185
186
187 # Filter all prefixes of an AS
188 ASNLIST=`echo "${PREFIXES}" | awk '{print $1}' | sort -n | uniq -c | awk '$1 >= '${BLOCK_ASN_THRESHOLD}' {print $2}'`
189
190 if [ -x "${BART_CLIENT}" ]; then
191   for AS in ${ASNLIST}; do
192     PREFIX_LIST=`${BART_CLIENT} announced_prefixes $AS`
193     for P in ${PREFIX_LIST}; do
194       delete_rule BLOCK_PREFIX -s ${P} -j DROP
195       insert_rule BLOCK_ASN -s ${P} -j DROP
196     done
197   done
198 fi
199
200
201
202 # Discard old entries
203 if [ "${UNBLOCK_AUTO}" -eq 1 ]; then
204   NOW=$(date +%s)
205   RULES=`${IPTABLES} -S | cut -d " " -f 2-`
206   for R in $RULES; do
207     TIMESTAMP=`echo "${R}" | grep -o '"Created: .*"' | tr -d '"' | awk '{print $2}'`
208     if [ "$TIMESTAMP" != "" ] && [ "$TIMESTAMP" -lt $(($NOW - $UNBLOCK_TIME)) ]; then
209       delete_rule $R
210     fi
211   done
212 fi
213 IFS=${OLD_IFS}

Rules are deleted by specifying the filter conditions, or by specifying the line number (call iptables with --line-numbers). Using line numbers entails the risk that changes have been made displaying the current rules and deletion through other scripts and that the script thus changes the wrong lines. To prevent this, you use the filter condition itself to remove the entries. Lines 53 to 60 is the delete_rule function. To account for the automatically added timestamp in the optional comments, the function first takes the rules apart and then puts them back together with a slightly modified parameter order.

Processing Chain-by-Chain

The setup and teardown functions can be executed by simply appending the terms as parameters of the script. During setup (lines 88 to 120), the script first creates the three chains if they do not exist. It then removes all the rules from the chains so there is always an empty initial status at setup time. The end of each chain requires a rule to return the package to the calling chain. Without this rule, iptables does not examine the package. The required steps are shown in lines 109 to 111, which use the BLOCK_ASN chain as an example. The helper function chain_exists checks upfront to see whether a chain already exists; add_rule then adds the rule. At the end of the function, the script adds the branch to the new chains in the INPUT chain (lines 115 to 118).

When removing rules with teardown (lines 121 to 141), the functions work in the opposite direction. First, the script removes the rules from the INPUT chain; then, after the mandatory clear-up, it removes the chains themselves (e.g., ${IPTABLES} -X BLOCK_ASN only deletes the BLOCK_ASN chain if it no longer contains any rules). After running teardown, no traces of the script exist any longer in the packet filter.

The core functionality, besides multilevel capability, lies in the filter rules for discovering the potential attackers' IP addresses. To make the script as flexible as possible, the filter rules are arbitrary calls for displaying and filtering logfiles, as well as regular expressions for digging the IP addresses out of the log entries.

Basically, you could call other DIY scripts that output the appropriate IP addresses line by line. The filters then run within a subshell later. For the call, it is necessary to mask these accordingly. Using an example of two simple log entries from the /var/log/secure file on a Gentoo system, the next section explains the approach outlined in Listing 2, in which you create one or more rules of this type for each logfile with relevant information.

Listing 2

Log Entry Examples

# Feb 22 08:00:00 hostname sshd[1234]: Failed keyboard-interactive/pam for root from 1.0.0.1 port 4321 ssh2
# Feb 22 8:00:10 AM hostname sshd[1234]: Failed keyboard-interactive/pam for invalid user mysql from 1.0.0.1 port 4322 ssh2
FILTER='
cat /var/log/secure | awk '\''$5~/^ssh/ && $6~/^Failed/ && $7~/^keyboard-interactive/ && $9~/[^invalid]/ {print $11}'\''
cat /var/log/secure | awk '\''$5~/^ssh/ && $6~/^Failed/ && $7~/^keyboard-interactive/ && $9~/[invalid]/ {print $13}'\''
'

The two entries in the logfile are based on the format of the software deployed. They can vary from system to system; therefore, it is important to test the filter expression thoroughly in advance. The example shows that the SSH daemon can generate different log entries. In the first case, the login for an existing user was entered for root on the system. The second entry also notes that the user ID mysql does not exist on the system. To prevent the filter from extracting incorrect values, the filter rule should be as precise as possible.

The cat command writes the contents of the logfile to the standard output channel. With the help of awk, you can check the individual fields (the default delimiter is the space character) in each line. The terms ssh , Failed , and keyboard-interactive clearly identify a failed login attempt using SSH. However, the IP address occurs in the first entry in field $11, and in the second entry in field $13; thus, you will want to check field $9, too. If this starts with a string that is not equal to invalid, the first filter delivers a result; otherwise, the IP is found two fields to the right and extracted by the second filter.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy ADMIN Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

comments powered by Disqus
Subscribe to our ADMIN Newsletters
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs



Support Our Work

ADMIN content is made possible with support from readers like you. Please consider contributing when you've found an article to be beneficial.

Learn More”>
	</a>

<hr>		    
			</div>
		    		</div>

		<div class=