關于SQL注入,你應該知道的那些事

pfmm 9年前發布 | 24K 次閱讀 SQL

英文出處:Troy Hunt 譯文來自:伯樂在線
戴上你的黑帽,現在我們來學習一些關于SQL注入真正有趣的東西。請記住,你們都好好地用這些將要看到的東西,好嗎?

SQL注入攻擊因如下幾點而是一種特別有趣的冒險:
1.因為能自動規范輸入的框架出現,寫出易受攻擊的代碼變得越來越難——但我們仍然會寫差勁的代碼。
2.因為你使用了存儲過程或者ORM框架,你不一定很清楚的是(雖然你意識到SQL注入可能穿透他們,對嗎) 我們在這些保護措施之下編寫的代碼依然是易受攻擊的。
3.通過精心設計的爬取web搜尋易受攻擊站點的自動化工具使這類站點更易遠程檢測出來。而我們依舊在發布它們(譯注:指站點)。
SQL注入攻擊因一個非常恰當的原因而被保留在OWASP(Open Web Application Security Project  開放Web應用安全項目) 的十大隱患列表中第一位——它特別常見,非常容易利用,而且影響十分劇烈。一個很微小的注入風險經常就能使整個系統中的所有數據都被泄漏——而我將要展示給你如何運用大量不同的技術自己來這樣做。

我幾年前寫《 the OWASP Top 10 for .NET developers》時展示過如何防范SQL注入攻擊,所以我不會專注在這些,這都是漏洞利用。受夠了那些無聊的防御工具,讓我們來攻擊別的東西。

如果我們能攻破查詢內容,你們的數據就都是我們的了

讓我們對讓SQL注入攻擊成為可能的原因做一個快速概括。簡而言之,這就是輸入查詢并解密數據。讓我把所說的可視化給你:比如說你有一個包含有類似于“id=1”之類的字符串參數的URL,容納后那個參數通過如下方式構造了一個SQL查詢。


關于SQL注入,你應該知道的那些事

這整個URL可能和這個東西看起來很像:


關于SQL注入,你應該知道的那些事

這是挺基礎的東西,而當你能掌控鏈接中的信息并改變傳遞給查詢的值時會變得有趣。好了,把1變成2會給你另一個你期待的東西,但是如果你這樣做呢?
http://widgetshop.com/widget/?id=1 or 1=1
那可能在數據庫服務器中存留成這樣的:

SELECT * FROM Widget WHERE ID = 1 OR 1=1

這告訴我們的是數據沒有被凈化——在上例中ID應該只是一個整數但“1 OR 1=1”的值也被接受。更重要的是,因為數據只是簡單地被添加到查詢中,它能夠改變語句的功能。這個查詢將能夠選擇所有的記錄而不是單個記錄,因為”1=1″語句是恒成立的。

或者,我們可以通過把“or 1=1”改成“and 1=2”來強制頁面不返回任何記錄,因為它一直都不成立所以沒有結果返回。在這兩個可選的方案中我們能方便地確定程序是否受注入攻擊威脅。

這是SQL注入攻擊的本質——通過不被信任的數據巧妙地操縱查詢的執行——而在開發者做這樣子事時發生。

query = "SELECT * FROM Widget WHERE ID = "+ Request.QueryString["ID"];
//Execute the query...
//執行查詢...

當然他們做的是將不被信任的數據參數化,但本文中我不會過多敘述(如果想要了解防范措施,轉回 part one of my OWASP series),而將更多談論如何發動攻擊。

好了,于是背景部分介紹了如何展示SQL注入風險存在,但你能拿它怎么辦?讓我們開始探尋一些普遍的注入模式。

抽絲剝繭:合并基于查詢的注入

讓我們舉個例子,表示我們想要返回一堆記錄的頁面,在這里是一個有一堆帶有“TypeId”1的小東西的URL。像這樣:

http://widgetshop.com/Widgets/?TypeId=1

頁面上的結果會像這樣:


關于SQL注入,你應該知道的那些事

我們會期待這個查詢進入到數據庫時變成像這樣的東西:

SELECT Name FROM Widget WHERE TypeId = 1

但是如果我們能應用我上述描繪的,也就是說我們可能能夠給查詢字符串中的數據添加SQL,我們可能會做出這樣的東西:

