Сегодня я расскажу об уязвимости, дающей возможность исполнять произвольный код в самой популярной 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
После этого не забудь отключить автообновление 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
и прочим.
022: final class WP_Post { ... 186: /** 187: * The post’s type, like post or page. ... 192: public $post_type = 'post';
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_attachment_metadata
содержит сериализованный объект, где располагается вся информация о загруженной картинке, которая может понадобиться WordPress. Главная проблема в том, что злоумышленник может перезаписать любые метаданные произвольными.
Как мы выяснили, загруженный файл в WordPress является экземпляром Post
. Поэтому за добавление и обновление данных о нем отвечает один и тот же метод — wp_insert_post
. Только в первом случае он почти сразу вызывается из функции wp_insert_attachment
.
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: }
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
.
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 );
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
. В итоге все это добро попадает в эту часть кода:
Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке
1 год7390 р. Экономия 1400 рублей! |
1 месяц720 р. 25-30 статей в месяц |
Уже подписан?
Читайте также
Последние новости