Следующая новость
Предыдущая новость

От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress

16.04.2019 13:32
От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress

Содержание статьи

  • Стенд
  • Анализ уязвимости
  • Демонстрация уязвимости (видео)
  • Выводы

Не прошло и месяца с последнего раза, как ребята из RIPS снова обнаружили уязвимость в WordPress. На этот раз уязвимость — в комментариях. Проблему усугубляет отсутствие токенов CSRF, в итоге уязвимость можно эксплуатировать, просто посетив сайт злоумышленника.

Корень проблемы в том, что текст комментария недостаточно фильтруется, если его оставляет администратор, а излишнее экранирование некоторых функций позволяет провести атаку типа межсайтовый скриптинг. Из-за особенностей администрирования WordPress XSS легко превращается в RCE.

Про баг снова сообщил Саймон Сканнелл (Simon Scannell) из RIPS Tech.

Стенд

Нам понадобится две машины: одна с WordPress, вторая же будет выступать в роли сайта злоумышленника. С него будет производиться атака «межсайтовая подделка запроса» (CSRF), результатом которой станет комментарий с полезной нагрузкой от имени администратора CMS.

Для этих целей используем пару контейнеров Docker. Начнем с WordPress. Сначала поднимаем базу данных MySQL.

$ docker run -d --rm -e MYSQL_USER="wpxss" -e MYSQL_PASSWORD="CdAT1pQ2lY" -e MYSQL_DATABASE="wpxss" --name=wpmysql --hostname=mysql mysql/mysql-server:5.7 

Теперь веб-сервер и сопутствующие пакеты.

$ docker run -it --rm -p80:80 --name=wpxss --hostname=wpxss --link=wpmysql debian /bin/bash $ apt-get update && apt-get install -y apache2 php php7.0-mysqli php-xdebug nano wget 

Если будешь заниматься отладкой, то наряду с установкой расширения xdebug нужно указать необходимые настройки.

$ 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 — это 5.1.

$ cd /tmp && wget https://wordpress.org/wordpress-5.1.tar.gz 

Затем распаковываем ее в веб-рут.

