metinfo <= 6.2.0前台任意文件上传漏洞GetShell
- 发表于
- Vulndb
Metinfo 存在一个前台任意文件上传漏洞。攻击者可以通过该漏洞直接获取网站权限GetShell,漏洞影响至 Metinfo 最新版,以下是分析文。
漏洞环境
- Windows环境
- PHP版本<=5.3 环境
影响版本
metinfo <= 6.2.0(目前最新版本为 6.2.0 )
漏洞分析
/app/system/include/module/uploadify.class.php
1 2 3 4 5 6 7 |
class uploadify extends web { public $upfile; function __construct(){ parent::__construct(); global $_M; $this->upfile = new upfile(); } |
uploadify类继承web类, 在构造方法中调用了父类的构造方法, web类是一个前台基类,所以并不会做权限验证则uploadify类无需登录即可使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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目录之后。
1 2 3 4 5 6 7 |
public function set($name, $value) { if ($value === NULL) { return false; } switch ($name) { case 'savepath': $this->savepath = path_standard(PATH_WEB.'upload/'.$value); |
在设置完上传的基本配置后,接着调用upload方法。
1 2 3 4 5 |
public function upload($formname){ global $_M; $back = $this->upfile->upload($formname); return $back; } |
然后调用upfile对象的upload方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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变量当中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$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
1 2 3 4 5 6 7 8 9 10 11 12 |
//文件名重命名 $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’]控制。
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 |
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; } } |
从该方法中可以看出保护,就算文件名不重命名, 在文件名中含有多个.的情况下, 除了最后一个.其他的都会被替换为_,所以并不能利用。
设置完文件名后, 又开始对this->savepath保存目录进行检验, 同样savepath也可以由_M[‘form’][‘savepath’]设置。首先通过strstr检测路径中是否含有./字符,如果存在直接结束流程,所以也不能使用../进行目录穿越。不过在windows中还可以使用..\实现目录穿越。
接着调用makedir处理目录,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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方法的作用为判断一个目录是否存在,如果不存在会一层一层的创建目录。在处理完保存路径后,将路径和文件名拼接起来成为上传的目标地址,最终实现上传。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//复制文件 $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 ,这样就会触发上边的 ./ 匹配规则,所以我们得想办法绕过。
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 |
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; } |
如果之前审计过 Metinfo 的 SQL注入漏洞,就知道可以通过 admin/index.php 来绕过 sqlinsert 函数。我们可以利用该文件中的 define('IN_ADMIN', true) ,然后通过构造 Web 路由完成漏洞触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?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
相关
原文连接
的情况下转载,若非则不得使用我方内容。