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. Оформи подписку на «Хакер», чтобы читать все материалы на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи один материал

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


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

  1. LorDo

    08.11.2017 at 21:36

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

  2. growpenny

    08.11.2017 at 21:40

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

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

Check Also

Облачные серверы компании Tesla использовались для майнинга криптовалюты

Эксперты RedLock обнаружили, что облачная AWS-инфраструктура компании Tesla использовалась…