When you first start reviewing Wordpress you’ll be immediately hit by what looks like weak system design and poor programming practices.
Developers writing SQL queries in Wordpress involves, in many cases, string concatenation and that developers remember to sanitize their inputs.
The following is an example in the class
WP_Term_Query which is used to query the Wordpress taxonomy, a feature that offers Wordpress developers the ability to create custom categories to describe posts, but this pattern is extremely prevalent in the Wordpress codebase. In the example below you’ll see that a
LIMIT SQL clause is being constructed using string concatenation:
Eventually the various clauses are combined further down in the file to form the full SQL statement:
The next question is to establish how
$number are validated. They’re validated further up the class in the
parse_query function which converts the input into an integer.
parse_query is called within the main entrypoint
get_terms. This means that the validation will always be called no matter how any developer integrates with this class.
So what’s the problem with this code then? We’ve established that validation works in this case and can’t be bypassed, everything is ok — right?
Well the answer to that question gets to the core of what it means to design a secure system. Perhaps we can define a system as secure if security researchers are not uncovering vulnerablities. It looks like the last publically disclosed instances of SQL injection being uncovered in the Wordpress core was in 2017. So in this sense, perhaps Wordpress is sufficiently secure against SQL Injection vulnerabilities.
Although it doesn’t mean that a security flaw will never emerge. Systems are constantly added to and removed from, so a vulnerability that doesn’t exist today may exist tomorrow, and vice versa. Perhaps a better definition of a secure system is one where the emergence of a flaw is less likely to lead to complete compromise of the system, i.e. there are sufficient mitigations in place. Under this definition, Wordpress, in my opinion, isn’t sufficiently secure against SQL injection. For every endpoint, any mistake in type validation could potentially lead to a serious vulnerability.
A secure system is one where the emergence of a flaw is unlikely to lead to complete compromise of the system.
The bigger issue with how Wordpress implements SQL queries is that developers and review processes are fallible. By relying on string concatenation and userland validation, every line of query code becomes security critical — since only one small mistake, one oversight, is all that will be needed for complete compromise of the database.
Other platforms and frameworks generally isolate this functionality into a specific place, a core abstraction that can be used by developers to implement queries. The result is that only one area of code must be tested and secured. For example in PHP’s symphony framework, an ORM is implemented. The limit clause creation would look something like this:
$entities = $em
->createQuery( '...' )
In this case, even if a developer forgets to convert the string input into a number, perhaps the request will fail but no vulnerability arises.
All of this so far sounds fairly damning and critical of the Wordpress developers, but before going on I think it’s important to reevaluate our own views and preconceptions in light of our findings. After all, my review did not uncover any SQL injection vulnerabilities, nor have there been any publically disclosed in quite some time. Perhaps the reality is that these flaws are of such a trivial nature, are so easy to review for, grep for and fix, that the risk is just theoretical. Perhaps the lack of any core abstractions, i.e. an ORM, makes the code easier to implement/review and therefore critical flaws have nowhere to hide except in plain sight.
The argument sound reasonable, but am I convinced? Not really. Are you? I’d love to hear your thoughts below.
I explored many other options in the time I allocated to Wordpress, in particular I looked at how values are serialized/deserialized in the Wordpress database. You’ll find that PHP
serialize is used in a number of key places in the core application. Some notable examples of this are the various
_meta tables and the options/transient api which is used for managing settings and cached data.
When reviewing how metadata values are retrieved, I found the code below interesting — why would a value only “maybe” be unserialized?
The answer is that when data in written in, it’s only serialized in the case that the value is an array or an object, or it passes the
is_serialized case. In other words, Wordpress wants to support writing scalar strings into the database —possibly to avoid the overhead of deserializing every entry in the metadata/options/transients tables.
If the alarm bells are ringing at this point, you’d be thinking along the same lines as me — is it possible we can find a string that will be written as a string but deserialized as a PHP object?
The answer was no, since the logic is symetrical enough to avoid exploitation. Even with complete control over the string value being written to a metadata/transients/options table, this is currently unexploitable due to this. Interestingly it looks like 12 years ago the developers of wordpress accidentally introduced an arbitrary deserialization flaw with the release of wordpress 3.6.1. This is ancient history in terms of security developments, so I don’t regard this as something to hold against the wordpress developers.
So why am I telling you about this if it is not a flaw? It’s notable for two reasons. The first is that it gives us a target for database writes if we find an SQL injection vulnerability. If we can combine this with the right deserialization gadgets, it could potentially be leveraged to achieve RCE.
The second reason is that PHP’s native serialization is inherently risky to use. It doesn’t implement any functionality that validates the serialized value was issued by the application or another known actor. This means that if an attacker can write something that looks like a serialized PHP value, it will be deserialized without any verification. Since most Wordpress applications include third party plugins, some of which will have a poor security posture, it’s certainly possible that attractive deserialization gadgets exist in many installations. Any developments in this space will be crucial to keep an eye on.
Anyone who knows anything about Wordpress, will have heard about Wordpress plugins. Wordpress is built around the concept that third party developers can write extensions to the Wordpress core. These plugins can then be installed by non-technical website administrators via the wordpress admin UI, if it is enabled. PHP plugins are a trope for achieving RCE in both the real-world and CTFs.
However, we’re not interested in admin functionality since Wordpress adminstrators are already extremely highly privileged. What I found interesting is that Wordpress has a secondary mechanism for loading plugins named must-use plugins which are loaded on boot regardless of whether the plugin is enabled by an admin member of staff. In the image below you can see the bootstrap code in
wp-settings which is included in all wordpress pages.
The only requirement is that a PHP file is dropped in the
WPMU_PLUGIN_DIR folder which is is the
wp-content/mu-plugins folder by default.
Whilst this alone isn’t a vulnerability, since as an unauthenticated user we have no mechanism for writing to the
WPMU_PLUGIN_DIR directory, this directory does offer an attractive and relatively stealthy alternative target for wordpress installations with plugins installed. If a situation arises in which you have an arbitrary filesystem write, but can’t write to the
/var/www directory, can’t get the web server to execute your script as PHP, or need persistence, this could be a potentially useful alternative.
The analysis I’ve done so far has looked at a very traditional route to RCE. Putting it all together, if a bug does emerge that gives us SQL Injection, the path to RCE could look like one of these scenarios: