Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Сегодня я расскажу об уязвимости, дающей возможность исполнять произвольный код в самой популярной CMS в мире — WordPress. Причина бага — в недостаточной фильтрации метаданных загруженного файла, что дает возможность выйти из директории, используя некорректную логику при кадрировании картинок. Злоумышленник может загрузить произвольный PHP-код в теле изображения и поместить его в папку, откуда будет возможен вызов.
_wp_attached_file
, который отвечает за путь загруженного аттача.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
Установка WordPress 5.0
После этого не забудь отключить автообновление CMS на всякий случай.
$ echo "define( 'WP_AUTO_UPDATE_CORE', false );" >> /var/www/html/wp-config.php
Проэксплуатировать уязвимость можно только от имени пользователей, которым разрешена загрузка медиафайлов. Роль author
вполне подойдет для этих целей, поэтому создадим нового пользователя с такими правами.
Теперь немного о загрузках медиафайлов. Помимо того что файл физически помещается в директорию wp-content/uploads
, в процессе загрузки его метаданные заносятся в таблицу wp_postmeta
. Для CMS нет особой разницы между записями, страницами и файлами, для системы все это объекты типа WP_Post
, и различаются они метаданными, атрибутом post_type
и прочим.
/wp-includes/class-wp-post.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
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
Ключ _wp_attachment_metadata
содержит сериализованный объект, где располагается вся информация о загруженной картинке, которая может понадобиться WordPress. Главная проблема в том, что злоумышленник может перезаписать любые метаданные произвольными.
Как мы выяснили, загруженный файл в WordPress является экземпляром Post
. Поэтому за добавление и обновление данных о нем отвечает один и тот же метод — wp_insert_post
. Только в первом случае он почти сразу вызывается из функции wp_insert_attachment
.
/wp-includes/post.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
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
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
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: }
Отладка функции редактирования данных загруженного файла
Как видишь, данные берутся прямо из запроса через доступ к ключам массива $_POST
. В итоге все это добро попадает в эту часть кода:
Источник: xakep.ru