在运维里,出现最普遍的问题就是网站访问慢甚至访问不到,到服务器上查看得出的结论是,这个网站被CC攻击了。CC攻击属于DDos攻击的一种,**者会利用大量“肉鸡”对攻击目标的网站发起请求,并且频率很快,这样会导致目标网站的服务器承受不住而瘫痪。

CC攻击虽然看起来跟正常的访问几乎没有什么区别,但如果我们仔细分析访问日志还是可以找到一些线索的,比如某个IP访问频次很高,或者某几个IP的user_agent是固定的等等特性,有的甚至会去模仿正规的搜索引擎,比如,把自己伪装成百度的“蜘蛛爬虫”。当遇到CC攻击时,只要你肯花费一些精力来分析访问日志,终究是可以找出发起CC攻击的真凶,然后我们只要封掉他们的IP就万事大吉了。本案例的需求为:

1)每分钟分析一次访问日志/data/logs/access_log,日志片段如下:

61.227.224.229 - [19/Sep/2018:09:30:07 + 0800 ] "/misc.php?mod=ranklist&type=menber&view=post" 200 "GET HTTP/1.1" "http://www.wenyou.com/"  "Mozilla/5.0(Windows NT 6.1;WOW64;rv:43.0)Gecko/20100101 Firefox/43.0"
61.227.224.229 - [19/Sep/2018:09:30:07 + 0800 ] "/misc.php?mod=ranklist&type=menber&view=onlinetime" 200 "GET HTTP/1.1" "http://www.wenyou.com/"  "Mozilla/5.0(Windows NT 6.1;WOW64;rv:43.0)Gecko/20100101 Firefox/43.0"

2)把访问量高于100的IP给封掉

3)封过的IP都要记录到一个日志中

4)每隔30分钟检查一次被封的IP,把没有访问量或者访问量很少的IP解封

5)解封的IP记录到另一个日志中


知识点一:awk用法

在shell脚本中,awk出现的概率是极高的,因为它在处理字符串上有很强的能力。先看个小例子:

# awk -F ":" '$3>500 {print $1,$3}' /etc/passwd
polkitd 999
chrony 998
mysql 1002

说明:awk最核心的功能是分段,可以用-F选项指定一个分隔符,然后针对某一字段进行处理,本例中用':'作为分隔符,去找第三段大于500的行,然后把第一段和第三段打印出来。在awk中可以使用>,<,>=,<=,==,!=等逻辑判断的符号,这和shell是有差异的。本例中>后面的500一定不要加双引号,否则它就不以数字作为比较对象了,而是把500当成是字符串,结果自然就不一样了。

如下:

# awk -F ":" '$3>"500" {print $1,$3}' /etc/passwd
shutdown 6
halt 7
mail 8
nobody 99
dbus 81
polkitd 999
postfix 89
sshd 74
chrony 998

awk的功能不止于此,它实际上跟shell一样属于一门脚本语言,可以写脚本。它的作者设计它的初衷是为了去格式化输出文本,它可以满足各种复杂的格式需求,不过我们平时写shell脚本时,仅仅把它作为一个命令来处理字符串。常见用法:

1)截取指定段

# awk -F ':|#' {print $2}' 1.txt

说明:分隔符可以是一个正则表达式,本例中的分隔符可以是:也可以是#。

2)匹配字符后的字符串

# awk -F ':' '$1 ~ "abc"' 1.txt

说明:过滤出第一段包含abc的行,其中这里的abc可以是一个正则表达式,例如:

# awk -F ':' '$1 ~ "^ro+"' 1.txt

说明:^表示开头,+表示+前面的字符至少有1个,所以^ro+可以匹配的字符串有:ro,roo,rooo…

3)多个语句同时使用

# awk -F ':' '$1 ~ "root" {print $1,$3};$3>100 {print $1,$2}' 1.txt

4)多个条件

# awk '$3<100 && $7 ~ "bash" {print $0}' 1.txt

说明:如果不指定分隔符,则以空白字符作为分隔符,在awk中可以用&&表示并且,用||表示或者。$0输出整行。

5)内置变量

