dede cookie.helper.php,dedecms前台任意用户登录漏洞重现及分析

0x00 概述

2018年1月,网上爆出dedecms v5.7 sp2前台任意用户登录漏洞,利用此漏洞可以让管理员登录前台,是重置管理员后台密码的组合拳中的第二式。

组合拳第一式:dedecms前台任意用户密码重置漏洞重现及分析

0x01漏洞重现

需要先将用户0000001通过审核,再访问

http://127.0.0.1:8999/lsawebtest/vulnenvs/dedecms/dedecms-v57-utf8-sp2-full/member/index.php?uid=0000001

d61871349f841f04fffde4899e62a390.png

获取cookie中last_vid_ckMd5值48741df1f12d04bd

再登录0000001帐号

然后设置DeDeUserID_ckMd5为last_vid_ckMd5的值,并设置DedeUserID为0000001。

18f403f5cc508c4bb29eff4f4932a808.png

成功以管理员登录前台

964e70b4a5dc21a6072629c276a3b5fe.png

0x02修复方案

若不影响业务,可以尝试注释下面代码

member/index.php:163~164

PutCookie(‘last_vtime’, $vtime, 3600*24, ‘/’);

PutCookie(‘last_vid’, $last_vid, 3600*24, ‘/’);

关注官方补丁。

0x03漏洞分析

判断用户登录的函数在

include\memberlogin.class.php:292

function IsLogin()

{

if($this->M_ID > 0) return TRUE;

else return FALSE;

}

再到138行

class MemberLogin

{

var $M_ID;

var $M_LoginID;

var $M_MbType;

var $M_Money;

var $M_Scores;

var $M_UserName;

var $M_Rank;

var $M_Face;

var $M_LoginTime;

var $M_KeepTime;

var $M_Spacesta;

var $fields;

var $isAdmin;

var $M_UpTime;

var $M_ExpTime;

var $M_HasDay;

var $M_JoinTime;

var $M_Honor = '';

var $memberCache='memberlogin';

//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}' ");

}

可以看到

$this->M_ID = $this->GetNum(GetCookie("DedeUserID"));

在看看GetNum函数,在398行

function GetNum($fnum){

$fnum = preg_replace("/[^0-9\.]/", '', $fnum);

return $fnum;

}

替换非数字字符为空。

还有

$this->M_ID = intval($this->M_ID);

整体来说就是从cookie中获取DedeUserID的值,去除非数字字符,再经过整形转化,形成mid,再进入数据库查询。

继续跟进GetCookie函数,

include\helpers\cookie.helper.php:54

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];

}

}

}

}

关键一行

if($_COOKIE[$key.'__ckMd5']!=substr(md5($cfg_cookie_encode.$_COOKIE[$key]),0,16))

这个$cfg_cookie_encode是未知的,需要任意文件读取或下载才能获得,这形似加salt的方式保证了cookie只能服务端生成,防止客户端伪造,这里需要DedeUserID__ckMd5的值 == ($cfg_cookie_encode.$_COOKIE[DedeUserID])的md5的前16位才能通过验证。

至此,进入下一阶段,看看DedeUserID_ckMd5和DedeUserID的来源,他们在登录后产生,来看看登录代码,

include\memberlogin.class.php:469

function CheckUser(&$loginuser, $loginpwd)

{

global $dsql;

//检测用户名的合法性

$rs = CheckUserID($loginuser,'用户名',FALSE);

//用户名不正确时返回验证错误,原登录名通过引用返回错误提示信息

if($rs!='ok')

{

$loginuser = $rs;

return '0';

}

//matt=10 是管理员关连的前台帐号,为了安全起见,这个帐号只能从后台登录,不能直接从前台登录

$row = $dsql->GetOne("SELECT mid,matt,pwd,logintime FROM `#@__member` WHERE userid LIKE '$loginuser' ");

if(is_array($row))

{

if($this->GetShortPwd($row['pwd']) != $this->GetEncodePwd($loginpwd))

{

return -1;

}

else

{

//管理员帐号不允许从前台登录

if($row['matt']==10) {

return -2;

}

else {

$this->PutLoginInfo($row['mid'], $row['logintime']);

return 1;

}

}

}

else

{

return 0;

}

}

