Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Не прошло и месяца с последнего раза, как ребята из 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 ) {
Отладка функции размещения комментария
Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям 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
Фильтрация комментария внутри функции 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: }
Фильтрация текста комментария при помощи kses
По умолчанию список разрешенных тегов включает в себя: a
, abbr
, acronym
, b
, blockquote
, cite
, code
, del
, em
, i
, q
, s
, strike
, strong
.
Список разрешенных в комментарии HTML-тегов
Наш комментарий состоит из одного лишь img
, и, как видишь, в списке он отсутствует. Поэтому, после того как функция отработает, весь текст комментария будет удален.
Текст комментария после прохождения фильтрации
Теперь ты понимаешь, через что приходится пройти комментарию прежде, чем он попадет в базу данных.
Сейчас авторизуемся от имени администратора и оставим комментарий с тегом a
, который разрешен.
Источник: xakep.ru