The WordPress plugin WooCommerce runs on approximately 2,300,000 live websites1 and is currently the most prominent eCommerce platform used on the Web. During our research we discovered a PHP object injection vulnerability in WooCommerce that allows to escalate privileges. The vulnerability was responsibly disclosed to the Automattic security team and was fixed last year with the release of version 3.2.4. In this blog post we investigate how recent changes in the WordPress core database driver opened the doors for this vulnerability. Furthermore, we describe how the circumstances could be exploited with a unique and interesting injection technique.
Installations with the following requirements are affected by this vulnerability:
The vulnerability discussed in the following can only be exploited by an attacker that already benefits of some higher privileges. The ability to edit/add products in WooCommerce are required but not a full administration account that would allow to execute code anyway. That means that the vulnerability can be used to escalate an already realized attack or account takeover.
Being able to successfully inject an arbitrary PHP object during runtime can lead to different malicious actions. In the worst case, an attacker can execute arbitrary code on the server and completely take over the shop with all its sensitive data.
If you are interested in the technical details of the vulnerability and want to learn a new fancy trick for PHP object injections, continue reading on. If you would like to skip the technical part, we recommend updating your WooCommerce version at least :)
Before we start with the vulnerability, we need to briefly look into two things: The new add_placeholder_escape()
method introduced to the WPDB
class in WordPress 4.8.3 as a security fix, and the structure of serialized objects in PHP.
The security release 4.8.3 of WordPress addressed a problem with the prepare()
method of wpdb
that, in some occasions, could lead to SQL injection.
In summary, the problem was with what happens when wpdb::prepare()
is applied more than once. The example below demonstrates this behavior:
|
|
Since both values that stem from user input (c1
and c2
) are bound through wpdb::prepare()
, one might think that this is secure and that no SQL can be injected. But in WordPress 4.8.2 wpdb::prepare()
would first quote the placeholders and then bind the parameters with vsprintf()
. Let’s look at what would happen if user input is set as follows:
|
|
In our example above, the first call to wpdb::prepare()
would add quotes around the placeholder %s
and escape the user input c1
. However, in our case the placeholder %s
is replaced with the user input that again contains the placeholder %s
. The $query
would end up like the following:
|
|
In the second call to wpdb::prepare()
, all placeholders in the query (%s
) would be quoted again and this confuses the quoting:
|
|
Finally, when the values are bound with vsprintf()
the query would look as follows:
|
|
As you can see, this results in a SQL injection vulnerability. This issue was first documented by Slavco Mihajloski and later disclosed by Anthony Ferrara.
The applied fix in WordPress 4.8.3 for this problem is to replace every occurrence of percent signs in wpdb::prepare()
with a random, 66 characters long placeholder after the values are bound. These placeholders will then be replaced back to percent signs just before the query is executed.
For a better understanding of the actual vulnerability described in this post, let’s take a quick look at how the PHP interpreter turns serialized data back into its respective data type. For this intend, let’s examine the following example that represents a serialized object of type stdClass
with two attributes firstname
and mail
:
|
|
During deserialization with the PHP function unserialize()
, this string will be interpreted one character at a time.
Seeing the first character O
, the PHP interpreter knows that an object is being unserialized. For restoring the state of an object, the name of the class, the name of the attributes and their respective values are needed. The rest of the data gets interpreted as follows:
8
characters is stdClass
.2
attributes.s
), has a length of 9
and a value of firstname
.s
), has a length of 4
and a value of rips
.The key point here is that values are always accompanied by their respective length. If the content of the first attribute would not be rips
but rips";:
, the special characters would not need any special encoding and would not break anything. Only the length of the value would need adjustment to 7
and the PHP interpreter would know that these characters need to be interpreted as part of the value.
With the technical background in mind, let’s get to the interesting exploitation part.
The main idea behind the exploitation technique is demonstrated with the following code sample:
|
|
In line 12 a simple array with two elements of type string gets serialized. Unserializing the content of $serialized
at this point would simply restore the original array. The critical part in this example is that the regular expression in line 15 modifies the serialized data. It replaces _xxxxxxxxxxxxxxxxxxx_
with %
. This results in the following malformed serialized data:
|
|
Notice the length of the first array element being 24
. However, this was the length of the element before we made our replacement. The content of the first array element now reads the 24 characters abc%";i:1;s:42:"whatever
due to its length not being adapted to the replacement we made. Thus, the second element will be O:16:"Object_Injection":0:{}"
which is our successfully injected PHP object.
Granted, this is a fairly crafted example and one might have doubts if such a situation can occur in real code. However, the replacement we made in the example above does not differ much from the placeholder replacement done in wpdb
before a query is executed.
The vulnerable code in WooCommerce is in WC_Shortcode_Products::get_products()
.
|
|
The WordPress function set_transient()
is often used for caching (line 10). It saves given data for a specified period of time in the database. If the data is an array or an object it will be serialized. This data can subsequently be retrieved with get_transient()
which deserializes it (line 3). The developers used this method to cache the whole WP_Query
object created in line 7 to save resources. The WP_Query()
object contains in its $posts
attribute the content of the products retrieved and was cached at this point to save complex database queries.
Besides containing the content of the retrieved posts/products, the WP_Query
object also holds the $request
attribute. This attribute contains the SQL query that was executed to retrieve the posts. The SQL query is constructed from various arguments passed to WP_Query
and still has the 66 characters long placeholders in place (see explanation above). When set_transient()
saves the object to the database, these placeholders get removed after the object is serialized. So if an attacker can get percent signs into the $request
attribute, they will be 66 characters long placeholders when the object is serialized but will be replaced back to one character when the serialized string is saved to the database. Let’s see how this can be exploited.
Adding the WooCommerce shortcode [products skus="testsku, test%%"]
to a post or a page will cause a WP_Query
object with the following $request
attribute to be serialized and cached in WC_Shortcode_Products::get_products()
.
|
|
Notice how test%%
was transformed with placeholders to test{9016c22…}{9016c22…}
. Now when the serialized object is saved to the database, it will look like this:
|
|
Notice in line 7 how test%%
was transformed back. However, the specified length of $request
is still 590
. During deserialization, more characters will be treated as the value of $request
which can go up to the content of the $post_content
attribute of the first post retrieved. The latter is the content of a product retrieved with the shortcode. This can be adjusted by an attacker and, with proper alignment, a PHP object can be injected.
We refrain from releasing a full working exploit at this moment.
Strictly speaking, the underlying issue that made this exploitation possible is more a WordPress core issue than it is a WooCommerce one. The issue has been reported to the WordPress security team but remains unpatched as the time of writing. Code in which the whole WP_Query
object is cached with the transient API and a user can get percent signs into the executed SQL query, might be susceptible to this vulnerability. Slavco Mihajloski has also identified the same vulnerability in the WordPress plugin WP Job Manager. Even though caching the WP_Query
object with the transient API is given as a main example of usage in the WordPress codex, we strongly advise against this practice and instead, recommend resorting to caching the IDs of the retrieved posts to avoid complex database queries on subsequent requests.
Date | What |
---|---|
2017/11/13 | Reported the vulnerability to Automattic on Hackerone |
2017/11/15 | Automattic acknowledged vulnerability |
2017/11/16 | Automattic released security patch for WooCommerce with version 3.2.4 |
2018/01/08 | Informed WordPress on H1 about the issue and its relation to a practice advocated in the WordPress codex |
2018/01/10 | WordPress acknowledged the issue |
2018/01/10 | Asked WordPress about time plan. No response |
2018/01/23 | Asked WordPress about status. No response |
2018/01/31 | Automattic patches similar issue in WP Job Manager v1.29.3 |
Passing unvalidated user input to PHP’s unserialize()
function is known to be the root cause for PHP object injections. In this blog post we demonstrated a new pitfall that can lead to object injection even though the data being deserialized is serialized before. Specifically, we showed how the modification of serialized data can have dramatic security consequences. A real-world example of this kind of attack was demonstrated with the popular WordPress eCommerce plugin WooCommerce.
We would like to thank the security team of Automattic for their professional collaboration and for quickly resolving the issues with the release of version 3.2.4. If you are still using an older version, we encourage to update.