Data-Hack SQL注入检测--用机器学习来做数据分析,从而检测SQL

0x00 前言


这篇文章的简要的主题就是,给"如何识别sql注入" 提供一种思路,这个思路的本身就是用数据科学的形式来解决问题,其实就是所谓的机器学习

为了达到我们的目标就需要一个过程:

  • 收集数据
  • 思考数据
  • 特征工程
  • 机器学习

0x01 准备


1. tools


这个系列主要以python为主,所以下面的是所需的python库,我不会教你怎么安装这些东西。

sqlparse (一个用于解析sql语法树的库) Scikit-Learn (python机器学习库) Pandas (用于快速处理一定量的数据) numpy (用于科学计算) matplotlib (用于数据可视化)

2. 什么是机器学习?


因为本文中用的是监督学习,那么我们会注入监督学习所需要的知识,机器学习顾名思义就是让机器具备学习的能力,假设我们已经有了一个算法能够进行学 习,那么我们该如何教给它知识,假设一个小孩,我们需要让它知道如何辨认水果,我们就会放两堆不同的水果,告诉他左边的是苹果,右边的是香蕉。然后等到他 学习了这玩意,我们就可以带着他去看一堆新的水果让后让他自己进行辨认了。 换句话说我们这次就是要准备一堆的数据,告诉算法,左边的是正常的sql请求,右边的是sql注入的请求,让后让他进行学习,最后我们再给他一堆未知的数 据进行测试。

3. SQL语法树


你觉得sql语言从输入数据库到放回内容都经过了怎样的处理,sql语言是一种DSL(领域特定语言),比如ruby,c,java,这些可以做任 何事,但有一些语言只能做某个领域的事,sql就是这样一种语言,它只能描述对于数据的操作。但是它在大归类的时候是被归类到编程语言里的,就需要经过词 法分析再到语法分析,对于这个过程不了解的同学可以看。 http://zone.wooyun.org/content/17006

0x02 准备数据


因为这次的数据已经准备好了,所以我们所需要就是写个小脚本把他读取出来,所需要的东西我会进行打包。

下载地址:下载

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import pandas as pd
  4. basedir = '/Users/slay/project/python/datahack/data_hacking/sql_injection/data'
  5. filelist = os.listdir(basedir)
  6. df_list = []
  7. # 循环读取 basedir下面的内容,文件名为 'legit'的是合法内容,malicious的是 恶意sql语句
  8. for file in filelist:
  9. df = pd.read_csv(os.path.join(basedir,file), sep='|||', names=['raw_sql'], header=None)
  10. df['type'] = 'legit' if file.split('.')[0] == 'legit' else 'malicious'
  11. df_list.append(df)
  12. # 将内容放入 dataframe对象
  13. dataframe = pd.concat(df_list, ignore_index=True)
  14. dataframe.dropna(inplace=True)
  15. # 统计内容
  16. print dataframe['type'].value_counts()
  17. # 查看前五个
  18. dataframe.head()


我们现在可以清楚的知道我们面临的是一堆什么样的数据了。

0x03 特征工程


1. 概念


So,然后呢?我们是不是就可以把数据丢进算法里然后得到一个高大上的sql防火墙了?那么我们现在来想一个问题,我们有两个sql语句,从admin表中查看*的内容。

  1. select user from admin;
  2. select hello from admin;

算法最后得到的输入是什么,是[1,1,0,1,1] 和 [1,0,1,1,1] 没看懂没关系,就是说得到了这样的东西。

  1. {select:1, user:1, hello:0, from:1, admin:1} {select:1, user:0, hello:1, from:1, admin:1}

是不是哪里不对,就是说在机器看来 user 和 hello 在本质来看是属于不同的类型的玩意,但是对于了解sql语言本身的你知道他们是一样的东西,所以我们就需要给同一种东西打一个标签让机器能够知道。

那么是否对什么是特征工程有了一些模糊的了解?要做好特征工程,就需要对于你所面临的问题有着深刻的了解,就是“领域知识”,带入这个问题就像你对 于sql语言的了解,在这个了解的基础上去处理特征,让算法更能将其分类。带入水果分类问题就是,你得告诉小孩,香蕉是长长的,黄色的,苹果是红色的,圆 圆的,当然,如果你直接把上面的玩意丢进算法里头,分类器也是可以工作的,准确度大概能过 70%,也许你看起来还行,当是我只能告诉你这是个灾难。这让我想起某次数据挖掘的竞赛,第一名和第一千名的分差是0.01,这群变态。

