WordPress — самая популярная CMS в мире. На ней работает от четверти до трети сайтов, и уязвимости в ней находят частенько. На этот раз мы разберем логическую недоработку, которая позволяет проводить широкий спектр атак — от обхода авторизации и SQL-инъекций до выполнения произвольного кода. Добро пожаловать в удивительный мир инъекций в WordPress!

Уязвимость кроется в одном из методов ядра, который отвечает за формирование запросов к базе данных. Для нее имеется хиленький PoC, и я его покажу, но главное — понять саму логику ошибки, почему она существует и каким образом ее можно проэксплуатировать. Этот материал будет особенно полезен тем, кто проводит аудиты исходников приложений, которые взаимодействуют с базами данных, так как в статье будут затронуты методы обнаружения ошибок в кастомных абстракциях для формирования и выполнения запросов к БД.

 

Стенд

Для начала, как водится, нам понадобится тестовый стенд. На момент написания статьи уязвимость была исправлена в последней версии WP — 4.8.3. Поэтому мы возьмем версию 4.7.4, там баг присутствует, и его можно потискать. Я рекомендую воспользоваться докер-контейнером, в котором уже есть все необходимое для запуска установки CMS и не нужно возиться с настройками веб-сервера. Благо в хабе «Докера» есть официальный репозиторий WordPress и можно развернуть рабочий сервер с нужной версией всего несколькими командами.

docker run -p3306:3306 -d --rm --name some-mysql -e MYSQL_ROOT_PASSWORD="toor" mysql
docker run -p80:80 -d --link some-mysql:mysql --rm -e WORDPRESS_DB_PASSWORD="toor" wordpress:4.7.4-php5.6-apache

После этого переходим в браузере по IP «Докера» и наблюдаем знаменитую пятисекундную установку.

Установка WP
Установка WP

Также мы создадим несколько скриптов на PHP, они понадобятся нам для понимания и отладки разных ступеней и вариантов эксплуатации. Все их будет объединять одно — загрузка ядра WordPress. Это можно сделать при помощи следующего кода:

<?php
# No need for the template engine
define( 'WP_USE_THEMES', false );
# Load WordPress Core
require_once( 'wp-load.php' );

На этом приготовления закончены, приступаем к серфингу по исходникам.

 

Работа с БД в WordPress

Для построения и выполнения запросов к базе данных WordPress использует класс wpdb.

