IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    thinksns(Arbitrary file upload)

    Joseph发表于 2016-12-22 08:19:36
    love 0

    快元旦了,不知道该写哪一类的文章就只有发一些自己手里有的各种坚果了。都会慢慢的发出来的,有知名cms,也会有不是很知名的

    漏洞文件:/apps/public/Lib/Action/AttachAction.class.php

    public function ajaxUpload()
        {
            
            $d['type_name'] = 11;
            D('feedback_type')->add($d);
            $attach_type = t($_REQUEST['type']);
    
            $options['uid'] = $this->mid;
    
            
            $options['allow_exts'] = t(jiemi($_REQUEST['exts']));
            $options['allow_size'] = t(jiemi($_REQUEST['size']));
            $jiamiData = jiemi(t($_REQUEST['token']));
            list($options['allow_exts'], $options['need_review'], $fid) = explode('||', $jiamiData);
            $options['limit'] = intval(jiemi($_REQUEST['limit']));
            $options['now_pageCount'] = intval($_REQUEST['now_pageCount']);
    
            $data['upload_type'] = $attach_type;
    
            $info = model('Attach')->upload($data, $options);
            //ÉÏ´«³É¹¦
            echo json_encode($info);
    }

    可以看见存在很多变量都被控制,在上传中type这种名称的变量一般都是用于判断是否上传图片或者上传file等。这里我们选择定义成file,因为image几乎都是有图片类型限制的,file有时候会有疏忽。Exts这个变量一看就是用于类型判断或者后缀判断,但这里可以看见他在输入后还进入到了一个jiemi(解密)命名的函数,进入看看

    function jiemi($text, $key = null)
    {
        if (empty($key)) {
            $key = C('SECURE_CODE');
        }
    
        return tsauthcode($text, 'DECODE', $key);
    }
    
    当key为空的时候就从配置文件中获取key的值
    	'DB_PORT'       => 3306,        // Êý¾Ý¿â¶Ë¿Ú
    	'DB_PREFIX'     => 'ts_',// Êý¾Ý¿â±íǰ׺£¨ÒòΪÂþÓεÄÔ­Òò£¬Êý¾Ý¿â±íǰ׺±ØÐëдÔÚ±¾Îļþ£©
    	'DB_CHARSET'    => 'utf8',      // Êý¾Ý¿â±àÂë
    	'SECURE_CODE'   => '283415851f59f1f6fe',  // Êý¾Ý¼ÓÃÜÃÜÔ¿
    	'COOKIE_PREFIX' => 'TS4_',      // # cookie
    );

    可以看见这里的code就是他的key,然后进入tsauthcode进行解密

    function tsauthcode($string, $operation = 'DECODE', $key = '')
    {
        $ckey_length = 4;
        $key = md5($key ? $key : SITE_URL);
        $keya = md5(substr($key, 0, 16));
        $keyb = md5(substr($key, 16, 16));
        $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
        $cryptkey = $keya.md5($keya.$keyc);
        $key_length = strlen($cryptkey);
        $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
        $string_length = strlen($string);
        $result = '';
        $box = range(0, 255);
        $rndkey = array();
        for ($i = 0; $i <= 255; $i++) {
            $rndkey[$i] = ord($cryptkey[$i % $key_length]);
        }
        for ($j = $i = 0; $i < 256; $i++) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }
        for ($a = $j = $i = 0; $i < $string_length; $i++) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }
        if ($operation == 'DECODE') {
            if ((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
                return substr($result, 26);
            } else {
                return '';
            }
        } else {
            return $keyc.str_replace('=', '', base64_encode($result));
        }
    }

    是不是看着很熟悉,没错就是Ucenter的解密函数。看到这里了,我们先假象一种场景,如果我们得到了key,自定义后缀或者类型然后进行加密控制exts变量。如果这里exts是用于后缀的验证话,那么是不是就是任意文件上传?既然提出了这种假象,我们就先记录下,继续往下面看。

    public function upload($data = null, $input_options = null, $thumb = false)
        {
            //echo json_encode($data);
            $system_default = model('Xdata')->get('admin_Config:attach');
            if (empty($system_default['attach_path_rule']) || empty($system_default['attach_max_size']) || empty($system_default['attach_allow_extension'])) {
                $system_default['attach_path_rule'] = 'Y/md/H/';
                $system_default['attach_max_size'] = '2';        // ĬÈÏ2M
                $system_default['attach_allow_extension'] = 'jpg,gif,png,jpeg,bmp,zip,rar,doc,xls,ppt,docx,xlsx,pptx,pdf';
                model('Xdata')->put('admin_Config:attach', $system_default);
            }
    这里是判断我们的system_default的内容是否为空,当为空的时候就进入if中赋值
    if ($data['upload_type'] === 'image') {
                $image_default = model('Xdata')->get('admin_Config:attachimage');
                $system_default['attach_max_size'] = $image_default['attach_max_size'];
                $system_default['attach_allow_extension'] = $image_default['attach_allow_extension'];
                $system_default['auto_thumb'] = $image_default['auto_thumb'];
            }
    
    这里就是我们提到的type,当type为image的时候就对$system_default进行传值
      $default_options = array();
            $default_options['custom_path'] = date($system_default['attach_path_rule']);                    // Ó¦Óö¨ÒåµÄÉÏ´«Ä¿Â¼¹æÔò£º'Y/md/H/'
            $default_options['max_size'] = floatval($system_default['attach_max_size']) * 1024 * 1024;        // µ¥Î»: Õ×
            $default_options['allow_exts'] = $system_default['attach_allow_extension'];                    // 'jpg,gif,png,jpeg,bmp,zip,rar,doc,xls,ppt,docx,xlsx,pptx,pdf'
            $default_options['save_path'] = UPLOAD_PATH.'/'.$default_options['custom_path'];
            $default_options['save_name'] = ''; //Ö¸¶¨±£´æµÄ¸½¼þÃû.ĬÈÏϵͳ×Ô¶¯Éú³É
            $default_options['save_to_db'] = true;

    这里在代码中有注释说明,是载入配置文件中的默认配置

    if ($data['upload_type'] == 'image') {
          
                $cloud = model('CloudImage');
                if ($cloud->isOpen()) {
                    return $this->cloudImageUpload($options);
                } else {
                    return $this->localUpload($options);
                }
            }

    这里就是判断我们的type是否是image,当是的时候则进入,这里的isOpen是判断是否开启云上传,当开启的时候则上传到云上,没开启的时候则进入localUpload这个上传中,我们可以看的时候没开的时候

    private function localUpload($options)
        {
            // ³õʼ»¯ÉÏ´«²ÎÊý
            $upload = new UploadFile($options['max_size'], $options['allow_exts'], $options['allow_types']);
            // ÉèÖÃÉÏ´«Â·¾¶
            $upload->savePath = $options['save_path'];
            // ÆôÓÃ×ÓĿ¼
            $upload->autoSub = false;
            // ±£´æµÄÃû×Ö
            $upload->saveName = $options['save_name'];
            // ĬÈÏÎļþÃû¹æÔò
            $upload->saveRule = $options['save_rule'];
            // ÊÇ·ñËõÂÔͼ
            if ($options['auto_thumb'] == 1) {
                $upload->thumb = true;
            }
    
            // ´´½¨Ä¿Â¼
            mkdir($upload->save_path, 0777, true);
    
            // Ö´ÐÐÉÏ´«²Ù×÷
            if (!$upload->upload()) {
                // ÉÏ´«Ê§°Ü£¬·µ»Ø´íÎó
                $return['status'] = false;
                $return['info'] = $upload->getErrorMsg();
    
                return $return;
            } else {
                $upload_info = $upload->getUploadFileInfo();
                // ±£´æÐÅÏ¢µ½¸½¼þ±í
                $data = $this->saveInfo($upload_info, $options);
                // Êä³öÐÅÏ¢
                $return['status'] = true;
                $return['info'] = $data;
                // ÉÏ´«³É¹¦£¬·µ»ØÐÅÏ¢
                return $return;
            }
    }

    前面的一系列传值我们跳过,直接进入upload中看

    foreach ($files as $key => $file) {
                //¹ýÂËÎÞЧµÄÉÏ´«
                if (!empty($file['name'])) {
                    $file['key'] = $key;
                    $file['extension'] = $this->getExt($file['name']);
                    $file['savepath'] = $savePath;
                    $file['savename'] = uniqid().substr(str_shuffle('0123456789abcdef'), rand(0, 9), 7).'.'.$file['extension'];
                    //$this->getSaveName($file);
    
                    if ($GLOBALS['fromMobile'] == true && empty($file['extension'])) {
                        //ÒÆ¶¯É豸ÉÏ´«µÄÎÞºó׺µÄͼƬ£¬Ä¬ÈÏΪjpg
                            $file['extension'] = 'jpg';
                        $file['savename'] = trim($file['savename'], '.').'.jpg';
                    } else {
                        // ×Ô¶¯¼ì²é¸½¼þ
                        if ($this->autoCheck) {
                            if (!$this->check($file)) {
                                return false;
                            }
                        }
                    }
    首先是遍历FILES信息把相应的值赋给对应的变量,然后
    if ($GLOBALS['fromMobile'] == true && empty($file['extension'])) {
                        //ÒÆ¶¯É豸ÉÏ´«µÄÎÞºó׺µÄͼƬ£¬Ä¬ÈÏΪjpg
                            $file['extension'] = 'jpg';
                        $file['savename'] = trim($file['savename'], '.').'.jpg';

    这里的frommobile默认是开启的true,但这里的$file[‘extension’]在上面遍历的时候有赋值所以不为空则为假不进入

    else {
                        // ×Ô¶¯¼ì²é¸½¼þ
                        if ($this->autoCheck) {
                            if (!$this->check($file)) {
                                return false;
                            }
                        }
                    }

    重点是check检查类型等

    if (!$this->checkType($file['type'])) {
                $this->error = 'ÉÏ´«ÎļþMIMEÀàÐͲ»ÔÊÐí£¡';
    
                return false;
            }
            //¼ì²éÎļþÀàÐÍ
            if (!$this->checkExt($file['extension'])) {
                $this->error = 'ÉÏ´«ÎļþÀàÐͲ»ÔÊÐí';
    
                return false;
            }

    类型是可以绕的,checkext瞅瞅

    private function checkExt($ext)
        {
            if (in_array($ext, array('php', 'php3', 'exe', 'sh', 'html', 'asp', 'aspx'))) {
                $this->error = '²»ÔÊÐíÉÏ´«¿ÉÖ´ÐеĽű¾Îļþ£¬È磺php¡¢exe¡¢htmlºó׺µÄÎļþ';
    
                return false;
            }
    
            if (!empty($this->allowExts)) {
                return in_array(strtolower($ext), $this->allowExts, true);
            }
    
            return true;
        }

    首先第一个in_array这里的判断一看就是黑名单,大小写什么的直接就可以绕过,而这里的allowexts则是我们最初的加密的那个内容。这里是当不为空的时候则判断,所以我们不填此参数达到绕过

    $return['info'] = $upload->getErrorMsg();
    
                return $return;
            } else {
                $upload_info = $upload->getUploadFileInfo();
                // ±£´æÐÅÏ¢µ½¸½¼þ±í
                $data = $this->saveInfo($upload_info, $options);
                // Êä³öÐÅÏ¢
                $return['status'] = true;
                $return['info'] = $data;
                // ÉÏ´«³É¹¦£¬·µ»ØÐÅÏ¢
                return $return;
            }

    由于上传到云是需要管理自己去配置的,所以默认是不开启,只要采用的是正常上传都会存在。本地亲测试成功复现.

    本文由91ri团队原创,转载请注明出处。

     



沪ICP备19023445号-2号
友情链接