$ tar xzf wordpress-5.1.tar.gz $ rm -rf /var/www/html/* && mv wordpress/* /var/www/html/ $ chown -R www-data:www-data /var/www/html/ 

После этого можно запускать сервер и приступать к установке CMS.

$ service apache2 start 
Инсталляция WordPress

После настройки основных параметров можно отключить автоматическое обновление, добавив в конфигурационный файл такую строку:

$ echo "define( 'WP_AUTO_UPDATE_CORE', false );" >> /var/www/html/wp-config.php 

С первым стендом мы закончили, переходим ко второму. Назовем его машиной атакующего.

$ docker run -it --rm -p8080:80 --name=attacker --hostname=attacker debian /bin/bash 

Устанавливаем веб-сервер и текстовый редактор.

$ apt-get update && apt-get install -y apache2 nano 

И это все, что нам здесь понадобится. Запускаем Apache, и стенд готов.

$ service apache2 start 

Анализ уязвимости

Баг у нас — в системе комментирования. Давай посмотрим на нее пристальнее. Вся логика находится в файле /wp-includes/comment.php. Попробуем оставить коммент с тегом HTML в его тексте.

<img src="a" onerror=alert()> 

Обработкой входящих комментариев занимается функция wp_handle_comment_submission, в нее информация попадает после нажатия на кнопку Post Comment.

wp-includes/comment.php
3112: function wp_handle_comment_submission( $comment_data ) { 
От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Отладка функции размещения комментария

Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям WordPress.

wp-includes/comment.php
3117:   if ( isset( $comment_data['comment_post_ID'] ) ) { 3118:       $comment_post_ID = (int) $comment_data['comment_post_ID']; 3119:   } 3120:   if ( isset( $comment_data['author'] ) && is_string( $comment_data['author'] ) ) { 3121:       $comment_author = trim( strip_tags( $comment_data['author'] ) ); 3122:   } 3123:   if ( isset( $comment_data['email'] ) && is_string( $comment_data['email'] ) ) { 3124:       $comment_author_email = trim( $comment_data['email'] ); 3125:   } 3126:   if ( isset( $comment_data['url'] ) && is_string( $comment_data['url'] ) ) { 3127:       $comment_author_url = trim( $comment_data['url'] ); 3128:   } 3129:   if ( isset( $comment_data['comment'] ) && is_string( $comment_data['comment'] ) ) { 3130:       $comment_content = trim( $comment_data['comment'] ); 3131:   } 3132:   if ( isset( $comment_data['comment_parent'] ) ) { 3133:       $comment_parent = absint( $comment_data['comment_parent'] ); 3134:   } 

После этого проверяется наличие авторизации в системе.

wp-includes/comment.php
3230:   // If the user is logged in 3231:   $user = wp_get_current_user(); 3232:   if ( $user->exists() ) { ... 3248:   } else { 3249:       if ( get_option( 'comment_registration' ) ) { 3250:           return new WP_Error( 'not_logged_in', __( 'Sorry, you must be logged in to comment.' ), 403 ); 3251:       } 3252:   } 

Так как в данный момент я не залогинен в системе, тело условия игнорируется и выполнение кода продолжается.

Наконец, мы доходим до вызова функции wp_new_comment. Она заносит информацию о новом комментарии в таблицу wp_comments базы данных.

wp-includes/comment.php
3293:   $comment_id = wp_new_comment( wp_slash( $commentdata ), true ); 

Пользовательские данные предварительно проходят санитизацию с помощью функции wp_slash.

wp-includes/formatting.php
5301: function wp_slash( $value ) { 5302:   if ( is_array( $value ) ) { 5303:       foreach ( $value as $k => $v ) { 5304:           if ( is_array( $v ) ) { 5305:               $value[ $k ] = wp_slash( $v ); 5306:           } else { 5307:               $value[ $k ] = addslashes( $v ); 5308:           } 5309:       } 5310:   } else { 5311:       $value = addslashes( $value ); 5312:   } 5313: 5314:   return $value; 5315: } 

И текст комментария превращается в <img src="a" onerror=alert()>. Затем, уже внутри wp_new_comment, выполняется фильтрация всех переданных данных вызовом wp_filter_comment.

wp-includes/comment.php
2024: function wp_new_comment( $commentdata, $avoid_die = false ) { ... 2071:   $commentdata = wp_filter_comment( $commentdata ); 
wp-includes/comment.php
1896: /** 1897:  * Filters and sanitizes comment data. ... 1907:  */ 1908: function wp_filter_comment( $commentdata ) { ... 1936:   /** 1937:    * Filters the comment content before it is set. 1938:    * 1939:    * @since 1.5.0 1940:    * 1941:    * @param string $comment_content The comment content. 1942:    */ 1943:   $commentdata['comment_content'] = apply_filters( 'pre_comment_content', $commentdata['comment_content'] ); 

Список фильтров состоит из нескольких функций:

  • convert_invalid_entities
  • wp_targeted_link_rel
  • wp_filter_kses
  • wp_rel_nofollow
  • balanceTags
От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Фильтрация комментария внутри функции wp_filter_comment

Больше всего нас интересует wp_filter_kses. Эта функция удаляет все нежелательные элементы и атрибуты HTML, а также выполняет ряд проверок, чтобы избежать межсайтового скриптинга (XSS).

wp-includes/kses.php
1884: function wp_filter_kses( $data ) { 1885:   return addslashes( wp_kses( stripslashes( $data ), current_filter() ) ); 1886: } 
wp-includes/kses.php
731: function wp_kses( $string, $allowed_html, $allowed_protocols = array() ) { 732:    if ( empty( $allowed_protocols ) ) { 733:        $allowed_protocols = wp_allowed_protocols(); 734:    } 735:    $string = wp_kses_no_null( $string, array( 'slash_zero' => 'keep' ) ); 736:    $string = wp_kses_normalize_entities( $string ); 737:    $string = wp_kses_hook( $string, $allowed_html, $allowed_protocols ); 738:    return wp_kses_split( $string, $allowed_html, $allowed_protocols ); 739: } 

Здесь последний вызов wp_kses_split убирает из текста комментария все HTML-теги, которые не разрешены разработчиками WordPress.

wp-includes/kses.php
943: function wp_kses_split( $string, $allowed_html, $allowed_protocols ) { 944:    global $pass_allowed_html, $pass_allowed_protocols; 945:    $pass_allowed_html      = $allowed_html; 946:    $pass_allowed_protocols = $allowed_protocols; 947:    return preg_replace_callback( '%(<!--.*?(-->|$))|(<[^>]*(>|$)|>)%', '_wp_kses_split_callback', $string ); 948: } ... 1012: function _wp_kses_split_callback( $match ) { 1013:   global $pass_allowed_html, $pass_allowed_protocols; 1014:   return wp_kses_split2( $match[0], $pass_allowed_html, $pass_allowed_protocols ); 1015: } ... 1038: function wp_kses_split2( $string, $allowed_html, $allowed_protocols ) { 1039:   $string = wp_kses_stripslashes( $string ); ... 1071:   if ( ! is_array( $allowed_html ) ) { 1072:       $allowed_html = wp_kses_allowed_html( $allowed_html ); 1073:   } 1074: 1075:   // They are using a not allowed HTML element. 1076:   if ( ! isset( $allowed_html[ strtolower( $elem ) ] ) ) { 1077:       return ''; 1078:   } 
От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Фильтрация текста комментария при помощи kses

По умолчанию список разрешенных тегов включает в себя: a, abbr, acronym, b, blockquote, cite, code, del, em, i, q, s, strike, strong.

От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Список разрешенных в комментарии HTML-тегов

Наш комментарий состоит из одного лишь img, и, как видишь, в списке он отсутствует. Поэтому, после того как функция отработает, весь текст комментария будет удален.

От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Текст комментария после прохождения фильтрации

Теперь ты понимаешь, через что приходится пройти комментарию прежде, чем он попадет в базу данных.

Сейчас авторизуемся от имени администратора и оставим комментарий с тегом a, который разрешен.

Продолжение доступно только подписчикам

Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.

Подпишись на «Хакер» по выгодной цене!

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

1 год

7490 р.

Экономия 1400 рублей!

1 месяц

720 р.

25-30 статей в месяц

Уже подписан?

Источник

Последние новости