PHPINFO为广大热爱网络技术的朋友无私的提供最新网络教程、网络工具、网络攻防、BUG漏洞,在这里能学到更好的网络技术技巧与经验!致力于安全研究 程序开发!
文章59 浏览73942

dedecms cookie伪造任意前台用户登录

【摘要】 cookie伪造任意前台用户登录 漏洞信息 漏洞公布时间:2018/01/17 影响版本:V5.7SP2正式版(2018-01-09) ...

cookie伪造任意前台用户登录

漏洞信息

  • 漏洞公布时间:2018/01/17
  • 影响版本:V5.7SP2正式版(2018-01-09)
  • 修复时间:未定
  • 漏洞条件:开启会员注册的功能、保证会员能够正常地注册

前言

dedecms在对前台用户认证时采用的是通过cookie的验证方式,通过Cookie中的DedeUserIDDedeUserID__ckMd5对当前的用户的身份以及登录状态进行验证。此漏洞的原因就是将DedeUserIDDedeUserID__ckMd5伪造成为了其他用户的值,这样就导致伪造其他用户的Cookie而能够以其他的用户登录。

 

漏洞的前提是要求开启前台用户注册的功能,保证用户能够顺利地进行注册(包括审核通过,能够查看个人空间)

漏洞说明

首先需要明确用户登录时后台的处理流程。当会员登录时,后台的处理流程时:

  1. 用户输入登陆信息时,后台是由include/memberlogin.class.php中的MemberLogin类来进行处理。
  2. 登录成功之后,用户跳转到http://localhost/member/index.php,之后便由member/index.php判断是由什么方法处理。

Cookie校验分析

在dedecms中,所有的cookie是成对出现的。如DedeUserID__ckMd5DedeUserID,以及DedeLoginTime__ckMd5DedeLoginTime。其中的DedeUserID__ckMd5这种形式是对DedeUserID进行了校验,防止前台的Cookie被任意地篡改。dedecms中所有的cookie操作都是由include/helpers/cookie.helper.php:PutCookie()方法设置的,如下:


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



$key.'__ckMd5'是由substr(md5($cfg_cookie_encode.$value),0,16)得到的,其中的cfg_cookie_encode是在程序安装时设置的。所以如果我们不知道这个值,我们是无法破解$key.'__ckMd5'。所以一般情况下,我们是无法破解$key.'__ckMd5'这类值的。

 

所有的取Cookie的操作都是由include/helpers/cookie.helper.php: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[$key . '__ckMd5']$_COOKIE[$key]进行了校验,这种方式就能够有效地避免前台任意第修改Cookie,这种方式本意上是好的,但是由于dedecms的实现问题,导致这种方式也会成为漏洞的根源。试想一下,如果我们同时修改了$_COOKIE[$key]$_COOKIE[$key . '__ckMd5']就可以绕过这个验证了。

登录校验函数

include/memberlogin.class.php中的MemberLogin

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)){
        //    some other php code
        }else{
            $this->ResetUser();
        }
    }
}
其中取得M_ID的函数是$this->M_ID = $this->GetNum(GetCookie("DedeUserID"));,其中GetNum()的做法是:



function GetNum($fnum){
    $fnum = preg_replace("/[^0-9\.]/", '', $fnum);
    return $fnum;
}



直接将不是数字的字符全部都替换掉。之后通过SQL语句Select * From '#@__member' where mid='{$this->M_ID}'来判断用户的身份。由于GetNum()采用的是替换的方法,那么如果Cookie中的DedeUserID是0000001,

 

正常情况下,程序中的DedeUserID就是当前用户的用户ID。但是如果将其中的DedeUserIDDedeUserID__ckMd5都替换为其他的账户,是否就意味着我们可以以其他的账户登录呢?

 

那么现在的问题就在于,如何进行伪造呢?我们知道管理员admin的mid是1,但是我们如何得到对应的DedeUserID__ckMd5呢?所以目前的问题转变为我们需要得到1所对应的DedeUserID__ckMd5的。

漏洞触发点

分析member/index.php中的122行之后的代码:

$uid=empty($uid)? "" : RemoveXSS($uid);
else
{
    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, '/');
            if($cfg_ml->IsLogin() && $cfg_ml->M_LoginID != $uid)
            {
                $vip = GetIP();
                $arr = $dsql->GetOne("SELECT * FROM `#@__member_vhistory` WHERE mid='{$_vars['mid']}' AND vid='{$cfg_ml->M_ID}' ");
                if(is_array($arr))
                {
                    $dsql->ExecuteNoneQuery("UPDATE `#@__member_vhistory` SET vip='$vip',vtime='$vtime',count=count+1 WHERE mid='{$_vars['mid']}' AND vid='{$cfg_ml->M_ID}' ");
                }
                else
                {
                    $query = "INSERT INTO `#@__member_vhistory`(mid,loginid,vid,vloginid,count,vip,vtime)
                             VALUES('{$_vars['mid']}','{$_vars['userid']}','{$cfg_ml->M_ID}','{$cfg_ml->M_LoginID}','1','$vip','$vtime'); ";
                    $dsql->ExecuteNoneQuery($query);
                }
            }
            $dsql->ExecuteNoneQuery("UPDATE `#@__member_tj` SET homecount=homecount+1 WHERE mid='{$_vars['mid']}' ");
        }
        $dpl->LoadTemplate($tplfile);
        $dpl->display();
        exit();
    }
    else
    {
        require_once(DEDEMEMBER.'/inc/space_action.php');
        exit();
    }
}

