<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/tips/5118

    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

    97av