The LayerSlider v6.2.0 suffers from multiple vulnerabilities:

Cross-site Request Forgery aka. CSRF

The ls_save_screen_options function does not validate the request with a nonce.

function ls_save_screen_options() {  
    $_POST['options'] = !empty($_POST['options']) ? $_POST['options'] : array();
    update_option('ls-screen-options', $_POST['options']);

This function is hooked to an ajax call with the same name:

// AJAX functions
        add_action('wp_ajax_ls_save_slider', 'ls_save_slider');
        add_action('wp_ajax_ls_save_screen_options', 'ls_save_screen_options');
        add_action('wp_ajax_ls_get_mce_sliders', 'ls_get_mce_sliders');
        add_action('wp_ajax_ls_store_opened', 'ls_store_opened');

An attacker can create a malicious web page that can POST to the following parameters: action = ls_save_screen_options
and anything in the options POST parameter will be stored on the WP Options under the ls-screen-options key.

Cross Site Scripting aka. XSS

If we look where the ls-screen-options option key gets used, we can see that it is used among other places, in the '/wp-content/plugins/LayerSlider/views/skin_editor.php' file. This file is called when the LayerSlider plugin pages in the admin area are viewed.
On line 8, it stores the content in the $lsScreenOptions variable.

// Get screen options
$lsScreenOptions = get_option('ls-screen-options', '0');
$lsScreenOptions = ($lsScreenOptions == 0) ? array() : $lsScreenOptions;
$lsScreenOptions = is_array($lsScreenOptions) ? $lsScreenOptions : unserialize($lsScreenOptions);

To be used later on line 168 for example

<form id="ls-screen-options-form" method="post" novalidate>  
    <h5><?php _e('Show on screen', 'LayerSlider') ?></h5>
    <label><input type="checkbox" name="showTooltips"<?php echo $lsScreenOptions['showTooltips'] == 'true' ? ' checked="checked"' : ''?>> <?php _e('Tooltips', 'LayerSlider') ?></label><br><br>

    <?php _e('Show me', 'LayerSlider') ?> <input type="number" name="numberOfSliders" min="8" step="4" value="<?php echo $lsScreenOptions['numberOfSliders'] ?>"> <?php _e('sliders per page', 'LayerSlider') ?>
    <button class="button"><?php _e('Apply', 'LayerSlider') ?></button>

As we can see, it does not escape the contents of the $lsScreenOptions['numberOfSliders'] variable, just echoes it. Although before, when it was used for mathematical calculations it was converted to an int.

// Pager
$maxItem = LS_Sliders::$count;
$maxPage = ceil($maxItem / (int) $lsScreenOptions['numberOfSliders']);
$maxPage = $maxPage ? $maxPage : 1;

Nonetheless, this is leading to a stored XSS, after a successful CSRF.

(Blind) SQL Injection aka. SQLi

Looking at the same file, we can observe that in case the order GET parameter in a non empty string, it will get added to the $filters array, which in turn will be passed to the find method of the LS_Sliders object.

if( ! empty($_GET['order']) ) {  
    $userFilters = true;
    $urlParamOrder = $_GET['order'];
    $filters['orderby'] = htmlentities($_GET['order']);

    if( $_GET['order'] === 'name' ) {
        $filters['order'] = 'ASC';


// Find sliders
$sliders = LS_Sliders::find($filters);

So from order GET parameter we get the orderby named element in the $filters array. It tries to escape the value by passing to the htmlentities function, but I think that was intended as an XSS filter, not something that would prevent a SQL injection at the order by clause.

Whenever I see something like that, I know that there might be some interesting thing down the road. And indeed there is.

In the /wp-content/plugins/LayerSlider/classes/ file we find the class definition of the LS_Sliders with the find method around line 55. The interesting bit is around line no. 128

// Build the query
global $wpdb;  
$table = $wpdb->prefix.LS_DB_TABLE;
$sliders = $wpdb->get_results("SELECT SQL_CALC_FOUND_ROWS {$args['columns']} FROM $table $where
                        ORDER BY {$args['orderby']} {$args['order']} LIMIT {$args['limit']}", ARRAY_A);

This whole method is full of SQL injection issues, but now we can focus on the {$args['orderby']} part. There are different methods to exploit a SQL Injection at the ORDER BY clause, but because usually we do not get the error message back, we will focus on the hidden error method.

Short info on Blind SQLi

THEN 1 ELSE 1*(SELECT table_name FROM information_schema.tables)END)=1  

This is a valid SQL and because 1 = 1, it will work, however if we modify the condition to be false, e.g. 1 = 2 it will try to evaluate the ELSE part and the sub query will return more than 1 row and will throw an error, saying #1242 - Subquery returns more than 1 row which means that the whole query fails so we do not get any results back.

As we can see again, the SQL language was optimized for SQL injection.

To recap: If the condition is TRUE we will get back some results, if it FALSE the query will fail with an error and thus it won't give us back any results. This in turn means that we can ask simple questions (true / false) from the database to which the response we can infer from the existence or lack of the results.

If you could ask a database a question to which it can only respond with a Yes or No, what would that be? I know what I would ask:

Is it true that the ASCII code of the first character of the admin password is 36?

ORDER BY 1,(SELECT CASE WHEN ((SELECT ASCII(MID(user_pass,1,1)) FROM wp_users WHERE user_login = 0x61646D696E LIMIT 1) = 36)  
THEN 1 ELSE 1*(SELECT table_name FROM information_schema.tables)END)=1  

0x61646D696E is just admin hex encoded. If the response is yes, then we know that there is a user with the username admin and the first character of the password hash is $

The catch

Because we do not need any fancy character to do a Blind SQL Injection, htmlentities will not encode/mess up our attack. That is the reason why, usually we write

SELECT ASCII(MID(user_pass,1,1)) FROM wp_users WHERE user_login = 0x61646D696E  

instead of

SELECT MID(user_pass,1,1) FROM wp_users WHERE user_login =  'admin'  

so we won't have to use quotation marks.


After a successful CSRF we can plant an XSS on the LayerSlider administration page. If we want we can trigger the XSS after the CSRF is done. We can inject a script which crates a new user with administrator privileges, log in with that user and exploit the SQLi using an automated tool like SQLmap. But this is pretty boring, how about CSRF to XSS to SQLi to reading the --admin user and pass-- current MySQL user?

Proof of Concept aka. POC

The following POC does exactly that. To test it, log in to a website with Admin privileges, visit the page, enter the URL and press "GET ME the current MySQL user" button.

LayerSlider 6.2.0 CSRF -> Sored XSS -> SQL Injection


8/10 . It is CSRF to stored XSS bug, but LayerSlider has more than 55.000 sales and God knows in how many WordPress themes it is bundled in. This puts it as a high impact vulnerability.


22 - 03 - 2017 - Vulnerability found.  
22 - 03 - 2017 - Vendor notified.  
25 - 03 - 2017 - Vendor fixed the issue in (6.2.1  
16 - 05 - 2017 - Vulnerability goes public.