Data-Hack SQL隱碼攻擊檢測

wyzsk發表於2020-08-19
作者: 大學生 · 2015/03/12 10:02

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 準備資料


因為這次的資料已經準備好了,所以我們所需要就是寫個小指令碼把他讀取出來,所需要的東西我會進行打包。

下載地址:下載

#!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()

enter image description here

我們現在可以清楚的知道我們面臨的是一堆什麼樣的資料了。

0x03 特徵工程


1. 概念


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,這群變態。

2. 轉化資料


所以現在我們需要的就是將原始資料轉化成特徵,這就是為什麼我剛才說到語法樹的,我們需要對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()

enter image description here

3. Other


理論上我們現在就可以直接把這些東西扔進演算法中,不過為了方便我在說點別的,分類器的效能很大程度上取決於特徵,假設這些無法很好的對資料進行分類,那我們就需要考慮對特徵進行一些別的處理,比如你覺得sql注入的話sql語句貌似都比較長,那麼可以將其轉化成特徵。

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

enter image description here

現在我們需要觀測下資料,看看長度是否有將資料進行分類的能力。

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

enter image description here

0x04 機器學習


1. Train & Test


這裡我就直接呼叫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()

enter image description here

混淆矩陣可以更加直觀的讓我們觀察資料,我們的資料氛圍 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,讓我們自己調整分類的機率。

#!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()

enter image description here

額,繼續用0.7吧。

0x05 結語


這是個系列,可能我這麼說也沒人信吧,中途開始就有點亂了。

上一句老話吧,也不知道誰說的,反正大家天天掛嘴邊。

資料探勘專案的表現,80%取決於特徵工程,剩下的20%才取決於模型等其他部分;又說資料探勘專案表現的上限由特徵工程決定,而其接近上限的程度,則由模型決定。

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

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章