metinfo <= 6.2.0前台任意文件上传漏洞GetShell

  • 发表于
  • Vulndb

Metinfo 存在一个前台任意文件上传漏洞。攻击者可以通过该漏洞直接获取网站权限GetShell,漏洞影响至 Metinfo 最新版,以下是分析文。

漏洞环境

  1. Windows环境
  2. PHP版本<=5.3 环境

影响版本

metinfo <= 6.2.0(目前最新版本为 6.2.0 )

漏洞分析

/app/system/include/module/uploadify.class.php

class uploadify extends web {
	public $upfile;
	function __construct(){
		parent::__construct();
		global $_M;
		$this->upfile = new upfile();
	}

uploadify类继承web类, 在构造方法中调用了父类的构造方法, web类是一个前台基类,所以并不会做权限验证则uploadify类无需登录即可使用。

public function doupfile(){
	global $_M;
	$this->upfile->set_upfile();
	$info['savepath'] = $_M['form']['savepath'];
	$info['format'] = $_M['form']['format'];
	$info['maxsize'] = $_M['form']['maxsize'];
	$info['is_rename'] = $_M['form']['is_rename'];
	$info['is_overwrite'] = $_M['form']['is_overwrite'];
	$this->set_upload($info);
	$back = $this->upload($_M['form']['formname']);
	if($_M['form']['type']==1){
		if($back['error']){
			$back['error'] = $back['errorcode'];
		}else{
			$backs['path'] = $back['path'];
			$backs['append'] = 'false';
			$back = $backs;
		}
	}
$back['filesize'] =round(filesize($back['path'])/1024,2);
	echo jsonencode($back);
}

$_M[‘form’] 是被metinfo处理后的GPC,所以能够被用户控制。
在该类的doupload方法当中,上传类所用到的部分配置能被用户控制,这里需要关注一下savepath,设置savepath时会被设置为绝对路径,我们可控的点为绝对路径的upload目录之后。

public function set($name, $value) {
	if ($value === NULL) {
		return false;
	}
	switch ($name) {
		case 'savepath':
			$this->savepath = path_standard(PATH_WEB.'upload/'.$value);

在设置完上传的基本配置后,接着调用upload方法。

public function upload($formname){
	global $_M;
	$back = $this->upfile->upload($formname);
	return $back;
}

然后调用upfile对象的upload方法

public function upload($form = '') {
	global $_M;
	if($form){
		foreach($_FILES as $key => $val){
			if($form == $key){
				$filear = $_FILES[$key];
			}
		}
	}
	if(!$filear){
		foreach($_FILES as $key => $val){
			$filear = $_FILES[$key];
			break;
		}
	}

在upload方法当中, 首先接收_FILES保存到filear变量当中。

$this->getext($filear["name"]); //获取允许的后缀
if (strtolower($this->ext)=='php'||strtolower($this->ext)=='aspx'||strtolower($this->ext)=='asp'||strtolower($this->ext)=='jsp'||strtolower($this->ext)=='js'||strtolower($this->ext)=='asa') {
	return $this->error($this->ext." {$_M['word']['upfileTip3']}");
}
if ($_M['config']['met_file_format']) {
	if($_M['config']['met_file_format'] != "" && !in_array(strtolower($this->ext), explode('|',strtolower($_M['config']['met_file_format']))) && $filear){
		return $this->error($this->ext." {$_M['word']['upfileTip3']}");
	}
} else {
	return $this->error($this->ext." {$_M['word']['upfileTip3']}");
}
if ($this->format) {
	if ($this->format != "" && !in_array(strtolower($this->ext), explode('|',strtolower($this->format))) && $filear) {
		return $this->error($this->ext." {$_M['word']['upfileTip3']}");
	}
}

接着获取上传文件名的后缀, 首先经过一次黑名单校验然后再继续白名单校验,在这里白名单校验后缀无法绕过所以只能上传以下格式文件
rar|zip|sql|doc|pdf|jpg|xls|png|gif|mp3|jpeg|bmp|swf|flv|ico

//文件名重命名
$this->set_savename($filear["name"], $this->is_rename);
//新建保存文件
if(stripos($this->savepath, PATH_WEB.'upload/') !== 0){
	return $this->error($_M['word']['upfileFail2']);
}
if(strstr($this->savepath, './')){
	return $this->error($_M['word']['upfileTip3']);
}
if (!makedir($this->savepath)) {
	return $this->error($_M['word']['upfileFail2']);
}

在通过白名单校验之后,开始设置文件名,如果this->is_rename为false,那么上传的文件就不会被重命名,而is_rename可以由_M[‘form’][‘is_rename’]控制。

protected function set_savename($filename, $is_rename) {
		if ($is_rename) {
			srand((double)microtime() * 1000000);
			$rnd = rand(100, 999);
			$filename = date('U') + $rnd;
			$filename = $filename.".".$this->ext;
		} else {
			$name_verification = explode('.',$filename);
			$verification_mun = count($name_verification);
			if($verification_mun>2){
				$verification_mun1 = $verification_mun-1;
				$name_verification1 = $name_verification[0];
				for($i=0;$i<$verification_mun1;$i++){
					$name_verification1 .= '_'.$name_verification[$i];
				}
				$name_verification1 .= '.'.$name_verification[$verification_mun1];
				$filename = $name_verification1;
			}
			$filename = str_replace(array(":", "*", "?", "|", "/" , "\\" , "\"" , "<" , ">" , "——" , " " ),'_',$filename);
			if (stristr(PHP_OS,"WIN")) {
				$filename_temp = @iconv("utf-8","GBK",$filename);
			}else
			{
				$filename_temp = $filename;
			}
			$i=0;
			$savename_temp=str_replace('.'.$this->ext,'',$filename_temp);
			while (file_exists($this->savepath.$filename_temp)) {
				$i++;
				$filename_temp = $savename_temp.'('.$i.')'.'.'.$this->ext;
			}
			if ($i != 0) {
				$filename = str_replace('.'.$this->ext,'',$filename).'('.$i.')'.'.'.$this->ext;
			}
		}

从该方法中可以看出保护,就算文件名不重命名, 在文件名中含有多个.的情况下, 除了最后一个.其他的都会被替换为_,所以并不能利用。

metinfo <= 6.2.0前台任意文件上传漏洞GetShell

设置完文件名后, 又开始对this->savepath保存目录进行检验, 同样savepath也可以由_M[‘form’][‘savepath’]设置。首先通过strstr检测路径中是否含有./字符,如果存在直接结束流程,所以也不能使用../进行目录穿越。不过在windows中还可以使用..\实现目录穿越。
接着调用makedir处理目录,

function makedir($dir){
	$dir = path_absolute($dir);
	@clearstatcache();
	if(file_exists($dir)){
		$result=true;
	}else{
		$fileUrl = '';
		$fileArr = explode('/', $dir);
		$result = true;
		foreach($fileArr as $val){
			$fileUrl .= $val . '/';
			if(!file_exists($fileUrl)){
				$result = mkdir($fileUrl);
			}
		}
	}
	@clearstatcache();
	return $result;
}

makedir方法的作用为判断一个目录是否存在,如果不存在会一层一层的创建目录。在处理完保存路径后,将路径和文件名拼接起来成为上传的目标地址,最终实现上传。

//复制文件
$upfileok=0;
$file_tmp=$filear["tmp_name"];
$file_name=$this->savepath.$this->savename;
if (stristr(PHP_OS,"WIN")) {
	$file_name = @iconv("utf-8","GBK",$file_name);
}
if (function_exists("move_uploaded_file")) {
	if (move_uploaded_file($file_tmp, $file_name)) {
		$upfileok=1;
	} else if (copy($file_tmp, $file_name)) {
		$upfileok=1;
	}
} elseif (copy($file_tmp, $file_name)) {
	$upfileok=1;
}

最终的保存文件名由目录和文件名拼接而成,文件名来自_FILES变量,目录来自GPC。在PHP的_FILES文件上传当中,并不存在00截断问题,并且多后缀文件名会被处理,所以这里我们重点关注目录。目录是来自_M[‘form’][‘savepath’]所以用户可控,那么如果存在截断漏洞可以尝试将目录控制为xxx.php\0最终保存路径类似c:/xxx/xxx.php\0/a.jpg实现上传php文件。不过在metinfo当中,在处理GPC保存到_M[‘form’][‘savepath’]时数据会经过addslashes处理,如果这里不会存在00截断问题。

虽然不存在00截断问题,但是在这里可以看到如果系统为windows,在保存文件前对保存路径使用iconv转换了字符集。

iconv truncate

低版本的 PHP 中, iconv 函数存在字符转换截断问题。

首先,程序会根据路径逐级判断目录是否存在。如果不存在,则创建目录。利用了 windows 的特性,构造出 payload: a.php%80\..\1.jpg 。

如果 payload: a.php%80\..\1.jpg 能顺利拼接成文件名,那也就完成了整个漏洞复现。然而在 Metinfo中,在未定义 define('IN_ADMIN', true); ,变量 $_M['form']['is_rename'] 中的数据就会经过 sqlinsert函数处理,而这个函数恰恰把 \ 符号给替换成 / 了,那么就变成了 payload: a.php%80/../1.jpg ,这样就会触发上边的 ./ 匹配规则,所以我们得想办法绕过。

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;
}

如果之前审计过 MetinfoSQL注入漏洞,就知道可以通过 admin/index.php 来绕过 sqlinsert 函数。我们可以利用该文件中的 define('IN_ADMIN', true) ,然后通过构造 Web 路由完成漏洞触发。

<?php
# MetInfo Enterprise Content Management System 
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved. 
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';
# 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.
?>

该文件中,设置了IN_ADMIN常量并且可以自己控制加载的module、class等且无权限验证,所以使用这个文件来加载uploadify类实现上传就能够绕过sqlinsert使用..\实现目录穿越。

metinfo <= 6.2.0 EXP

metinfo <= 6.2.0前台任意文件上传漏洞GetShell

相关