http://widgetshop.com/Widgets/?TypeId=1 union all select name from sysobjects where xtype=’u’

然后它將產生一個如下的SQL查詢:

SELECT Name FROM Widget WHERE TypeId = 1 union all select name from sysobjects where xtype='u'

現在記好了系統對象表列舉數據庫中所有對象,而在這個例子中我們用 xtype “u” 來篩選這個表,換言之,用戶表。

當一個注入風險存在的時候將會有如下的輸出:


關于SQL注入,你應該知道的那些事

這就是叫做合并基于查詢的注入攻擊,就像我們剛才簡單地像原始結果添加一項,它直接到了HTML輸出中——簡單吧!既然我們已經知道有一個數據表叫“User”,我們可以做這樣的事:

http://widgetshop.com/Widgets/?TypeId=1 union all select password from [user]

如果數據表中“user”不被中括號括起來,考慮到“user”這個詞在數據庫看來有其他含義,SQL服務器會變得不易控制。不管怎樣,這是它返回的:


關于SQL注入,你應該知道的那些事

當然,UNION ALL語句只在第一個SELECT語句和第二個有相同的字段時起作用。這很容易被發現,你只需試試一些“union all select ‘a’”,如果它查詢失敗就試試“union all select ‘a’, ‘b’”之類的,以此類推。根本上你是在不斷猜測列數直到你構造的查詢發揮作用。

我們可以繼續研究這個方面并揪出各種數據,但還是學習下一種攻擊方式吧。有時一個基于合并查詢的注入不會發揮作用,與輸入格式、查詢中添加的數據甚至結果如何顯示都有關。為了繞開它我們需要變得更有創造性一些。

讓程序自己泄密:基于錯誤信息的注入

http://widgetshop.com/widget/?id=1 or x=1

等一下,這不是一個合法的SQL語句,那個“x=1”不會被處理,至少在沒有一個叫做x的列時不會被處理。那么它不會拋出一個異常嗎?嚴格地說,事實上你將會看到像這樣的異常:


關于SQL注入,你應該知道的那些事

這是一個ASP.NET的錯誤,而其他的框架也有類似的樣式。但是重要的是這些錯誤信息暴露了內部的實現方式,換言之,這告訴我們數據庫中沒有叫做“x”的字段。為什么這很重要?從根本上說,這是因為你一旦確立了一個應用程序在泄漏SQL異常,你就可以做這樣的事:

http://widgetshop.com/widget/?id=convert(int,(select top 1 name from sysobjects where id=(select top 1 id from (select top 1 id from sysobjects where xtype=’u’ order by id) sq order by id DESC)))


這有好多需要吸收理解,我等會將回來詳細解釋。更重要的是通過那條語句你能夠在瀏覽器中得到這樣的結果:


關于SQL注入,你應該知道的那些事

現在我們得到了,我們已經發現那數據庫里有一個表單叫做“Widget”。你將經常能看到這中注入攻擊因依賴于數據庫內部的錯誤而被稱作“基于錯誤信息的注入”。讓我們解構URL中的這個查詢:

convert(int, (
    select top 1 name from sysobjects where id=(
      select top 1 id from (
        select top 1 id from sysobjects where xtype='u' order by id
      ) sq order by id DESC
    )
  )
)

從最深層的開始理解,我們先按照ID的順序從sysobjects表獲取第一個有記錄的ID。在那里,我們獲取最后一個ID(這就是為什么它是按降序排列),并把它傳遞到第一個select語句。那個語句接下來只會將那個表單名稱轉換成一個整數。這個轉換將大多數情況下失敗(各位,不要用“1”或“2”或其他整數來命名數據表就是這個原因!),而這個異常暴露了UI中的表單名稱。

為什么是三個select語句?因為這意味著我們可以進入最深層的那個并把“top1”改為“top2”,得到如下結果:


關于SQL注入,你應該知道的那些事

現在我們知道了這個數據庫有一個數據表叫做“User”。利用這種方法我們可以發現各個表單的字段名稱(只需向syscolumns表應用同樣的思路)。我們可以更進一步擴展這個思路


關于SQL注入,你應該知道的那些事

