前言
放了好多年的一个漏洞, 不过也没看到有人发出来过,都快放忘了,最近朋友hw遇到了一个Metinfo,帮忙用这个洞打了一下还成功了。自己又去官网下载了最新的Metinfo 62版本看了下,最新版加了一些过滤不过还是能够绕过。
漏洞成功利用场景
最新版本无需登录, Windows + php<5.4
漏洞分析
/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 23 24 25
| 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 6
| 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 16 17
| 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 17 18
| $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 13 14
| $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 36 37 38
| 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 20 21 22
| 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 17 18
| $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截断问题。
1 2 3 4 5 6
| 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)) {
|
虽然不存在00截断问题,但是在这里可以看到如果系统为windows,在保存文件前对保存路径使用iconv转换了字符集。
iconv truncate
在iconv转换字符集时,如果字符串中存在源字符集序列不允许的字符时会造成截断问题。UTF-8在单字节时允许的范围为0x00-0x7F, 如果转换的字符不在该范围之内会出PHP_ICONV_ERR_ILLEGAL_SEQ错误, 并且在出错之后不再处理后面的字符造成截断。
不过从上图可以看出在php<5.4时,转换字符集能够造成截断,但在5.4及以上版本中会返回false。
从上图可以看出,在PHP5.3当中,只要out_buffer不为空无论err为何值都能正常返回。
而在PHP5.4当中, 只有当err为PHP_ICONV_ERR_SUCCESS且out_buffer不为空时才会正常返回, 否则返回FALSE。
再回到metinfo当中,首先尝试把savepath设置为xxx.php%81测试,失败。
1 2 3
| if (!makedir($this->savepath)) { return $this->error($_M['word']['upfileFail2']); }
|
这是因为metinfo会调用makedir对目录处理,如果目录不存在那么会调用mkdir方法进行处理。这里xxx.php%81目录肯定不存在那么会调用mkdir创建该目录,但是mkdir时如果目录名存在不合法字符会创建失败,一旦目录创建失败将会退出流程。所以这里我们需要使用目录穿越, 将savepath控制为类似c:/xxxx/upload/xxx.php\x80/../,在windows当中就算目录不存在也能够实现目录穿越,所以该目录会判断为存在就不会再调用mkdir来创建目录。
之前也谈到了,在对savepath的校验中有检测是否含有./字符,所以不能再使用../实现目录穿越,但是在windows下可以使用..\实现目录穿越。
不过测试发现,目录设置为a.php%81/..\时, 直接被保存到了upload中,自己设置的目录消失了。
在metinfo中,对GPC处理保存到_M[‘form’]时
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| protected function load_form() { global $_M; $_M['form'] =array(); isset($_REQUEST['GLOBALS']) && exit('Access Error');
foreach($_COOKIE as $_key => $_value) { $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value); } foreach($_POST as $_key => $_value) { $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value); } foreach($_GET as $_key => $_value) { $_key{0} != '_' && $_M['form'][$_key] = daddslashes($_value); }
|
调用daddslashes对GPC处理,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function daddslashes($string, $force = 0) { !defined('MAGIC_QUOTES_GPC') && define('MAGIC_QUOTES_GPC', get_magic_quotes_gpc()); if(!MAGIC_QUOTES_GPC || $force) { if(is_array($string)) { foreach($string as $key => $val) { $string[$key] = daddslashes($val, $force); } } else { if(!defined('IN_ADMIN')){ $string = trim(addslashes(sqlinsert($string))); }else{ $string = trim(addslashes($string)); } } } return $string; }
|
可以看到除了addslashes处理,如果没有设置IN_ADMIN常量还会经过sqlinsert处理,
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
| 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; }
|
在该方法当中,会将\替换为/,并且如果替换后的字符串不等于替换前的字符串那么将会直接被设置为’’
所以savepath被置空,文件就被保存到了upload目录当中。
不过这里是可以绕过的,如果能够找到一个设置了IN_ADMIN常量并且能够加载任意类的文件就能够绕过sqlinsert。
admin/index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
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';
?>
|
该文件中,设置了IN_ADMIN常量并且可以自己控制加载的module、class等且无权限验证,所以使用这个文件来加载uploadify类实现上传就能够绕过sqlinsert使用..\实现目录穿越。
利用
打个码, 需要的自己调一下代码吧。