### 简述 Dedecms是一款开源的PHP开源网站管理系统。 DeDecms(织梦CMS) V5.7.72 正式版20180109 (最新版) 前台会员模块是采用Cookie中的 DedeUserID+DedeUserID__ckMd5字段进行身份鉴别 DedeUserID用于定位区别用户,DedeUserID__ckMd5则是服务器生成散列,用于安全验证 Dedecms一处代码由于逻辑不够严谨,导致可以输入字符并获得服务器生成散列 劫持DedeUserID__ckMd5字段,绕过安全校验,配合类型转换造成任意用户登录漏洞 漏洞详细原理 文件位置:dedecms/member/index.php:110行 ``` require_once(DEDEMEMBER . '/inc/config_space.php'); 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 {...
### 简述 Dedecms是一款开源的PHP开源网站管理系统。 DeDecms(织梦CMS) V5.7.72 正式版20180109 (最新版) 前台会员模块是采用Cookie中的 DedeUserID+DedeUserID__ckMd5字段进行身份鉴别 DedeUserID用于定位区别用户,DedeUserID__ckMd5则是服务器生成散列,用于安全验证 Dedecms一处代码由于逻辑不够严谨,导致可以输入字符并获得服务器生成散列 劫持DedeUserID__ckMd5字段,绕过安全校验,配合类型转换造成任意用户登录漏洞 漏洞详细原理 文件位置:dedecms/member/index.php:110行 ``` require_once(DEDEMEMBER . '/inc/config_space.php'); 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字段不存在就会走进这个分支 ``` } else { $last_vid = $uid; } ``` 也就变为`$last_vid`可控,然后`$last_vid`经过`PutCookie`函数进行处理 ### 顺便一提 文件位置:dedecms/include/helpers/cookie.helper.php PutCookie这个函数是Dedecms在setcookie时封装的函数 GetCookie这个函数是Dedecms在获取Cookie中值封装的函数 如果Set一个键值对,PutCookie会Set两对Cookie,一个是要SET的键值对 另一个是值和key进行md5的哈希再截取前十六位的安全校验字符串(键名为$key+'__ckMd5') ``` if ( ! function_exists('PutCookie')) { function PutCookie($key, $value, $kptime=0, $pa="/") { global $cfg_cookie_encode,$cfg_domain_cookie; setcookie($key, $value, time()+$kptime, $pa,$cfg_domain_cookie); setcookie($key.'__ckMd5', substr(md5($cfg_cookie_encode.$value),0,16), time()+$kptime, $pa,$cfg_domain_cookie); } } ``` GetCookie在返回键值之前,会通过PutCookie生成的十六位安全校验字符串对键值进行安全校验 确保获得的键值对有效且为服务器Set,增强安全性(但这里并不能抵御密文重放) ``` 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]; } } } } ``` 在`$last_vid`经过`PutCookie`函数进行处理后,我们已经在`Cookie`可以获得校验哈希,绕过安全校验 ### Payload注入点 文件位置:dedecms/include/memberlogin.class.php:161行 ``` 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(); } } } ``` 我们注入0000001(注册账户的账户名)和对应的`__ckMd`5校验值 ``` $this->M_ID = $this->GetNum(GetCookie("DedeUserID")); ``` 这里必须注册名0000001为的账户,不然没法通过另一个校验页面(校验账户是否存在) 文件位置:dedecms/member/inc/config_space.php ``` if(!is_array($_vars)) { ShowMsg("你访问的用户可能已经被删除!","javascript:;"); exit(); } ``` `$this->M_ID`赋值后变为0000001,然后巧妙地经过intval类型转换,变为1,也就是admin的id,也可以是任意用户的id ``` $this->M_ID = intval($this->M_ID); ``` 然后带入了SQL查询语句,然后使用查询结果对登录信息进行赋值,造成任意用户登录。 ``` 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']); ``` ### 漏洞演示 ①注册0000001账户(用于登录admin,其他账户类推) ②注入Payload并获安全校验值  ③  ### 漏洞更深入(管理员密码重置) ①利用之前的老漏洞重置admin的密码,这时只重置了member表里面的admin密码 ②利用现在这个漏洞登录admin,然后访问member/edit_baseinfo.php页面,member/edit_baseinfo.php中的修改信息的老密码判断是跟member表进行对比,刚好被我们上面步骤①重置了 ③满足条件后,修改密码的时候会同时修改admin.member两张表的密码 文件位置:dedecms/member/edit_baseinfo.php:109行 ``` if( !in_array($sex, array('男','女','保密')) ) { ShowMsg('请选择正常的性别!','-1'); exit(); } $query1 = "UPDATE `#@__member` SET pwd='$pwd',sex='$sex'{$addupquery} where mid='".$cfg_ml->M_ID."' "; $dsql->ExecuteNoneQuery($query1); //如果是管理员,修改其后台密码 if($cfg_ml->fields['matt']==10 && $pwd2!="") { $query2 = "UPDATE `#@__admin` SET pwd='$pwd2' where id='".$cfg_ml->M_ID."' "; $dsql->ExecuteNoneQuery($query2); } // 清除会员缓存 $cfg_ml->DelCache($cfg_ml->M_ID); ShowMsg('成功更新你的基本资料!','edit_baseinfo.php',0,5000); exit(); ```