2. 转化数据


所以现在我们需要的就是将原始数据转化成特征,这就是为什么我刚才说到语法树的,我们需要对sql语句进行处理,对同一种类型的东西给予同一种标示,现在我们使用sqlparse 模块建立一个函数来处理sql语句。

  1. import sqlparse
  2. import string
  3. def parse_sql(raw_sql):
  4. parsed_sql = []
  5. sql = sqlparse.parse(unicode(raw_sql,'utf-8'))
  6. for parse in sql:
  7. for token in parse.tokens:
  8. if token._get_repr_name() != 'Whitespace':
  9. parsed_sql.append(token._get_repr_name())
  10. return parsed_sql
  11. sql_one = parse_sql("select 2 from admin")
  12. sql_two = parse_sql("INSERT INTO Persons VALUES ('Gates', 'Bill', 'Xuanwumen 10', 'Beijing')")
  13. print "sql one :%s"%(sql_one)
  14. print "sql two :%s"%(sql_two)

输出 sql one :['DML', 'Integer', 'Keyword', 'Keyword'] sql two :['DML', 'Keyword', 'Identifier', 'Keyword', 'Parenthesis']

我们可以看到 select 和 insert都被认定为 dml,那么现在我们要做的就是观测数据,就是查看特征是否拥有将数据分类的能力,现在我们先对sql语句进行转换。

  1. dataframe['parsed_sql'] = dataframe['raw_sql'].map(lambda x:parse_sql(x))
  2. dataframe.head()


3. Other


理论上我们现在就可以直接把这些东西扔进算法中,不过为了方便我在说点别的,分类器的性能很大程度上取决于特征,假设这些无法很好的对数据进行分类,那我们就需要考虑对特征进行一些别的处理,比如你觉得sql注入的话sql语句貌似都比较长,那么可以将其转化成特征。

  1. dataframe['len'] = dataframe['parsed_sql'].map(lambda x:len(x))
  2. dataframe.head()

现在我们需要观测下数据,看看长度是否有将数据进行分类的能力。

  1. %matplotlib inline
  2. import matplotlib.pyplot as plt
  3. dataframe.boxplot('len','type')
  4. plt.ylabel('SQL Statement Length')


0x04 机器学习


1. Train & Test


这里我就直接调用python库了,因为解释起来很麻烦,而且就我对于这次要使用的随机森林(Random Forest)的了解层度,我觉得还不如不讲,对于其数学原理有兴趣的可以参考下面的paper,是我见过对随机森林解释的最清楚的。

Gilles Louppe《随机森林:从理论到实践》 http://arxiv.org/pdf/1407.7502v1.pdf

接下来我们再对特征做一次处理,转换成0和1的向量形式,x是我们的特征数据,y表示结果。

  1. import numpy as np
  2. from sklearn.preprocessing import LabelEncoder
  3. from sklearn.feature_extraction.text import CountVectorizer
  4. import string
  5. vectorizer = CountVectorizer(min_df=1)
  6. le = LabelEncoder()
  7. X = vectorizer.fit_transform(dataframe['parsed_sql'].map(lambda x:string.join(x,' ')))
  8. x_len = dataframe.as_matrix(['len']).reshape(X.shape[0],1)
  9. x = X.toarray()
  10. y = le.fit_transform(dataframe['type'].tolist())
  11. print x[:100]
  12. print y[:100]

输出

[[0 0 0 ..., 2 0 0]
 [0 0 0 ..., 1 0 0]
 [0 0 0 ..., 0 0 0]
 ..., 
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]
 [0 0 0 ..., 0 0 0]]
