前言:
马上就没有工作了,趁找不到工作这段时间潜心学习PHP代审,多多少少给自己增加一丢丢竞争力,刚毕业半年可太难了。
-
会员中心任意用户密码修改:
这是dedecms比较出名的一个漏洞,此漏洞位于会员中心——修改密码
,这里需要注意的是,dedecms默认是关闭会员中心的,需要在后台开启会员中心。
漏洞代码分析:/dedecms/member/resetpassword.php
else if($dopost == "safequestion")
{
$mid = preg_replace("#[^0-9]#", "", $id);
$sql = "SELECT safequestion,safeanswer,userid,email FROM #@__member WHERE mid = '$mid'";
$row = $db->GetOne($sql);
if(empty($safequestion)) $safequestion = '';
if(empty($safeanswer)) $safeanswer = '';
if($row['safequestion'] == $safequestion && $row['safeanswer'] == $safeanswer)
{
sn($mid, $row['userid'], $row['email'], 'N');
exit();
}
else
{
ShowMsg("对不起,您的安全问题或答案回答错误","-1");
exit();
}
}
这里可以得知:
- 可以通过安全问题验证来修改密码,
$dopost
、userid
用户可控- 将数据库中查询出来的
$row['safequestion']
和$row['safeanswer']
进行了弱类型的比较,$safequestion
和$safeanswer
用户可控,这也是最关键的地方
我们首先尝试先注册一个用户,不设置安全问题,也不填写问题答案:
执行上述代码的SQL语句查看返回结果:
也就是说$row['safequestion']
= 0,由于这里进行了弱类型比较,可以使$safequestion
=00,绕过empty()函数,$row['safeanswer']
默认为空,这里不理它就能使$row['safeanswer'] == $safeanswer
恒成立。接下来跟踪sn函数:
/**
* 查询是否发送过验证码
*
* @param string $mid 会员ID
* @param string $userid 用户名称
* @param string $mailto 发送邮件地址
* @param string $send 为Y发送邮件,为N不发送邮件默认为Y
* @return string
*/
function sn($mid,$userid,$mailto, $send = 'Y')
{
global $db;
$tptim= (60*10);
$dtime = time();
$sql = "SELECT * FROM #@__pwd_tmp WHERE mid = '$mid'";
$row = $db->GetOne($sql);
if(!is_array($row))
{
//发送新邮件;
newmail($mid,$userid,$mailto,'INSERT',$send);
}
//10分钟后可以再次发送新验证码;
elseif($dtime - $tptim > $row['mailtime'])
{
newmail($mid,$userid,$mailto,'UPDATE',$send);
}
//重新发送新的验证码确认邮件;
else
{
return ShowMsg('对不起,请10分钟后再重新申请', 'login.php');
}
}
这里没啥,继续追踪newmail函数
function newmail($mid, $userid, $mailto, $type, $send)
{
global $db,$cfg_adminemail,$cfg_webname,$cfg_basehost,$cfg_memberurl;
$mailtime = time();
$randval = random(8);
$mailtitle = $cfg_webname.":密码修改";
$mailto = $mailto;
$headers = "From: ".$cfg_adminemail."\r\nReply-To: $cfg_adminemail";
$mailbody = "亲爱的".$userid.":\r\n您好!感谢您使用".$cfg_webname."网。\r\n".$cfg_webname."应您的要求,重新设置密码:(注:如果您没有提出申请,请检查您的信息是否泄漏。)\r\n本次临时登陆密码为:".$randval." 请于三天内登陆下面网址确认修改。\r\n".$cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid;
if($type == 'INSERT')
{
$key = md5($randval);
$sql = "INSERT INTO `#@__pwd_tmp` (`mid` ,`membername` ,`pwd` ,`mailtime`)VALUES ('$mid', '$userid', '$key', '$mailtime');";
if($db->ExecuteNoneQuery($sql))
{
if($send == 'Y')
{
sendmail($mailto,$mailtitle,$mailbody,$headers);
return ShowMsg('EMAIL修改验证码已经发送到原来的邮箱请查收', 'login.php','','5000');
} else if ($send == 'N')
{
return ShowMsg('稍后跳转到修改页', $cfg_basehost.$cfg_memberurl."/resetpassword.php?dopost=getpasswd&id=".$mid."&key=".$randval);
}
}
else
{
return ShowMsg('对不起修改失败,请联系管理员', 'login.php');
}
}
当$type == 'INSERT'
时,会在数据表生成该用户的临时密码,其中pwd为8位随机字符串的key通过md5加密后的值。接着由于传入的$send = N
,会返回一个修改用户密码的地址,接着跳转:
尝试构造数据包:
美滋滋,可以修改任意用户的密码了:
-
前台文件上传漏洞(CVE-2018-20129):
漏洞代码分析:dedecms/include/dialog/select_images_post.php
require_once(dirname(__FILE__)."/config.php");
require_once(dirname(__FILE__)."/../image.func.php");
继续追踪所包含的PHP文件:common.inc.php
,然后定位到如下:
//转换上传的文件相关的变量及安全处理、并引用前台通用的上传函数
if($_FILES)
{
require_once(DEDEINC.'/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 !');
}
}
}
通过上述代码可以知道,上传的文件名不得有php等敏感后缀名,并且对Content-Type进行了限制,似乎并没有什么问题,但是在dedecms/include/dialog/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();
}
$nowtme = time();
$sparr = Array("image/pjpeg", "image/jpeg", "image/gif", "image/png", "image/xpng", "image/wbmp");
$imgfile_type = strtolower(trim($imgfile_type));
if(!in_array($imgfile_type, $sparr))
{
ShowMsg("上传的图片格式错误,请使用JPEG、GIF、PNG、WBMP格式的其中一种!","-1");
exit();
}
第二次过滤将\r
、\n
、\t
、*
、%
、\
、?
等特殊字符进行了置空处理,那么问题来了,假如上传文件名为test.jpg.p?hp
,Content-Type:image/jpeg
就能绕过上述不能包含php等敏感后缀名的限制
php文件上传成功且成功执行:
-
URL重定向:
漏洞代码分析:/dedecms/plus/download.php
else if($open==1)
{
//更新下载次数
$id = isset($id) && is_numeric($id) ? $id : 0;
$link = base64_decode(urldecode($link));
if ( !$link )
{
ShowMsg('无效地址','javascript:;');
exit;
}
$row = $dsql->GetOne("SELECT * FROM `#@__softconfig` ");
$sites = explode("\n", $row['sites']);
$allowed = array();
foreach($sites as $site)
{
$site = explode('|', $site);
$domain = parse_url(trim($site[0]));
$allowed[] = $domain['host'];
}
if ( !in_array($linkinfo['host'], $allowed) )
{
ShowMsg('非下载地址,禁止访问','javascript:;');
exit;
}
header("location:$link");
exit();
}
首先注意到header("location:$link");
,然后发现$link = base64_decode(urldecode($link));
,对$link
进行了url解码并进行了base64解码,根据解码的内容进行跳转:
最开始还担心if ( !in_array($linkinfo['host'], $allowed) )
此处判断会提前中断,但是全局搜索$linkinfo
并没有相关的信息,这就很多余了。
-
后台 GetShell:
- 方式一
漏洞代码分析:dedecms/dede/sys_verifies.php
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文件中,而$refiles是用户可控的,这时可以考虑是否存在写入恶意代码到inc文件中的可能性,关键代码:
fwrite($fp, '$files['.$i.'] = "'.$filename.'";'."\r\n");
然后我们再观察common.inc.php
文件中是否存在全局过滤:
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v)
{
if($_k == 'nvarname') ${$_k} = $_v;
else ${$_k} = _RunMagicQuotes($_v);
}
}
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);
从代码中可以得知,对用户的输入进行了全局过滤,对所有的参数值都执行了addslashes()
,那么如果想要注入恶意代码就需要解决两个问题:
- 绕过addslashes
- 闭合双引号
接着我们注意到这段代码:$filename = substr($filename,3,strlen($filename)-3);
灰常奇怪,它去掉了输入的前三个字符,如果输入:\"
,经过addslashes函数后变成:\\\"
,然后去掉前三个字符,刚好能闭合双引号,注入恶意代码:
然后就去找包含了modifytmp.inc的php文件即可实现getshell:
else if($action=='down')
{
$cacheFiles = DEDEDATA.'/modifytmp.inc';
require_once($cacheFiles);
- 方式二
漏洞代码分析:dedecms/dede/stepselect_main.php
首先注意到包含了这个文件:require_once(DEDEINC.'/enums.func.php');
查看相关代码,又发现了if(!file_exists(DEDEDATA.'/enums/system.php')) WriteEnumsCache();
/enums/system.php
文件默认不存在,追踪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 '成功更新所有枚举缓存!';
}
$egroup
默认为空,从代码中发现会从sql查询中得到的egroup直接写入到文件中,并没有任何的过滤处理:fwrite($fp,'<'."?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n");
这时考虑是否可以插入恶意的代码到数据表中,然后发现如下代码:
else if($action=='addenum_save')
{
if(empty($ename) || empty($egroup))
{
Showmsg("类别名称或组名称不能为空!","-1");
exit();
}
if($issign == 1 || $topvalue == 0)
{
$enames = explode(',', $ename);
foreach($enames as $ename)
{
$arr = $dsql->GetOne("SELECT * FROM `#@__sys_enum` WHERE egroup='$egroup' AND (evalue MOD 500)=0 ORDER BY disorder DESC ");
if(!is_array($arr)) $disorder = $evalue = ($issign==1 ? 1 : 500);
else $disorder = $evalue = $arr['disorder'] + ($issign==1 ? 1 : 500);
$dsql->ExecuteNoneQuery("INSERT INTO `#@__sys_enum`(`ename`,`evalue`,`egroup`,`disorder`,`issign`)
VALUES('$ename','$evalue','$egroup','$disorder','$issign'); ");
}
WriteEnumsCache($egroup);
ShowMsg("成功添加枚举分类!".$dsql->GetError(), $ENV_GOBACK_URL);
exit();
}
$ename
和$egroup
用户可控,当$issign=1
时可以将$egroup
插入到数据表中,然后执行WriteEnumsCache($egroup);
,这里要考虑到"?php\r\nglobal \$em_{$egroup}s;\r\n\$em_{$egroup}s = array();\r\n"
,故构造egroup=;phpinfo();$
,即<?php\r\nglobal $em_;phpinfo();$s;\r\n\$em_{$egroup}s = array();\r\n
- 方式三(CVE-2020-18917)
漏洞代码分析:dedecms/plus/search.php
//查找栏目信息
if(empty($typeid))
{
$typenameCacheFile = DEDEDATA.'/cache/typename.inc';
if(!file_exists($typenameCacheFile) || filemtime($typenameCacheFile) < time()-(3600*24) )
{
$fp = fopen(DEDEDATA.'/cache/typename.inc', 'w');
fwrite($fp, "<"."?php\r\n");
$dsql->SetQuery("Select id,typename,channeltype From `#@__arctype`");
$dsql->Execute();
while($row = $dsql->GetArray())
{
fwrite($fp, "\$typeArr[{$row['id']}] = '{$row['typename']}';\r\n");
}
fwrite($fp, '?'.'>');
fclose($fp);
}
跟方式二一样的问题,直接将SQL语句查询出来的结果写入到typename.inc文件中,并没有进行任何的过滤处理,那么想办法将精心构造payload插入到数据表中,全局搜索:__arctype
:
发现 dedecms/dede/catalog_add.php
允许插入数据到相关数据表中
//创建目录
if($ispart != 2)
{
$true_typedir = str_replace("{cmspath}", $cfg_cmspath, $typedir);
$true_typedir = preg_replace("#\/{1,}#", "/", $true_typedir);
if(!CreateDir($true_typedir))
{
ShowMsg("创建目录 {$true_typedir} 失败,请检查你的路径是否存在问题!","-1");
exit();
}
}
$in_query = "INSERT INTO `#@__arctype`(reid,topid,sortrank,typename,typedir,isdefault,defaultname,issend,channeltype,
tempindex,templist,temparticle,modname,namerule,namerule2,
ispart,corank,description,keywords,seotitle,moresite,siteurl,sitepath,ishidden,`cross`,`crossid`,`content`,`smalltypes`)
VALUES('$reid','$topid','$sortrank','$typename','$typedir','$isdefault','$defaultname','$issend','$channeltype',
'$tempindex','$templist','$temparticle','default','$namerule','$namerule2',
'$ispart','$corank','$description','$keywords','$seotitle','$moresite','$siteurl','$sitepath','$ishidden','$cross','$crossid','$content','$smalltypes')";
if(!$dsql->ExecuteNoneQuery($in_query))
{
ShowMsg("保存目录数据时失败,请检查你的输入资料是否存在问题!","-1");
exit();
}