- Critical Thinking - Bug Bounty Podcast
- Posts
- [HackerNotes Ep. 55] Popping WordPress Plugins - Reviewing and Exploiting WordPress
[HackerNotes Ep. 55] Popping WordPress Plugins - Reviewing and Exploiting WordPress
Popping WordPress Plugins: Ram Deep Dives His Research, Common Plugin Flaws, Code Review Methodology and Creative Escalation Vectors
Hacker TLDR;
Ram’s Research Unveiled: Ram, a distinguished WordPress Security Expert, delves into the intricacies of his latest research, covering discoveries ranging from RCE to SSRF within some of the most widely used WordPress plugins.
Common Design Flaws: Justin and Ram share a wealth of insights on prevalent vulnerabilities discovered during their hunting endeavours, including:
CSRF in WordPress: WordPress relies on numbers used once (nonce) values for CSRF protection. Plugins have the flexibility to modify the behaviour of these values, including how they are generated and handled which can lead to some serious design flaws.
WordPress behaviour quirks: WordPress has no shortage of unexpected behaviours which could be useful gadgets. Some of these include using null bytes
%00
to trick WordPress into thinking you’re navigating to another page, being able to send a body with a GET request and WordPress still processing the body (in some scenarios), and many more.
Sources, sinks and the inner workings of WordPress: Ram and Justin share their code review methodology with sources and sinks they look for - we couldn’t fit them all but here’s a taster of what to expect:
Common WordPress Sources: get_query_var, filter_input add_action, add_filter, Register_rest_routes, add_submenu_page, Shortcodes
Common WordPress Sinks: update_option, add_post/update_post/delete_post/trash_post, update_user_meta, move_uploaded_file / unzip_file, wp_remote_get
SQLI Specific Sinks: Common sinks and design patterns which often result in SQLi.
Deserialization and POP chains: Some more exotic exploits in WordPress using PHP PHAR files with some great writeups of this being abused in the wild.
Plugin Escalation Chains: Some seriously creative ways of going from low privilege user to RCE are dropped:
File Deletion: Deleting the wp-config.php forces WordPress into a setup state, allowing you to reconfigure the configuration of the site.
Stored Blind XSS: Finding a BXSS which targets an admin user provides a means of accessing plugins, meaning a plugin can be modified to include a shell.
Update Options: A plugin which supports updating options with user registration could lead to an easy privilege escalation. You can set the default signup role to administrator, and enable registration and signup.
Password Reset Functionality: Plugins commonly implement weak checks or use easily guessable values within reset functionality. It can often allow swapping of user IDs or brute force values, resulting in ATO.
WordPress Plugin Bounties: Justin shares his repo publically to help spot new Plugin code for WordFence’s bug bounty program, which has a current 6.25x multiplier on payouts!
Ram Gall - WordPress Security Expert
If you’re looking for tips when it comes to WordPress testing then it's your lucky day. Ram Gall, part of WordFence, is somewhat of an expert in this field and has some seriously impressive knowledge to share.
This episode was littered with tips, tricks and weird WordPress behaviour. Regardless of whether you’re a complete newcomer or well-versed in the WordPress ecosystem, there's going to be some takeaways in this episode.
When we’re talking WordPress vulnerabilities we’re clearly in Ram's domain, with countless findings under his belt he mentions some of his more notable ones:
RCE In Elementor: Now Ram's writeup for this one can be found here, but he identified a vulnerability in the Elementor plugin which allowed any authenticated user to upload arbitrary PHP code. This wasn’t just a random plugin with a small user base either - Elementor is one of the most popular plugins available with over 5M users.
Getwid SSRF: Ram found that authenticated users (including subscribers) could abuse functionality in the plugin to achieve SSRF. In this instance, Ram chained this with the fact the host was in AWS and hit the metadata endpoint, which can be used to gain access to the associated AWS environment. The full writeup for this can be found here.
Shield Security XSS: Even security-related plugins are vulnerable sometimes! In this instance the user agent header was being logged didn’t appropriately input validate or output encode in the backend admin dashboard. This resulted in Ram being able to exploit stored XSS.
WordPress Fundamentals
Before we dive straight into source code we have to learn some fundamentals of how WordPress operates. As you’ll discover, WordPress has quite a few components and names which don’t behave or are named intuitively.
If you are completely fresh when it comes to WordPress, it's essentially an open-source content management system (CMS) written in PHP. One of the reasons for its popularity is its flexibility and ability to customize functionality through WordPress plugins. Plugins are the focus of this episode and work by hooking into WordPress core.
A quick 101 on WordPress and how this happens under the hood before we go deeper:
WP Ajax: Almost everything uses it including things that aren’t specifically Ajax. Ajax is one of 3 main methods to hook into WordPress core functionality (others are Rest_routes and XML_RPC). By default, you have to be authenticated to use it but
wp_ajax_nopriv_
allows unauthenticated access.WordPress allows the creation of hooks in the application that correlate to different paths in WP core functionality. These hooks are used to interact with the underlying core files of WordPress.
Numerous hooks exist (more on these shortly), and these can be triggered by hitting endpoints such as
admin-post.php
andadmin-ajax.php
(which by the way, are often called in completely non-admin related functionality) and can be used to trigger callback functions.Callback functions are usually the foothold of exploitation, and can be abused as these are usually heavily dependent on user input.
WordPress heavily depends on a nonce (number used once) value to implement CSRF and sometimes even access control protections.
It's worth mentioning that WordPress has a few user roles including administrator, editor, author, and contributor.
A solid resource we at Critical Thinking highly recommend reading if you want to brush up on the inner workings and how they can be exploited can be found here.
CSRF in WordPress
There are a few common pitfalls when it comes to WordPress plugins. These are often a symptom of not fully understanding the inner workings of WordPress, and developers trying to implement their versions of native WordPress functions inside plugins, often leading to CSRF due to mishandling of the nonce. Some points on the nonce:
Nonces are issued every 24 hours but are actually valid for 48 hours in total.
Plugins can create nonces and they can be created for different purposes - they can be created globally (not recommended in some cases), created per action, and so on.
Nonces are not only used for CSRF protections but are sometimes used to implement access controls.
The Ajax endpoints and callback functions which most functionality depends are often candidates for CSRF and broken access control issues, due to the lack of in-place checks. To add to this, WordPress plugins have a lot of flexibility when it comes to implementation.
Some functions that are responsible for verifying the value include check_admin_referrer
(don’t worry, we’re all thinking it - why is it named that?) this flexibility and ambiguity naturally lead to some misimplementations when plugins are created.
Some plugins try to protect the nonces by only displaying them if the user is on specific pages, ie a backend configuration panel. Under the hood, this is done by checking the global page_now
to determine which admin page the user is on.
In some configurations, due to the use of PHPs self
, you can trick WordPress into thinking you are on another page via https://target.com/normalpage%00targetpage
- this could be fruitful when you are trying to grab a nonce for a function on a specific page.
One of the downsides of this implementation but a massive upside for exploitation is if we can leak this nonce value, we could bypass any form of CSRF protection or access control associated with it.
Finding the sources
We’ve so far mentioned some aspects being heavily dependent on user input, so how do we find the source of our inputs? A lot of the standard PHP functions can be used as sources, but there’s a whole bunch of WordPress-specific ones.
From Justin and Rams’s experience, trying to automate tracing the source of user input can become tricky to get reliable results due to the amount of abstraction that can happen if a plugin is doing custom request parsing. A huge variety of coding styles also makes it difficult to do reliable static analysis.
Due to this, it often requires a bit of manual intervention and time tracing sources by hand. Luckily, the episode was littered with knowledge on sources so we decided to create a cheatsheet of all the ones covered:
get_query_var
This is specific to WordPress search functionality and retrieves a value of the given query variable
filter_input
Grabs input and filters it. Doesn’t sanitise input unless you tell it to BTW.
add_action
Allows you to specify callback functions for specific hooks that you can trigger - ie
admin_init
hook which can be triggered by browsing to any admin endpoint such asadmin_post
,wp_ajax_
. All great methods to trigger the code you want to reach, and they also usually don’t have native defences against CSRF or auth checks.
add_filter
WordPress-specific function that allows you to create callbacks on specific functions that occur when the apply filter is run. Accepts WordPress and custom filters.
For example, you can add a filter that adds a callback whenever a new post or setting is updated.
Doesn’t execute until apply_filters is used on it.
Allows room for exploitation as sometimes users can abuse this to hit paths in the codebase which aren’t supposed to be accessible in their user's context.
Register_rest_routes
Allows developers to define their own REST API endpoints beyond the default WordPress REST routes. It’s possible to abuse this to view all registered routes on the WP instance and dig into all of the namespaces by browsing to
/wp-json
or/?rest_route=/
If no nonce is provided to a rest route, it will assume you are unauthenticated.
add_submenu_page
Takes a capability and uses it to determine whether or not to include a page in the menu.
The capability check is performed during page registration then a callback check to whatever function renders the page. Not all plugins use this method to call the function, some use
admin-ajax
,admin-ini
so keep your eyes peeled.
It will perform a check to see what the current page is to see if you are meant to be there.
Shortcodes
Shortcodes are a way for people who have the ability to write posts to embed specific functionality in posts such as contact forms via the syntax
[shortcode_name attribute1="value1"]
.Plugins register shortcodes which can then be referenced which WordPress parses when looking through posts with a custom regex parser. If it spots one which has been registered, it then runs the function associated with it.
This has been historically a ripe vector for XSS.
Posts which are published get checked by an admin or editor user at some point during the life cycle (with a small caveat being an author can publish their own posts), meaning this can allow for some pretty tasty XSS-based privilege escalation scenarios.
It’s worth mentioning a unique behaviour in WordPress when looking at these sources - You can use GET requests, or any other request methods for that matter, with a body just as you would for a POST request and WordPress seamlessly parses the body as well as the URL query arguments.
A good resource to get familiar with these sources and WordPress in general is the WordPress Developer Resources portal - https://developer.wordpress.org/reference/functions/.
Finding the Sinks
Now we know where user input starts, how do we find where it's executed? WordPress offers many sinks when it comes to code execution. Some of the common native sinks include:
update_option
WordPress options include information such as default roles and whether or not users can register.
If you can set the default role to administrator and allow user registration, you can escalate privileges to an admin user.
update_option
is commonly used for API keys, but it often does not escape on output. This can be used for XSS when specifying an API key in the option, and it will execute when an administrator visits the page where they set the API key.Justin and Kodai used this in an exploit themselves. The exploit submits a form to a new tab, on the old tab it performs a time-out, and after 2 seconds it redirects to the new tab poisoned with the payload.
add_post/update_post/delete_post/trash_post
These sinks as the name suggests are all sinks related to adding, updating, and deleting posts (unlike the admin_referrer mentioned earlier)
If input ends up in
add_post
without any form of sanitisation during the flow, it can be another easy vector for XSS.These could also combine CSRF to achieve XSS and subsequently RCE. As admin and editor users have access to input raw HTML in posts, if you CSRF a user with this privilege containing an XSS payload on the functionality associated with these sinks, you can effectively turn a CSRF into an XSS, which could then be abused to upload a malicious plugin resulting in RCE. Not bad huh?
It’s worth noting that the default way of updating posts often has nonce protections, so the plugin would either have protections disabled or you’d have to find an additional gadget around the nonce to exploit it.
Another weird tip - if you double trash a post, it actually permanently deletes the post.
update_user_meta
Used to update the user metadata. A user's role, capabilities and so on are all stored in user metadata. This means if you find a way to update arbitrary user metadata, you could privilege escalate to an admin.
A common place this happens is membership-related plugins, sometimes allowing you to set your role upon registration.
move_uploaded_file / unzip_file
move_uploaded_file
is responsible for checking file uploads and moving them to a specified location.unzip_file
unzips a provided file to a given location on a file system.If file types aren’t checked within the zip it can provide means of exploitation - think your normal file upload exploitation vectors, zip slip and so on.
Keep an eye out if a plugin isn’t using these standard functions to perform similar functionality as it could lead to something tasty.
wp_remote_get
Great vector for SSRF, performs an HTTP request using a GET and returns its response.
wp_handle_upload
Another file upload function. Some plugins can override the default configuration, allowing additional overrides for specific extensions, mime types and so on.
It does however perform thorough extension and mime checking on uploaded files if the default configuration is used.
is_file , file_exists or file_get_contents
All file-related functions which are used to check the existence of a file and get the contents of the specified file.
When combined with PHP PHAR input, they can result in RCE (more on this in the next section).
_e
It is used to echo out the text or translate the text if one is available in the requested language.
Can be combined with file upload capability to upload malicious translation files resulting in XSS.
Echo With request_page or $_REQUEST Instead of $_GET or $_POST
It’s possible to overload variables in WordPress when
Request_page
or$_REQUEST
is used.If a page parameter is included in a POST request, WordPress will incorporate it into the request page. This can be exploited to provide two
page
parameters in a single request—one in the GET parameter and another in the body. The page specified in the body will be echoed onto the page.
This can be used as a vector for XSS by putting an XSS payload in the request body.
If you want to read more about the behaviour which allows for this exploitation and see how this was exploited you can read one of Ram's writeups here.
SQLI Sinks
Wpdb::prepare + Concatenation
A common sink for SQLI is using prepared statements for part of the SQL query but then concatenating a string directly to it
This is often an oversight from developers - make sure to check all prepare calls for any concatenated input as it can provide a means of exploitation.
Wpdb::prepare using %1s instead of %s doesn’t add quotes
If using
%1s
for a labelled string instead of%s
for a standard string the function doesn’t properly add quotes, providing a means for SQL injection. This behaviour was implemented for backward compatibility reasons.
wpdb::get_row / wpdb::get_query
Used for interactions with the database - a common sink which could lead to SQLi if used unsafely.
Esc_sql + non-quoted strings
Although the function performs some escaping, depending on the context of the SQLi query if there are no quotes to escape, you can still use plain old character-based SQLI - no need to break out of any string-related contexts.
Useful in type juggling-based scenarios where it is possible to provide a different type.
Escaping mechanisms
Now there’s a lot of attack surface in WordPress that we just mentioned, especially for XSS. So how do developers defend against it? Some functions to help do that are detailed below. Fortunately for us, these aren’t all foolproof and have their nuances.
Ram mentioned on the pod from his own experience that although some of these escaping mechanisms are battle-tested, they have to be used in the right context.
If for example escape_json
is being used on an input that isn’t JSON, it could provide a means of bypassing the escaping in place. Developers can often make assumptions on provided input which can often lead to ineffective, or no sanitisation at all.
wp_kses
is a common function used for XSS sanitisation. It relies on some pretty complex regex-based sanitisation which has been tried and tested, also working against attribute-based XSS.
The caveat is its context-dependent and is mainly effective when run on complete HTML contexts instead of individual inputs. If used on incomplete HTML, this method of escaping can sometimes fall prey to short code-based XSS.
Deserialization, POP Chains, and Their Importance in a WP Context
WordPress, under the hood, serializes data everywhere. Plugins commonly try to follow this behaviour and spin their own implementation which often leads to exploitable behaviour.
A common example of this is in plugins setting serialized session data in a cookie and serializing setting configurations. This is where POP chains come in handy…
POP (Property Oriented Programming) chains are useful in deserialization scenarios when using PHAR-based deserialization. PHAR-based deserialization occurs when a PHAR (PHP archive file) is deserialized, but PHAR files have a few unique properties.
They occur when you can inject an arbitrary URL into a function such as is_file
, file_exists
or file_get_contents
. If you provide a PHAR it will load the file and deserialize the metadata associated with it. Due to the properties and metadata you can set in a PHAR file, this more often than not leads to RCE.
Exploitation can get tricky, requiring a class you can instantiate and setting attributes of the class, then an additional class you can use to trigger the class. It's worth noting this is only vulnerable in PHP versions 7.x and below.
Ram has done a great write-up on this in the context of WordPress here, resulting in RCE.
WP Escalation Chains
We’ve touched upon some of these throughout the post but WordPress’s ecosystem offers an attacker many paths to privilege escalate. Due to the severity of privilege escalation attacks in WordPress with admin users being able to achieve RCE, we thought it deserved its own section.
Some common vectors to look out for when reviewing a plugin:
File Deletion -> Delete wp-config.php
If you have a means of deleting arbitrary files then you’re in luck. The wp-config.php
file is used to store WordPress configuration settings.
If this file no longer exists WordPress enters a setup state, which means an attacker could re-configure the site to connect with a remote database under their control.
One important thing to note here is the old database could be trashed in the process, so make sure you have backups before trying.
Stored Blind XSS -> RCE via Admin Hijacking -> Plugin Edit
We’ve touched upon this earlier on in the post, but admins can edit plugins. A plugin can be edited to include malicious code containing a shell, resulting in RCE.
Identifying a stored XSS in a plugin could allow you to craft a variety of payloads for an admin user. If you could craft a payload to check if the user is an admin, browse to the plugins and modify an existing plugin or upload a new one, you could modify the plugins to contain a shell.
Update Options
This one was also mentioned earlier but the idea here is once you have a gadget to arbitrarily update options, set the default role to administrator and enable user registration. Then, you can self-register as an admin user.
Roll Your own Password Reset Functionality
Lots of plugins support password reset functionality and roll their own instead of using native WordPress core methods.
More often than not, they set a reset code that is easily guessable or will specify the user ID in the password reset flow, which can then be swapped for another user’s ID.
This is commonly seen in plugins which track user memberships or in some way manage users.
Hacking Plugins Yourself
If you’d like to get hands-on with some review Justin has created a GitHub repo which you can check for diffs in plugins, giving you a giant playground to put all this knowledge into practice. The URL for this is https://github.com/WordpressPluginDirectory.
Wordfence also offers a bug bounty program covering a tonne of plugins. If you want to start hunting, be sure to check it out here: https://www.wordfence.com/threat-intel/bug-bounty-program/.
If you haven’t listened to the pod already, I highly recommend doing so. Ram didn’t stop dropping WordPress wisdom and weird behaviours he discovered during his time testing plugins.
If you’re interested in his research on WordPress plugins you can find all of his writeups here: https://www.wordfence.com/blog/author/ramwf/
Until we meet again, dive into those WordPress plugins!