這個例子顯示出使用注釋符號能夠用來判斷請求語句是否被順利地結束了,如果添加了注釋符號且沒有產生錯誤,這就意味著注釋符號前的語句已經順利地被結束。如果出現了錯誤,這就需要攻擊者進行更多的請求嘗試。
3.2 判斷數據庫類型
攻擊者一旦確定了正確的注入句法后,就會開始利用注入去判斷后臺數據庫的類型,這個步驟比確定注入句法要簡單得多。攻擊者一般會使用以下幾種技巧,這些技巧是基于不同類型數據庫引擎在具體實現上的差異。下面只介紹如何區分Oracle和MS SQL Server:
最簡單的辦法,就是前面提到的利用字符串的連結符號,在注入句法已經確定的情況下,攻擊者可以對WHERE語句自由地添加額外的表達式,那么就可以利用字符串的比較來區分數據庫,例如:
AND 'xxx' = 'x' + 'xx' (或者 AND %27xxx%27+%3D+%27x%27+%2B+%27xx%27)
通過將+替換成||,就可以判斷出是數據庫是Oracle還是MS SQL Server,或者是其他類型。
其他的辦法是利用分號字符(即;),在SQL中,分號是用來將幾個SQL語句連接在同一行中。在注入時,也可以在注入代碼中使用分號,但Oracle驅動程序卻不允許這樣使用分號。假設在前面使用注釋符號時沒有出現錯誤,那么在注釋符號前加上分號對MS SQL Server是沒有影響的,但如果是Oracle就會產生錯誤。另外,還可以使用COMMIT語句來確認是否允許在分號后再執行其他語句(例如,注入語句xxx' ; COMMIT --),如果沒有出現錯誤就可以認為允許多句執行。
最后,表達式還可以被替換成能返回正確值的系統函數,由于不同類型的數據庫使用的系統函數也是不同的,因此也可以通過使用系統函數來確定數據庫類型,比如2.3節提到的MS SQL Server的日期函數getdate()與Oracle的sysdate.
3.3 構造注入利用代碼
當所有相關的信息都已獲得后,攻擊者就可以開始進行注入利用,而且在構造注入利用代碼過程中也不再需要詳細的錯誤信息,構造利用代碼本身可以參考其他描述標準SQL注入攻擊的文檔。
由于對于普通的SQL注入利用,已經有很多其他論文進行了詳細的討論,故本文只會在下一節介紹一種UNION SELECT注入。
4 UNION SELECT注入
盡管通過篡改SELECT…WHERE語句來注入對于很多應用程序非常有效,但在盲注情況下,攻擊者仍然愿意使用UNION SELECT語句,這是因為與WHERE語句所進行的操作不同,使用UNION SELECT可以讓攻擊者在沒有錯誤信息的情況下依然能訪問數據庫中所有表。
進行UNION SELECT注入需要預先獲知數據庫的表中的字段個數和類型,而這些信息一般被認為在沒有詳細錯誤信息的提示下是不可能獲得的,但本文下面就將給出解決該問題的方法。
另外需要注意的是,進行UNION SELECT的前提是攻擊者已經確定了正確的注入句法,本文的前面一節已經闡明了這在盲注條件下是可以實現的,而且在使用UNION SELECT語句之前,SQL語句中所有的插入語符號都應該已經完成配對,從而可以自由地使用UNION或者其它指令進行注入。UNION SELECT還要求當前語句和最初的語句查詢的信息必須具有相同的數和相同的數據類型,不然就會出錯。
4.1 統計列數
當錯誤信息沒有被屏蔽時,要獲取列數只需要在進行UNION SELECT注入時每次嘗試使用不同的字段數即可,當錯誤信息由“列數不匹配”變成“列的類型不匹配”時,當前嘗試的列數就是正確的。但在盲注條件下,由于我們對無法獲悉錯誤信息究竟是哪個,所以該方法也就失去了作用。
新的辦法是利用ORDER BY語句,在SELECT語句最后加上ORDER BY能夠改變返回的記錄集的次序,一般是按一個指定的列名的值進行排序。例如,當通過產品號查詢產品時,一個有效的注入語句如下:
SELECT ProdNum FROM Products WHERE (ProdID=1234) ORDER BY ProdNum --
AND ProdName=’Computer’) AND UserName=’john’
人們往往會忽略的是ORDER BY語句后還可以使用數字指代列名,在上例中如果ProdNum是查詢請求返回的記錄中的第一列,則注入1234) ORDER BY 1--返回的結果是一樣的。由于上例查詢請求只返回一個字段,注入1234) ORDER BY 2 --就會出錯,即返回的記錄無法按指定的第二個字段排序。這樣,ORDER BY就可以被利用來對列數進行統計了。由于每個SELECT語句都至少返回一個字段,故攻擊者可以先在注入句法中添加ORDER BY 1來確定語句是否能被正確執行,有時對字段的排序也可能會產生錯誤,這時添加關鍵字ASC或DESC可以解決該問題。一旦確定ORDER BY句法是有效的,攻擊者就會對排序列號從列1到列100進行遍歷(或者到列1000,直到列號被確定為無效),理論上當出現第一個錯誤時,前一個列號就是要統計的列數,但在實際情況中,有些字段可能不允許排序,那么在出現第一次錯誤時可以再多嘗試一到兩個數字,以確認列號已遍歷完。
4.2 判斷列的數據類型
在統計完列數后,攻擊者需要再判斷列的數據類型,在盲注情況下判斷類型也是有技巧的,由于UNION SELECT要求前后查詢語句查詢的字段類型相同,故如果字段數有限,可以簡單地利用UNION SELECT語句對字段類型進行暴力窮舉(brute force),但如果字段數較多,判斷就會出現問題。根據前文,字段的類型只有數字、字符串和日期三種可能的類型,一旦字段數有10個,那么就意味著有310(約60,000)種可能的組合,假設每一秒可以自動進行20次嘗試,窮舉一遍也需要近一個小時,如果字段數更多,那么測試所需時間就會令人難以忍受。
一種簡單的辦法是利用SQL的關鍵字NULL,與靜態字段的注入需要區分是數字類型還是字符類型不同,NULL可以匹配任何一種數據類型。因此可以注入一個所有查詢字段都為NULL的UNION SELECT語句,那么就不會出現任何類型不匹配的錯誤。讓我們再舉一個與前面類似的例子:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234 AND ProdName=’ Computer’) AND UserName=’john’
假設攻擊者已經獲得了列數(在該例中為4),那么就可以很簡單地構造一個UNION SELECT語句,其中所有查詢字段都為NULL,還需要構造一個不會產生權限問題的FROM語句。對于MS SQL Server,即使忽略FROM語句也不會出錯,但對于Oracle,則可以使用一個名叫dual的表。最后,還需要一個值一定為FALSE的WHERE語句(比如WHERE 1=2),這是為了確保查詢不會返回只包含null值的記錄集,以杜絕產生其他可能的錯誤。那么針對MS SQL Server的注入語句如下:
SELECT ProdNum,ProdType,ProdPrice,ProdProvider FROM Products
WHERE (ProdID=1234) UNION SELECT NULL,NULL,NULL,NULL
WHERE 1=2 -- AND ProdName=’ Computer’) AND UserName=’john’
這個NULL注入語句有兩個目的,主要目的是構造一個不會產生任何錯誤的UNION SELECT語句以測試UNION語句是否可以被執行,另一個目的是為了對數據庫類型的判斷進行100%確認(可以通過在FROM語句里添加一個數據庫開發商預置的表名進行測試)。
如果NULL注入語句被順利執行,那么就可以快速地對每個列的類型進行判斷。在每一輪嘗試中,只對一個字段類型進行測試,由于類型只有三類,所以每個字段最多被測試三次就會有結果,這樣嘗試的次數最多是列數的三倍,而不是以3為底數以列數為指數的次數。假設ProdNum屬于數字類型,其它三個字段都屬于字符串類型,那么以下順序的注入語句就可以判斷出正確的類型:
1234) UNION SELECT NULL,NULL,NULL,NULL WHERE 1=2 --
無錯 句法正確,使用的是MS SQL Server數據庫
1234) UNION SELECT 1,NULL,NULL,NULL WHERE 1=2 --
無錯 第一個字段是數字類型
1234) UNION SELECT 1,2,NULL,NULL WHERE 1=2 --
出錯 第二個字段不是數字類型
1234) UNION SELECT 1,’2’,NULL,NULL WHERE 1=2 --
無錯 第二個字段是字符串類型
1234) UNION SELECT 1,’2’,3,NULL WHERE 1=2 --
出錯 第三個字段不是數字類型
1234) UNION SELECT 1,’2’,’3’,NULL WHERE 1=2 --
無錯 第三個字段是字符串類型
1234) UNION SELECT 1,’2’,’3’,4 WHERE 1=2 --
出錯 第四個字段不是數字類型
1234) UNION SELECT 1,’2’,’3’,’4’ WHERE 1=2 --
無錯 第四個字段是字符串類型
攻擊者現在就已經獲得了每一列的數據類型,盲注還可以被應用于從數據庫的表中獲取數據,比如獲得數據表的列表以及它們各自的列名,還可以從應用程序中獲得數據,而這些技術在其他一些關于SQL注入的論文中已經有討論,故本文不再繼續介紹。