去年的強網杯,出了一道mysql堆疊注入叫隨便注,這道題被好多比賽玩了一整年,直到現在還是有各種新姿勢,但是今天我忽然想到似乎沒有對這個題目有一個很認真的分析,因此這裡總結一下這個題目的出題用意和原本的預期做法:
堆疊注入
Stacked injections:堆疊注入。從名詞的含義就可以看到應該是一堆sql語句(多條)一起執行。而在真實的運用中也是這樣的,我們知道在mysql中,主要是命令行中,每一條語句結尾加 ; 表示語句結束。這樣我們就想到了是不是可以多句一起使用。這個叫做stacked injection。代碼中和一般查詢不同的是,使用了multi_query函數
在SQL中,分號(;)是用來表示一條sql語句的結束。試想一下我們在 ; 結束一個sql語句後繼續構造下一條語句,會不會一起執行?因此這個想法也就造就了堆疊注入。而union injection(聯合注入)也是將兩條語句合併在一起,兩者之間有什麼區別麼?區別就在於union 或者union all執行的語句類型是有限的,可以用來執行查詢語句,而堆疊注入可以執行的是任意的語句。
源碼分析
<?php
$inject = $_GET['inject'] ?? false;
if ($inject) {
$preg_match = 'return preg_match("/select|update|show|use|updatexml|extractvalue|exp|pow|char|delete|ascii|substr|sleep|if|strcmp|left|mid|concat|drop|insert|where|\./i", $inject);';
if (eval($preg_match)) {
echo "您輸入了敏感字符!";
exit();
}
if(stristr($inject, "set") && stristr($inject, "prepare")){
echo "請不要同時輸入set和prepare";
exit();
}
當時的強網杯似乎在stristr這個函數上漏寫了i導致大家用大小寫繞過,但是沒事這個不是重點(逃)
這裡過濾了很多查詢關鍵字,比如select,比如盲注用的函數,基本上是沒有辦法的,但是這裡是堆疊注入,就給了我們以執行多條sql語句的機會。
很多同學自然就想到了set+prepare的預處理語句,但是這裡規定了不能夠同時輸入set和prepaere,又被堵死了,但是mysql還有一個可以讓語句分開執行且達到等同於一起執行的效果,這裡介紹一下正解,mysql的存儲過程。
存儲過程
存儲過程(Stored Procedure)是一種在資料庫中存儲複雜程序,以便外部程序調用的一種資料庫對象。
存儲過程是為了完成特定功能的SQL語句集,經編譯創建並保存在資料庫中,用戶可通過指定存儲過程的名字並給定參數(需要時)來調用執行。
存儲過程思想上很簡單,就是資料庫 SQL 語言層面的代碼封裝與重用。類比面向對象編程的類。說白了,存儲過程就是具有名字的一段代碼,用來完成一個特定的功能。
大家看下面這個很熟悉的例子,這個例子在網絡上是教程範例,給大家熟悉一下。無非就是in和out。
create 存儲過程,然後call調用。
鑑於很多人不太理解這個in和out的區別,我們再簡單提一提。
in——傳入參數(只索取,不給予)
調用者可以給"過程"一個值,但是過程不會把這個值返回給你。比如
mysql> delimiter $$
mysql> set @p_in=1;
mysql> create procedure in_param(in p_in int)
-> begin
-> select p_in;
-> set p_in=2;
-> select p_in;
-> end$$
mysql> delimiter ;
mysql> call in_param(@p_in);
+------+
| p_in |
+------+
| 1 |
+------+
1 row in set (0.00 sec)
#因為這裡先set了p_in=1,所以存儲過程裡select p_in的值為1
+------+
| p_in |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
#然後set p_in=2,所以存儲過程裡第二個select p_in的值為2
Query OK, 0 rows affected (0.00 sec)
mysql> select @ p_in;
+------+
| p_in |
+------+
| NULL |
+------+
1 row in set (0.00 sec)
#存儲過程執行完,他不會把這個值調用者,調用者在過程外是不能夠使用這個變化後的值的
in參數就是只能輸入,不能夠輸出,執行完過程,in參數是不會改變的。
out——傳出參數(不索取,只給予)
out這個參數剛好就是反過來,調用者無論怎麼給參數賦初始值,"過程"裡都是當作空值開始處理,然後將處理好的值返回給調用者。
分離預處理語句
好了說了這麼多,我們還是要結合題目來看看,題目這裡明顯是要用預處理set 和 prepare來做,但是不能夠同時輸入。經過上面的講解,我們可以想到,雖然set和prepare不能夠同時輸入,但是我只要把set給封裝到一個"過程"中去,是不是就可以利用存儲過程來代替set呢?
再理一遍大家很熟悉的set preare的注入poc怎麼寫:
這個地方的string就是我們要執行的sql語句的16進位表示,stmt則是預處理語句的別名。然後我們把他分為兩個部分:
首先我們需要創建一個將set包含進去的存儲過程,然後分析輸入和輸出參數:
in參數是sql注入語句的16進位,因為需要繞過敏感字符過濾,並且我們需要輸入這個hex給過程拿去利用;out參數則是我們的set裡的@string——預處理語句,在"過程"中賦值好以後,拿出來給我們的prepare使用。因此我們的poc可以這麼寫:下面的uuid代表php代碼生成的隨機數。第一次輸入存儲過程的定義:
114514';
create procedure `{$uuid}`(out string text(1024), in hex text(1024))
BEGIN
SET string = hex;
END;
;--
第二次用call調用這個存儲過程(@decoded其實就是傳參,傳到上一個poc的string位置,為了和string區分開,就用了另一個名詞):
之後就會正常的執行set+prepare的注入了。
總結
其實大家如果實在不清楚這個in和out,可以使用inout來代替,inout參數是既可以輸入,又可以輸出。
說白了存儲過程就是sql語句裡的函數,可以封裝代碼,所以比賽中如果遇到了不能一起使用的關鍵字,可以嘗試著使用存儲過程將其分割開。
合天網安實驗室相關實驗推薦==繞過UNION&SELECT過濾(針對於過濾,我們要善用編碼,來繞過關鍵字過濾。本實驗主要介紹繞過UNION&SELECT過濾的技巧。)