/**

* 保存用户cookie

*

* @access public

* @param string $uid 用户ID

* @param string $logintime 登录限制时间

* @return void

*/

function PutLoginInfo($uid, $logintime=0)

{

global $cfg_login_adds, $dsql;

//登录增加积分(上一次登录时间必须大于两小时)

if(time() - $logintime > 7200 && $cfg_login_adds > 0)

{

$dsql->ExecuteNoneQuery("Update `#@__member` set `scores`=`scores`+{$cfg_login_adds} where mid='$uid' ");

}

$this->M_ID = $uid;

$this->M_LoginTime = time();

$loginip = GetIP();

$inquery = "UPDATE `#@__member` SET loginip='$loginip',logintime='".$this->M_LoginTime."' WHERE mid='".$uid."'";

$dsql->ExecuteNoneQuery($inquery);

if($this->M_KeepTime > 0)

{

PutCookie('DedeUserID',$uid,$this->M_KeepTime);

PutCookie('DedeLoginTime',$this->M_LoginTime,$this->M_KeepTime);

}

else

{

PutCookie('DedeUserID',$uid);

PutCookie('DedeLoginTime',$this->M_LoginTime);

}

}

可以看到登录验证成功就

PutCookie('DedeUserID',$uid,$this->M_KeepTime);

而这个$uid是mid(不是用户名),

自然要来看看PutCookie方法了,

跟进PutCookie方法,

include\helpers\cookie.helper.php:21

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);

}

}

关键是这几行

setcookie($key, $value, time()+$kptime, $pa,$cfg_domain_cookie);

setcookie($key.'__ckMd5', substr(md5($cfg_cookie_encode.$value),0,16),

这里就设置了DedeUserID和DedeUserID_ckMd5。

现在再进入下一个阶段,若需要伪造cookie使管理员登录前台,必须满足

DedeUserID__ckMd5的值 == ($cfg_cookie_encode.$_COOKIE[DedeUserID])的md5的前16位

而管理员的DedeUserID已知为1,$cfg_cookie_encode无法得到,所以关键在于找到满足这个条件的DedeUserID__ckMd5密文。

那就搜索PutCookie看看哪个键会满足这个条件,

c1003b53908a3e7f953730ac33b81e0c.png

member/index.php:139

关键代码:

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, '/');

可以看出$last_vid为空则把$uid赋值给$last_vid,而这个$uid就是可控的用户名,再

PutCookie('last_vid', $last_vid, 3600*24, '/');

这里假如构造出类似0000001或1abcde这样的用户名,因为last_vid__ckMd5的值 == ($cfg_cookie_encode.$_COOKIE[last_vid])的md5的前16位,满足条件!

mid=return $_COOKIE[$key];

接着在登录类的构造函数中mid经过GetNum和intval函数的过滤,就形成了1,接着进入数据库查询再展示到页面。

漏洞触发流程:

第一阶段:

访问member/index.php?uid=0000001产生last_vid和last_vid__ckMd5

第二阶段:

登录成功—>PutCookie(设置$key和$key.’__ckMd5′)—>产生DedeUserID(uid)和DedeUserID__ckMd5—>修改DedeUserID(uid)和DedeUserID__ckMd5分别为last_vid和last_vid__ckMd5—>满足GetCookie的验证条件

第三阶段:

IsLogin(M_ID)—>__construct(GetCookie(“DedeUserID”))—>GetCookie(验证DedeUserID__ckMd5值是否等于substr(md5($cfg_cookie_encode.$_COOKIE[DedeUserID]),0,16))

—>满足条件—>返回0000001给mid—>GetNum和intval函数过滤—>mid=1—>入库查询—>管理员登录前台

0x04结语

还是比较犀利的一个漏洞,是修改管理员后台密码的组合拳的第二式。

0x05参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值