Не прошло и месяца с последнего раза, как ребята из 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
После настройки основных параметров можно отключить автоматическое обновление, добавив в конфигурационный файл такую строку:
$ 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.
3112: function wp_handle_comment_submission( $comment_data ) {
Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям WordPress.
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: }
После этого проверяется наличие авторизации в системе.
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
базы данных.
3293: $comment_id = wp_new_comment( wp_slash( $commentdata ), true );
Пользовательские данные предварительно проходят санитизацию с помощью функции wp_slash
.
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
.
2024: function wp_new_comment( $commentdata, $avoid_die = false ) { ... 2071: $commentdata = wp_filter_comment( $commentdata );
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
Больше всего нас интересует wp_filter_kses. Эта функция удаляет все нежелательные элементы и атрибуты HTML, а также выполняет ряд проверок, чтобы избежать межсайтового скриптинга (XSS).
1884: function wp_filter_kses( $data ) { 1885: return addslashes( wp_kses( stripslashes( $data ), current_filter() ) ); 1886: }
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.
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: }
По умолчанию список разрешенных тегов включает в себя: a
, abbr
, acronym
, b
, blockquote
, cite
, code
, del
, em
, i
, q
, s
, strike
, strong
.
Наш комментарий состоит из одного лишь img
, и, как видишь, в списке он отсутствует. Поэтому, после того как функция отработает, весь текст комментария будет удален.
Теперь ты понимаешь, через что приходится пройти комментарию прежде, чем он попадет в базу данных.
Сейчас авторизуемся от имени администратора и оставим комментарий с тегом a
, который разрешен.
Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.
Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке
1 год7490 р. Экономия 1400 рублей! |
1 месяц720 р. 25-30 статей в месяц |
Уже подписан?
Читайте также
Последние новости