sql注入簡單總結(jié)

最近兩周刷了一下sqli-labs,對(duì)sql注入有了一個(gè)基本的認(rèn)識(shí)。這里寫個(gè)總結(jié)。

1.sql注入原理簡單介紹
在一般的編程語言中,字符串是作為一種基本數(shù)據(jù)類型儲(chǔ)存的,不會(huì)被編譯器解析。但是有些函數(shù)會(huì)接受一個(gè)參數(shù)并將其解析為代碼語句執(zhí)行,如php中的eval()函數(shù)。有時(shí)我們需要接受用戶的參數(shù),并將其作為字符串的一部分傳入類eval()函數(shù)(或類似的函數(shù)),若此時(shí)沒有對(duì)用戶參數(shù)進(jìn)行嚴(yán)格過濾,就會(huì)產(chǎn)生各種安全漏洞,如js中的xss,php中eval()函數(shù)產(chǎn)生的webshell,以及數(shù)據(jù)庫交互中產(chǎn)生的sql注入漏洞等。
很多編程語言都提供了與數(shù)據(jù)庫進(jìn)行交互的函數(shù),如php中的mysql_query()/mysqli_query()函數(shù),通過拼接用戶傳入的參數(shù)和特定語句構(gòu)成sql查詢語句。如

$un = $_POST['username'];
$pw = $_POST['password'];
$sql = "SELECT * FROM users WHERE name = '$un' AND password = '$pw'";
$result = mysql_query($sql);

以上語句即將用戶傳入的$id參數(shù)與select語句拼接構(gòu)成sql語句后通過mysql_query()函數(shù)進(jìn)行查詢。
假設(shè)有一個(gè)惡意用戶傳入了參數(shù)username=admin&password=0' or 1=1#,而后端又沒用對(duì)輸入進(jìn)行過濾/轉(zhuǎn)義。拼接后的sql語句為"SELECT * FROM users WHERE name = 'admin' AND password = '0' or 1=1#'"。'#'在sql中作為注釋符使用。此時(shí)的sql語句中由于1=1恒為真,與前面語句進(jìn)行or運(yùn)算后仍恒為真,從而繞過了對(duì)password的校驗(yàn)實(shí)現(xiàn)了admin賬號(hào)密碼繞過。
在sql中除了#號(hào)外,-- (注意末尾有空格)也被視為注釋符。有時(shí)頁面以get方式提交數(shù)據(jù),會(huì)直接寫在url上。由于#在url中有特殊的含義(錨點(diǎn)),因此一般使用其url編碼%23,后端接受到數(shù)據(jù)后會(huì)自動(dòng)進(jìn)行url解碼。同樣由于一般注釋符都出現(xiàn)在變量的最后也就是url的最后,而url最末尾的空格在傳輸時(shí)會(huì)被略去,所以使用--+代替,+會(huì)被解碼為空格。
另外在sql注入中,payload不一定是用戶提交的參數(shù),也可能在請(qǐng)求頭中。需要根據(jù)具體的后端邏輯判斷注入點(diǎn)。

2.注入類型判斷

(1).注入類型

  • 數(shù)字型
    $sql = SELECT * FROM users WHERE id = $id

  • 單字符型
    $sql = SELECT * FROM users WHERE id = '$id'

  • 雙字符型
    $sql = SELECT * FROM users WHERE id = "$id"

  • 加括號(hào)
    $sql = SELECT * FROM users WHERE id = ($id)

    $sql = SELECT * FROM users WHERE id = ('$id')

    $sql = SELECT * FROM users WHERE id = ("$id")

    $sql = SELECT * FROM users WHERE id = (($id))

    以上括號(hào)可以任意個(gè)