# awk -F ':' {print NF,NR}' 1.txt

说明:NF为段数,NR为行数

6)数学运算

# awk -F ':' '$7=$3+$4 {print $0}' 1.txt

因为$7的值被重新定义了,所以打印$0时,并不会包含分隔符,当然也可以定义分隔符:

# awk -F ':' -v OFS='#' '$7=$3+$4 {print $0}' 1.txt

说明:用-v定义变量,这个OFS也是一个内置变量,它表示输出的结果的分隔符。

计算某一段的总和,这种用法也比较常见,例如:

# awk -F ':' '{(tot=tot+$3)}; END {print tot}' 1.txt



知识点二:sort排序

语法:sort [-t 分隔符] [-kn1,n2] [-nru] 这里的n1<n2

-t 分隔符:作用跟cut的-d一个意思

-n:使用纯数字排序

-r:反向排序

-u:去重复

-kn1,n2:由n1区间排序到n2区间,可以只写-kn1,即对n1字段排序


知识点三:uniq去重复

这个命令常用选项只有一个:-c,统计重复的行数,并把行数写在前面。

# uniq 1.txt 
111
222
333
111

使用uniq的前提是先要给文件排序,否则不管用,因此常和sort一起用,如下:

# sort 1.txt |uniq
111
222
333
# sort 1.txt |uniq -c
      2 111
      1 222
      1 333


知识点四:用iptables工具封/解封IP

1)查看filter表的iptables规则

# iptables -nvL -t filter

说明:在centOS7上netfilter有5个表,分别是:filter、nat、mangle、raw、secuirty。我们用得比较多的是前两个,本案例中的封IP就是用filter表。该命令中的-t filter可以省略,默认就是filter表。

2)增加规则

iptables -A INPUT -p tcp --dport 80 -s 1.1.1.1 -j DROP

说明:-A选项是增加一条规则,更准确的说是追加一条规则,因为iptables的规则是分前后的,越靠上的规则就越先生效,也就是说优先级更高。所以,使用-A追加的规则只能排到最后面,但如果想增加一条优先级更高的规则,就用-I选项(插入规则)即可。

也可以不针对端口,只写IP,同时也可以指定目标IP地址,如下:

# iptables -I INPUT -s 3.3.3.3 -d 4.4.4.4 -j DROP

3)删除规则

删除规则有两个办法,一是全部清空,另外一个就是删除一条。

# iptables -t filter -F

删除一条规则,比较麻烦,因为你需要记得当时创建这条规则的命令,把-A换成-D即可,当然也有比较简单的做法,首先列出规则的序号,如下:

# iptables -nvL --line-number

最左边第一列的num就是规则的序号了,每个链的规则都是从1开始,假如要删除INPUT链的第2条规则,只需要执行:

# iptables -D INPUT 2

这样会把INPUT链下的第2条规则删除掉。删除规则后,重新列出规则序号,会发现序号依然是从1开始。


当我们执行iptables -nvL时,列出的结果中第一列pkts表示被iptables规则作用的数据包个数,而第二列的bytes表示被iptables规则作用的数据量大小。这个数字会不断变化,如果重启iptables或firewalld服务这两列数字会清零,当然也可以用一个选项让它清零:

# iptables -Z

也可以指定某个链,如:

# iptables -Z INPUT

以上介绍的知识点主要是针对本案例的,iptables的用法还有很多。


知识点五:shell脚本中的函数

在shell脚本中如果某一段代码出现1次以上,应该把这段代码封装到一个函数里,这样后续调用它会很方便,而且代码看起来也很美观。下面用一个例子来说明函数的好处。

需求:检查系统中是否安装vim-enhanced、expect、wget包,如果没有安装则安装一下,传统的shell代码是这样写的:

if ! rpm -q vim-enhanced &>/dev/null
then
   yum install -y vim-enhanced
fi

if ! rpm -q expect &>/dev/null
then
   yum install -y expect
fi

if ! rpm -q wget &>/dev/null
then
   yum install -y wget
fi

试问,如果检查100个包,难道要写100遍这段代码吗?用函数就简洁多了,如下:

