前言 MetInfo是一个企业网站管理系统,采用PHP+Mysql架构,用户众多。
漏洞影响 攻击者可以通过该漏洞直接获取网站权限,漏洞直接影响到现在官网最新版6.2.0
危险评级 高危
利用条件 1.前台 2.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 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截断问题。
1 2 3 4 5 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转换字符集时,如果字符串中存在源字符集序列不允许的字符时会造成截断问题。UTF-8在单字节时允许的范围为0x00-0x7F, 如果转换的字符不在该范围之内会出PHP_ICONV_ERR_ILLEGAL_SEQ错误, 并且在出错之后不再处理后面的字符造成截断。
首先尝试把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 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 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 <?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使用..\实现目录穿越。
漏洞验证(Poc)
具体exp你们自己构造我就不放了,实在有需要扫描文章下方二维码进圈子查看
参考文章 参考地址一 参考地址二