DedeCMS的include目录下common.ini.php的_RunMagicQuotes函数对用户的输入进行较为严格的过滤,但是并未对_SERVER和_FILE 进行过滤。
include/common.inc.php
function _RunMagicQuotes(&$svar)
{
if (!@get_magic_quotes_gpc()) {
if (is_array($svar)) {
foreach ($svar as $_k => $_v) {
$svar[$_k] = _RunMagicQuotes($_v);
}
} else {
if (strlen($svar) > 0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#', $svar)) {
exit('Request var not allow!');
}
$svar = addslashes($svar);
}
}
return $svar;
}
回到漏洞本身,文件包含的漏洞触发点位于plus/flink.php 文件的ShowMsg函数。
if ($dopost == 'save') {
$validate = isset($validate) ? strtolower(trim($validate)) : '';
$svali = GetCkVdValue();
if ($validate == '' || $validate != $svali) {
ShowMsg('验证码不正确!', '-1');
exit();
}
ShowMsg函数又由include/common.func.php 定义。
function ShowMsg($msg, $gourl, $onlymsg = 0, $limittime = 0)
{
if (empty($GLOBALS['cfg_plus_dir'])) {
$GLOBALS['cfg_plus_dir'] = '..';
}
if ($gourl == -1) { #1
$gourl = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; #2
if ($gourl == "") {
$gourl = -1;
}
}
$htmlhead = "
<html>\r\n<head>\r\n<title>DedeCMS提示信息
...
<script>\r\n&#34;;
$htmlfoot = &#34;
</script>
...
</body>\r\n</html>\r\n&#34;;
$litime = ($limittime == 0 ? 1000 : $limittime);
$func = &#39;&#39;;
//...
if ($gourl == &#39;&#39; || $onlymsg == 1) {
//...
} else {
//...
$func .= &#34;var pgo=0;
function JumpUrl(){
if(pgo==0){ location=&#39;$gourl&#39;; pgo=1; }
}\r\n&#34;;
$rmsg = $func;
//...
if ($onlymsg == 0) {
if ($gourl != &#39;javascript:;&#39; && $gourl != &#39;&#39;) {
$rmsg .= &#34;<br /><a href=&#39;{$gourl}&#39;>如果你的浏览器没反应,请点击这里...</a>&#34;; #3
$rmsg .= &#34;<br/></div>\&#34;);\r\n&#34;;
$rmsg .= &#34;setTimeout(&#39;JumpUrl()&#39;,$litime);&#34;;
} else {
//...
}
} else {
//...
}
$msg = $htmlhead . $rmsg . $htmlfoot; #4
}
$tpl = new DedeTemplate();
$tpl->LoadString($msg); #5
$tpl->Display(); #6
}
在include/common.func.php文件中有几个关键点;
当$gourl==-1时,$gourl=$SERVER[&#39;HTTP_REFERER&#39;],$SERVER不在过滤文件中,而$_SERVER[&#39;HTTP_REFERER&#39;]用户可控。
$rmsg中包含$gourl;
$msg = $htmlhead . $rmsg . $htmlfoot;
$msg传入LoadString中,$tpl->LoadString($msg);<==>$gourl传入LoadString中;
最后调用Display函数;
LoadString函数和Display函数位于
include/dedetemplate.class.php
class DedeTemplate
{
//...
public function LoadString($str = &#39;&#39;)
{
$this->sourceString = $str; #1
$hashcode = md5($this->sourceString);
$this->cacheFile = $this->cacheDir . &#34;/string_&#34; . $hashcode . &#34;.inc&#34;;
$this->configFile = $this->cacheDir . &#34;/string_&#34; . $hashcode . &#34;_config.inc&#34;;
$this->ParseTemplate();
}
//...
public function Display()
{
global $gtmpfile;
extract($GLOBALS, EXTR_SKIP);
$this->WriteCache(); #2
include $this->cacheFile; #3
}
在LoadString中,将接收到的$msg赋值给$this->sourceString变量;
在Display函数中,调用了WriteCache函数,同时包含了cacheFile;
继续跟进WriteCache函数;
public function WriteCache($ctype = &#39;all&#39;)
{
if (!file_exists($this->cacheFile) || $this->isCache == false
|| (file_exists($this->templateFile) && (filemtime($this->templateFile) > filemtime($this->cacheFile)))
) {
if (!$this->isParse) {
//...
}
$fp = fopen($this->cacheFile, &#39;w&#39;) or dir(&#34;Write Cache File Error! &#34;);
flock($fp, 3);
$result = trim($this->GetResult()); #1
$errmsg = &#39;&#39;;
if (!$this->CheckDisabledFunctions($result, $errmsg)) { #2
fclose($fp);
@unlink($this->cacheFile);
die($errmsg);
}
fwrite($fp, $result);
fclose($fp);
//...
}
在WriteCache中,调用了GetResult函数,将函数返回值赋值$result变量
而在GetResult中,最后返回$this->SourceString赋值$result;
在LoadString函数中,在LoadString中,将接收到的$msg赋值给$this->sourceString变量,$msg中又包含$gourl,也就是用户可控的referer头;
然后调用了CheckDisabledFunctions函数,并将$result传入。
/**
* 把分析模板输出到一个字符串中,并返回
*
* @access public
* @return string
*/
public function GetResult()
{
$ResultString = &#39;&#39;;
if ($this->Count == -1) {
return $this->SourceString;
}
$this->AssignSysTag();
$nextTagEnd = 0;
$strok = &#34;&#34;;
for ($i = 0; $i <= $this->Count; $i++) {
$ResultString .= substr($this->SourceString, $nextTagEnd, $this->CTags[$i]->StartPos - $nextTagEnd);
$ResultString .= $this->CTags[$i]->GetValue();
$nextTagEnd = $this->CTags[$i]->EndPos;
}
$slen = strlen($this->SourceString);
if ($slen > $nextTagEnd) {
$ResultString .= substr($this->SourceString, $nextTagEnd, $slen - $nextTagEnd);
}
return $ResultString;
}
CheckDisabledFunctions函数主要功能利用in_array函数是对输入的$str进行过滤,不允许出现对应的危险函数。
public function CheckDisabledFunctions($str, &$errmsg = &#39;&#39;)
{
global $cfg_disable_funs;
$cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : &#39;phpinfo,eval,exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_put_contents,fsockopen,fopen,fwrite&#39;;
// 模板引擎增加disable_functions
if (!defined(&#39;DEDEDISFUN&#39;)) {
$tokens = token_get_all_nl($str);
$disabled_functions = explode(&#39;,&#39;, $cfg_disable_funs);
foreach ($tokens as $token) {
if (is_array($token)) {
if ($token[0] = &#39;306&#39; && in_array($token[1], $disabled_functions)) {
$errmsg = &#39;DedeCMS Error:function disabled &#34;&#39; . $token[1] . &#39;&#34; <a href=&#34;http://help.dedecms.com/install-use/apply/2013/0711/2324.html&#34; target=&#34;_blank&#34;>more...</a>&#39;;
return false;
}
}
}
}
return true;
}
最后回到WriteCache函数,将$result写入缓存文件中;
并在LoadString函数中包含该缓存文件;
而referer头中并未过滤双引号,所以可以使用双引号绕过in_array函数;
![](http://picx.zhimg.com/v2-65a4b4c5ae958a4e5e3b61d19b552718_r.jpg?source=1940ef5c)
RCE漏洞的利用链如图所示:
![](http://picx.zhimg.com/v2-47a084829f8765aab975a4a3a2949d78_r.jpg?source=1940ef5c)
dedecms已经对漏洞修复,用户无法无法控制gourl变量,并去掉了模板渲染,改成echo。但挖掘的思路和思考的方式很值得学习,功能越复杂,漏洞越容易产生。 |