實例講解 SQL 注入攻擊
http://blog.jobbole.com/83092/一位客戶讓我們針對只有他們企業員工和顧客能使用的企業內網進行滲透測試。這是安全評估的一個部分,所以儘管我們之前沒有使用過SQL注入來滲透網絡,但對其概念也相當熟悉了。最後我們在這項任務中大獲成功,現在來回顧一下這個過程的每一步,將它記錄為一個案例。
「SQL注入」是一種利用未過濾/未審核用戶輸入的攻擊方法(「緩存溢出」和這個不同),意思就是讓應用運行本不應該運行的SQL代碼。如果應用毫無防備地創建了SQL字符串並且運行了它們,就會造成一些出人意料的結果。
其他的SQL文章包含了更多的細節,但是這篇文章不僅展示了漏洞利用的過程,還講述了發現漏洞的原理。
目標內網
展現在我們眼前的是一個完整定製網站,我們之前沒見過這個網站,也無權查看它的源代碼:這是一次「黑盒」攻擊。『刺探』結果顯示這台服務器運行在微 軟的IIS6上,並且是ASP.NET架構。這就暗示我們數據庫是微軟的SQL server:我們相信我們的技巧可以應用在任何web應用上,無論它使用的是哪種SQL 服務器。登陸頁有傳統的用戶-密碼表單,但多了一個 「把我的密碼郵給我」的鏈接;後來,這個地方被證實是整個系統陷落的關鍵。
當鍵入郵件地址時,系統假定郵件存在,就會在用戶數據庫裡查詢郵件地址,然後郵寄一些內容給這個地址。但我的郵件地址無法找到,所以它什麼也不會發給我。
對於任何SQL化的表單而言,第一步測試,是輸入一個帶有單引號的數據:目的是看看他們是否對構造SQL的字符串進行了過濾。當把單引號作為郵件地址提交以後,我們得到了500錯誤(服務器錯誤),這意味著「有害」輸入實際上是被直接用於SQL語句了。就是這了!
我猜測SQL代碼可能是這樣:
1
2
3
| SELECT fieldlist FROM table WHERE field = '$EMAIL' ; |
當我們鍵入steve@unixwiz.net『 -注意這個末端的引號 – 下面是這個SQL字段的構成:
1
2
3
| SELECT fieldlist FROM table WHERE field = 'steve@unixwiz.net' '; |
這個數據呈現在WHERE的從句中,讓我們以符合SQL規範的方式改變輸入試試,看看會發生什麼。鍵入anything' OR 『x'=『x, 結果如下:
1
2
3
| SELECT fieldlist FROM table WHERE field = 'anything' OR 'x' = 'x' ; |
但與每次只返回單一數據的「真實」查詢不同,上面這個構造必須返回這個成員數據庫的所有數據。要想知道在這種情況下應用會做什麼,唯一的方法就是嘗試,嘗試,再嘗試。我們得到了這個:
你的登錄信息已經被郵寄到了 random.person@example.com.
我們猜測這個地址是查詢到的第一條記錄。這個傢伙真的會在這個郵箱裡收到他忘記的密碼,想必他會很吃驚也會引起他的警覺。
我們現在知道可以根據自己的需要來篡改查詢語句了,儘管對於那些看不到的部分還不夠瞭解,但是我們注意到了在多次嘗試後得到了三條不同的響應:
- 「你的登錄信息已經被郵寄到了郵箱」
- 「我們不能識別你的郵件地址」
- 服務器錯誤
模式字段映射
第一步是猜測字段名:我們合理的推測了查詢包含「email address」和「password」,可能也會有「US Mail address」或者「userid」或「phone number」這樣的字段。我們特別想執行 SHOW TABLE語句, 但我們並不知道表名,現在沒有比較明顯的辦法可以拿到表名。我們進行了下一步。在每次測試中,我們會用我們已知的部分加上一些特殊的構造語句。我們已經知道這個SQL的執行結果是email地址的比對,因此我們來猜測email的字段名:
1
2
3
| SELECT fieldlist FROM table WHERE field = 'x' AND email IS NULL ; --'; |
如果我們得到了服務器錯誤,意味著SQL有不恰當的地方,並且語法錯誤會被拋出:更有可能是字段名有錯。如果我們得到了任何有效的響應,我們就可以 猜測這個字段名是正確的。這就是我們得到「email unknown」或「password was sent」響應的過程。
我們也可以用AND連接詞代替OR:這是有意義的。在SQL的模式映射階段,我們不需要為猜一個特定的郵件地址而煩惱,我們也不想應用隨機的氾濫的 給用戶發「這是你的密碼」的郵件 - 這不太好,有可能引起懷疑。而使用AND連接郵件地址,就會變的無效,我們就可以確保查詢語句總是返回0行,永遠不會生成密碼提醒郵件。
提交上面的片段的確給了我們「郵件地址未知」的響應,現在我們知道郵件地址的確是存儲在email字段名裡。如果沒有生效,我們可以嘗試email_address或mail這樣的字段名。這個過程需要相當多的猜測。
接下來,我們猜測其他顯而易見的名字:password,user ID, name等等。每次只猜一個字段,只要響應不是「server failure」,那就意味著我們猜對了。
1
2
3
| SELECT fieldlist FROM table WHERE email = 'x' AND userid IS NULL ; --'; |
- passwd
- login_id
- full_name
尋找數據庫表名
應用的內建查詢指令已經建立了表名,但是我們不知道是啥:有幾個方法可以找到表名。其中一個是依靠subselect(字查詢)。
一個獨立的查詢
1
| SELECT COUNT (*) FROM tabname |
1
2
3
| SELECT email, passwd, login_id, full_name FROM table WHERE email = 'x' AND 1=( SELECT COUNT (*) FROM tabname); --'; |
1
2
3
| SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x' AND members.email IS NULL ; --'; |
找用戶賬號
我們對members表的結構有了一個局部的概念,但是我們僅知道一個用戶名:任意用戶都可能得到「Here is your password」的郵件。回想起來,我們從未得到過信息本身,只有它發送的地址。我們得再弄幾個用戶名,這樣就能得到更多的數據。
首先,我們從公司網站開始找幾個人:「About us」或者「Contact」頁通常提供了公司成員列表。通常都包含郵件地址,即使它們沒有提供這個列表也沒關係,我們可以根據某些線索用我們的工具找到它們。
LIKE從句可以進行用戶查詢,允許我們在數據庫裡局部匹配用戶名或郵件地址,每次提交如果顯示「We sent your password」的信息並且郵件也真發了,就證明生效了。
警告:這麼做拿到了郵件地址,但也真的發了郵件給對方,這有可能引起懷疑,小心使用。
我們可以查詢email name或者full name(或者推測出來的其他信息),每次放入%通配符進行如下查詢:
1
2
3
| SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x' OR full_name LIKE '%Bob%' ; |
密碼暴力破解
可以肯定的是,我們能在登陸頁進行密碼的暴力破解,但是許多系統都針對此做了監測甚至防禦。可能有的手段有操作日誌,帳號鎖定,或者其他能阻礙我們行動的方式,但是因為存在未過濾的輸入,我們就能繞過更多的保護措施。
我們在構造的字符串裡包含進郵箱名和密碼來進行密碼測試。在我們的例子中,我們用了受害者bob@example.com 並嘗試了多組密碼。
1
2
3
| SELECT email, passwd, login_id, full_name FROM members WHERE email = '<a href="mailto:bob@example.com">bob@example.com</a>' AND passwd = 'hello123' ; |
這個過程可以使用perl腳本自動完成,然而,我們在寫腳本的過程中,發現了另一種方法來破解系統。
數據庫不是只讀的
迄今為止,我們沒做查詢數據庫之外的事,儘管SELECT是只讀的,但不代表SQL只能這樣。SQL使用分號表示結束,如果輸入沒有正確過濾,就沒有什麼能阻止我們在字符串後構造與查詢無關的指令。The most drastic example is:
這劑猛藥是這樣的:
1
2
3
| SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x' ; DROP TABLE members; --'; -- Boom! |
這表明我們不僅僅可以切分SQL指令,而且也可以修改數據庫。這是被允許的。
添加新用戶
我們已經瞭解了members表的局部結構,添加一條新紀錄到表裡視乎是一個可行的方法:如果這成功了,我們就能簡單的用我們新插入的身份登陸到系統了。不要太驚訝,這條SQL有點長,我們把它分行顯示以便於理解,但它依然是一條語句:
1
2
3
4
5
| SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x' ; INSERT INTO members ( 'email' , 'passwd' , 'login_id' , 'full_name' ) VALUES ( 'steve@unixwiz.net' , 'hello' , 'steve' , 'Steve Friedl' ); --'; |
- 在web表單裡,我們可能沒有足夠的空間鍵入這麼多文本(儘管可以用腳本解決,但並不容易)。
- web應用可能沒有members表的INSERT權限。
- 母庸置疑,members表裡肯定還有其他字段,有一些可能需要初始值,否則會引起INSERT失敗。
- 即使我們插入了一條新紀錄,應用也可能不正常運行,因為我們無法提供值的字段名會自動插入NULL。
- 一個正確的「member」可能額不僅僅只需要members表裡的一條紀錄,而還要結合其他表的信息(如,訪問權限),因此只添加一個表可能不夠。
一個可行的辦法是猜測其他字段,但這是一個勞力費神的過程:儘管我們可以猜測其他「顯而易見」的字段,但要想得到整個應用的組織結構圖太難了。
我們最後嘗試了其他方式。
把密碼郵給我
我們意識到雖然我們無法添加新紀錄到members數據庫裡,但我們可以修改已經存在的,這被證明是可行的。
從上一步得知 bob@example.com 賬戶在這個系統裡,我們用SQL注入把數據庫中的這條記錄改成我們自己的email地址:
1
2
3
4
5
6
| SELECT email, passwd, login_id, full_name FROM members WHERE email = 'x' ; UPDATE members SET email = <a href= "mailto:'steve@unixwiz.net" >'steve@unixwiz.net</a> ' WHERE email = <a href="mailto:' bob@example.com"> 'bob@example.com</a>' ; |
之後,我們使用了「I lost my password」的功能,用我們剛剛更新的email地址,一分鐘後,我們收到了這封郵件:
1
2
3
4
5
6
7
| From : <a href= "mailto:system@example.com" >system@example.com</a> To : <a href= "mailto:steve@unixwiz.net" >steve@unixwiz.net</a> Subject: Intranet login This email is in response to your request for your Intranet log in information. Your User ID is : bob Your password is : hello |
我們發現這個企業內部站點內容特別多,甚至包含了一個全用戶列表,我們可以合理的推出許多內網都有同樣的企業Windows網絡帳號,它們可能在所 有地方都使用同樣的密碼。我們很容易就能得到任意的內網密碼,並且我們找到了企業防火牆上的一個開放的PPTP協議的VPN端口,這讓登錄測試變得更簡 單。
我們又挑了幾個帳號測試都沒有成功,我們無法知道是否是「密碼錯誤」或者「企業內部帳號是否與Windows帳號名不同」。但是我們覺得自動化工具會讓這項工作更容易。
其他方法
在這次特定的滲透中,我們得到了足夠的權限,我們不需要更多了,但是還有其他方法。我們來試試我們現在想到的但不夠普遍的方法。我們意識到不是所有的方法都與數據庫無關,我們可以來試試。
調用xp_cmdshell
微軟的SQLServer支持存儲過程xp_cmdshell有權限執行任意操作系統指令。如果這項功能允許web用戶使用,那webserver被滲透是無法避免的。
迄今為止,我們做的都被限制在了web應用和數據庫這個環境下,但是如果我們能執行任何操作系統指令,再厲害的服務器也禁不住滲透。xp_cmdshell通常只有極少數的管理員賬戶才能使用,但它也可能授權給了更低級的用戶。
繪製數據庫結構
在這個登錄後提供了豐富功能應用上,已經沒必要做更深的挖掘了,但在其他限制更多的環境下可能還不夠。
能夠系統的繪製出數據庫可見結構,包含表和它們的字段結構,可能沒有直接幫助。但是這為網站滲透提供了一條林萌大道。
從網站的其他方面收集更多有關數據結構的信息(例如,「留言板」頁?「幫助論壇」等?)。不過這對應用環境依賴強,而且還得靠你準確的猜測。
減輕危害
我們認為web應用開發者通常沒考慮到「有害輸入」,但安全人員應該考慮到(包括壞傢伙),因此這有3條方法可以使用。
輸入過濾
過濾輸入是非常重要的事,以確保輸入不包含危險代碼,無論是SQL服務器或HTM本身。首先想到的是剝掉「惡意字符」,像引號、分號或轉義符號,但這是一種不太好的方式。儘管找到一些危險字符很容易,但要把他們全找出來就難了。
web語言本身就充滿了特殊字符和奇怪的標記(包括那些表達同樣字符的替代字符),所以想要努力識別出所有的「惡意字符」不太可能成功。
換言之,與其「移除已知的惡意數據」,不如移除「良好數據之外的所有數據」:這種區別是很重要的。在我們的例子中,郵件地址僅能包含如下字符:
1
2
3
4
| abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 @.-_+ |
某個特殊的email地址會讓驗證程序陷入麻煩,因為每個人對於「有效」的定義不同。由於email地址中出現了一個你沒有考慮到的字符而被拒絕,那真是糗大了。
真正的權威是RFC 2822(比RFC822內容還多),它對於」允許使用的內容「做了一個規範的定義。這種更學術的規範希望可以接受&和*(還有更多)作為有效的email地址,但其它人 - 包括作者 – 都樂於用一個合理的子集來包含更多的email地址。
那些採用更限制方法的人應當充分意識到沒有包含這些地址會帶來的後果,特別是限制有了更好的技術(預編譯/執行,存儲過程)來避免這些「奇怪」的字符帶來的安全問題。
意識到「過濾輸入」並不意味著僅僅是「移除引號」,因為即使一個「正規」的字符也會帶來麻煩。在下面這個例子中,一個整型ID值被拿來和用戶的輸入作比較(數字型PIN):
1
2
3
| SELECT fieldlist FROM table WHERE id = 23 OR 1=1; -- Boom! Always matches! |
輸入項編碼/轉義
現在可以過濾電話號碼和郵件地址了,但你不能通過同樣的方法處理「name」字段,要不然可能會排除掉Bill O'Reilly這樣的名字:對於這個字段,這裡的引號是合法的輸入。
有人就想到過濾到單引號的時候,再加上一個引號,這樣就沒問題了 – 但是這麼幹要出事啊!
預處理每個字符串來替換單引號:
1
2
3
| SELECT fieldlist FROM customers WHERE name = 'Bill O' 'Reilly' ; -- works OK |
1
2
3
| SELECT fieldlist FROM customers WHERE name = '\'' ; DROP TABLE users; --'; -- Boom! |
比如MySQL的函數mysql_real_escape_string()和perl DBD 的 $dbh->quote($value)方法,這些方法都是必用的。
參數綁定 (預編譯語句)
儘管轉義是一個有用的機制,但我們任然處於「用戶輸入被當做SQL語句」這麼一個循環裡。更好的方法是:預編譯,本質上所有的數據庫編程接口都支持預編譯。技術上來說,SQL聲明語句是用問號給每個參數佔位創建的 – 然後在內部表中進行編譯。
預編譯查詢執行時是按照參數列表來的:
Perl中的例子
1
2
3
| $sth = $dbh-> prepare ( "SELECT email, userid FROM members WHERE email = ?;" ); $sth-> execute ($email); |
不安全版
1
2
3
| Statement s = connection .createStatement(); ResultSet rs = s.executeQuery( "SELECT email FROM member WHERE name = " + formField); // *boom* |
1
2
3
4
| PreparedStatement ps = connection .prepareStatement( "SELECT email FROM member WHERE name = ?" ); ps.setString(1, formField); ResultSet rs = ps.executeQuery(); |
如果預編譯查詢語句多次(只編譯一次)執行,也會帶來性能上的提升,但是與大量安全方面的巨大提升相比,這顯得微不足道。這可能是我們保證web應用安全最重要的一步。
限制數據庫權限和隔離用戶
在這個案例中,我們觀察到只有兩個交互動作不在登錄用戶的上下文環境中:「登錄」和「發密碼給我」。web應用應該對數據庫連接做權限的限制:對於members表只能讀,並且無法操作其他表。
作用是即使一次「成功的」SQL注入攻擊也只能得到非常有限的成功。噢,我們將不能做有授權的UPDATE請求,我們要求助於其他方法。
一旦web應用確定登錄表單傳遞來的認證是有效的,它就會切換會話到一個有更多權限的用戶上。
對任何web應用而言,不使用sa權限幾乎是根本不用說的事。
對數據庫的訪問採用存儲過程
如果數據庫支持存儲過程,請使用存儲過程來執行數據庫的訪問行為,這樣就不需要SQL了(假設存儲過程編程正確)。
把查詢,更新,刪除等動作規則封裝成一個單獨的過程,就可以針對基礎規則和所執行的商業規則來完成測試和歸檔(例如,如果客戶超過了信用卡限額,「添加新記錄」過程可能拒絕訂單)。
對於簡單的查詢這樣做可能僅僅能獲得很少的好處,不過一旦操作變複雜(或者被用在更多地方),給操作一個單獨的定義,功能將會變得更穩健也更容易維護。
注意:動態構建一個查詢的存儲過程是可以做到的:這麼做無法防止SQL注入 – 它只不過把預編譯/執行綁定到了一起,或者是把SQL語句和提供保護的變量綁定到了一起。
隔離web服務器
實施了以上所有的防禦措施,仍然可能有某些地方有遺漏,導致了服務器被滲透。設計者應該在假定壞蛋已經獲得了系統最高權限下來設計網絡設施,然後把它的攻擊對其他事情產生的影響限制在最小。
例如,把這台機器放置在極度限制出入的DMZ網絡「內部」,這麼做意味著即便取得了web服務器的完全控制也不能自動的獲得對其他一切的完全訪問權限。當然,這麼做不能阻止所有的入侵,不過它可以使入侵變的非常困難。
配置錯誤報告
一些框架的錯誤報告包含了開發的bug信息,這不應該公開給用戶。想像一下:如果完整的查詢被現實出來了,並且指出了語法錯誤點,那要攻擊該有多容易。
對於開發者來說這些信息是有用的,但是它應該禁止公開 – 如果可能 - 應該限制在內部用戶訪問。
注意:不是所有的數據庫都採用同樣的方式配置,並且不是所有的數據庫都支持同樣的SQL語法(「S」代表「結構化」,不是「標準的」)。例如,大多 數版本的MySQL都不支持子查詢,而且通常也不允許單行多條語句(multiple statements):當你滲透網絡時,實際上這些就是使問題複雜化的因素。
再強調一下,儘管我們選擇了「忘記密碼」鏈接來試試攻擊,但不是因為這個功能不安全。而是幾個易攻擊的點之一,不要把焦點聚集在「忘記密碼」上。
這個教學示例不準備全面覆蓋SQL注入的內容,甚至都不是一個教程:它僅僅是一篇我們花了幾小時做的滲透測試的記錄。我們看了其他的關於SQL注入文章的討論,但它們只給出了結果而沒有給出過程。
但是那些結果報告需要技術背景才能看懂,並且滲透細節也是有價值的。在沒有源代碼的情況下,滲透人員的黑盒測試能力也是有價值的。
感謝 David Litchfield 和 Randal Schwartz對本文的貢獻,還有Chris Mospaw的排版(© 2005 by Chris Mospaw, used with permission).
其他資源
- (more) Advanced SQL Injection, Chris Anley, Next Generation Security Software.
- SQL Injection walkthrough, SecuriTeam
- GreenSQL, an open-source database firewall that tries to protect against SQL injection errors — note; I don't have any direct experience with this tool
- 「Exploits of a Mom」 — Very good xkcd cartoon about SQL injection
- SQL Injection Cheat Sheet — by Ferruh Mavituna
- This page translated into Belorussian by Bohdan Zograf (thanks!)
關於作者: zer0Black ( @lxtalx )
關注信息安全,網絡安全,目前為移動開發工程師,android和IOS兼有涉獵。半路出道,基礎薄弱,正努力補習計算機基礎中。近日習得「遍歷」學習法,正欲嘗試之。(新浪微博:@Zer0Black)查看zer0Black的更多文章 >>