xcorp::When it rains, it pours.

"The nice thing about rain," said Eeyore, "is that it always stops. Eventually."

すくりぷつ

きむちぃの国やら 4000 年の歴史の国やら SHOGUN 様の国などからお客様がたくさんいらしてくださるおかげで、いろいろ大繁盛な今日この頃ですが、みなさまいかがお過ごしでしょうか。
てなわけで、貴重な 産卵シーン 睡眠時間とか携帯電話の電池が減る一方ということで、ひとつスクリプトをこさえてみました。1 台の ftp サーバだけだとオーバスペックなのですが、複数台の ftp サーバが同じ NAS の同じ領域をマウントしていることを前提に、ある 1 台の ftp が大繁盛になってしまった場合に、そのサーバだけではなく他のサーバでもお引き取りいただけるようになっています。
ログファイルのパス (TARGET_LOG) と引っかける文字列 ('no such user found from') と IP アドレスの場所 (awk '{ print $15 }' ${TEMP_FILE}) を変えてやれば、http や ssh にも応用できます。複数台での運用を前提にしているので、HOGE_DIR は NAS をマウントしている領域上でなければ意味がありません。
使い方は、root で

[root@boinc ~]# ./hoge.sh

です。デフォルトでは、ログの最新 2000 行の中から、'no such user found from' が 50 以上見つかった場合に、その IP アドレスを iptables に登録します。ログの最新 25000 行の中から 128 以上見つかった場合に iptables に登録するには、

[root@boinc ~]# ./hoge.sh 128 25000

とします。新しく iptables に登録した IP アドレスは、メールで通知 (MAILTO) します。
んで、以下ソース。

#!/bin/sh
#
# usage: hoge.sh <threshold> <numlines>

TARGET_LOG='/var/log/proftpd/proftpd.log'
HOGE_DIR='/var/tmp/hoge'
BLACKLIST="${HOGE_DIR}/blacklist"
BLACKLIST_BACKUP="${BLACKLIST}.${YMDHMS}"
IPTABLES_CONF='/etc/sysconfig/iptables'
IPTABLES_CONF_BACKUP="${IPTABLES_CONF}.${YMDHMS}"

TEMP_FILE="${HOGE_DIR}/temp"
IPLIST_FILE="${HOGE_DIR}/iplist"
UNIQ_FILE="${HOGE_DIR}/uniqlist"
LOCK_FILE="${HOGE_DIR}/.lock"
NEW_FILE="${HOGE_DIR}/newlist"
UPDATE_FILE="${HOGE_DIR}/.updated"

MAILTO='hoge@example.com'
REMOVAL_DAYS='7'
THIS=`basename ${0}`
HOSTNAME=`${HOSTNAME}`
YMD=`date +%Y%m%d`
YMDHMS=`date +%Y%m%d%H%M%S`
LOGGING="logger -p local0.info -i -t hoge"

exit_proc() {
	rm -f ${TEMP_FILE} ${IPLIST_FILE} ${UNIQ_FILE} ${LOCK_FILE} ${NEW_FILE}
}

THIS_UID=`id -u`

if [ ${THIS_UID} -ne 0 ]
then
	echo "${THIS}: need root priviledges" >&2
	exit 1
fi

if [ ! -d ${HOGE_DIR} ]
then
	mkdir -p ${HOGE_DIR}
fi

