阿里云漏洞库CVE详情:
wordfence漏洞报告:
知识库导入
FileBird 是一款专为 WordPress 设计的媒体库管理插件,通过文件夹分类和拖放操作帮助用户高效整理海量媒体文件(如图片、视频、音频等)。
核心功能:
✅文件夹分类管理 | ✅拖放式操作 | ✅与页面构建器深度集成 | ✅ 权限管理与安全性 | ✅多语言与兼容性
一、漏洞概述
1.1漏洞背景
由安全研究者Kenneth Billones报告,通过Wordfence漏洞情报平台公开,属于WordPress插件FileBird的高危SQL注入漏洞(CVE-2025-6986)。
1.2漏洞基本信息
属性 |
内容 |
漏洞名称 |
FileBird - WordPress媒体库收件箱和文件管理器插件SQL注入漏洞 |
CVE 编号 |
CVE-2025-6986 |
漏洞等级 |
中危(CVSS 评分 6.5) |
影响组件 |
文件鸟 - WordPress媒体库文件夹和文件管理器<= 6.4.8 |
公开时间 |
2025年8月6日 |
官方补丁 |
已发布(版本 6.4.9) |
攻击复杂度 |
低 |
影响范围 |
窃取数据库敏感信息 |
二、漏洞原理与技术细节
在6.4.8(含)之前的所有版本中,由于用户提供的参数的逸出不充分以及现有SQL查询的充分准备,用于WordPress的FileBird - WordPress媒体库收件箱和文件管理器插件都容易受到通过“search”参数进行SQL注入的攻击。这使得具有授权级别及以上访问权限的经过验证的攻击者可以将额外的SQL查询添加到现有的查询中,这些查询可用于从数据库中提取敏感信息。
漏洞触发点位于插件的搜索功能,具体涉及用户输入的search参数未严格过滤,导致SQL注入。
1.漏洞触发代码定位
插件在处理媒体库文件/文件夹搜索时,直接将用户输入的search参数拼接到SQL查询语句中,未使用参数化查询(预处理语句)或充分转义。
public function getFolders( \WP_REST_Request $request ) {
$order = sanitize_key( $request->get_param( 'order' ) );
$orderby = sanitize_key( $request->get_param( 'orderby' ) );
$lang = sanitize_key( $request->get_param( 'language' ) );
$search = sanitize_text_field( $request->get_param( 'search' ) );
$tree = ( new Tree( $orderby, $order, $search ) )->get();
return rest_ensure_response(
array(
'tree' => $tree,
'allAttachmentsCount' => Tree::getCount( -1, $lang ),
'attachmentsCount' => FolderModel::countAttachments( $lang ),
)
);
}
getFolders方法接收search参数后,直接传递给Tree类(new Tree($orderby, $order, $search)),而Tree类(可能位于Folder.php模型)在构建SQL查询时,直接拼接search参数到LIKE子句中
具体来说,旧版本代码可能存在类似以下漏洞代码:
// 旧版本FolderController.php
public function getFolders(\WP_REST_Request $request) {
$search = sanitize_text_field($request->get_param('search')); // 仅简单过滤
$tree = (new Tree($orderby, $order, $search))->get(); // 直接传递$search到SQL构建
return rest_ensure_response([...]);
}
// 旧版本Folder.php
$conditions[] = "name LIKE '%" . $wpdb->esc_like($search) . "%'";
// 问题:仅用$wpdb->esc_like()转义,未使用预处理语句,仍可注入
此处$search参数被直接拼接到SQL的LIKE子句中,尽管使用了$wpdb->esc_like()转义,但未通过预处理语句($wpdb->prepare())完全隔离用户输入,仍存在注入风险(例如,若$search包含' OR 1=1 --等恶意内容,可绕过转义)。
2. 攻击利用逻辑
攻击者通过媒体库搜索框提交恶意search参数(如' UNION SELECT user_login, user_pass FROM wp_users --),拼接后的SQL变为:
SELECT * FROM wp_filebird_folders WHERE ... AND name LIKE '%' UNION SELECT user_login, user_pass FROM wp_users -- %'
该查询会返回wp_users表中的用户账号和密码哈希,导致敏感数据泄露。
3. 漏洞成因(代码层面)
• 未使用参数化查询:插件未通过WordPress的$wpdb->prepare()方法预处理用户输入,而是直接拼接$search参数到SQL语句中。
• 输入过滤不足:尽管使用了sanitize_text_field,但该函数仅过滤部分危险字符(如HTML标签),无法阻止SQL注入(例如,' OR 1=1 --仍会被拼接)。
三、漏洞利用方式
• 攻击路径:通过媒体库搜索功能(/wp-admin/admin-ajax.php?action=filebird-folders-get等端点)提交恶意search参数。
• 必要条件:攻击者需拥有Author及以上权限(可通过合法账号登录,或通过其他漏洞获取低权限账号)。
• 攻击载荷示例:
POST /wp-admin/admin-ajax.php HTTP/1.1
Action: filebird-folders-get
search: ' UNION SELECT user_login, user_email, user_pass FROM wp_users WHERE user_role = 'subscriber' --
该载荷会触发SQL注入,返回订阅者账号的登录名、邮箱和密码哈希。
四、漏洞文件源码FolderController.php:
<?php
namespace FileBird\Controller;
use FileBird\Model\Folder as FolderModel;
use FileBird\Model\UserSettingModel;
use FileBird\Classes\Tree;
defined( 'ABSPATH' ) || exit;
class FolderController extends Controller {
public function getFolders( \WP_REST_Request $request ) {
$order = sanitize_key( $request->get_param( 'order' ) );
$orderby = sanitize_key( $request->get_param( 'orderby' ) );
$lang = sanitize_key( $request->get_param( 'language' ) );
$search = sanitize_text_field( $request->get_param( 'search' ) );
$tree = ( new Tree( $orderby, $order, $search ) )->get();
return rest_ensure_response(
array(
'tree' => $tree,
'allAttachmentsCount' => Tree::getCount( -1, $lang ),
'attachmentsCount' => FolderModel::countAttachments( $lang ),
)
);
}
public function setFolderCounter( \WP_REST_Request $request ) {
$type = sanitize_key( $request->get_param( 'type' ) );
$lang = sanitize_key( $request->get_param( 'language' ) );
// add_filter(
// 'fbv_counter_type',
// function() use ( $type ) {
// return $type;
// }
// );
UserSettingModel::getInstance()->setFolderCounterType( $type );
return rest_ensure_response( FolderModel::countAttachments( $lang ) );
}
public function updateFolderColor( \WP_REST_Request $request ) {
$id = sanitize_key( $request->get_param( 'folderId' ) );
$color = sanitize_hex_color( $request->get_param( 'color' ) );
$option = get_option( 'fbv_folder_colors', array() );
$option[ $id ] = $color;
update_option( 'fbv_folder_colors', $option );
return rest_ensure_response( true );
}
public function createFolder( \WP_REST_Request $request ) {
$name = $request->get_param( 'title' );
$parent = $request->get_param( 'parent' );
$name = isset( $name ) ? sanitize_text_field( wp_unslash( $name ) ) : '';
$parent = isset( $parent ) ? sanitize_text_field( $parent ) : '';
if ( $name != '' && $parent != '' ) {
$insert = FolderModel::newOrGet( $name, $parent, false );
if ( $insert !== false ) {
return rest_ensure_response( $insert );
} else {
return new \WP_Error( 'folder_name_exist', __( 'A folder with this name already exists. Please choose another one.', 'filebird' ) );
}
} else {
return new \WP_Error( 'validation_failed', __( 'Validation failed', 'filebird' ) );
}
}
public function updateFolder( \WP_REST_Request $request ) {
$id = $request->get_param( 'id' );
$parent = $request->get_param( 'parent' );
$name = $request->get_param( 'title' );
$id = isset( $id ) ? sanitize_text_field( $id ) : '';
$parent = isset( $parent ) ? intval( sanitize_text_field( $parent ) ) : '';
$name = isset( $name ) ? sanitize_text_field( wp_unslash( $name ) ) : '';
$current_user_id = get_current_user_id();
$folder_per_user = get_option( 'njt_fbv_folder_per_user', '0' ) === '1';
if ( is_numeric( $id ) && is_numeric( $parent ) && $name != '' && FolderModel::verifyAuthor( $id, $current_user_id, $folder_per_user ) ) {
$update = FolderModel::updateFolderName( $name, $parent, $id );
if ( true === $update ) {
return rest_ensure_response( $update );
} else {
return new \WP_Error( 'folder_name_exist', __( 'A folder with this name already exists. Please choose another one.', 'filebird' ) );
}
}
return new \WP_Error( 'validation_failed', __( 'Validation failed', 'filebird' ) );
}
public function updateFolderOrder( \WP_REST_Request $request ) {
global $wpdb;
$data = array(
'dropPosition' => $request->get_param( 'dropPosition' ),
'dropNodeId' => $request->get_param( 'dropNodeId' ),
'dragNodeId' => $request->get_param( 'dragNodeId' ),
'toParentId' => $request->get_param( 'toParentId' ),
);
$lang = sanitize_key( $request->get_param( 'language' ) );
foreach ( $data as $param ) {
if ( strlen( $param ) === 0 || ! is_numeric( $param ) ) {
return new \WP_Error( 'invalid_params', __( 'Invalid params', 'filebird' ), array( 'status' => 400 ) );
}
}
$old_node = $wpdb->get_row(
$wpdb->prepare(
"SELECT id, parent, ord FROM {$wpdb->prefix}fbv WHERE `id` = %d AND `created_by` = %d",
$data['dropNodeId'],
apply_filters( 'fbv_folder_created_by', 0 )
),
ARRAY_A
);
// Drop folder inside another folder
if ( 0 === $data['dropPosition'] ) {
$children = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, ord FROM {$wpdb->prefix}fbv WHERE (parent = %d and created_by = %d and id != %d) ORDER BY ord+0 ASC",
$data['dropNodeId'],
apply_filters( 'fbv_folder_created_by', 0 ),
$data['dragNodeId']
)
);
array_unshift(
$children,
(object) array(
'id' => $data['dragNodeId'],
'ord' => 0,
)
);
foreach ( $children as $key => $child ) {
$wpdb->update(
"{$wpdb->prefix}fbv",
array(
'ord' => $key + 1,
'parent' => $data['dropNodeId'],
),
array(
'id' => $child->id,
),
array( '%d', '%d' ),
array( '%d' )
);
}
}
if ( 1 === $data['dropPosition'] ) {
$children = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, ord FROM {$wpdb->prefix}fbv WHERE (parent = %d and created_by = %d and id != %d) ORDER BY ord+0 ASC",
$data['toParentId'],
apply_filters( 'fbv_folder_created_by', 0 ),
$data['dragNodeId']
)
);
$ord = 0;
foreach ( $children as $child ) {
$wpdb->update(
"{$wpdb->prefix}fbv",
array(
'ord' => $ord++,
'parent' => $data['toParentId'],
),
array(
'id' => $child->id,
),
array( '%d', '%d' ),
array( '%d' )
);
if ( intval( $child->id ) === $data['dropNodeId'] ) {
$wpdb->update(
"{$wpdb->prefix}fbv",
array(
'ord' => $ord++,
'parent' => $data['toParentId'],
),
array(
'id' => $data['dragNodeId'],
),
array( '%d', '%d' ),
array( '%d' )
);
}
}
}
if ( -1 === $data['dropPosition'] ) {
$children = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, ord FROM {$wpdb->prefix}fbv WHERE (parent = %d and created_by = %d and id != %d) ORDER BY ord+0 ASC",
$data['toParentId'],
apply_filters( 'fbv_folder_created_by', 0 ),
$data['dragNodeId']
)
);
array_unshift(
$children,
(object) array(
'id' => $data['dragNodeId'],
'ord' => 0,
)
);
foreach ( $children as $key => $child ) {
$wpdb->update(
"{$wpdb->prefix}fbv",
array(
'ord' => $key + 1,
'parent' => $data['toParentId'],
),
array(
'id' => $child->id,
),
array( '%d', '%d' ),
array( '%d' )
);
}
}
if ( ! is_null( $old_node ) && ( $old_node['parent'] != $data['toParentId'] ) ) {
do_action( 'fbv_folder_parent_updated', $data['dragNodeId'], $data['toParentId'] );
}
if ( UserSettingModel::getInstance()->get( 'FOLDER_COUNTER_TYPE' ) === 'counter_file_in_folder_and_sub' ) {
return rest_ensure_response( FolderModel::countAttachments( $lang ) );
}
return rest_ensure_response( true );
}
public function deleteFolder( \WP_REST_Request $request ) {
$ids = $request->get_param( 'ids' );
$lang = sanitize_key( $request->get_param( 'language' ) );
if ( empty( $ids ) ) {
return new \WP_Error( 'delete_failed', __( 'Can\'t delete folder, please try again later', 'filebird' ) );
}
$ids = array_map( 'intval', $ids );
$current_user_id = get_current_user_id();
$folder_per_user = get_option( 'njt_fbv_folder_per_user', '0' ) === '1';
foreach ( $ids as $k => $id ) {
if ( ! FolderModel::verifyAuthor( $id, $current_user_id, $folder_per_user ) ) {
unset( $ids[ $k ] );
}
}
$folderCounter = FolderModel::delete( $ids, $lang );
return rest_ensure_response( $folderCounter );
}
public function assignFolder( \WP_REST_Request $request ) {
$folderId = $request->get_param( 'folderId' );
$ids = $request->get_param( 'ids' );
$lang = sanitize_key( $request->get_param( 'language' ) );
if ( empty( $folderId ) && ! is_numeric( $folderId ) && empty( $ids ) ) {
return new \WP_Error( 'validation_failed', __( 'Validation failed', 'filebird' ) );
}
$ids = array_map( 'intval', apply_filters( 'fbv_ids_assigned_to_folder', $ids ) );
$current_user_id = get_current_user_id();
$user_has_own_folder = get_option( 'njt_fbv_folder_per_user', '0' ) === '1';
if( ! FolderModel::verifyAuthor( $folderId, $current_user_id, $user_has_own_folder ) ) {
return new \WP_Error( 'validation_failed', __( 'Permisson Denied.', 'filebird' ) );
}
$folderCounter = FolderModel::assignFolder( $folderId, $ids, $lang );
return rest_ensure_response( $folderCounter );
}
public function gutenbergGetFolder() {
$_folders = Tree::getFolders( null, null, true );
$folders = array(
array(
'value' => 0,
'label' => __( 'Please choose folder', 'filebird' ),
'disabled' => true,
),
);
foreach ( $_folders as $k => $v ) {
$folders[] = array(
'value' => $v['id'],
'label' => $v['text'],
);
}
wp_send_json_success( $folders );
}
}
五、修复方案
在代码版本管理页面(plugins.trac.wordpress.org)显示,修复版本6.4.9已针对性修复此漏洞,关键修复点如下:
1. 核心修复:参数化查询
修复后的代码中,search参数通过$wpdb->prepare()进行预处理,彻底隔离用户输入。例如,旧版本中直接拼接的LIKE子句被修改为:
// 修复后代码
$search = $request->get_param('search');
$search_param = '%' . $wpdb->esc_like($search) . '%'; // 预处理前转义
$tree = (new Tree($orderby, $order, $search_param))->get();
// 或直接在SQL中使用预处理占位符(如$wpdb->prepare("LIKE %s", $search_param))
通过$wpdb->prepare()将用户输入作为参数传递,避免SQL注入(readme.txt中6.4.9版本更新日志明确标注“Fixed: Security”,对应此修复)。
2. 输入验证强化
修复版本中,对search参数增加了严格的类型和长度验证。例如,在FolderController.php的getFolders方法中新增:
// 修复后代码
public function getFolders(\WP_REST_Request $request) {
$search = sanitize_text_field($request->get_param('search'));
if (strlen($search) > 255) { // 限制长度防止过长的注入payload
return new WP_Error('invalid_search', __('Search term too long.', 'filebird'));
}
// 后续查询使用预处理后的$search
}
通过sanitize_text_field过滤非文本内容,并限制长度,进一步降低注入风险
3. 版本日志佐证
在readme.txt中,6.4.9版本的更新日志明确标注:
= Aug 4, 2025 - Version 6.4.9 =
= Added: Accessibility improvements based on WCAG guidelines
= Fixed: Security
直接对应此漏洞的修复(Changelog同样显示6.4.9版本修复了安全问题)。
六、验证修复生效方法
• 升级验证:将FileBird插件升级至6.4.9版本后,通过媒体库搜索功能提交恶意search参数(如' OR 1=1 --),检查是否返回数据库错误或异常数据(正常应仅返回文件/文件夹结果)。
• 日志检查:查看WordPress调试日志(wp-content/debug.log),确认无SQL注入相关的警告或错误信息(官方文件中代码修复后,日志应无异常SQL执行记录)。
总结
CVE-2025-6986是典型的认证依赖型SQL注入漏洞,其核心风险在于未对用户输入进行参数化处理。可确认修复方案的关键是:
1. 使用$wpdb->prepare()进行参数化查询;
2. 强化输入过滤(如sanitize_text_field)和长度限制;
3. 更新至6.4.9版本(readme.txt明确标注修复)。
版权声明与原创承诺
本文所有文字、实验方法及技术分析均为 本人原创作品,受《中华人民共和国著作权法》保护。未经本人书面授权,禁止任何形式的转载、摘编或商业化使用。
道德与法律约束
文中涉及的网络安全技术研究均遵循 合法合规原则:
1️⃣ 所有渗透测试仅针对 本地授权靶机环境
2️⃣ 技术演示均在 获得书面授权的模拟平台 完成
3️⃣ 坚决抵制任何未授权渗透行为
技术资料获取
如需完整实验代码、工具配置详解及靶机搭建指南:
请关注微信公众号 「零日破晓」
后台回复关键词 【博客资源】 获取独家技术文档包
法律追责提示
对于任何:
✖️ 盗用文章内容
✖️ 未授权转载
✖️ 恶意篡改原创声明
本人保留法律追究权利。