# Data-Hack SQL注入检测

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-03-12T02:02:00Z
Canonical: https://wepostx.com/topics/743

> 乌云历史资料归档
>
> **原始作者：** 大学生
> **原始编号：** superkieran-wooyundrops:499
> **原始发布时间：** 2015-03-12 10:02
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 前言

这个系列教程我本来打算的是翻译，后来过了一下文章发现教学过程不是很友好，所以大体是按他的思路，不过其中做了很多改动，还有个事情就是我假定读者已经了解基础的python和SQL注入的知识。还有一个需要注意的是我是写在ipython notebook中，所以文中的代码可能需要一点改动才能用。
我觉得这篇文章的简要的主题就是，给"如何识别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 准备数据

因为这次的数据已经准备好了，所以我们所需要就是写个小脚本把他读取出来，所需要的东西我会进行打包。
下载地址:[下载](http://drops.wooyun.org/wp-content/uploads/2015/03/data.zip)

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

![原文图片](/media/2016/06/d001d7570b8fb218b9c57ce13d7bb15f)

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

# 0x03 特征工程

## 1. 概念

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

```text
#!sql select user from admin; select hello from admin;
```
算法最后得到的输入是什么，是[1,1,0,1,1] 和 [1,0,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语句。

```text
#!python import sqlparse import string def parse_sql(raw_sql): parsed_sql = [] sql = sqlparse.parse(unicode(raw_sql,'utf-8')) for parse in sql: for token in parse.tokens: if token._get_repr_name() != 'Whitespace': parsed_sql.append(token._get_repr_name()) return parsed_sql sql_one = parse_sql("select 2 from admin") sql_two = parse_sql("INSERT INTO Persons VALUES ('Gates', 'Bill', 'Xuanwumen 10', 'Beijing')") print "sql one :%s"%(sql_one) print "sql two :%s"%(sql_two)
```
输出 sql one :['DML', 'Integer', 'Keyword', 'Keyword'] sql two :['DML', 'Keyword', 'Identifier', 'Keyword', 'Parenthesis']
我们可以看到 select 和 insert都被认定为 dml，那么现在我们要做的就是观测数据，就是查看特征是否拥有将数据分类的能力,现在我们先对sql语句进行转换。

```text
#!python dataframe['parsed_sql'] = dataframe['raw_sql'].map(lambda x:parse_sql(x)) dataframe.head()
```

![原文图片](/media/2016/06/0ebad285323119b89c0c13beb982a3da)

## 3. Other

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

```text
#!python dataframe['len'] = dataframe['parsed_sql'].map(lambda x:len(x)) dataframe.head()
```

![原文图片](/media/2016/06/15d66aca67a135e80177277b75fcea6a)

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

```text
#!python %matplotlib inline import matplotlib.pyplot as plt dataframe.boxplot('len','type') plt.ylabel('SQL Statement Length')
```

![原文图片](/media/2016/06/7cc3559f96ef4cb4edf03efa7ea24ba5)

# 0x04 机器学习

## 1. Train & Test

这里我就直接调用python库了，因为解释起来很麻烦，而且就我对于这次要使用的随机森林(Random Forest)的了解层度，我觉得还不如不讲，对于其数学原理有兴趣的可以参考下面的paper，是我见过对随机森林解释的最清楚的。
Gilles Louppe《随机森林：从理论到实践》 http://arxiv.org/pdf/1407.7502v1.pdf
接下来我们再对特征做一次处理，转换成0和1的向量形式,x是我们的特征数据，y表示结果。

```text
#!python import numpy as np from sklearn.preprocessing import LabelEncoder from sklearn.feature_extraction.text import CountVectorizer import string vectorizer = CountVectorizer(min_df=1) le = LabelEncoder() X = vectorizer.fit_transform(dataframe['parsed_sql'].map(lambda x:string.join(x,' '))) x_len = dataframe.as_matrix(['len']).reshape(X.shape[0],1) x = X.toarray() y = le.fit_transform(dataframe['type'].tolist()) print x[:100] print y[:100]
```
输出

```text
[[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]
```
输入

```text
#!python import sklearn.ensemble clf = sklearn.ensemble.RandomForestClassifier(n_estimators=30) scores = sklearn.cross_validation.cross_val_score(clf, x, y, cv=10, n_jobs=4) print scores
```
输出

```text
[ 0.97699497 0.99928109 0.99928058 1. 1. 0.97192225 0.99928006 0.99856012 1. 1. ]
```
上面的cross_validation是我们测试分类器的一种方法，原理就是把训练后的分类器在一些分割后的数据集上测试结果，从得出的多个评分中可以更好的评估性能，我们得出了一个貌似不错的结果，接下来让我们训练分类器

```text
#!python from sklearn.cross_validation import train_test_split # 将数据分割为 训练数据 和 测试数据，训练数据用于训练模型，测试数据用于测试分类器性能。 X_train, X_test, y_train, y_test, index_train, index_test = train_test_split(x, y, dataframe.index, test_size=0.2) # 开始训练 clf.fit(X_train, y_train) # 预测 X_pred = clf.predict(X_test)
```
如果刚才那些数值无法直观的看出你训练了个什么玩意出来，那么你就需要一个混淆矩阵。

```text
#!python %matplotlib inline import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix cm = confusion_matrix(X_pred,y_test) print cm # Show confusion matrix in a separate window plt.matshow(cm) plt.title('Confusion matrix') plt.colorbar() plt.ylabel('True label') plt.xlabel('Predicted label') plt.show()
```

![原文图片](/media/2016/06/f914dabeadab5b8aed95496e2693a0b1)

混淆矩阵可以更加直观的让我们观察数据，我们的数据氛围 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，让我们自己调整分类的概率。

```text
#!python loss = np.zeros(2) y_probs = clf.predict_proba(X_test)[:,1] thres = 0.7 # 用0.7的几率来分类 y_pro = np.zeros(y_probs.shape) y_pro[y_probs>thres]=1. cm = confusion_matrix(y_test, y_pro) print cm
```
输出

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

```text
#!python def f(s_x): loss = np.zeros(2) y_probs = clf.predict_proba(X_test)[:,1] thres = s_x # This can be set to whatever you'd like y_pro = np.zeros(y_probs.shape) y_pro[y_probs>thres]=1. cm = confusion_matrix(y_test, y_pro) counts = sum(cm) count = sum(counts) if counts[0]>0: loss[0]=float(cm[0,1])/count else: loss[0]=0.01 if counts[1]>0: loss[1]=float(cm[1,0])/count else: loss[1]=0.01 return loss # 0.1 到 0.9 之前的 100个数值 x = np.linspace(0.1,0.9,100) # x输入f(x)之后得到的结果 y = np.array([f(i) for i in x]) # 可视化 plt.plot(x,y) plt.show()
```

![原文图片](/media/2016/06/91c64730b6ab6193f86cc185aa464fa0)

额，继续用0.7吧。

# 0x05 结语

这是个系列，可能我这么说也没人信吧，中途开始就有点乱了。
上一句老话吧，也不知道谁说的，反正大家天天挂嘴边。
数据挖掘项目的表现，80%取决于特征工程，剩下的20%才取决于模型等其他部分；又说数据挖掘项目表现的上限由特征工程决定，而其接近上限的程度，则由模型决定。
source:http://nbviewer.ipython.org/github/ClickSecurity/data_hacking/blob/master/sql_injection/sql_injection.ipynb

## Replies