if [ $# -eq 0 ]
then
	THRESHOLD=100
	NUMLINES=2000
elif [ $# -eq 1 ]
then
	THRESHOLD=${1}
	NUMLINES=2000
elif [ $# -eq 2 ]
then
	THRESHOLD=${1}
	NUMLINES=${2}
else
	echo "${THIS}: invalid usage" >&2
	echo "${THIS} [threshold] [numlines]" >&2
	exit 1
fi

# lock
while true
do
	if [ ! -e ${LOCK_FILE} ]
	then
		touch ${LOCK_FILE}
		echo $$ >> ${LOCK_FILE}
		sleep 3
		PID=`cat ${LOCK_FILE}`

		if [ ${PID} -eq $$ ]
		then
			break
		fi
	fi

	sleep 1
done

if [ ! -e ${BLACKLIST} ]
then
	touch ${BLACKLIST}
fi

mv ${BLACKLIST} ${BLACKLIST_BACKUP}

if [ $? -ne 0 ]
then
	echo "failed to backup blacklist" | ${LOGGING}
	rm -f ${BLACKLIST_BACKUP}
	exit_proc
	exit 0
fi

# update blacklist
LINES=`wc -l ${BLACKLIST_BACKUP} | awk '{ print $1 }'`

if [ ${LINES} -ne 0 ]
then
	for LINE in `cat ${BLACKLIST_BACKUP}`
	do
		ADD_YMD=`echo ${LINE} | awk -F: '{ print $2 }'`
		DEL_YMD=`date -d "${REMOVAL_DAYS} days ago" +%Y%m%d`

		if [ ${ADD_YMD} -ge ${DEL_YMD} ]
		then
			echo ${LINE} >> ${BLACKLIST}
		fi
	done
fi

# retrieve attacker's IP addresses
tail -n ${NUMLINES} ${TARGET_LOG} | grep 'no such user found from' > ${TEMP_FILE}

if [ $? -eq 0 ]
then
	awk '{ print $15 }' ${TEMP_FILE} > ${IPLIST_FILE}

	if [ $? -ne 0 ]
	then
		echo "failed to retrieve attacker's IP address" | ${LOGGING}
		rm -f ${BLACKLIST_BACKUP}
		exit_proc
		exit 1
	fi

	if [ ! -s ${IPLIST_FILE} ]
	then
		echo "missing attacker's IP address list...proftpd logging format changed ?" | ${LOGGING}
		rm -f ${BLACKLIST_BACKUP}
		exit_proc
		exit 1
	fi

	sort ${IPLIST_FILE} | uniq > ${UNIQ_FILE}

	if [ $? -ne 0 ]
	then
		echo "failed to sort/uniq attacker's IP address" | ${LOGGING}
		rm -f ${BLACKLIST_BACKUP}
		exit_proc
		exit 1
	fi

	if [ ! -s ${UNIQ_FILE} ]
	then
		echo "missing sorted list" | ${LOGGING}
		rm -f ${BLACKLIST_BACKUP}
		exit_proc
		exit 1
	fi

	# add attacker's IP addresses to the blacklist
	for IPADDR in `cat ${UNIQ_FILE}`
	do
		CNT=`grep ${IPADDR} ${IPLIST_FILE} | wc -l`

		if [ $CNT -lt ${THRESHOLD} ]
		then
			continue
		fi

		grep -F ${IPADDR} ${BLACKLIST} > /dev/null 2>&1

		if [ $? -eq 0 ]
		then
			continue
		fi

		echo "${IPADDR}:${YMD}" >> ${BLACKLIST}
		echo "add ${IPADDR} to ${BLACKLIST}" | ${LOGGING}
		echo "${IPADDR}" >> ${NEW_FILE}
	done
fi

# unlock
rm -f ${LOCK_FILE}

# check updated
if [ ! -e ${NEW_FILE} -a ! -e ${UPDATE_FILE} ]
then
	echo "${THIS}: no update iptables" | ${LOGGING}
	rm -f ${BLACKLIST_BACKUP}
	exit_proc
	exit 0
elif [ ! -e ${NEW_FILE} -a -e ${UPDATE_FILE} ]
then
	grep -F ${HOSTNAME} ${UPDATE_FILE} > /dev/null 2>&1

	if [ $? -eq 0 ]
	then
		echo "${THIS}: update iptables for all hosts done" | ${LOGGING}
		rm -f ${BLACKLIST_BACKUP}
		rm -f ${UPDATE_FILE}
		exit_proc
		exit 0
	fi
fi

# backup iptables conf file
cp -a ${IPTABLES_CONF} ${IPTABLES_CONF_BACKUP}

if [ $? -ne 0 ]
then
	echo "failed to backup iptables conf file" | ${LOGGING}
	rm -f ${BLACKLIST_BACKUP}
	exit_proc
	exit 0
fi

# update iptables conf file
cat << _EOF_ > ${IPTABLES_CONF}
# Firewall configuration written by system-config-securitylevel
# Manual customization of this file is not recommended.
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:RH-Firewall-1-INPUT - [0:0]
-A INPUT -j RH-Firewall-1-INPUT
-A FORWARD -j RH-Firewall-1-INPUT
-A RH-Firewall-1-INPUT -i lo -j ACCEPT
-A RH-Firewall-1-INPUT -p icmp --icmp-type any -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
_EOF_

for LINE in `cat ${BLACKLIST}`
do
	IPADDR=`echo ${LINE} | awk -F: '{ print $1 }'`

	if [ $? -ne 0 ]
	then
		echo "failed to create iptables conf file, restore backup" | ${LOGGING}
		cp -a ${IPTABLES_CONF_BACKUP} ${IPTABLES_CONF}
		rm -f ${BLACKLIST_BACKUP}
		exit_proc
		exit 1
	fi

	echo "-A RH-Firewall-1-INPUT -m state --state NEW -i eth0 -s ${IPADDR} -j DROP" >> ${IPTABLES_CONF}
done

cat << _EOF_ >> ${IPTABLES_CONF}
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 21 -j ACCEPT
-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT
-A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited
COMMIT
_EOF_

if [ -e ${NEW_FILE} ]
then
	echo ${HOSTNAME} > ${UPDATE_FILE}
	mail -s "${HOSTNAME}: iptables blacklist updated" ${MAILTO} < ${NEW_FILE}
fi

service iptables restart
exit_proc
echo "update blacklist successfully." | ${LOGGING}
exit 0

あ、そうそう。iptables のヒアドキュメントの部分は、環境に合わせて変更してくださいね。
こいつを cron で動かせば、多い日も安心 (?) なこと請け合いです。アクセスの多いサーバでは 10 分ごと、そうでもないサーバでは 1 時間ごとくらいでいいでしょう。
最後に、このすくりぷつにはバグがあります。最初にそれを指摘した方には 100 円差し上げますw