原由:
某天某项目网站被一些IP恶意DDOS,因为没有卖运营商的流量清洗等等之类的防护服务,导致该项目无法访问
产生了三个处理需求:
需要识别恶意IP进行封堵
需要定位被攻击的页面查找攻击弱点
需要定位攻击频繁的时段进行监控
这三个需求其实都不能实质解决问题
因为IP是封不完的,监控也是只能知道自己有没有被攻击而已,而找到了被攻击的页面也只是找到了本次被攻击的弱点而已
类似的漏洞是数不胜数的,解决办法只有上架流量清洗设备或者从运营商那里买防护服务或者请专门的白帽子团队进行测试
测试数据:
tomcat的access日志格式配置:server.conf
pattern="%{X-FORWARDED-FOR}i %l %u %t %r %s %b %D %q %{User-Agent}i %T"
测试数据是生产某项目的真实access日志做了IP和url的脱敏处理之后的数据
识别恶意IP:
需求1:根据访问IP的访问量生成报表
分析1:如果以空格作为分隔符,那么就是以第1列作为分析的数据列,进行分类汇总排序生成报表,单维度的分析报表
命令1:
cat tomcat8_access.log.2018-05-09.txt|\
awk '{IPs[$1]++}END{for(ip in IPs) print ip,IPs[ip]}'|\
sort -nk2|column -t|tail
解释1:
1. 使用cat命令读取整个文件
2. awk命令分为两部分 {IPs[\$1]++} 和 END{for(ip in IPs) print ip,IPs[ip]}
3. 第一部分 {IPs[\$1]++} 将每一行的\$1,也就是第1列作为下标名,存入名为IPs的数组
IPs数组在创建前并没有定义,因此是一个空的数组
每一行的$1如果是首次存入该数组,存入前该下标元素也是不存在的,为空
这里的 ++ 相当于C语言的 i++ 操作,也就是加1,如果原来为空,则操作后为1,否则为自加1
这就实现了ip为下标的IPs的数组元素的创建和自增计数:
ip第一次出现的时候创建,值为1
如果不是第一次出现,那么则自加
值就是ip出现的次数,那么也就代表访问IP的访问量
4. 第二部分 END{for(ip in IPs) print ip,IPs[ip]}
这里的END表示所有的数据行全部处理完之后再做操作
如果没有END,则表示每一行都进行这两部分的操作,数组自加和打印,处理逻辑就是错误的
所有的数据行全部处理完之后得到的IPs这个数组是一个一维数组,用for循环进行遍历打印
打印每个ip和该ip作为数组下标在IPs中的值,即访问IP的访问量
5. 使用sort命令对第二列进行升序排序 -k2,以数字类型进行排序 -n
6. 使用column命令格式化输出,使用tail命令取出尾部几行进行验证
定位被攻击的页面:
可以简单的以页面的访问量生成报表:
cat tomcat8_access.log.2018-05-09.txt|\
awk '{URLs[$7]++}END{for(url in URLs) print url,URLs[url]}'|\
sort -nk2|column -t|tail
但是这是不准确的,有些页面如首页是每次正常访问都会被点击的,因此应该是根据IP和URL两个维度生成报表
需求2:根据访问IP的访问量和被访问的页面生成报表
分析2:如果以空格作为分隔符,那么就是第1列和第7列作为分析的数据列,生成双维度的分析报表
命令2_1:
cat tomcat8_access.log.2018-05-09.txt|\
awk '{IPs[$1]++;IPsURLs[$1":"$7]++}
END{for(ip in IPs)
{for(ipurl in IPsURLs)
{
split(ipurl,INFO,":")
if(INFO[1]==ip)
printf("IP:%s\n\t总访问量:%d\n\tIP访问的相应URL:%s\n\t该URL访问量:%d\n\tINFO中的IP:%s\n\tINFO中的URL:%s\n",ip,IPs[ip],ipurl,IPsURLs[ipurl],INFO[1],INFO[2])
}
}
}'|head -18
命令2_2,上一个命令基础上优化展示信息:
cat tomcat8_access.log.2018-05-09.txt|\
awk '{IPs[$1]++;IPsURLs[$1":"$7]++}
END{for(ip in IPs)
{for(ipurl in IPsURLs)
{
split(ipurl,INFO,":")
if(INFO[1]==ip)
printf("IP:%s 总访问量:%d 访问的URL:%s 访问量:%d\n",ip,IPs[ip],INFO[2],IPsURLs[ipurl])
}
}
}'|sort -nk2 -nrk4|column -t|tail -50
解释2_1:
根据解释1,不难理解awk分为两个部分
第一部分将$1存入IPs数组,将$1和$7用冒号拼接成一列,存入IPsURLs数组
第二部分使用for循环遍历IPs数组,遍历IPs的每一个ip的时候
再次使用for循环遍历IPsURLs数组,遍历IPsURLs的每一个ipurl的时候
将该ipurl做切分,存入INFO数组,下标从1开始的一个临时数组
此时我们就拥有了这些数据:
IPs中的ip,IPs中的访问量IPs[ip]
INFO中的ip,INFO中的url,和该ip对应的url的访问量IPsURLs[ipurl]
因为这是两个for循环的嵌套遍历,因此当IPs中的ip和INFO中的ip相等时就是所需数据
其他情况则是嵌套循环遍历生成的冗余数据,这个地方类似于笛卡尔积和等值连接的区别
解释2_2:
我们换个角度去解释这个awk的报表:
这里有两张关系表:
IPs(ip,pv)
列ip是主键,列pv代表点击量,其值就是IPs[ip]
IPsURLs('ip:url',pv)
列'ip:url'可以看作是两字段的组合主键
那么ip相当于外键将两表关联成了一对多关系
列pv依然代表点击量,但是代表的是某一个ip对某一个url的点击量
其值就是IPsURLs['ip:url']
这就是awk的第一部分做的两个数据的生成操作
第二部分操作,首先要对IPsURLs进行处理
因为列'ip:url'实际上是两列做的字符串拼接出来的一列
因此使用awk的内建函数split,指定分隔符为冒号,将分割出的俩列存入INFO中
INFO是下标从1开始一个列表,相当于将每一个ipurl临时生成了一个INFO表:INFO(ip,url)
此时我们就拥有了IPs表的ip和该ip的点击量
INFO表的ip和url以及这俩维度相关联的点击量IPsURLs['ip:url']
那么INFO表的ip和IPs表的ip相等时就是所需要的数据
将第二部分写成SQL:
with info as(
select regexp_substr('ip:url','[^:]+',1,1) as split_ip,
regexp_substr('ip:url','[^:]+',1,2) as split_url,
pv as split_pv
from IPsURLs)
select ip,pv,split_url,split_pv
from info,IPs
where info.split_ip=IPs.ip;
-- 这个是在oracle执行的sql,对mysql的高级sql没有爱
-- 这个sql只是凭经验写的一个解释性的sql,没有经过数据验证
通过这个解释,我们看出,实际上是用awk做了关系数据的生成和拆解查询
所有的所谓报表基本上都是对数据集合进行的操作
只不过是过程化处理+集合处理相结合的方式进行处理
这也是为啥BI职位需要C或者Python等语言的编码能力和SQL能力
看到这里,玩过hive的筒子们就能想到了,这不就是hive对此类需求的处理原理么!
平面文件-->格式转换-->导入hive库-->查询分析-->展示
定位攻击频繁的时段:
实际上满足需求只需要根据IP和小时时刻两个维度进行生成报表即可
上面两个需求我们分析了单维度和双维度的报表,这个需求我们做一下三维度的报表
根据IP、小时时刻和url生成报表,单纯的使用空格分隔符是无法直接切出来这三个信息的
需要使用空格和冒号两个分隔符来切分数据:
head tomcat8_access.log.2018-05-09.txt |awk -F' |:' '{print $1,$5,$10}'
使用BEGIN设置分隔符,替代-F参数,显得更高大上一点
然后尽量以格式化的方法写命令:
head tomcat8_access.log.2018-05-09.txt |\
awk 'BEGIN{FS=" |:"}
{c1s[$1]++;c5s[$5]++;c10s[$10]++;
c1_5s[$1":"$5]++;c1_10s[$1":"$10]++;c5_10s[$5":"$10]++
c1_5_10s[$1":"$5":"$10]}
END{for(c1 in c1s) {print c1,c1s[c1]}
{print "\n\n"}
for(c5 in c5s) {print c5,c5s[c5]}
{print "\n\n"}
for(c10 in c10s) {print c10,c10s[c10]}
{print "\n\n"}
for(c1_5 in c1_5s) {print c1_5,c1_5s[c1_5]}
{print "\n\n"}
for(c1_10 in c1_10s) {print c1_10,c1_10s[c1_10]}
{print "\n\n"}
for(c5_10 in c5_10s) {print c5_10,c5_10s[c5_10]}
{print "\n\n"}
for(c1_5_10 in c1_5_10s) {print c1_5_10,c1_5_10s[c1_5_10]}
{print "\n\n"}
}'
head tomcat8_access.log.2018-05-09.txt |\
awk 'BEGIN{FS=" |:"}
{c1s[$1]++;c5s[$5]++;c10s[$10]++;
c1_5s[$1":"$5]++;c1_10s[$1":"$10]++;c5_10s[$5":"$10]++
c1_5_10s[$1":"$5":"$10]}
END{for(c1_5 in c1_5s)
{split(c1_5,L1_5,":")
printf("Col1: %s Col5: %s\n",L1_5[1],L1_5[2])}
{print "\n\n"}
for(c1_10 in c1_10s)
{split(c1_10,L1_10,":")
printf("Col1: %s Col10: %s\n",L1_10[1],L1_10[2])}
{print "\n\n"}
for(c5_10 in c5_10s)
{split(c5_10,L5_10,":")
printf("Col1: %s Col5: %s\n",L5_10[1],L5_10[2])}
{print "\n\n"}
for(c1_5_10 in c1_5_10s)
{split(c1_5_10,L1_5_10,":")
printf("Col1: %s Col5: %s Col10: %s\n",L1_5_10[1],L1_5_10[2],L1_5_10[3])}
{print "\n\n"}
}'
剩下的就是根据维度展示的需求,for循环多层嵌套,if做等值判断之后的打印操作了
公式化套用:
把常用的单维度和双维度报表抽成公式,以便以后直接套用:
cat ...|\
awk 'BEGIN{FS="..."}
{Colns[$n]++}
END{for(Coln in Colns) print Coln,Colns[Coln]}'|\
sort -nk2|column -t
cat ...|\
awk 'BEGIN{FS="..."}
{Colxs[$x]++;Colxys[$x":"$y]++}
END{for(Colx in Colxs)
{for(Colxy in Colxys)
{
split(Colxy,TEMP,":")
if(TEMP[1]==Colx)
print Colx,Colxs[Colx],Colxys[Colxy],TEMP[1],TEMP[2]
}
}
}'|sort -nk2 -nrk4|column -t
[TOC]