(2).類型判斷

  • 三種基本類型判斷
    一般通過直接在參數(shù)后接上引號(hào)來判斷,如在單字符類型中,如果我們傳入?yún)?shù)id=0',由于參數(shù)中的單引號(hào)閉合了變量前的引號(hào),導(dǎo)致后面的引號(hào)未閉合,會(huì)產(chǎn)生查詢報(bào)錯(cuò)。如果傳入?yún)?shù)id=0",就不會(huì)產(chǎn)生報(bào)錯(cuò)。在頁面查詢錯(cuò)誤和無查詢結(jié)果回顯不同時(shí)就可以通過這種方式來判斷。
    若報(bào)錯(cuò)和無查詢空白結(jié)果回顯相同(如都返回空白頁面),我們也可以通過在參數(shù)后加#號(hào)的方式來判斷,如單字符中傳入?yún)?shù)id=0' or 1=1#,參數(shù)中的引號(hào)會(huì)閉合變量前的引號(hào),而變量后的引號(hào)由于被#號(hào)注釋掉,不會(huì)產(chǎn)生查詢報(bào)錯(cuò),又由于or 1=1恒為真,因此會(huì)返回有效的查詢結(jié)果。若不是單字符類型,0' or 1=1就全部被包含在字符串內(nèi)不會(huì)報(bào)錯(cuò),也不會(huì)查詢到結(jié)果。但是也可能有注釋符被過濾的情況??偸亲⑷腩愋偷呐袛嗖豢偸乔宦桑枰`活應(yīng)對(duì)。

  • 括號(hào)的判斷
    判斷括號(hào)前首先要判斷出基本輸入類型,這里假設(shè)為數(shù)值型。我們傳入?yún)?shù)id=2 or 1=0。無括號(hào)時(shí),sql語句為SELECT * FROM users WHERE id = 2 or 1=0會(huì)返回id=2的結(jié)果,有括號(hào)時(shí),sql語句為SELECT * FROM users WHERE id = (2 or 1=0)這里由于括號(hào)內(nèi)進(jìn)行了或運(yùn)算,2被作為布爾類型與1=0進(jìn)行或運(yùn)行會(huì)返回1,最后會(huì)返回id=1的查詢結(jié)果。通過這種方式判斷出有無括號(hào)后,再通過逐次添加括號(hào)的方式判斷括號(hào)數(shù)量。
    這些只是注入類型最基礎(chǔ)的判斷方式,實(shí)際上我們可能遇到各種過濾/轉(zhuǎn)義,需要具體分析,靈活應(yīng)對(duì)。

3.數(shù)據(jù)庫名表名列名獲取

  • 系統(tǒng)庫
    一般來說數(shù)據(jù)庫軟件中會(huì)有一個(gè)系統(tǒng)庫儲(chǔ)存了所有數(shù)據(jù)庫信息。通過對(duì)這個(gè)庫進(jìn)行注入可獲取這個(gè)軟件里全部的數(shù)據(jù)庫信息。這里我們以mysql為例。
    mysql中的information_schema庫儲(chǔ)存了所有數(shù)據(jù)庫信息。其中的scehmata表的schema_name列儲(chǔ)存了所有數(shù)據(jù)庫名;tables表的table_name儲(chǔ)存了所有列名,table_schema儲(chǔ)存了其對(duì)應(yīng)的庫名;columns表的column_name儲(chǔ)存所有列名,table_name對(duì)應(yīng)其表名,table_schema對(duì)應(yīng)其庫名。
    關(guān)于這部分,攻防世界中有個(gè)題叫NewsCenter,就是通過對(duì)information_schema庫的查詢進(jìn)而獲取表名列名最終獲取flag。這題中是通過聯(lián)合查詢的方式獲取信息的,聯(lián)合查詢union語句是最簡單的sql注入方式,可以同時(shí)查詢多個(gè)表中的信息,具體會(huì)在下面介紹。

