SSRF逆向分析
0x00 前言
之前有復現過一些漏洞,但是每次按照別人的思路復現完了之后感覺還是有很多疑問,知道了怎么做但是不知道為什么這么做,所以這次我嘗試自己從補丁一步步找到攻擊鏈,構造poc。
0x01 收集情報
補丁地址:
https://gitee.com/ComsenzDiscuz/DiscuzX/commit/41eb5bb0a3a716f84b0ce4e4feb41e6f25a980a3
查看補丁,發現如下:
刪去了followlocation,也就是說對於301/302請求,curl不會去跟蹤跳轉。
既然這里存在一個跳轉ssrf,下面就是逆向調用鏈,找到程序的入口。
0x02 嘗試逆向找到觸發點
首先這個存在漏洞的函數是_dfsockopen,通過Ctrl+Alt+F大法找到了位於function_core.php的dfsockopen方法。
繼續向上找,找到了一處import_block方法。
通過對dfsockopen的第一個參數進行分析,發現其剛好是import_block的第一個參數經過一些處理之后的結果。
由於參數可控,繼續向上。
雞凍人心的發現!第一個參數直接以$_GET傳了進去!
0x03 嘗試構造payload
下面看一下如何訪問到這個語句:
首先,直接通過文件肯定是訪問不了的(L10-12)。下面根據L19和L21確定url中基本要必須存在的參數。經過一系列的嘗試和Ctrl+Alt+F,終於找到了入口:
/upload/admin.php?action=blockxml&operation=add
跟進一下submitcheck()
繼續跟進getgpc()
大概就是返回$_GET[$k],由於這里的$k就是從前面的submitcheck('addsubmit')傳進來的,所以這里只要保證$_GET['addsubmit']即可,構成的url如下:
/upload/admin.php?action=blockxml&operation=add&addsubmit=test
繼續跟,
可以看到getgpc返回了$_GET['addsubmit']的值,由於我們的url參數中有此參數,因此進入到了else語句塊。繼續跟進submitcheck
這里又有一個相同的getgpc(),由於參數跟剛剛也相同,就不繼續跟了,直接進入到else語句塊。可以看到,首先22行有個if語句,必須把條件滿足成True,否則是False的話就直接進入Else語句塊,這條鏈就直接中斷掉了。仔細看一下這個if條件:
$allowget || ($_SERVER['REQUEST_METHOD'] == 'POST' &&
!empty($_GET['formhash']) && $_GET['formhash'] == formhash() &&
empty($_SERVER['HTTP_X_FLASH_VERSION']) &&
(empty($_SERVER['HTTP_REFERER']) || strncmp($_SERVER['HTTP_REFERER'], 'http://wsq.discuz.com/', 22)
=== 0 || preg_replace("/https?:\/\/([^\:\/]+).*/i", "\\1",
$_SERVER['HTTP_REFERER']) == preg_replace("/([^\:]+).*/", "\\1",
$_SERVER['HTTP_HOST'])))
最外層是個or,如果$allowget是True就直接省事兒了,可是這是此方法的第二個參數,默認為0,pass。剩下的邏輯如下:
1)必須是POST請求 &&
2)GET請求中必須有formhash參數 &&
3)formhash的值必須等於formhash() &&
4)請求頭中沒有HTTP_X_FLASH_VERSION &&
5.1)refer為空 ||
5.2)referer的值以http://wsq.discuz.com/開頭 ||
5.3)referer與host的主機名部分必須相同
第1、4、5條件好滿足,直接抓包改即可。主要看第二個請求和第三個請求,即如何獲取這個formhash。看一下函數定義:
其大致是計算一個數的MD5,這個數由幾個$_G變量組成。既然不是一個固定的值,那么首先肯定是服務端先發給客戶端,然后客戶端才能帶着這個$_GET['formhash']來進行請求,下面全局搜一下formhash,發現很多頁面中都有這個字段:
然后隨手在頁面上查找一下,沒想到真找到了:
(經過一些測試,這里有個比較坑的點是這個formhash在同一個session請求中是不會變的,不過前台和后台的formhash不是同一個,你不能拿前台獲取的formhash作為參數去訪問后台的接口)。
formhash的問題到這里就解決了,會看一下上面的條件,構成的url暫時如下:
POST ..../upload/admin.php?action=blockxml&operation=add&addsubmit=test&formhash=2b23ba6f
並且去掉referer頭。
請求之后可以發現,成功進入了if語句,然后順其自然的到了return True。
然后就終於回到了最開始的地方,成功調用import_block()
由於這里需要$_GET['xmlurl'],我們暫且傳入http://127.0.0.1:2222。
可以看到,最后url賦值給了$signurl,其值變成:
http://127.0.0.1:2222?charset=utf-8&clientid=&op=getconfig&sign=
沒有什么太大的變化,繼續跟進后就到了最開始說的那個可能存在ssrf的_dfsockopen方法了。通過下圖可以看到,先是在33行調用parse_url對用戶傳來的url進行解析,然后調用_isLocalip()來檢查host是否是內網地址,如果是內網地址則直接return掉。所以就算這里存在ssrf,我們的url中也是不能直接傳內網地址進來的。
接着看,這里在88行發送了請求,我在這次請求中傳入的url是:upload/admin.php?action=blockxml&operation=add&addsubmit=test&formhash=2b23ba6f&xmlurl=http://127.0.0.1:2222
這里看一下我本地監聽的2222端口:
訪問成功了。
下面的整理下思路,由於程序對內網地址進行了限制,導致了除127.0.0.1之外的內網地址都會直接return掉,因此這里我們需要通過一個301跳轉,來實現繞過程序對內網url的限制。
可是如果想要curl自動重定向到第一個url返回的地址中去,就必須先要將此curl的CURLOPT_FOLLOWLOCATION屬性設置為true才行。然而這一點在本文一開始就已經確認了:
下面就可以通過在vps上上傳一個301跳轉的php腳本,內容如下:
下面把我們之前的payload中的xmlurl改成我的公網vps的ip,然后重放,同時在本地監聽9999端口。
請求結果如下,可以發現,本地的9999端口果然收到了discuz-curl發來的請求!
我的vps的http日志:
至此,這條ssrf的攻擊鏈就已經形成了。
0x04 總結
這次跟下來還是學到了一些東西的,比如構造payload時會遇到的一些坑,然后自己對ssrf也有了跟深入的一些理解。