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

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

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

Cтатьи из последних выпусков журнала можно покупать отдельно только через два месяца после публикации. Чтобы читать эту статью, необходимо купить подписку.

Подпишись на журнал «Хакер» по выгодной цене!

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

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

Подпишитесь на ][, чтобы участвовать в обсуждении

Обсуждение этой статьи доступно только нашим подписчикам. Вы можете войти в свой аккаунт или зарегистрироваться и оплатить подписку, чтобы свободно участвовать в обсуждении.

Check Also

Tips’n’Tricks из арсенала андроидовода. Самые интересные, полезные и нестандартные трюки с Android

Многие годы мы рассказывали про самые разные способы оптимизировать, модифицировать и твик…