在上一個截圖中,我已經發現了叫做User的表單和名為Password的列,現在我需要做的就是把那個表單選出來(當然,你可以用嵌套的select語句來一個一個枚舉所有的記錄),并通過將字符串轉換成整數來構造異常(你總是能夠在數據后面通過加一個英文字符來看它到底是不是一個整數,之后嘗試將整個字符串轉換為整數時就會產生一個異常)。如果你想要進一步理解這可以有多簡單,我去年錄制了一個 我教3歲兒子用Havij來自動注入的視頻,那里運用了這個技術。

但是這里有一個問題——它唯一能成功的可能是因為那個app有些淘氣并將內部的錯誤信息展示給公眾。事實上那個app差不多直接告訴了我們表單和列表的名字并當我們做出恰當詢問時返回數據,那么如果那個app不這樣做又會怎樣呢?我的意思是,如果那個app設定恰當而沒有泄漏內部的錯誤信息呢?

這就是我們運用“blind”SQL(多譯為盲注)注入的地方,那真的是一個有趣的東西。

盲目地嘗試注入

在上一個例子中(事實上也在很多成功的注入攻擊先例中),攻擊依賴于受攻擊的app明確地將內部的細節,要么是合并表單,要么是將數據返回,要么將錯誤信息傳回瀏覽器。泄漏內部的實現方法一直都是一鍵不好的事,因為正如你之前看到的那樣,像這樣不安全的錯誤處理可以促使不僅僅是應用程序的架構泄漏,更會使你極易從中獲取數據。

一個恰當設定的app應當能夠在得到一個未經處理的異常時返回一個和下面這個相似的錯誤信息:


關于SQL注入,你應該知道的那些事

這是新ASP.NET的app在處理自定義錯誤時的默認錯誤頁面,但是類似的樣式也在別的technology stacks中出現。現在這個頁面已經和之前那個顯示內部SQL異常的頁面一模一樣了,只不過是用一個有好的錯誤信息代替直接展示出來的異常。假如我們同時也不能實現一個基于合并查詢的攻擊,SQl注入風險就完全不存在了嗎?不一定……

盲目地SQl注入攻擊依賴于我們變得能夠得到不言而喻的信息,換言之,我們能夠通過觀察app并沒有直接告訴我們的表單名稱或者在瀏覽器中直接顯示的列表數據來下結論。當然問題來了——我們如何讓app按照一個可以觀察到的格式來揭示我們之前有的信息,而并不顯式地告訴我們?

我們將去欣賞兩種嘗試:基于布爾值的和基于時間的。

去詢問(APP),然后你將被回答:基于布爾值的注入

這只有你詢問app正確的問題時成立。之前,我們能夠明確地詢問這樣的問題,比如“你有什么表單?”或“每個表單中你有什么數據列?”,然后數據庫會明確地告訴我們。現在我們需要稍微變換一線詢問的方式,比如像這樣:

http://widgetshop.com/widget/?id=1 and 1=2 Clearly this equivalency test can never be true – one will never be equal to two. How an app at risk of injection responds to this request is the cornerstone of blind SQLi and it can happen in one of two different ways.

顯然這個相等測試永遠不會成立——1永遠都不等于2。那么一個app如何處理這樣的查詢決定了它的SQL注入風險,可能會有兩種方式。

第一種,如果沒有記錄返回,它可能只拋回一個異常。通常開發者會假設那里存在一個與查詢的字符串有關的記錄,因為經常會是app自己產生那個鏈接并在另一個頁面中獲取數據。而當那里沒有數據可以返回時,事情就不一樣了。或者第二種,那個app可能拋出一個異常并同時不會展示記錄,因為那個相等永遠都是錯的。不管怎樣,那個app都會隱含地告訴我們數據庫中沒有記錄被返回。

現在我們試試這個:

1 and( 
 select top 1 substring(name, 1, 1) from sysobjects where id=(  
  select top 1 id from (
      select top 1 id from sysobjects where xtype='u' order by id    
) sq order by id desc
 )
) = 'a'

要記住用這整個語句塊來替換剛才那個查詢串的“?id=1”,這實際上是一個在前一個詢問上做出的小變化,試圖獲取表單名稱。事實上主要的區別在于現在不是試圖通過將字符串轉換為整數來構造異常,而是運用相等測試來檢查是否有一個表單首字母為“a”(假設這里對大小寫不敏感)。如果這個查詢和“?id=1”給我們的信息一樣,那么它就相當于向我們證實相等測試成立了,sysobjects里確實有一個首字母開頭為“a”的表單。如果它給我們之前我們提到過的兩種情景之一,那么我們就知道表單并沒有以“a”開頭,因為沒有信息被返回。

