metinfo6.0.0-6.1.2sql注入[复现]

前言

ubuntu 19
php5.6+mysql
phpstorm+burpsuit

写在前面,菜的明明白白

复现这个漏洞,对cms的路由有了进一步的了解。意识到debug的重要性,函数之间的流转,使用debug能较好的了解。断点测试,能很好的了解参数的值,学长说代码问题都能够使用debug解决,所以学会熟练使用debug.然后编程能力有待提高,编写这次爆破的脚本很简单,但是我写的又慢,又烂……

参考文章

Metinfo 6.1.2 SQL注入
MetInfo-6.1.2前台SQL注入
Metinfo6.0.0-6.1.2前台注入漏洞生命线

漏洞成因

对用户提交的数据没有进行严格的过滤,导致的sql注入

POC

这个漏洞,目前我所了解到的入口有3个
其一是利用admin/index.php文件调用带do方法触发sql漏洞
其二是利用message(在线留言)
其三是利用feedback(在线反馈)
漏洞点都是一样的,一点差别而已

先放一个POC,演示在线留言处触发漏洞


这里在id处存在bool盲注,并且时间盲注也是可以的。

分析漏洞成因

根据POC看到/message/index.php

1
2
3
4
5
define('M_NAME', 'message');
define('M_MODULE', 'web');
define('M_CLASS', 'message');
define('M_ACTION', 'domessage');
require_once '../app/system/entrance.php';

这里定义了module为web,meessage类,调用方法domessage,个人感觉这些信息比较有用,在后续寻找路由的时候有很大的作用,当然这是我这个菜鸟的想法。

看到../app/system/entrance.php

1
load::module();

跟踪../app/system/include/class/load.class.php::funtion module()

1
return self::_load_class($path, $modulename, $action);

然后跟踪到../app/system/include/class/load.class.php::function load_class()

1
2
if(method_exists($newclass, $action)){
call_user_func(array($newclass, $action));

这一路下来,这个路由就很清楚了,然后根据之前index.php定义的值,看到message.class.php
看到函数domessage()

1
2
3
4
5
6
global $_M;
if($_M['form']['action'] == 'add'){
$this->check_field();
$this->add($_M['form']);

}

调用add()函数,跟踪看看函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function add($info) {
global $_M;
if(!$_M[form][id]){
$message=DB::get_one("select * from {$_M[table][column]} where module= 7 and lang ='{$_M[form][lang]}'");
$_M[form][id]=$message[id];
}
$met_fd_ok=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and name= 'met_fd_ok' and columnid = {$_M[form][id]}");
$_M[config][met_fd_ok]= $met_fd_ok[value];
if(!$_M[config][met_fd_ok])okinfo('javascript:history.back();',"{$_M[word][Feedback5]}");//反馈已关闭
if($_M[config][met_memberlogin_code]){
if(!load::sys_class('pin', 'new')->check_pin($_M['form']['code'])){

okinfo(-1, $_M['word']['membercode']);//验证码错误
}
}

关键代码在于

1
$met_fd_ok=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and  name= 'met_fd_ok' and columnid = {$_M[form][id]}");

我们可以查看数据库验证一下

所以问题就出在这儿,然后我们在看看过滤情况,我们回过头去看发现message类继承了web类,web类继承了common类
common类为一级基类,参考文章metinfo-一级基类

common类中获取变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/**
* 获取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);
}

然后我们跟踪至函数common.func.php::daddslashes()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function daddslashes($string, $force = 0) {
!defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_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;
}

common类将会对输入的内容进行addslashes()处理,然后看到web.class.php::load_form()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 重写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'] = '';
}
}

对common来的参数进行重写操作,进行两步验证,其一是sqlinsert(),其二是is_numberc(),非数字就做置空操作。均是针对sql注入的。看向common.func.php::sqlinsert()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function sqlinsert($string){
if(is_array($string)){
foreach($string as $key => $val) {
$string[$key] = sqlinsert($val);
}
}else{
$string_old = $string;
$string = str_ireplace("\\","/",$string);
$string = str_ireplace("\"","/",$string);
$string = str_ireplace("'","/",$string);
$string = str_ireplace("*","/",$string);
$string = str_ireplace("%5C","/",$string);
$string = str_ireplace("%22","/",$string);
$string = str_ireplace("%27","/",$string);
$string = str_ireplace("%2A","/",$string);
$string = str_ireplace("~","/",$string);
$string = str_ireplace("select", "\sel\ect", $string);
$string = str_ireplace("insert", "\ins\ert", $string);
$string = str_ireplace("update", "\up\date", $string);
$string = str_ireplace("delete", "\de\lete", $string);
$string = str_ireplace("union", "\un\ion", $string);
$string = str_ireplace("into", "\in\to", $string);
$string = str_ireplace("load_file", "\load\_\file", $string);
$string = str_ireplace("outfile", "\out\file", $string);
$string = str_ireplace("sleep", "\sle\ep", $string);
$string = strip_tags($string);
if($string_old!=$string){
$string='';
}
$string = trim($string);
}
return $string;
}

因为sleep()被过滤了,几乎sql注入的关键词都被过滤了,然后可以选择使用布尔盲注,时间盲注的话,可以使用benchmark(),所以我们绕过其中第一个过滤,is_number()的绕过

1
2
3
4
5
6
class message extends web {
public function __construct() {
global $_M;
parent::__construct();
$this->upfile = load::sys_class('upfile', 'new');
}

我们输入的payload在parent::__construct();后被置空,而经过$this->upfile = load::sys_class('upfile', 'new');后,我们的payload又被重新填入,所以这样就绕过该函数,成功注入。

这里解释一下为什么返回验证码错误,为我们判断依据。看向函数domessage()

1
2
3
4
5
6
global $_M;
if($_M['form']['action'] == 'add'){
$this->check_field();
$this->add($_M['form']);

}

跟进check_field()函数

1
2
3
4
5
foreach (array_keys($para) as $val) {
if($para[$val]['wr_ok']==1 && !in_array($val,$paraarr)){
$info="【{$para[$val]['name']}】".$_M[word][noempty];
okinfo('javascript:history.back();',$info);
}

所以我们只要正确填充个参数就能过check_field()函数,进入add()函数后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$met_fd_ok=DB::get_one("select * from {$_M[table][config]} where lang ='{$_M[form][lang]}' and  name= 'met_fd_ok' and columnid = {$_M[form][id]}");
$_M[config][met_fd_ok]= $met_fd_ok[value];
if(!$_M[config][met_fd_ok])okinfo('javascript:history.back();',"{$_M[word][Feedback5]}");
/*
先进行sql查询,获取返回值,当我们的语句正确,且注入符合逻辑时,$met_fd_ok[value]不为空
说明sql查询有效
*/
if($_M[config][met_memberlogin_code]){
if(!load::sys_class('pin', 'new')->check_pin($_M['form']['code'])){

okinfo(-1, $_M['word']['membercode']);
}
}
/*
这里进行验证码验证
*/

0%