作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/TRCdXZU8v1NsbFhZKLa1Qw
0x00 漏洞信息簡介
Crossday Discuz! Board(簡稱 Discuz!)是北京康盛新創科技有限責任公司推出的一套通用的社區論壇軟件系統。自2001年6月面世以來,Discuz!已擁有15年以上的應用歷史和200多萬網站用戶案例,是全球成熟度最高、覆蓋率最大的論壇軟件系統之一。目前最新版本Discuz! X3.4正式版于2017年8月2日發布,去除了云平臺的相關代碼,是 X3.2 的穩定版本。此次漏洞位于/source/module/misc/misc_imgcropper.php中的54行處的$prefix可控導致SSRF。
文章參考Discuz x3.4 前臺 SSRF 分析。該漏洞公開時間為2018年12月3日,文章地址為Discuz x3.4前臺SSRF,由于該文章存在密碼,可以查看轉載地址:文章轉載地址
0x01 漏洞詳細分析
Discuz開源地址為Gitee,使用git clone 克隆到本地
git clone https://gitee.com/Discuz/DiscuzX
根據補丁提交記錄來切換到漏洞修復前的前一個commit版本
git checkout a5c1b95dc4464ee3da0ebd4655d30867f85d6ae9
本地搭建好運行環境之后首先訪問頁面http://www.a.com/dz/DiscuzX/upload/misc.php?mod=imgcropper,然后點擊裁切按鈕并抓包

攔截之后重放數據包在提交內容位置添加參數cutimg和picflag,紅框處填寫需要請求的IP地址并發送數據包
&cutimg=/dz/DiscuzX/upload/member.php%3fmod%3dlogging%26action%3dlogout%26referer%3d//c%2523%2540192.168.163.131%26quickforward%3d1&picflag=2

這時服務器將成功收到請求

下面來看看后端是怎么處理的,斷點地址為source/module/misc/misc_imgcropper.phpline 54。當傳遞的picflag為2時取$_G['setting']['ftp']['attachurl']變量的值"/"。接下來55行接收拼接可控變量cutimg。

既然可控,那么就要看看它后面是怎么處理的,來到Thumb方法

進入init方法,到達parse_url方法后在source中解析出了host。此時就會進入dfsockopen
//dz/DiscuzX/upload/member.php?mod=logging&action=logout&referer=//c%23%40192.168.163.131&quickforward=1

parse_url支持//baidu.com/s這種形式的url解析

繼續跟進dfsockopen方法,在該方法中又進行了解析,處理同上,由于不存在協議所以scheme為null,這樣在最后拼接出來的URL就是://xx.com/,這樣的鏈接會自動補上協議,所以最后為http://://xx.com/。

在windows中使用curl請求該地址最終解析到了192.168.163.1,也就是某個網卡的本地地址。請求路徑為http://192.168.163.1/xx.com

