前言

一个偶然的原因,看到了一个站点,进去后发现有明显的被黑的痕迹,查看整个站点发现采用的是Metinfo 5.3.19 CMS,google一下,发现关于这个版本的CMS有管理员密码重置的漏洞,网上已经有几篇文章来描述这个问题了,这里也顺便参考这自己分析一遍,整理一下整个漏洞的来龙去脉。
Metinfo 5.3.19

漏洞分析

在官网上有5.3.19版本下载,下载后发现整个站点的code都拿到手了,看到问题出现在连接 admin/admin/getpassword.php这里,打开这个文件,在第四行

1
require_once '../include/common.inc.php';

这里直接包含了include/common.inc.php,打开,找到其中关于post的代码,

1
2
3
4
5
6
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);
}
}

这里存在变量覆盖漏洞,
然后在admin/admin/getpassword.php中查找关于关于重置密码的处理过程

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_id='$admin_mobile' and usertype='3'");
if($admin_list && $admin_list['admin_email']=='')okinfo('../admin/getpassword.php',$lang_password14);
if(!$admin_list){
if(!is_email($admin_mobile))okinfo('../admin/getpassword.php',$lang_password7);
$admin_list = $db->get_one("SELECT * FROM $met_admin_table WHERE admin_email='$admin_mobile' and usertype='3'");
if(!$admin_list)okinfo('../admin/getpassword.php',$lang_password14);
}
if($admin_list){
$met_fd_usename=$met_fd_usename;
$met_fd_fromname=$met_fd_fromname;
$met_fd_password=$met_fd_password;
$met_fd_smtp=$met_fd_smtp;
$met_webname=$met_webname;
$met_weburl=$met_weburl;
$adminfile=$url_array[count($url_array)-2];
$from=$met_fd_usename;
$fromname=$met_fd_fromname;
$to=$admin_list['admin_email'];
$usename=$met_fd_usename;
$usepassword=$met_fd_password;
$smtp=$met_fd_smtp;
$title=$met_webname.$lang_getNotice;
$x = md5($admin_list[admin_id].'+'.$admin_list[admin_pass]);
$outime=3600*24*3;
$String=authcode($admin_list[admin_id].".".$x,'ENCODE', $met_webkeys, $outime);
$String=urlencode($String);
$mailurl= $met_weburl.$adminfile.'/admin/getpassword.php?p='.$String;
$body ="<style type='text/css'>\n";
$body .="#metinfo{ padding:10px; color:#555; font-size:12px; line-height:1.8;}\n";
$body .="#metinfo .logo{ border-bottom:1px dotted #333; padding-bottom:5px;}\n";
$body .="#metinfo .logo img{ border:none;}\n";
$body .="#metinfo .logo a{ display:block;}\n";
$body .="#metinfo .text{ border-bottom:1px dotted #333; padding:5px 0px;}\n";
$body .="#metinfo .text p{ margin-bottom:5px;}\n";
$body .="#metinfo .text a{ color:#70940E;}\n";
$body .="#metinfo .copy{ color:#BBB; padding:5px 0px;}\n";
$body .="#metinfo .copy a{ color:#BBB; text-decoration:none; }\n";
$body .="#metinfo .copy a:hover{ text-decoration:underline; }\n";
$body .="#metinfo .copy b{ font-weight:normal; }\n";
$body .="</style>\n";
$body .="<div id='metinfo'>\n";
if($met_agents_type<=1){
$body .="<div class='logo'><a href='$met_weburl' title='$met_webname'><img src='http://www.metinfo.cn/upload/200911/1259148297.gif' /></a></div>";
}
$body .="<div class='text'><p>".$lang_hello.$admin_name."</p><p>$lang_getTip1</p>";
$body .="<p><a href='$mailurl'>$mailurl</a></p>\n";
if($met_agents_type<=1){
$body .="<p>$lang_getTip2</p></div><div class='copy'>$foot</a></div>";
}
require_once ROOTPATH.'include/jmail.php';
$sendMail=jmailsend($from,$fromname,$to,$title,$body,$usename,$usepassword,$smtp);
if($sendMail==0){
require_once ROOTPATH.'include/export.func.php';
$post=array('to'=>$to,'title'=>$title,'body'=>$body);
$met_file='/passwordmail.php';
$sendMail=curl_post($post,30);
if($sendMail=='nohost')$sendMail=0;
}

$text=$sendMail?$lang_getTip3.$lang_memberEmail.':'.$admin_list['admin_email']:$lang_getTip4;
okinfo('../index.php',$text);
}

注意这里的发送,

1
2
3
4
5
6
7
8
9
require_once ROOTPATH.'include/jmail.php';
$sendMail=jmailsend($from,$fromname,$to,$title,$body,$usename,$usepassword,$smtp);
if($sendMail==0){
require_once ROOTPATH.'include/export.func.php';
$post=array('to'=>$to,'title'=>$title,'body'=>$body);
$met_file='/passwordmail.php';
$sendMail=curl_post($post,30);
if($sendMail=='nohost')$sendMail=0;
}