4.簡單的注入方式介紹

  • 聯(lián)合查詢注入
    聯(lián)合查詢利用的是union語句,union語句可以同時(shí)查詢多個(gè)表但是只會(huì)返回第一個(gè)查詢結(jié)果。所以我們需要令第一個(gè)查詢結(jié)果為空,讓其返回我們構(gòu)造的union語句查詢到的結(jié)果。同時(shí)union查詢語句查詢的列數(shù)要和第一個(gè)查詢語句相同,所以我們要首先判斷其查詢列數(shù)。
    判斷查詢列數(shù)一般用order by語句。order by用于對(duì)查詢結(jié)果進(jìn)行排序,如order by id就是根據(jù)id進(jìn)行排序。但order by后面不僅可以跟列名/別名也可以跟數(shù)字,代表第幾列。在SELECT id,name,pass FROM users ORDER BY 2中查詢結(jié)果會(huì)根據(jù)name值進(jìn)行排序。如果這里換成order by 4就會(huì)產(chǎn)生報(bào)錯(cuò)因?yàn)椴樵兘Y(jié)果并沒有第4列。通過這種方式就可以判斷出第一個(gè)查詢語句的查詢列數(shù)。有時(shí)候order被過濾/轉(zhuǎn)義也可以通過union select 1,2,3這種方式一直到不報(bào)錯(cuò)未知就可以知道查詢列數(shù)了。這種方式還有一個(gè)作用就是判斷回顯的數(shù)據(jù)是查詢結(jié)果的哪部分,因?yàn)?code>select 1,2,3的返會(huì)結(jié)果始終是1,2,3,所以通過顯示了哪幾個(gè)數(shù)字就可以知道查詢的哪幾列被顯示出來了。
    有的時(shí)候查詢語句中會(huì)通過limit語句限制返回第一條結(jié)果(或者通過php語句限制只返回回顯第一個(gè)結(jié)果),我們可以使用group_concat()函數(shù)將查詢結(jié)果合并為一條。group_concat()函數(shù)用于合并group by分組后的同組內(nèi)的值,如果我們沒用指定group by的依據(jù),就會(huì)合并所有的值。

  • 報(bào)錯(cuò)注入
    報(bào)錯(cuò)注入是將我們需要的信息包含在報(bào)錯(cuò)信息中回顯出來,有的時(shí)候原sql語句可能不是select查詢語句,而是update/insert等語句,這樣我們就無法構(gòu)造聯(lián)合查詢,這時(shí)我們就可以利用報(bào)錯(cuò)注入。在有報(bào)錯(cuò)回顯時(shí)才可以利用報(bào)錯(cuò)注入,并且由于存在報(bào)錯(cuò)語句字符數(shù)量限制,所以不能一次獲得大量數(shù)據(jù)。報(bào)錯(cuò)注入大致分兩種,一種是雙查詢注入,一種是updatexml()/extractvalue()注入。

    • 雙查詢注入
      在雙查詢注入中需要用到幾個(gè)函數(shù),分別是rand(),floor(),count(),另外還需要用到group by語句。rand()函數(shù)產(chǎn)生一個(gè)[0,1)的隨機(jī)數(shù);floor()函數(shù)向下取整;count()函數(shù)用于對(duì)查詢結(jié)果進(jìn)行計(jì)數(shù),如果使用了group by語句,就會(huì)對(duì)分組后的結(jié)果分別計(jì)數(shù)。
      對(duì)于SELECT * FROM users WHERE id = '$id'這個(gè)語句,如果提交參數(shù)id=0' union select count(*),concat((select id from users limit 0,1),floor(rand()*2)) as a from information_schema.schemata group by a#最終拼接而成的sql語句為SELECT * FROM users WHERE id = '0' union select count(*),concat((select id from users limit 0,1),floor(rand()*2)) as a from information_schema.schemata group by a#'
      這個(gè)語句由幾個(gè)部分組成,首先是一個(gè)普通的select查詢語句,然后是一個(gè)union select聯(lián)合查詢語句。聯(lián)合查詢的內(nèi)容是count()和concat()兩個(gè)函數(shù),后面的as語句用于給concat()函數(shù)的查詢結(jié)果取一個(gè)別名,這里的別名是a用于后面group by語句分組。count(*)用來對(duì)group by語句分組后對(duì)每個(gè)組進(jìn)行計(jì)數(shù);concat()函數(shù)用來拼接它的兩個(gè)參數(shù)分別是一個(gè)select語句和一個(gè)floor()函數(shù),這里的select語句即為雙查詢,即在select語句中包含一個(gè)select語句,floor(rand()*2)會(huì)產(chǎn)生一個(gè)隨機(jī)數(shù)0或者1。最終concat()函數(shù)會(huì)將參數(shù)里的select語句查詢結(jié)果和floor()函數(shù)產(chǎn)生的隨機(jī)數(shù)進(jìn)行拼接,由group by語句對(duì)其進(jìn)行分組再由count()函數(shù)對(duì)其進(jìn)行計(jì)數(shù)。
      在使用group by語句和count()函數(shù)時(shí),mysql會(huì)建立一個(gè)臨時(shí)的虛擬表用來對(duì)查詢結(jié)果進(jìn)行分組計(jì)數(shù)。若查詢到的鍵(查詢結(jié)果)不在表中時(shí),mysql會(huì)將其插入作為一個(gè)新的鍵;若該鍵已經(jīng)存在,則令其計(jì)數(shù)器+1。假設(shè)第一次floor(rand()*2)的結(jié)果為0,concat()函數(shù)中select語句查詢結(jié)果為value,那么拼接后的鍵值為value0,虛擬表中不存在這個(gè)值所以將concat((select id from users limit 0,1),floor(rand()*2))的值插入,注意這里插入時(shí)floor(rand()*2)又被執(zhí)行了一次,所以可能插入value0也可能插入value1。我們假設(shè)插入了value1,那么再判斷第二條結(jié)果,如果這次floor(rand()*2)結(jié)果為0,那么合并的值為value0,因?yàn)樘摂M表中不存在這個(gè)鍵值,所以插入,插入時(shí)又執(zhí)行了floor(rand()*2)函數(shù),如果插入時(shí)其產(chǎn)生的結(jié)果為1,那么鍵值為value1,虛擬表中已經(jīng)存在這個(gè)鍵值,就會(huì)出現(xiàn)鍵值沖突從而引發(fā)mysql報(bào)錯(cuò)Duplicate entry 'value1' for group_key。從這個(gè)報(bào)錯(cuò)語句中我們就獲得了concat()函數(shù)里select語句查詢的結(jié)果value。
      雙查詢報(bào)錯(cuò)使用時(shí)存在一些限制,比如因?yàn)榈谝淮尾迦肭疤摂M表中不存在鍵值,所以第一次插入一定不會(huì)產(chǎn)生報(bào)錯(cuò),因此聯(lián)合查詢的結(jié)果數(shù)量必須兩個(gè)以上。這里我們使用information_schema庫中的schemata表因?yàn)檫@里儲(chǔ)存了數(shù)據(jù)庫信息,一般來說數(shù)據(jù)庫軟件會(huì)有幾個(gè)系統(tǒng)庫,可以滿足查詢結(jié)果多于兩條的要求,當(dāng)然也可以選擇tables表或者其他表。另外由于插入是否報(bào)錯(cuò)是由rand()函數(shù)的結(jié)果決定的,而rand()函數(shù)產(chǎn)生的是一個(gè)隨機(jī)值,因此可能不會(huì)每次都能成功爆出信息,需要多刷新幾次。
    • updatexml()/extratvalue()注入
      updatexml()函數(shù)有三個(gè)參數(shù),第一個(gè)和第三個(gè)參數(shù)是普通的字符串類型,而第二個(gè)參數(shù)為xpath格式的字符串,第一個(gè)字符串為xml文檔名,updatexml()函數(shù)的作用是在第一個(gè)參數(shù)指定的xml文檔中通過第二個(gè)參數(shù)匹配節(jié)點(diǎn),然后用第三個(gè)參數(shù)替換匹配到的節(jié)點(diǎn)值。
      extractvalue()有連個(gè)參數(shù),第一個(gè)參數(shù)為普通字符串,第二個(gè)參數(shù)為xpath格式字符串。這兩個(gè)參數(shù)意義同updatexml()函數(shù)。extractvalue()函數(shù)會(huì)從第一個(gè)參數(shù)指定的xml文檔中通過第二個(gè)參數(shù)匹配并返回匹配結(jié)果。
      雖然他們的作用不同,但他們?cè)趕ql注入的用法完全相同,都是利用拼接不符合xpath格式的字符串作為參數(shù)從而導(dǎo)致mysql查詢報(bào)錯(cuò),從報(bào)錯(cuò)中獲取需要的信息。例如我們構(gòu)造參數(shù)id=0' or updatexml(1,concat('$',(select database())),1)#,就會(huì)返回查詢報(bào)錯(cuò)UnKnown XPATH variable at: '$databasename'從而獲取了數(shù)據(jù)庫名databasename。
      這兩個(gè)函數(shù)與雙查詢注入相比,有幾個(gè)優(yōu)點(diǎn),一是不存在隨機(jī)數(shù)的問題,一次就可以判斷有沒有注入成功;二是payload更短,寫起來更方便;三是在函數(shù)中可以更方便的用括號(hào)代替空格,當(dāng)空格被過濾的時(shí)候這就是一種有效的繞過手段。
      但是這兩個(gè)函數(shù)的漏洞在最新的mysql版本中已經(jīng)被修復(fù)。并且要注意xpath參數(shù)有最大32位的長度限制。一般來說我用updatexml()函數(shù)更多(因?yàn)椴粫?huì)拼extractvalue。
  • outfile文件寫入
    文件寫入是利用sql中的into outfile語句將特定語句寫入外部文件,通過這種方式我們可以構(gòu)造webshell等。但是由于mysql對(duì)于文件的權(quán)限限制很嚴(yán)格,所以這種方法很難利用。

  • 堆疊注入
    sql中通過分號(hào)分隔多個(gè)語句,因此產(chǎn)生堆疊注入這種思路。我們?cè)谔峤坏膮?shù)中加入分號(hào)結(jié)束前面的語句,就可以在分號(hào)后執(zhí)行任何我們需要的語句。但是php中的mysql_query()函數(shù)并不能執(zhí)行多條sql語句,必須為mysql_multi_query()函數(shù),因此這種方法使用也很有限(但是ctf里好像很常見?
    但是由于mysql_multi_query()函數(shù)只會(huì)返回第一個(gè)語句的結(jié)果,所以一般后面構(gòu)造的payload都用來執(zhí)行insert或update等語句。關(guān)于堆疊注入,buuctf里面有一個(gè)強(qiáng)網(wǎng)杯2019隨便注,姿勢(shì)特別騷,可以去看看。

  • 二次注入
    二次注入的概念類似于存儲(chǔ)型xss,是將payload寫入數(shù)據(jù)庫中,然后在程序取出數(shù)據(jù)庫內(nèi)數(shù)據(jù)并且拼接為新的sql語句時(shí)產(chǎn)生注入。這部分可以參考sqli-labs Less-24,這里提供了一個(gè)登錄頁面,并且開放了注冊(cè)接口。我們可以注冊(cè)一個(gè)名為admin' #的用戶并登錄,然后修改該賬號(hào)的密碼。修改完成后用修改后的密碼登錄admin賬號(hào)發(fā)現(xiàn)登錄成功,即注入成功。

  • 盲注
    在頁面即不存在報(bào)錯(cuò)回顯,又不回顯查詢到的數(shù)據(jù)時(shí),大部分注入方式都無法利用,這時(shí)可以考慮盲注。盲注大致分為兩種分別是布爾盲注和時(shí)間盲注。由于盲注一次只能獲取一比特的信息,所以一般需要很多次判斷才能獲取部分有效的信息,這部分可以使用腳本來完成。并且有些站點(diǎn)可能存在掃描限制不允許同一ip短時(shí)間內(nèi)大量訪問,腳本可能會(huì)收到大量的502響應(yīng)。因此一般在上面的方法都無法注入的時(shí)候才會(huì)考慮盲注。

    • 布爾盲注
      在查詢成功和查詢失敗回顯不同時(shí)可以考慮布爾注入,我們可以使用length()函數(shù)判斷出查詢結(jié)果的長度,再通過substr()函數(shù)和二分法一次找出查詢結(jié)果每一位的值,最終可以得到查詢的結(jié)果。例如我們提交參數(shù)id=1' and length((select database()))=8--+,如果length()=8為真,頁面會(huì)返回id=1的查詢結(jié)果,否則返回未查詢到結(jié)果,這樣我們就判斷出來數(shù)據(jù)庫名的長度為8位。再通過id=1' and substr((select database()),1,1)>'A'--+判斷是否數(shù)據(jù)庫名第一位大于'A'。這里要注意一點(diǎn)就是sql中substr()函數(shù)截取字符串索引是從1開始而不是0開始的。
    • 時(shí)間盲注
      在頁面返回結(jié)果始終相同時(shí),就不能通過頁面返回結(jié)果來判斷注入結(jié)果了,這時(shí)候可以嘗試時(shí)間盲注。我們可以使用sql中的if語句來執(zhí)行sleep()函數(shù)從而判斷我們?cè)O(shè)定的條件是否正確。例如我們構(gòu)造urlid=1' and if((length((select database()))=8),sleep(1),1)--+,通過頁面回顯是否延遲判斷if條件是否正確。除了if語句,我們也可以通過and和or來實(shí)現(xiàn)時(shí)間盲注,因?yàn)槿绻耙粋€(gè)語句為真,就不會(huì)判斷or后的語句;同理如果前一個(gè)結(jié)果為假,就不會(huì)判斷and后的語句。通過這個(gè)思路可以提交參數(shù)id=1' and length((select database()))=8 and sleep(1)--+,通過頁面回顯是否延遲判斷條件真假。

