這個系列教程我本來打算的是翻譯,后來過了一下文章發現教學過程不是很友好,所以大體是按他的思路,不過其中做了很多改動,還有個事情就是我假定讀者已經了解基礎的python和SQL注入的知識。還有一個需要注意的是我是寫在ipython notebook中,所以文中的代碼可能需要一點改動才能用。
我覺得這篇文章的簡要的主題就是,給"如何識別sql注入" 提供一種思路,這個思路的本身就是用數據科學的形式來解決問題,其實就是所謂的機器學習。
為了達到我們的目標就需要一個過程:
這個系列主要以python為主,所以下面的是所需的python庫,我不會教你怎么安裝這些東西。
sqlparse (一個用于解析sql語法樹的庫) Scikit-Learn (python機器學習庫) Pandas (用于快速處理一定量的數據) numpy (用于科學計算) matplotlib (用于數據可視化)
因為本文中用的是監督學習,那么我們會注入監督學習所需要的知識,機器學習顧名思義就是讓機器具備學習的能力,假設我們已經有了一個算法能夠進行學習,那么我們該如何教給它知識,假設一個小孩,我們需要讓它知道如何辨認水果,我們就會放兩堆不同的水果,告訴他左邊的是蘋果,右邊的是香蕉。然后等到他學習了這堆狗屎玩意,我們就可以帶著他去看一堆新的水果讓后讓他自己進行辨認了。 換句話說我們這次就是要準備一堆的數據,告訴算法,左邊的是正常的sql請求,右邊的是sql注入的請求,讓后讓他進行學習,最后我們再給他一堆未知的數據進行測試。
你覺得sql語言從輸入數據庫到放回內容都經過了怎樣的處理,sql語言是一種DSL(領域特定語言),比如ruby,c,java,這些可以做任何事,但有一些語言只能做某個領域的事,sql就是這樣一種語言,它只能描述對于數據的操作。但是它在大歸類的時候是被歸類到編程語言里的,就需要經過詞法分析再到語法分析,對于這個過程不了解的同學可以看。 http://zone.wooyun.org/content/17006
因為這次的數據已經準備好了,所以我們所需要就是寫個小腳本把他讀取出來,所需要的東西我會進行打包。
下載地址:下載
#!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()
我們現在可以清楚的知道我們面臨的是一堆什么樣的數據了。
So,然后呢?我們是不是就可以把數據丟進算法里然后得到一個高大上的sql防火墻了?那么我們現在來想一個問題,我們有兩個sql語句,從admin表中查看*的內容。
#!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,這群變態。
所以現在我們需要的就是將原始數據轉化成特征,這就是為什么我剛才說到語法樹的,我們需要對sql語句進行處理,對同一種類型的東西給予同一種標示,現在我們使用sqlparse 模塊建立一個函數來處理sql語句。
#!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語句進行轉換。
#!python
dataframe['parsed_sql'] = dataframe['raw_sql'].map(lambda x:parse_sql(x))
dataframe.head()
理論上我們現在就可以直接把這些東西扔進算法中,不過為了方便我在說點別的,分類器的性能很大程度上取決于特征,假設這些無法很好的對數據進行分類,那我們就需要考慮對特征進行一些別的處理,比如你覺得sql注入的話sql語句貌似都比較長,那么可以將其轉化成特征。
#!python
dataframe['len'] = dataframe['parsed_sql'].map(lambda x:len(x))
dataframe.head()
現在我們需要觀測下數據,看看長度是否有將數據進行分類的能力。
#!python
%matplotlib inline
import matplotlib.pyplot as plt
dataframe.boxplot('len','type')
plt.ylabel('SQL Statement Length')
這里我就直接調用python庫了,因為解釋起來很麻煩,而且就我對于這次要使用的隨機森林(Random Forest)的了解層度,我覺得還不如不講,對于其數學原理有興趣的可以參考下面的paper,是我見過對隨機森林解釋的最清楚的。
Gilles Louppe《隨機森林:從理論到實踐》 http://arxiv.org/pdf/1407.7502v1.pdf
接下來我們再對特征做一次處理,轉換成0和1的向量形式,x是我們的特征數據,y表示結果。
#!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]
輸出
[[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]
輸入
#!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
輸出
[ 0.97699497 0.99928109 0.99928058 1. 1. 0.97192225
0.99928006 0.99856012 1. 1. ]
上面的cross_validation是我們測試分類器的一種方法,原理就是把訓練后的分類器在一些分割后的數據集上測試結果,從得出的多個評分中可以更好的評估性能,我們得出了一個貌似不錯的結果,接下來讓我們訓練分類器
#!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)
如果剛才那些數值無法直觀的看出你訓練了個什么玩意出來,那么你就需要一個混淆矩陣。
#!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()
混淆矩陣可以更加直觀的讓我們觀察數據,我們的數據氛圍 0,1兩類,比如 [0,0]=196 就是legit被正確分類的樣本,[0,1]=3是被錯誤分類的樣本,那么第二行就是惡意樣本分類的情況。
現在我們看起來分類起似乎工作的不錯,達到了99%的正確率,可是你想象這個問題,每199個正確樣本就有3個被錯誤分類,一般來說一個中型的網站需要處理的sql語句就可能會達到 上面的1000倍,就是說你可能會有3000個無害的語句被攔截。所以下面我們需要的是降低legit被錯誤分類的概率。
sklearn大部分的模型有個功能叫predict_proba,就是說預測的概率,predict其實就是內部調用下predict_proba,然后按50%。我們可以裝變一下直接調用predict_proba,讓我們自己調整分類的概率。
#!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
輸出
[[ 197 0]
[ 5 2577]]
legit被錯誤分類的概率降低了,但是0.7只是我們隨意想出來的一個參數,能不能簡單的想個辦法優化一下呢?讓我們簡單定義一個函數f(x),會隨著我們輸入的參數輸出誤分類的概率。
#!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()
額,繼續用0.7吧。
這是個系列,可能我這么說也沒人信吧,中途開始就有點亂了。
上一句老話吧,也不知道誰說的,反正大家天天掛嘴邊。
數據挖掘項目的表現,80%取決于特征工程,剩下的20%才取決于模型等其他部分;又說數據挖掘項目表現的上限由特征工程決定,而其接近上限的程度,則由模型決定。
source:http://nbviewer.ipython.org/github/ClickSecurity/data_hacking/blob/master/sql_injection/sql_injection.ipynb