ReLuQ
我们经历了admin/123456随便入的年代,经历了sql注入,XSS满天飞的年代,经历了反序列化,RCE批量刷的年代,直至今日,这些安全风险仍然存在并威胁着大量的网站系统。
------写在文前
世上本没有安全,黑的人多了,也就有安全了
------写在文前之二
在安全领域,攻防对抗时永恒不变的话题,从古至今的密码加密与解密,网站的攻击与防御,到如今基于图像活体的认证于欺骗。攻击与防御永远是此消彼长,忙个不停2333
笔者认为对于企业来说,安全和可用是缺一不可同等重要的,而针对网络,网站,信息系统的攻击从未停止过。
本文旨在对探究在攻击检测方面使用机器学习算法的可能性
想要进行这样一项任务,我们需要知道,一个正常访问和一个异常攻击有什么样的区别。如果你是一个网站的运营人员,你可能会在服务器的access.log中发现这样的奇奇怪怪的请求:
第一次见看到你可能会疑惑,这是些啥?
这些就是有人在......盘你
第一条是黑客常用来探测ID参数是否存在sql注入的语句;第二条是黑客在搜索框中插入的一条XSS攻击payload;第三条则是黑客在尝试是否存在目录跳转
那么我们的任务,就是通过机器学习来找出这种令人厌恶的攻击行为
本次使用的数据是脱敏之后的真是网站数据,其中包含5000条正常用户访问产生的数据以及6000条人工/扫描器产生的数据
管说不做没用,先开始吧
老样子,先把要用的包导一导
import os
import sys
import numpy as np
import pandas as pd
import sklearn
import matplotlib.pyplot as plt
from urllib import parse as urlparse
import urllib
import math
from sklearn import preprocessing
from sklearn.linear_model import LogisticRegression
from sklearn.utils import shuffle
from sklearn.svm import SVC
import warnings
warnings.filterwarnings("ignore")
然后,使用pandas读取我们的normal.csv和risk.csv,这两个我那件分别存储了正常请求URL以及攻击请求URL:
normal_data = pd.read_csv("normal.csv")
abnormal_data = pd.read_csv("risk.csv")
我们想一想一个正常请求和一场请求到底有啥区别?
打个比方,一个页面,可能正常情况下,我访问一个新闻页面,他的url是长这个样子的:
http://www.aaa.com/article.php?id=2333
而一条sql注入攻击呢?他可能长这个样子:
http://www.aaa.com/article.php?id=2333 and 200=201 union select 1,2,3,user(),5#--
因为它需要插入一条可以带入数据库查询的语句,那么会不会,他比正常请求看起来,要长一些呢?
我们将他作为一个特征:
def getlen(x):
return len(x)
再想一想,不对呀。可能这个链接本来就是参数比较多或者路径比较复杂导致他比较长呢?这不久误判了吗?好像是这样,因此我们在加一个特征:url中最长参数的长度:
def getfirstlen(x):
parsed_tuple = urlparse.urlparse(urllib.parse.unquote(x))
url_query = urlparse.parse_qs(parsed_tuple.query,True)
url_first_arg_len = 0
if len(url_query) == 0:
url_first_arg_len = 0
elif len(url_query) == 1:
url_first_arg_len = len(url_query[list(url_query.keys())[0]][0])
else:
max_len = 0
for i in url_query.keys():
if len(url_query[i][0]) > max_len:
max_len = len(url_query[i][0])
url_first_arg_len = max_len
return url_first_arg_len
再看一看还会有什么,还是看到前面的攻击场景下的url,是不是看上去乱七八糟的?没错,混乱,一个攻击请求由于具有更多的字符或者想要隐藏自己等目的,一定会变得更加混乱,有很多甚至会进行一些编码转换(如base64,utf-7等等),混乱怎么表示?香浓告诉你:熵
def getshan(x):
url = x
tmp_dict = {}
url_len = len(url)
for i in range(0,url_len):
if url[i] in tmp_dict.keys():
tmp_dict[url[i]] = tmp_dict[url[i]] + 1
else:
tmp_dict[url[i]] = 1
shannon = 0
for i in tmp_dict.keys():
p = float(tmp_dict[i]) / url_len
shannon = shannon - p * math.log(p,2)
return shannon
再想一想,我一个正常的请求,里面可能会有select,union,script,etc/passwd这种玩意吗?不存在的,因此我们可以依照我们的安全经验,来看一看,是否存在一些具有攻击特征的字符:
比如:
sqli类:select and or union information_schema sleep banchmark delay if concat substr ascii updatexml ......
XSS类:script alert prompt onload onerror onmouseover onmousedown ......
RE类:system cmdexec exec eval cmd_shell......
文件读写删包含上传类:unlink fputs fgets file_get_contents ......
def getchar(x):
lower = x
url_ilg_sql = lower.count('select')+lower.count('and')+lower.count('or')+lower.count('insert')+lower.count('update')+lower.count('sleep')+lower.count('benchmark')+\
lower.count('drop')+lower.count('case')+lower.count('when')+lower.count('like')+lower.count('schema')+lower.count('&&')+lower.count('^')+lower.count('*')+lower.count('--')+lower.count('!')+lower.count('null') +\
lower.count('%')+lower.count(' ')
url_ilg_xss = lower.count('script')+lower.count('>')+lower.count('<')+lower.count('')+lower.count('chr')+lower.count('fromcharcode')+lower.count(':url')+\
lower.count('iframe')+lower.count('div')+lower.count('onmousemove')+lower.count('onmouseenter')+lower.count('onmouseover')+lower.count('onload')+lower.count('onclick')+lower.count('onerror')+lower.count('#')+lower.count('expression')+lower.count('eval')
url_ilg_file = lower.count('./')+lower.count('file_get_contents')+lower.count('file_put_contents')+lower.count('load_file')+lower.count('include')+lower.count('require')+lower.count('open')
count = url_ilg_sql + url_ilg_xss + url_ilg_file
return count
OK,暂时就先这么多,当然还有很多其他的特征,比如说,headers头信息,比如说请求频率
对啦,还有标签,监督学习可不能没他:
def getlabel(x):
if x == 0:
return 0
elif x == 1:
return 1
数据处理与特征工程(最下面一行是由于笔者的risk.csv数据中还包含其他几个columns的数据,因此删除掉,其实这些数据都是可以进一步挖掘特征的):
normal_data['len'] = normal_data['url'].map(lambda x:getlen(x)).astype(int)
normal_data['first'] = normal_data['url'].map(lambda x:getfirstlen(x)).astype(int)
normal_data['shan'] = normal_data['url'].map(lambda x:getshan(x)).astype(float)
normal_data['char'] = normal_data['url'].map(lambda x:getchar(x)).astype(int)
normal_data['label'] = normal_data['url'].map(lambda x:getlabel(0)).astype(int)
normal_data = normal_data.drop(['url','len'],axis = 1)
abnormal_data['len'] = abnormal_data['url'].map(lambda x:getlen(x)).astype(int)
abnormal_data['first'] = abnormal_data['url'].map(lambda x:getfirstlen(x)).astype(int)
abnormal_data['shan'] = abnormal_data['url'].map(lambda x:getshan(x)).astype(float)
abnormal_data['char'] = abnormal_data['url'].map(lambda x:getchar(x)).astype(int)
abnormal_data['label'] = abnormal_data['url'].map(lambda x:getlabel(1)).astype(int)
abnormal_data = abnormal_data.drop(['url','len','id','risk_type','request_time','http_status','http_user_agent','host','cookie_uid','source_ip','destination_ip','last_update_time'],axis = 1)
接下来就是常规流程啦,归一化和一些其他的处理
train_data = pd.concat([normal_data,abnormal_data],axis = 0)
train_data.info()
train_data.head()
scaler = preprocessing.StandardScaler()
first_scaler_param = scaler.fit(train_data['first'].values.reshape(-1,1))
train_data['first_scaled'] = scaler.fit_transform(train_data['first'].values.reshape(-1,1),first_scaler_param)
lab = train_data['label']
train_data.drop(['label'],axis = 1,inplace = True)
train_data.insert(0,'label',lab)
划分一下训练集和验证集,然后带入我们的SVM训练,由于特征维度少,因此我们就使用linear作为svm的核函数
train_data = shuffle(train_data)
train = train_data.as_matrix()
y = train[0:8000,0]
x = train[0:8000,1:]
y_t = train[8000:,0]
x_t = train[8000:,1:]
lr = SVC(kernel='linear',C=0.4).fit(x,y)
res = lr.predict(x_t)
最终结果如下:
from sklearn.metrics import accuracy_score
accuracy_score(res, y_t)
OUT:0.8492163009404389
差强人意的结果,因为这只是一个4,5个维度的demo2333,笔者在使用多个更多维度并进行二阶交叉组合特征的情况下使用SVM达到了CV98%+的准确率,有兴趣可以试试哈~
编辑于 17:13
原文链接:https://zhuanlan.zhihu.com/p/58689080