Taking a look at the CVE List for WordPress, most vulnerabilities aren’t found within the WordPress core but inside of third-party plugins and themes.
Today, let’s talk about WordPress.
Performing a WordPress assessment might seem boring at first as core functionality [tested] and configuration does not allow for extensive security misconfigurations. Luckily, most instances use plugins and themes to add features not offered by the WordPress core.
In this blog post I would like to discuss the findings and how I discovered them. Also, I will describe different vendor responsiveness reaching from not responding at all, to not understanding the issue to fast and professional responses kindly asking for a review of the updated code ready for deployment.
WordPress allows plugins to register API routes by registering actions prefixed with either wp_ajax_
for authenticated or wp_ajax_nopriv_
for unauthenticated calls (see Plugin API)
eventON
eventON offers a event calendar plugin as core and multiple additional plugins to extend it’s functionality by booking, RSS feed, ticket systems, an review system and an CSV importer.
The assessed system had only the core and the CSV importer installed. Both plugins had vulnerabilities, we will discuss in to following.
Vulnerabilities
First, let’s take a look at the eventON core plugin. Using the following code the plugin registers a handler for the evo_mdt
AJAX action:
add_action( 'wp_ajax_evo_mdt', array( $this, 'evomdt_ajax' ) ); add_action( 'wp_ajax_nopriv_evo_mdt', array( $this, 'evomdt_ajax' ) );
A quick look into the evomdt_ajax
function show the use of unvalidated POST data being passed to mdt_form
:
function evomdt_ajax(){ if(empty($_POST['type'])) return; $type = $_POST['type']; $output = ''; switch($type){ case 'newform': echo json_encode(array( 'content' =>$this->mdt_form($_POST['eventid'], $_POST['tax']), 'status'=>'good' )); exit; break; case 'editform': echo json_encode(array( 'content' =>$this->mdt_form($_POST['eventid'], $_POST['tax'],$_POST['termid'] ), 'status'=>'good' )); exit; break; }
The function mdt_form
creates HTML output reflecting the POST parameters specified in the AJAX request.
function mdt_form($eventid, $tax, $termid = ''){ ob_start(); ?> <div class='ev_admin_form'> <div class='evo_tax_entry evoselectfield_saved_data sections'> <input type="hidden" class='field' name='eventid' value='<?php echo $eventid;?>'/> <input type="hidden" class='field' name='termid' value='<?php echo $termid;?>'/> <input type="hidden" class='field' name='tax' value='<?php echo $tax;?>'/> [...] return ob_get_clean(); }
The json_encode
function is used without the option parameter, resulting in PHP not escaping any other characters than "
inside of values (see JSON_HEX_TAG). Therefore, we can inject arbitrary HTML into the response. But no browser will evaluate HTML inside a JSON response right? Well, only if your JSON response tells the browser that it should actually be JSON.
HTTP/1.1 200 OK [...] Content-Type: text/html; charset=UTF-8 {"content":[...]
How did I find this vulnerability? Well, grep
ftw!
First, let’s grep for all the registered AJAX actions:
grep -rain "add_action([ ]*['\"]wp_ajax_"
This gives us a pretty good overview of privileged and unprivileged actions available. Taking a quick look at the handlers might reveal vulnerabilities like the one described above.
Let’s take a look at the CSV importer. As the plugin is intended to create new events, access to it should be restricted to authorized users. Using the grep command mentioned above gives us the following:
$ grep -rain "add_action([ ]*['\"]wp_ajax_" includes/class-ajax.php:13: add_action( 'wp_ajax_'. $ajax_event, array( $this, $class ) ); includes/class-ajax.php:14: add_action( 'wp_ajax_nopriv_'. $ajax_event, array( $this, $class ) );
Interestingly the plugin registers a wp_ajax_nopriv
action. Checking the code reveals:
$ajax_events = array( 'evocsv_001'=>'evocsv_001', ); foreach ( $ajax_events as $ajax_event => $class ) { add_action( 'wp_ajax_'. $ajax_event, array( $this, $class ) ); add_action( 'wp_ajax_nopriv_'. $ajax_event, array( $this, $class ) ); }
And the handler function:
public function evocsv_001(){ if(!is_admin()) exit; if(!isset($_POST['events'])){ [...] exit; }else{ [...] foreach($event_data as $event){ [...] $status = $eventon_csv->admin->import_event($processedDATA); } } [...] echo json_encode($return_content); exit; }
Interestingly, the code does check for is_admin()
. So this request should be safe, right? Let’s check the documentation:
Whether the current request is for an administrative interface page. […] Does not check if the user is an administrator; current_user_can() for checking roles and capabilities.
As AJAX actions in WordPress are accessed through the wp-admin/admin-ajax.php
file, is_admin()
will always return true
.
This plugin effectively allows any user without authentication to create events on your WordPress instance.
Another useful grep line is a search for $_GET
and $_POST
.
grep -rain "\\$_\(GET\|POST\)"
A quick check on the CSV importer reveals another possible vulnerability
$ grep -rain "\\$_\(GET\|POST\)" [...] includes/class-settings.php:33: $_POST['settings-updated']='Successfully updated values.'; includes/class-settings.php:53: $updated_code = (isset($_POST['settings-updated']))? '<div class="updated fade"><p>'.$_POST['settings-updated'].'</p></div>':null; [...]
To verify that this line is vulnerable, we take a look at the file:
[...] if( isset($_POST['evocsv_noncename']) && isset( $_POST ) ){ if ( wp_verify_nonce( $_POST['evocsv_noncename'], AJDE_EVCAL_BASENAME ) ){ [...] $_POST['settings-updated']='Successfully updated values.'; [...] $updated_code = (isset($_POST['settings-updated']))? '<div class="updated fade"><p>'.$_POST['settings-updated'].'</p></div>':null; echo $updated_code;
We can see, that if the nonce in the request is not set or invalid, that POST parameter will be reflected if set. That’s why one should not use POST variables to store application data.
Vendor contact
Sadly, we did not receive any response from the vendor.
Google Map Pro
As the name suggests, Google Map Pro offers Google Maps integration to WordPress pages.
Vulnerability
This vulnerability was found by using the following grep line which basically searches for opened tags (either PHP or HTML) which are followed by an echo of a variable.
grep -rain "]*echo[ ]\{1,\}\\$"
The output should now contain most occurrences where variables are written into a response. Eventually, I found the following line in my output
core/class.plugin-overview.php:165: <div class=" flippercode-ui fcdoc-product-info" data-current-product=productTextDomain; ?> data-current-product-slug=productSlug; ?> data-product-version = productVersion; ?> data-product-name = "productName; ?>" >
A quick check of the echoed variables revealed
$skin = $_GET['skin'];
Using this parameter, an attacker could forge a link containing a simple payload to execute JavaScript in the context of an administrator.
Vendor contact
The vendor did reach out to us informing us, that the vulnerability has been fixed already and that the vulnerability is not critical, as only the administrative portal is affected!?
Jupiter
Jupiter is a theme for WordPress featuring a WYSIWYG editor for page elements.
Vulnerability
For this vulnerability to show up in our grep output, we modify the regex to include echos that concatenate strings with variables.
grep -rain "echo \(\(['][^']*[']\|[\"][^\"]*[\"]\)[ ]*.[ ]*\)*\\$"
From that point all concatenated variables must be checked if they are tainted (contain user-controlled input). Using the given command multiple lines echoing metadata of a given post were identified. Setting the metadata to a fitting payload an editor could execute JavaScript code when displaying blog posts, without being visible in the administrative interface.
Vendor contact
Artbees did respond quite fast providing a patched PHP file for us to check. After few minor adjustments the vulnerabilities we identified were fixed.
Media Library Assistant
The Media Library Assistant plugin.
Vulnerability
Using reflected GET parameters in the included documentation of the plugin’s options site, an attacker could forge links that lead to arbitrary HTML (and therefore JavaScript) being being included inside the administrative portal. As the plugin’s admin page requires a valid nonce to present in the request, attacks on this vulnerability may not be practical.
Vendor contact
The vendor did respond very quick, providing fixes for the reported and additional occurrences of the vulnerability.
Conclusion
We could identify vulnerabilities in nearly all installed plugins of the WordPress instance. Some of the plugins are free, some are not. The more critical vulnerabilities were identified in the non-free plugin. As seen earlier, installing a WordPress plugin may have a severe security impact on the whole website.
Recommendation
As PHP plugins are delivery as source code, I’d recommend a quick check with grep
, which may already identify possible security vulnerabilities. The grep
commands I used are:
grep -rain "add_action([ ]*['\"]wp_ajax_"
grep -rain "echo \(\(['][^']*[']\|[\"][^\"]*[\"]\)[ ]*.[ ]*\)*\\$"
grep -rain "<[^>]*echo[ ]\{1,\}\\$"
grep -rain "echo \(\(['][^']*[']\|[\"][^\"]*[\"]\)[ ]*.[ ]*\)*\\$"
Maybe those can help you identify vulnerable WordPress plugins in your own instance as well.
Cheers,
Malte