Blog: Leaking private posts of more than 700.000 sites

Author: Nico D.
Published on

Patrowl's blog - Lire les articles privés de plus de 700 000 sites 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: Patrowl's blog - Lire les articles privés de plus de 700 000 sites

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: Patrowl's blog - Lire les articles privés de plus de 700 000 sites Now, without being authenticated, let's use the ajax nopriv endpoint to query the title of all the articles: Patrowl's blog - Lire les articles privés de plus de 700 000 sites The request contains the following parameters:

  • action=tribe_dropdown, to trigger the wp_ajax_nopriv_tribe_dropdown action
  • source=search_posts, to call the right function
  • args[nopaging]=true, to disable paging in the WP_Query object and list all data
  • args[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!

Blog: CaRE program: healthcare facilities close cybersecurity gap with Patrowl