歡迎光臨
每天分享高質量文章

關於SQL註入,你應該知道的那些事


戴上你的黑帽,現在我們來學習一些關於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查詢。

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

這是挺基礎的東西,而當你能掌控連結中的資訊並改變傳遞給查詢的值時會變得有趣。好了,把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

頁面上的結果會像這樣:

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

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” 來篩選這個表,換言之,使用者表。


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

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

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

如果資料表中“user”不被中括號括起來,考慮到“user”這個詞在資料庫看來有其他含義,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的列時不會被處理。那麼它不會丟擲一個異常嗎?嚴格地說,事實上你將會看到像這樣的異常:

這是一個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)))

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

現在我們得到了,我們已經發現那資料庫裡有一個表單叫做“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”,得到如下結果:

現在我們知道了這個資料庫有一個資料表叫做“User”。利用這種方法我們可以發現各個表單的欄位名稱(只需向syscolumns表應用同樣的思路)。我們可以更進一步擴充套件這個思路

在上一個截圖中,我已經發現了叫做User的表單和名為Password的列,現在我需要做的就是把那個表單選出來(當然,你可以用巢狀的select陳述句來一個一個列舉所有的記錄),並透過將字串轉換成整數來構造異常(你總是能夠在資料後面透過加一個英文字元來看它到底是不是一個整數,之後嘗試將整個字串轉換為整數時就會產生一個異常)。如果你想要進一步理解這可以有多簡單,我去年錄製了一個我教3歲兒子用Havij來自動註入的影片,那裡運用了這個技術。

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

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

盲目地嘗試註入

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

一個恰當設定的app應當能夠在得到一個未經處理的異常時傳回一個和下麵這個相似的錯誤資訊:

這是新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”列是什麼型別的


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

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

但是註入攻擊可以透過從HTTP中獲取資訊而更進一步,比如那裡有能給攻擊者機器指令碼的載體或者試試另一個離題的——為什麼不試試直接透過HTML獲取那該死的東西?你就建立一個本地的SQL伺服器並透過1433埠遠端連線到SQL Server Management Studio!等一下,你會需要那個網頁app用來連結資料庫來創造使用者的帳號,是嗎?是的,而且大部分人都需要,事實上你只需透過Google就能找到它們(譯註:用度娘會告訴你找不到)(當然這種情況下SQL註入攻擊就沒有必要了,因為資料庫此時已經能公開獲取)

最後,如果關於SQl註入攻擊及漏洞的流行和在當今軟體行業的影響還有什麼疑問,就在上週就有一篇 關於可以說是迄今為止最大的駭客方案之一的新聞,據稱它造就了3億損失

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

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



英文出處:Troy Hunt

譯文出處:伯樂線上-SCaffrey
譯文連結:
http://blog.jobbole.com/85683/

贊(0)

分享創造快樂