此時我們能夠進行內網請求,但是地址并不可控,所以需要找到一個discuz可以進行任意url跳轉的漏洞,再請求該路徑跳轉出去。discuz在退出的時候會取get參數referer中的值來進行跳轉,下面來分析跳轉處的代碼。
function dreferer($default = '') {
global $_G;
$default = empty($default) && $_ENV['curapp'] ? $_ENV['curapp'].'.php' : '';
//獲取 $_GET['referer']參數
$_G['referer'] = !empty($_GET['referer']) ? $_GET['referer'] : $_SERVER['HTTP_REFERER'];
$_G['referer'] = substr($_G['referer'], -1) == '?' ? substr($_G['referer'], 0, -1) : $_G['referer'];
if(strpos($_G['referer'], 'member.php?mod=logging')) {
$_G['referer'] = $default;
}
$reurl = parse_url($_G['referer']);
//如果存在協議則判斷是否為http和https,不存在則不判斷
if(!$reurl || (isset($reurl['scheme']) && !in_array(strtolower($reurl['scheme']), array('http', 'https')))) {
$_G['referer'] = '';
}
if( !empty($reurl['host']) &&
//判斷解析的host是否為 $_SERVER['HTTP_HOST']
!in_array($reurl['host'], array($_SERVER['HTTP_HOST'], 'www.'.$_SERVER['HTTP_HOST'])) &&
//判斷 $_SERVER['HTTP_HOST']是否存在于解析出的host中
!in_array($_SERVER['HTTP_HOST'], array($reurl['host'], 'www.'.$reurl['host']))) {
if(!in_array($reurl['host'], $_G['setting']['domain']['app']) &&
!isset($_G['setting']['domain']['list'][$reurl['host']])) {
//截取解析的host第一個.后面的所有內容,沒有.則當長度為1時則返回為空
$domainroot = substr($reurl['host'], strpos($reurl['host'], '.')+1);
//$_G['setting']['domain']['root']為array且為空
if(empty($_G['setting']['domain']['root']) ||
(is_array($_G['setting']['domain']['root']) &&
//想要不進入這個判斷需要保證 $domainroot為空,這樣referer才不會被覆蓋,才能實現任意地址跳轉
!in_array($domainroot, $_G['setting']['domain']['root']))) {
$_G['referer'] = $_G['setting']['domain']['defaultindex'] ? $_G['setting']['domain']['defaultindex'] : 'index.php';
}
}
} elseif(empty($reurl['host'])) {
$_G['referer'] = $_G['siteurl'].'./'.$_G['referer'];
}
$_G['referer'] = durlencode($_G['referer']);
return $_G['referer'];
}
在上面的代碼中只要我們做到$_G['referer']不被覆蓋即可,首先解析的host中存在協議則判斷是否為http和https,不存在則不判斷,所以我們可以不傳入協議。第二處判斷解析的host是否為$_SERVER['HTTP_HOST'],如果是則不進入if覆蓋$_G['referer']。但是這樣的話在實際的ssrf跳轉場景中$_SERVER['HTTP_HOST']為空。

所以這個條件無法生效。后面的一個關鍵判斷$domainroot = substr($reurl['host'], strpos($reurl['host'], '.')+1);會截取解析的host第一個.后面的所有內容,沒有.并且當長度為1時則返回為空。返回為空時后面的!in_array($domainroot, $_G['setting']['domain']['root']))這個條件就為false。也就不會進入if判斷覆蓋$_G['referer']了。但是這兒存在一個問題,如果我們host為a那么最后通過curl跳轉的時候就往a跳轉了。不能指定任意地址。此時可以利用parse_url和curl的解析差異來繞過這個限制。構造//a#@1.1.1.1,那么parse_url將解析host為a,而curl解析host為1.1.1.1。所以就得到了構造的完整url。

0x02 漏洞利用手段
最后的利用流程為:ssrf訪問本地接口進行URL跳轉 ====301====> 跳轉到目標服務器,服務器上使用跳轉腳本進行協議轉換或者任意路徑訪問
=====302=====>
通過指定協議如gopher,訪問指定路徑/_test...等
首先通過服務器搭建跳轉腳本index.php
<?php
header("Location: gopher://127.0.0.1:2333/_test");
?>
本地進行nc監聽

ssrf跳轉服務器地址

成功將請求轉發到本地發送test消息

0x03 漏洞修復方法
官方在補丁提交記錄[2]的版本提交中對漏洞進行了修復,修復方式為重寫了dfsockopen中調用的parse_url為_parse_url,在該方法中判斷了parse_url是否能夠解析出協議,無法解析則退出。


0x04 漏洞思考總結
這個漏洞的成功利用離不開對parse_url解析特性的了解,parse_url成功從cutimg變量中解析出host,才能調用dfsockopen方法,在該方法中使用curl請求拼接了前綴的地址://xx.com。這將請求本地地址,通過尋找discuz的url跳轉來將本地請求轉發出去。而在url跳轉的利用中使用到了parse_url和curl對//a#@1.1.1.1的解析差異來完成任意地址訪問。最后在訪問地址使用302跳轉來達到使用指定協議請求指定路徑或發送數據的目的。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1726/
暫無評論