对其中的last_vid的赋值情况进行分析:

  1. 11行,$uid=empty($uid)? "" : RemoveXSS($uid);,前台通过参数可以传入。
  2. 134行,$last_vid = GetCookie('last_vid');初次赋值
  3. 141-162行,$last_vid!='';$last_vid = $uid;,判断last_vid是否为空。如果为空,则将uid赋值给last_vid
  4. 164行,PutCookie('last_vid', $last_vid, 3600*24, '/');,将last_vid加入到Cookie中。那么此时Cookie中就会存在last_vidlast_vid__ckMd5

那么这就是存在一个漏洞的触发点。如果我们将$uid设置为001,那么得到的last_vidlast_vid__ckMd5刚好就是一对可以使用的Cookie,同时001也刚好可以绕过管理员的验证。

 

但是需要注意的是,在124行存在代码require_once(DEDEMEMBER.'/inc/config_space.php');,引入了config_space.php文件。跟进member/inc/config_space.php,

$_vars = GetUserSpaceInfos();
// some other php code
function GetUserSpaceInfos() {
    global $dsql, $uid, $cfg_memberurl;
    $_vars = array();
    $userid = preg_replace("#[\r\n\t \*%]#", '', $uid);
    $query = "SELECT m.mid,m.mtype,m.userid,m.uname,m.sex,m.rank,m.email,m.scores,
                            m.spacesta,m.face,m.logintime,
                            s.*,t.*,m.matt,r.membername,g.msg
                  From `#@__member` m
                  LEFT JOIN `#@__member_space` s on s.mid=m.mid
                  LEFT JOIN `#@__member_tj` t on t.mid=m.mid
                  LEFT JOIN `#@__arcrank` r on r.rank=m.rank
                  LEFT JOIN `#@__member_msg` g on g.mid=m.mid
                  where m.userid like '$uid' ORDER BY g.dtime DESC ";
    $_vars = $dsql->GetOne($query);
    if (!is_array($_vars)) {
        ShowMsg("你访问的用户可能已经被删除!", "javascript:;");
        exit();
    }
    // some other php code
    return $_vars;
}

在数据库查询的语句中where m.userid like '$uid' ORDER BY g.dtime DESC ",其中传入的$uid作为了userid。如果不存在程序就会退出。那么这就意味着我们传入的uid必须是已经存在的一个用户名,所以我们注册用户名时注册的用户名最好是001这种。

 

至此,所有的漏洞触发点集合在一起就能够触发这个漏洞了。整个漏洞的触发流程是:

  1. 正常注册用户,注册的用户名为001。(要求后台管理员能够正常地通过审核,用户能够正常地浏览自己的空间)
  2. 访问自己的首页member/index.php。添加参数member/index.php?uid=001,其中的uid的值需要和之前注册的用户名保持一致,这样才不会出现数据库中找不到用户,报你访问的用户可能已经被删除错误。
  3. 得到last_vidlast_vid__ckMd5。替换DedeUserIDDedeUserID__ckMd5,此时就相对于将账号替换为了管理员的账号
  4. 重新访问member/index.php,此时已经变为了管理员。

漏洞复现

  1. 注册一个用户名为001的用户

    740345_XQ72B7M3P4HQJX5.jpg

  2. 注册成功之后,跳转到会员首页(会员首页地址是member/index.php)。可以发现此时还是正常用户001,只有4个Cookie值,分别是DedeUserID__ckMd5DedeUserIDDedeLoginTime__ckMd5DedeLoginTime

    740345_TDHUV7ZJRQBBXE4.jpg

  3. 访问链接member/index.php?uid=001。此时Cookie中会增加一些内容,其中最为重要的是last_vidlast_vid__ckMd5,取得他们的值。在我本地演示中,他们分别是001c6fda866fabaca07

    740345_XQ7J6XF7KMJK297.jpg

  4. 最为关键的第一步,替换掉Cookie中的DedeUserID001,DedeUserID__ckMd5为第四步last_vid__ckMd5得到的值c6fda866fabaca07

    740345_8N8PDW37MRM97VF.jpg

    修改完毕之后,访问会员首页member/index.php,此时已经变成了管理员了。

    740345_YKGZPYN6YWAM8Q7.jpg

演示中主要是登录了管理员的身份。因为其中我们中间访问时利用的是member/index.php?uid=001。如果需要登录其他账户的身份,比如id为7的用户,那么我们就需要注册一个名为007的账户,同时中途需要访问的是member/index.php?uid=007链接,将其中的last_vidlast_vid__ckMd5进行替换,就可以以007的账户登录。以此类推,我们就可以以任意的账户进行登录了。

总结

以上就完成了对整个漏洞的分析与复现。本漏洞其实主要是因为dedecms是通过Cookie中的DedeUserID对用户身份进行鉴别的,虽然使用了DedeUserID__ckMd5,但是我们看到还是可以被绕过的,最终形成了Cookie伪造导致任意用户登录的漏洞。

 

对于身份认证这类的信息,最好是放在Session中,前台的用户不容易修改,就不会出现这种Cookie伪造的问题了。


相关推荐:

发表评论

游客 表情
看不清楚?点图切换 送你一朵小花花~

众说纷纭(0)