/wp-includes/wp-db.php
38: /**
39:  * WordPress Database Access Abstraction Object
...
51:  */
52: class wpdb {

В качестве драйвера используется MySQLi или MySQL, если первая библиотека отсутствует в системе.

/wp-includes/wp-db.php
632:        /* Use ext/mysqli if it exists and:
...
638:        if ( function_exists( 'mysqli_connect' ) ) {
639:            if ( defined( 'WP_USE_EXT_MYSQL' ) ) {
640:                $this->use_mysqli = ! WP_USE_EXT_MYSQL;
641:            } elseif ( version_compare( phpversion(), '5.5', '>=' ) || ! function_exists( 'mysql_connect' ) ) {
642:                $this->use_mysqli = true;
643:            } elseif ( false !== strpos( $GLOBALS['wp_version'], '-' ) ) {
644:                $this->use_mysqli = true;
645:            }
646:        }

Ядро CMS использует метод prepare для обработки всех запросов — это кастомная реализация так называемых подготовленных выражений или связываемых переменных (prepared statements).

/wp-includes/wp-db.php
1257:   /**
1258:    * Prepares a SQL query for safe execution. Uses sprintf()-like syntax.
1259:    *
1260:    * The following directives can be used in the query format string:
1261:    *   %d (integer)
1262:    *   %f (float)
1263:    *   %s (string)
1264:    *   %% (literal percentage sign - no argument needed)
...
1291:   public function prepare( $query, $args ) {
1292:       if ( is_null( $query ) )
1293:           return;
...
1300:       $args = func_get_args();
1301:       array_shift( $args );
...
1309:       array_walk( $args, array( $this, 'escape_by_ref' ) );
1310:       return @vsprintf( $query, $args );
1311:   }

Основная идея — в использовании шаблонов форматирования строк, аналогичных функции sprintf, для указания типа и местоположения переменных в запросе. Именно результат работы vsprintf и возвращает prepare. А эта функция — не что иное, как такой же sprintf, только в качестве аргумента она принимает массив параметров.

Например, попробуем выполнить какой-нибудь запрос (тестовый файл, настало твое время!).

$wpdb->prepare("SELECT id, user_login FROM $wpdb->users WHERE ID = %d", 1);
Запрос к базе данных с помощью функций WordPress
Запрос к базе данных с помощью функций WordPress

В процессе работы %d заменяется на параметр, который мы передали, в данном случае это единица. В результате мы получаем данные пользователя с ID=1.

Но такие статичные запросы нас мало интересуют, давай рассмотрим запрос с пользовательскими данными.

$wpdb->prepare("SELECT id, user_login FROM $wpdb->users WHERE user_login = %s", $_GET['login']);

Попробуем передать в качестве логина что-то в духе стандартной SQL-инъекции — admin' or 1=1. Разумеется, такой вектор не отработает. Пробежимся по пути обработки запроса. Как запрос и параметр попадают в метод prepare?

Все переданные параметры сваливаются в переменную args, а запрос находится в query.

/wp-includes/wp-db.php
1300:       $args = func_get_args();
1301:       array_shift( $args );

Если в качестве аргументов уже был передан массив, то используем его.

1302:       // If args were passed as an array ( in vsprintf), move them up
1303:       if ( isset( $args[0] ) && is_array($args[0]) )
1304:           $args = $args[0];

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

1305:       $query = str_replace( "'%s'", '%s', $query ); // in case someone mistakenly already singlequoted it
1306:       $query = str_replace( '"%s"', '%s', $query ); // doublequote unquoting

Затем идет проверка значений типа float, которая не сильно нас интересует.

1307:       $query = preg_replace( '|(?<!%)%f|' , '%F', $query ); // Force floats to be locale unaware

Следующее регулярное выражение добавляет одинарные кавычки к переменным, которые должны быть строками. При этом не учитываются экранированные плейсхолдеры типа %%s.

1308:       $query = preg_replace( '|(?<!%)%s|', "'%s'", $query ); // quote the strings, avoiding escaped strings like %%s

Далее пробегаемся по переданным аргументам функцией escape_by_ref.

1309:       array_walk( $args, array( $this, 'escape_by_ref' ) );

Функция, как ты можешь понять из названия, выполняет экранирование переданных в args значений. На самом деле это лишь обертка с небольшим условием для _real_escape, которая прогоняет все отправленные переменные через функцию mysqli_real_escape_string.

/wp-includes/wp-db.php
1252:   public function escape_by_ref( &$string ) {
1253:       if ( ! is_float( $string ) )
1254:           $string = $this->_real_escape( $string );
1255:   }
/wp-includes/wp-db.php
1168:   function _real_escape( $string ) {
1169:       if ( $this->dbh ) {
1170:           if ( $this->use_mysqli ) {
1171:               return mysqli_real_escape_string( $this->dbh, $string );
1172:           } else {
1173:               return mysql_real_escape_string( $string, $this->dbh );
1174:           }
1175:       }

Затем данные улетают в функцию vsprintf, которая вставляет их в запрос, и он возвращается пользователю для дальнейших манипуляций.

/wp-includes/wp-db.php
1310:       return @vsprintf( $query, $args );

Поэтому на выходе запрос с псевдоинъекцией будет корректно экранирован и выполнен.

Экранирование потенциально опасных данных с помощью prepare
Экранирование потенциально опасных данных с помощью prepare

С виду вроде бы все логично и аккуратно. Какие же тут могут быть подводные камни?

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

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


2 комментария

  1. Аватар

    LorDo

    08.11.2017 at 21:36

    Не существует методов где использкется двойной prepare и не нужны права author, обычный юзер, или вообще без авторизац)

  2. Аватар

    growpenny

    08.11.2017 at 21:40

    Там, похоже, создатели вордпресса особо не парились секьюрити.

Оставить мнение

Check Also

Министерство внутренней безопасности США предупредило, что иранские хакеры уничтожают данные

По данным американских властей, иранские хакеры все чаще применяют малварь, направленную н…