##定义if_install函数
if_install() {
  if ! rpm -q $1 &>/dev/null
    then
       yum install -y $1
  fi
}

##用for循环把所有rpm包逐一检查一遍
for pkg in vim-enhanced expect wget
do
    if_install $pkg
done


知识点六:sed用法

sed和grep、awk频繁出现在shell脚本中,它们三个经常被叫做“正则三剑客”,可见这三个工具和正则表达式之间的关系非同一般。对于sed工具的用法,常见用法:

1)打印指定行

sed -n '10p' 1.txt

说明:10指的是行号,p表示print(打印),加上-n后只显示第10行,否则会把1.txt所有行都显示出来,这里的单引号,也可以是双引号。也可以指定多行:

sed -n '1,5p' 1.txt

说明:打印1到5行,如果是不连续的行,可以这样:

sed -n '1p;5p' 1.txt

sed也有和grep类似的过滤指定行的功能,如下:

sed -n '/abc/p' 1.txt

说明://为固定写法,里面就是要匹配的字符串,可以是正则,例如:

sed -n '/[a-z0-9]/p' 1.txt

在正则表达式中,有几个特殊符号属于扩展正则范畴,它们是+,?,|,(),{}。在grep中要使用它们需加上-E选项或者用egrep命令,而在sed中要使用它们,需加上-r选项。

2)删除指定行

sed '10d' 1.txt

说明:只是在打印1.txt时第10行未打印出来,并没有真正删除文件里的内容,加上-i选项可以对文件里的内容进行更改操作。

3)查找替换

把1.txt中出现的全部aming替换为inux

sed 's/aming/linux/g' 1.txt

说明:s表示替换,g表示全局替换,不加-g则只替换每行中出现的第一个aming。


本案例参考脚本

#!/bin/bash
##把访问量比较大的IP封掉,如果20分钟被封的IP没有请求或者请求很少,需要解封
##作者:
##日期:
##版本:v0.1

#定义1分钟以前的时间,用于过滤1分钟以前的日志
t1=`data -d "-1 min" +%Y:%H:%M`
log=/data/logs/access_log

block_ip(){
#过滤出前一分钟任何秒数的日志,0-5代表59秒的5,+则代表任意的一个0-9。
egrep "$t1:[0-5]+" $log > /tmp/tmp_last_min.log

#把1分钟内访问量高于100的ip记录到个临时文件中
awk '{print $1}' /tmp/tmp_last_min.log |sort -n|uniq -c|sort -n|awk '$1>100 {print $2}' > /tmp/bad_ip.list

#计算ip的数量
n=`wc -l /tmp/bad_ip.list |awk '{print $1}'`

#当ip数大于0时,才会用iptables封掉它
    if [ $n -ne 0 ]
    then
        for ip in `cat /tmp/bad_ip.list`
        do
            iptables -I INPUT -s $ip -j REJECT
        done

        #将这些被封IP记录到日志里
        echo "`date` 封掉的IP有:" >> /tmp/block_ip.log
        cat /tmp/bad_ip.list >> /tmp/block_ip.log
    fi
}

unblock_ip()
{
    #首先将包个数小于5的ip记录到一个临时文件里,把他们标记为白名单IP
    iptables -nvL INPUT |sed '1d' |awk '$1<5 {print $8}' > /tmp/good_ip.list
    n=`wc -l /tmp/good_ip.list |awk '{print $1}'`
    if [ $n -ne 0 ]
      then
        for ip in `cat /tmp/good_ip.list`
        do
            iptables -D INPUT -s $ip -j REJECT
        done

        echo "`date` 解封的IP有:" >> /tmp/unblock_ip.log
        cat /tmp/good_ip.list >> /tmp/unblock_ip.log
    fi

    #当解封完白名单IP后,将计数器清零,进入下一个计数周期
    iptables -Z
}

#取当前时间的分钟数
t=`date +%M`

#当分钟数为00或30时(即每隔30分钟),执行解封IP的函数,其他时间只执行封IP的函数
if [ $t == "00" ] || [ $t == "30" ];then
    unblock_ip
    block_ip
else
    block_ip
fi