$sendMail==0时,信息会通过变量$post调用curl_post发送出去。curl_post定义在include/export.func.php中:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function curl_post($post,$timeout){
global $met_weburl,$met_host,$met_file;
$host=$met_host;
$file=$met_file;
if(get_extension_funcs('curl')&&function_exists('curl_init')&&function_exists('curl_setopt')&&function_exists('curl_exec')&&function_exists('curl_close')){
$curlHandle=curl_init();
curl_setopt($curlHandle,CURLOPT_URL,'http://'.$host.$file);
curl_setopt($curlHandle,CURLOPT_REFERER,$met_weburl);
curl_setopt($curlHandle,CURLOPT_RETURNTRANSFER,1);
curl_setopt($curlHandle,CURLOPT_CONNECTTIMEOUT,$timeout);
curl_setopt($curlHandle,CURLOPT_TIMEOUT,$timeout);
curl_setopt($curlHandle,CURLOPT_POST, 1);
curl_setopt($curlHandle,CURLOPT_POSTFIELDS, $post);
$result=curl_exec($curlHandle);
curl_close($curlHandle);
}
else{
if(function_exists('fsockopen')||function_exists('pfsockopen')){
$post_data=$post;
$post='';
@ini_set("default_socket_timeout",$timeout);
while (list($k,$v) = each($post_data)) {
$post .= rawurlencode($k)."=".rawurlencode($v)."&";
}
$post = substr( $post , 0 , -1 );
$len = strlen($post);
if(function_exists(fsockopen)){
$fp = @fsockopen($host,80,$errno,$errstr,$timeout);
}
else{
$fp = @pfsockopen($host,80,$errno,$errstr,$timeout);
}
if (!$fp) {
$result='';
}
else {
$result = '';
$out = "POST $file HTTP/1.0\r\n";
$out .= "Host: $host\r\n";
$out .= "Referer: $met_weburl\r\n";
$out .= "Content-type: application/x-www-form-urlencoded\r\n";
$out .= "Connection: Close\r\n";
$out .= "Content-Length: $len\r\n";
$out .="\r\n";
$out .= $post."\r\n";
fwrite($fp, $out);
$inheader = 1;
while(!feof($fp)){
$line = fgets($fp,1024);
if ($inheader == 0) {
$result.=$line;
}
if ($inheader && ($line == "\n" || $line == "\r\n")) {
$inheader = 0;
}

}

while(!feof($fp)){
$result.=fgets($fp,1024);
}
fclose($fp);
str_replace($out,'',$result);
}
}
else{
$result='';
}
}
$result=trim($result);
if(substr($result,0,7)=='metinfo'){
return substr($result,7);
}
else{
return 'nohost';
}
}

可以发现$met_host表示右键内容发送地点,并且这个参数可以通过变量覆盖进行控制,那么如何让$sendMail==0,触发发送邮件程序呢?这里包含了jmail.php,打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
global $met_fd_port,$met_fd_way;
$mail = new PHPMailer();
//$mail->SMTPDebug = 3;

$mail->CharSet = "UTF-8"; // charset
$mail->Encoding = "base64";
$mail->Timeout = 15;
$mail->IsSMTP(); // telling the class to use SMTP

//system
if(stripos($smtp,'.gmail.com')===false){
$mail->Port = $met_fd_port;
$mail->Host = $smtp; // SMTP server
if($met_fd_way=='ssl'){
$mail->SMTPSecure = "ssl";
}else{
$mail->SMTPSecure = "";
}
}

这里met_fd_port指定了邮件的发送端口,属于系统配置。因此倘若我们利用前面的变量覆盖漏洞修改端口,即可导致邮件发送失败,进入到curl_post

漏洞复现

环境搭建

首先搭建测试环境,本来想在vps上搭建lamp环境然后上传站点的,但是考虑到安全问题,以及各种依赖的升级等等就搁置了。直接在本地Windows部署,然后通过Kali虚拟机和vps测试一下。
首先在本地搭建phpstudy环境。Windows搭建phpstudy还是比肩方便的下载下来PHPstudy,解压,几乎是一键式安装啊,安装过后直接启动就好了。
phpstudy启动
然后是MetInfo5.3.19部署。这里直接从米拓官网下载历史版本源码即可,下载下来是个zip压缩包,这里讲压缩版保存在安装phpstudy是创建的www路径下,比如E:\phpstudy\PHPTutorial\WWW\,直接解压即可。然后在浏览器输入http://127.0.0.1:port这里port为你设置的Apache运行端口,这里可在phpstudy运行管理的端口设置那里进行自主配置。然后浏览器就会出现安装的一系列步骤,按提示来即可。ps.要是想删除重新安装,记得删除/config文件夹下的install.lock

渗透测试

首先在vps上监听80端口nc -lvv 80,应为curl_post使用http协议,默认工作在80端口。

这里直接在kali虚拟机上用burpsuite一把梭就好了。构造找回密码请求如下:
找回密码
利用burpsuite构造如下请求,met_host处为用于接收重置链接的vps地址:
构造请求
请求发送后,会在vps上收到如下数据信息:
收到数据
注意,这里的

1
http://192.168.1.105:2333/admin/admin/getpassword.php?p=09f7Rd6YXI0qJlu2HY2qe23Prf5j8UAn4Vo8CEXXX9GqAfbj%2BkJGFJUhHeSlkwTBUjtPci0XQX23qgm8pYNupqQsRw

就是密码重置链接,到这里我们就差不多已经成功了。然后就可以登录链接,设置密码,登录后台,

密码重置
登录后台
测试结束。

措施

官方已经放出最新版本,请及时升级。

总结

码代码是要时刻注意变量覆盖危险,做好代码审计和测试工作。


声明:
文章标题:【Web渗透】Metinfo 5.3.19 管理员密码重置漏洞分析
文章作者:RookieHacker
文章链接:http://blog.rookiehacker.org/2019/02/22/metinfotest/
文章版权属本博主所有,有问题或者建议欢迎在下方评论。欢迎转载、引用,但请标明作者和原文地址,谢谢。


喜欢,就支持我一下吧~