Blog: We Wanted to Talk About Cyberattacks During the Olympics, but We Have Nothing to Say
Blog: Leaking private posts of more than 700.000 sites
Author: Nico D.
Published on
After the previous article presenting the discovery of a finding in the UpdraftPlus
Wordpress plugin, let's continue to deep dive into popular WordPress plugins.
Wordpress is a fascinating piece of technology. It's first version was released in 2003, more than 20 years ago, and it has made its way up to being the most used Content Management System (CMS). It is used by no more than around 43% of all websites in the world.
At Patrowl, we are doing automated pentesting at large scale with almost 18.000 assets being pentested and more than 200.000 assets in passive recon and what we see confirms these statistics: Wordpress is used everywhere! With corporate websites, blogs or e-commerce platforms, WordPress is known for being usable in almost any circumstance. And this is only possible thanks to its famous plugin system.
WordPress is made so that any plugin can interact with absolutely everything using components such as actions, filters or blocks. Because of these, a plugin can change the way WordPress looks, how it behaves and, by the way, it can actually significantly reduce its security level.
Looking for targets
Just as for the previous vulnerability, we always choose a target based on information reported by our continuous and automated pentesting solution. Indeed, beside exploitable vulnerabilities, it also returns technical information about outdated components, verbose error messages, technologies used, etc. We only provide our customers with validated and consolidated vulnerabilities, but we use all this information to guide our research.
Recently, on an high value target, we got warned about an outdated plugin:
This plugin version was released 2 months ago and a few vulnerabilities has already been reported for previous versions of this plugin. With more than 700.000 active installations, it is an interesting target for a security oriented code review!
CVE-2023-6557 - The Events Calendar <= 6.2.8.2 - Unauthenticated Sensitive Information Exposure
Description
The Events Calendar plugin helps users to create and manage an events calendar on a WordPress site. It is the #1 calendar plugin for Wordpress with more than 700.000 active installations.
While reviewing the security of this plugin, Patrowl found that it is vulnerable to an authorization mismanagement that could enable an attacker to get access to information about private, draft or password-protected posts. Let's dive into the source code!
Code source analysis
At Patrowl, we mainly perform black box pentests. Therefore, we often aim for unauthenticated vulnerabilities.
In WordPress, multiple hooks can be triggered by an anonymous user.
Especially, it is possible to use the wp_ajax_nopriv_{$action}
hook that will be triggered when a non-authenticated user sends a request to /wp-admin/admin-ajax.php
with the parameter action={$action}
.
In The Events Calendar plugin, we can find the use of this hook in the Tribe__Ajax_Dropdown
class:
// common/src/Tribe/Ajax/Dropdown.php L.17
public function hook() {
add_action( 'wp_ajax_tribe_dropdown', [ $this, 'route' ] );
add_action( 'wp_ajax_nopriv_tribe_dropdown', [ $this, 'route' ] );
}
This class is created and this method called when the plugin loads and is therefore loaded for any user, in any circumstance.
The route
function looks like this:
// common/src/Tribe/Ajax/Dropdown.php L.265
public function route() {
// Push all POST params into a Default set of data
$args = $this->parse_params( empty( $_POST ) ? [] : $_POST );
if ( empty( $args->source ) ) {
$this->error( esc_attr__( 'Missing data source for this dropdown', 'tribe-common' ) );
}
// Define a Filter to allow external calls to our Select2 Dropdowns.
$filter = sanitize_key( 'tribe_dropdown_' . $args->source );
if ( has_filter( $filter ) ) {
$data = apply_filters( $filter, [], $args->search, $args->page, $args->args, $args->source );
} else {
$data = call_user_func_array( [ $this, $args->source ], array_values( (array) $args ) );
}
// If we've got a empty dataset we return an error.
if ( empty( $data ) ) {
$this->error( esc_attr__( 'Empty data set for this dropdown', 'tribe-common' ) );
} else {
$this->success( $data );
}
}
There are two interesting parts:
- POST data are parsed using the
$this->parse_params
function and placed into the$args
variable - The result of this function is passed to
call_user_func_array
with$args->source
being used as function name and the all$args
variable being used as function arguments
The parse_params
method is basically a wrapper around the wp_parse_args
function which merges user defined arguments into defaults array.
The call_user_func_array
is used so that it is only possible to call methods of the same class. Among all implemented methods, the search_posts
is one is interesting:
// common/src/Tribe/Ajax/Dropdown.php L.113
public function search_posts( $search, $page = 1, $args = [], $selected = null ) {
if ( ! empty( $search ) ) {
$args['s'] = $search;
}
$args['paged'] = $page;
$args['update_post_meta_cache'] = false;
$args['update_post_term_cache'] = false;
$results = new WP_Query( $args );
$has_pagination = $results->post_count < $results->found_posts;
return $this->format_posts_for_dropdown( $results->posts, $selected, $has_pagination );
}
This function performs a search using the WP_Query
object and uses the user-controlled variable $args
to configure the search.
The query result is formatted using the format_posts_for_dropdown
method that returns only the title and id of each post.
Having full control over the WP_Query arguments from an anonymous user could lead to significant data leak.
Exploitation
Let's create a new WordPress instance with the vulnerable version of The Events Calendar
plugin and create some posts with different status:
Now, without being authenticated, let's use the ajax nopriv endpoint to query the title of all the articles:
The request contains the following parameters:
action=tribe_dropdown
, to trigger thewp_ajax_nopriv_tribe_dropdown
actionsource=search_posts
, to call the right functionargs[nopaging]=true
, to disable paging in the WP_Query object and list all dataargs[post_status][]=all
, to list all posts, including private ones
Because of the vulnerable code in The Events Calendar plugin, it is possible to list the title of all posts, including draft, password-protected and private ones.
Attempt to dump a post content
Being able to read the title of all posts is interesting but it would be way more interesting to dump the actual content of any post published.
The WP_Query
object enables to search for specific patterns in posts. It is therefore theoretically possible to dump the content of a post.
When performing a search using WP_Query
, multiple rules are implemented to soften the search and returns as many potential results as possible. However these rules make it difficult to use WP_Query
to recover the full content of an article.
While exploring the source code of WP_Query
, we found multiple arguments that could help to dump the content of a post using the previous vulnerability:
p
: defines the id of the article to search for. It can be useful to prevent searching in all posts created. Be careful this argument will not work for private posts.search_columns
: list of columns in the database where to look for matches. There is a whitelist so that only the following columns are allowed:post_title
post_excerpt
post_content
: It is the most interesting for our use case
sentence
: if enabled, the search term is considered as a single block. No parsing is performed and it ensures the exact searched content is in the searched columns.
By mixing these parameters, it is possible to search for a specific precise content and to retrieve the content of an article, character by character.
Obviously, this search method is slow being in O(n*m)
where n is the number of chars in the post content and m is the size of the alphabet.
It is possible to reduce the size of m
by first checking if a character is present in the content.
This is doable thanks to a rule in the search pattern stating that if the pattern starts with -
, WP_Query
will perform a reverse search. Therefore, if searching for -c
returns a result, it means that the c
char is not in the post content.
We scripted this search method and confirmed it is possible to recover short posts in a reasonable time, significantly increasing the severity of the vulnerability.
Remediation
To fix the vulnerability, the vendor has implemented a protection that prevents users from manipulating any key other than taxonomy
and post_type
in the args
parameter, and has set post_status
to publish
.
This simple correction effectively prevents requests for data on private posts. Moreover, the method described to dump the content of a post is no longer exploitable.
Simple, but efficient!
Conclusion
At Patrowl, we love automated and continuous pentest. Our unique scanning and testing automation guides us into manual searching to ensure we only return real vulnerabilities to our customers. We will continue to look for new vulnerabilities based on findings we get on our clients' assets and stay tuned for future posts!
Patrowl Raises €11m in Series A Funding: Continuous Protection of Internet Exposed Assets
Blog: RegreSSHion, critical vulnerability on OpenSSH CVE-2024-6387