5.基本繞過姿勢(shì)

一般正常的題都不會(huì)這么直接給注的,都會(huì)有一些過濾,這時(shí)候要考慮怎么繞過這些過濾。對(duì)于檢測(cè)到的關(guān)鍵字也可能有不同的處理,有的會(huì)直接過濾而有些會(huì)進(jìn)行轉(zhuǎn)義。對(duì)于不同的情況,可以嘗試不同的繞過方法。

  • 過濾繞過
    • 過濾注釋符
      單字符或雙字符注入時(shí),因?yàn)槲覀冊(cè)趨?shù)中加入了引號(hào)閉合原sql語句中得到引號(hào)導(dǎo)致后面的引號(hào)未閉合,一般可以通過注釋符將其注釋掉。但是在注釋符被注釋掉的情況下,無法通過這種方式閉合引號(hào),我們就可以考慮通過order by等語句來閉合引號(hào),例如payload?id=0' union select 1,group_concat(username),group_concat(password) from users order by '1
      這里通過最后order by語句中的引號(hào)閉合了原sql語句中的引號(hào)。也可以通過group by等語句。
    • 過濾關(guān)鍵字
      這種是通過黑名單檢測(cè)sql關(guān)鍵字比如select,union等。但是關(guān)鍵字過濾存在過濾次數(shù)的問題,例如對(duì)字符串"qorororore"過濾'or',過濾完變成"qe",但是如果是字符串"oorr",過濾完可能就是'or',因?yàn)檎齽t只能匹配到中間的'or',如果要過濾兩邊的or,必須要進(jìn)行兩遍過濾。一般來說不會(huì)進(jìn)行很多次過濾因?yàn)檎5膮?shù)里也可能包含'or'等。
      除了一般的繞過方法,一些特別的關(guān)鍵字也可以使用符號(hào)替代,比如||替代'or',&&替代'and',空格可以使用
      • %09 TAB 鍵(水平)
      • %0a 新建一行
      • %0b TAB 鍵(垂直)
      • %0c 新的一頁
      • %0d return 功能
      • %a0 空格
        等替代。
  • 轉(zhuǎn)義繞過
    轉(zhuǎn)義一般是在特殊符號(hào)前加上\消去其特殊含義,例如addslashes()/mysql_real_escape_string()函數(shù)。這時(shí)可以考慮寬字節(jié)注入,當(dāng)數(shù)據(jù)庫設(shè)置了寬字節(jié)編碼時(shí)(如gbk),會(huì)將大于127的字節(jié)和其后一個(gè)字節(jié)當(dāng)作一個(gè)漢字。因此我們可以在特殊符號(hào)前加上%ed,轉(zhuǎn)義后%ed和\被編碼為一個(gè)漢字從而失去了轉(zhuǎn)義的作用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容