現在我們得到的只有sysobjects中表單的第一個字母,當你想要得到第二個字母是substring語句需要變成現在這樣:

select top 1 substring(name, 2, 1) from sysobjects where id=(

你能看到它現在從2開始而不是1.當然,這很費力:你在枚舉sysobjects中所有表單后枚舉了所有字母表中可能組成的詞,直到你最后得到了結果,然后你又要表單名稱的每一個字符重復這個過程。但是,有一種像這樣的快捷方式:

1 and
(  
select top 1 ascii(lower(substring(name, 1, 1))) from sysobjects where
 id=(    
select top 1 id from ( 
     select top 1 id from sysobjects where xtype='u' order by id    
) sq order by id desc
  )
) > 109

這里有一個微妙但很重要的區別,它沒有檢查單個字符匹配,而是查找字符在ASCII表中的位置。事實上,它先將表單名稱轉換為小寫字母,這樣我們只需要處理26個字符(當然,假設命名中只有字母),然后它獲取那個字母的ASCII值。在上一個例子中,它接著檢查表單中是否有以在“m”(ASCII值為109)之后的字母開頭的,然后相同的潛力成功描述了之前應用的(要么一個記錄被返回要么沒有)。主要的區別在于,沒有進行26次嘗試猜測字母(并連續進行26次HTTP請求),它現在將會在5次嘗試中窮盡所有可能——你只需要不斷將可能的ASCII值區間減半直到最后只有一種可能剩余。

比如,如果一個字符ASCII值比109大,那么它一定在“n”和“z”之間所以你分割(大致地)這個區間為一半,然后嘗試大于115那個。如果那是錯誤的那么正確的字符就一定在“n”和“s”之間,所以你再將區間減半,然后嘗試大于112的那個。那時正確的所以現在只有三個字符剩下了,所以你可以在至多兩次嘗試中將區間減小至長度為1。一句話就是至多26次猜測(平均起來13次),現在只需要5次,如果你只是簡單地每次將答案區間減半。

通過構造恰當的詢問app將依舊告訴你之前它通過明確的錯誤信息告訴你的東西,只不過它現在有些怕羞,你需要哄它才會得到答案。這經常被叫做“基于布爾值”的SQL注入,而它在之前演示過的“基于合并查詢”的和“基于錯誤信息”的方案不好用時能夠發揮作用。但這并非萬無一失,讓我們看看另一個途徑,這回我們將要有一些耐心。

耐心等待泄漏:基于時間的盲目注入

所有實時的方案成功發揮作用都是基于一個假設:app會通過HTML輸出來泄漏信息。在之前的例子中基于合并查詢的和基于錯誤信息的嘗試是在瀏覽器中給我們數據來明確地告訴我們對象名稱和泄漏的內部數據。在盲目的基于布爾值的例子中,我們被隱含地告知同一份信息借助于HTML和基于真假相等測試得到的結果不同。那么當這份信息不能通過HTML泄漏時,不論是明確地還是隱含地,怎么辦?

讓我們想像有另一個攻擊媒介是這個URl:
http://widgetshop.com/Widgets/?OrderBy=Name
在這個例子中很正常假設查詢會被翻譯成像這樣的東西:

SELECT * FROM Widget ORDER BY Name

顯然我們不能直接開始向ORDER BY語句直接加東西(盡管那里已經有其他角度你可以掛載一個基于布爾值的攻擊),所以我們需要嘗試另一種途徑。一個很常見的SQL注入技巧是終止一個語句并隨后附加一個語句,比如像這樣:

http://widgetshop.com/Widgets/?OrderBy=Name;SELECT DB_NAME()

這是一個無害的語句(盡管在查找數據庫的名字是可能會有用),一個更有害的途徑可能會是類似于“DROP TABLE Widget”的東西。當然web app連接數據庫所調用的帳號需要有這樣的權限,問題在于一旦你開始將鏈接連接起來,它的潛力就開始發揮。

回到那個盲目的SQL注入攻擊,現在我們需要做的是找到一個在附加語句中運用之前討論到的基于布爾值的測試。要做到這點我們需要用WAITFOR DELAY語句來產生延時。試試這個,看看尺寸:

這和之前的例子只有一個微小的變化,之前是通過操縱WHERE語句改變返回的記錄的書目,而現在只是用一個新的語句來查找sysobjects中是否存在一個表單以一個比“m”大的字母開頭,并且如果存在,查詢將稍微等待5秒鐘。我們仍舊需要縮小表單名稱的范圍而且需要嘗試表單名中的每一個字符而我們仍舊需要查詢sysobjects中的其他表單(當然還要看看syscolumns并將數據提取出來),但所有這一切完全可以用一點時間。5秒鐘可能比需要的有些長了或者它可能不夠長,這一切都歸結于應用程序的響應時間如何保持一致,因為最終這都被設計來操作一個能被觀察到的行為——從開始查詢到最后得到結果要經過多長時間。

這個攻擊——還有之前那些——當然被可以完全地自動化,因為除了簡單枚舉和條件邏輯之外不剩別的了。當然它可能會占用一些時間,但那是一個相對的概念:如果一個正常的查詢需要1秒鐘,而5次嘗試只有一半需要完成的話,你應該期待每17.5秒得到一個字符,比如有數據庫中平均有10個字符的話,就是需要大概3分鐘得到一個表單,而可能一個數據庫中有20個表單,我們就認為大概一小時你就能得到系統中的每一個表單名稱。而這是你用單線程方式做這些的情況。

到這里沒有結束……

這是那些有一堆不同角度觀點的話題,不只因為有太多的數據庫、app框架、服務器的組成,更不要說一整個防御體系比如網絡應用的防火墻。一個事情變得棘手的例子是如果你需要求 助于基于時間的攻擊而數據庫還沒有支持延遲功能,比如一個Access數據庫(是的,游戲而事實上在網站中用這些!)這里的一個途徑是用叫做 heavy queries的方案,查詢由于本身的性質會導致響應是緩慢的。

另一件關于SQL注入攻擊值得一提的是攻擊是否成功有兩個關鍵因素:第一個是app在輸入方面的規范,這決定了app最終會接收到什么字符并傳給數據庫。通常我們會看到很零零碎碎的途徑,比如尖括號和引號被剝離,但其他一切是允許的。當這種情況出現時,攻擊者需要變得有創意,考慮如何構造恰當的查詢使得“路障”被避免。而這正是第二點——攻擊者的SQl實力是至關重要的。這不是指你運用TSQL的SELECT FROM的能力,那些優秀的SQl注入者掌握大量能夠繞過輸入檢測的竅門并從系統中選擇數據而使它們能通過網頁來檢索。比如說,搜尋一個列的類型可以通過像這樣的小技巧:

http://widgetshop.com/Widget/?id=1 union select sum(instock) from widget

在這個例子中,基于錯誤的注入攻擊將在錯誤信息返回到UI時(當然,如果沒有報錯就是指它是整型的)會告訴你“InStock”列是什么類型的


關于SQL注入,你應該知道的那些事

或者一旦你完全厭倦了那個該死的易受攻擊的站點仍然在網絡上留存,試試這個:

http://widgetshop.com/Widget/?id=1;shutdown

但是注入攻擊可以通過從HTTP中獲取信息而更進一步,比如那里有能給攻擊者機器腳本的載體或者試試另一個離題的——為什么不試試直接通過HTML獲取那該死的東西?你就創建一個本地的SQL服務器并通過1433端口遠程連接到SQL Server Management Studio!等一下,你會需要那個網頁app用來鏈接數據庫來創造用戶的帳號,是嗎?是的,而且大部分人都需要,事實上你 只需通過Google就能找到它們(譯注:用度娘會告訴你找不到)(當然這種情況下SQL注入攻擊就沒有必要了,因為數據庫此時已經能公開獲取)

最后,如果關于SQl注入攻擊及漏洞的流行和在當今軟件行業的影響還有什么疑問,就在上周就有一篇 關于可以說是迄今為止最大的黑客方案之一的新聞,據稱它造就了3億損失

這起訴書也暗示那些黑客,在大多數情況下,沒有部署很復雜的方案來進入企業網絡。這篇報道也展示了在大多數情況下缺口是通過SQL注入漏洞的道德——這一威脅已經被徹底證明并領悟遠超過十年了。

可能SQL注入攻擊沒有像某些人相信的那樣被人理解。

 本文由用戶 pfmm 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!