[1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

输入

  1. clf = sklearn.ensemble.RandomForestClassifier(n_estimators=30)
  2. scores = sklearn.cross_validation.cross_val_score(clf, x, y, cv=10, n_jobs=4)
  3. print scores

输出

[ 0.97699497  0.99928109  0.99928058  1.          1.          0.97192225
  0.99928006  0.99856012  1.          1.        ]

上面的cross_validation是我们测试分类器的一种方法,原理就是把训练后的分类器在一些分割后的数据集上测试结果,从得出的多个评分中可以更好的评估性能,我们得出了一个貌似不错的结果,接下来让我们训练分类器

  1. from sklearn.cross_validation import train_test_split
  2. # 将数据分割为 训练数据 和 测试数据,训练数据用于训练模型,测试数据用于测试分类器性能。
  3. X_train, X_test, y_train, y_test, index_train, index_test = train_test_split(x, y, dataframe.index, test_size=0.2)
  4. # 开始训练
  5. clf.fit(X_train, y_train)
  6. # 预测
  7. X_pred = clf.predict(X_test)

如果刚才那些数值无法直观的看出你训练了个什么玩意出来,那么你就需要一个混淆矩阵。

  1. %matplotlib inline
  2. import matplotlib.pyplot as plt
  3. from sklearn.metrics import confusion_matrix
  4. cm = confusion_matrix(X_pred,y_test)
  5. print cm
  6. # Show confusion matrix in a separate window
  7. plt.matshow(cm)
  8. plt.title('Confusion matrix')
  9. plt.colorbar()
  10. plt.ylabel('True label')
  11. plt.xlabel('Predicted label')
  12. plt.show()


混淆矩阵可以更加直观的让我们观察数据,我们的数据氛围 0,1两类,比如 [0,0]=196 就是legit被正确分类的样本,[0,1]=3是被错误分类的样本,那么第二行就是恶意样本分类的情况。

现在我们看起来分类起似乎工作的不错,达到了99%的正确率,可是你想象这个问题,每199个正确样本就有3个被错误分类,一般来说一个中型的网站 需要处理的sql语句就可能会达到 上面的1000倍,就是说你可能会有3000个无害的语句被拦截。所以下面我们需要的是降低legit被错误分类的概率。

2. 调整


sklearn大部分的模型有个功能叫predict_proba,就是说预测的概率,predict其实就是内部调用下predict_proba,然后按50%。我们可以装变一下直接调用predict_proba,让我们自己调整分类的概率。

  1. loss = np.zeros(2)
  2. y_probs = clf.predict_proba(X_test)[:,1]
  3. thres = 0.7 # 用0.7的几率来分类
  4. y_pro = np.zeros(y_probs.shape)
  5. y_pro[y_probs>thres]=1.
  6. cm = confusion_matrix(y_test, y_pro)
  7. print cm

输出

[[ 197    0]
 [   5 2577]]

legit被错误分类的概率降低了,但是0.7只是我们随意想出来的一个参数,能不能简单的想个办法优化一下呢?让我们简单定义一个函数f(x),会随着我们输入的参数输出误分类的概率。

  1. def f(s_x):
  2. loss = np.zeros(2)
  3. y_probs = clf.predict_proba(X_test)[:,1]
  4. thres = s_x # This can be set to whatever you'd like
  5. y_pro = np.zeros(y_probs.shape)
  6. y_pro[y_probs>thres]=1.
  7. cm = confusion_matrix(y_test, y_pro)
  8. counts = sum(cm)
  9. count = sum(counts)
  10. if counts[0]>0:
  11. loss[0]=float(cm[0,1])/count
  12. else:
  13. loss[0]=0.01
  14. if counts[1]>0:
  15. loss[1]=float(cm[1,0])/count
  16. else:
  17. loss[1]=0.01
  18. return loss
  19. # 0.1 到 0.9 之前的 100个数值
  20. x = np.linspace(0.1,0.9,100)
  21. # x输入f(x)之后得到的结果
  22. y = np.array([f(i) for i in x])
  23. # 可视化
  24. plt.plot(x,y)
  25. plt.show()

额,继续用0.7吧。

0x05 结语


数据挖掘项目的表现,80%取决于特征工程,剩下的20%才取决于模型等其他部分;又说数据挖掘项目表现的上限由特征工程决定,而其接近上限的程度,则由模型决定。

  1. source:http://nbviewer.ipython.org/github/ClickSecurity/data_hacking/blob/master/sql_injection/sql_injection.ipynb