发布于1小时前1小时 本文记录一类在授权渗透测试中较常见的 WordPress 插件漏洞:插件注册了可由未登录用户访问的 AJAX 操作,但处理函数只校验请求参数或 nonce,没有验证当前用户是否具备修改权限,最终导致未授权修改站点配置、文章元数据或业务对象。背景与影响范围该问题通常出现在自研插件、二次开发模板和长期未维护的业务插件中。典型特征是使用 wp_ajax_nopriv_ 注册接口,同时在回调中直接调用 update_option()、update_post_meta() 或自定义 SQL。未登录用户可以调用接口;请求参数可控,且缺少业务对象归属校验;nonce 只用于防 CSRF,不能替代权限判断;修改内容可能影响站点配置、支付参数、页面内容或用户状态。如果接口只能修改低敏感度内容,通常评估为中危;如果能够修改管理员邮箱、登录相关配置、模板内容或外部回调地址,则应按高危场景处理。问题定位审计插件代码时,优先搜索 AJAX 注册点和状态修改函数:grep -RInE \"wp_ajax(_nopriv)?_|update_option|update_post_meta|\\$wpdb->(query|prepare)\" wp-content/plugins/custom-plugin/以下代码展示了存在缺陷的处理方式:add_action('wp_ajax_nopriv_cp_update_notice', 'cp_update_notice'); add_action('wp_ajax_cp_update_notice', 'cp_update_notice'); function cp_update_notice() { $notice = isset($_POST['notice']) ? wp_unslash($_POST['notice']) : ''; // 仅校验 nonce,未校验登录状态和业务权限 if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', 'cp_notice')) { wp_send_json_error('bad nonce', 403); } update_option('cp_notice', sanitize_textarea_field($notice)); wp_send_json_success(); }这里即使 nonce 校验存在,仍然不能证明请求者有权限修改配置。尤其是当 nonce 被输出到公开页面、缓存页面,或由其他接口泄露时,权限边界仍然不存在。授权验证步骤验证前应确认目标属于测试授权范围,并使用无害的标记值。不要直接修改管理员凭据、支付配置或生产业务数据。可以先通过代码确认接口是否注册为 nopriv,再使用独立测试站点或低敏感度字段复现。# 使用无害标记验证未登录请求是否能够触发状态变化 curl -i -s -c /tmp/wp-cookie.txt \\ -X POST ' \\ --data-urlencode 'action=cp_update_notice' \\ --data-urlencode 'notice=AUTHZ_AUDIT_20250301' \\ --data-urlencode '_wpnonce=从测试页面取得的nonce'验证结果不能只看 HTTP 状态码。应同时检查响应 JSON、插件对应的数据存储位置,以及前台页面是否出现标记值。例如:# 在授权测试主机上检查 WordPress 配置项 wp option get cp_notice --path=/var/www/html --format=json # 检查最近的 PHP-FPM/Nginx 访问记录 grep 'admin-ajax.php' /var/log/nginx/access.log | tail -n 50 grep -E 'cp_update_notice|AUTHZ_AUDIT_' /var/log/php*-fpm.log 2>/dev/null如果未登录请求返回成功,且 cp_notice 被修改,即可确认存在权限控制缺失。若接口返回成功但数据未变化,需要继续检查 nonce、缓存、插件分支逻辑和实际存储位置,避免将参数校验失败误判为漏洞。技术分析该漏洞的核心不是“能否调用 AJAX”,而是状态变更前缺少服务端授权判断。安全控制至少应覆盖以下三个层面:身份:接口是否要求登录;能力:当前用户是否具备对应 capability,例如 manage_options;对象权限:当前用户是否有权修改指定文章、订单或租户数据。nonce 只用于降低跨站请求伪造风险,不能作为身份认证或权限授权依据。对于管理员配置,通常应在修改前执行 current_user_can('manage_options');对于文章对象,应使用 current_user_can('edit_post', $post_id),而不是仅判断用户已登录。修复示例add_action('wp_ajax_cp_update_notice', 'cp_update_notice'); // 删除 wp_ajax_nopriv_ 注册,禁止未登录用户访问 function cp_update_notice() { if (!is_user_logged_in()) { wp_send_json_error('authentication required', 401); } if (!current_user_can('manage_options')) { wp_send_json_error('forbidden', 403); } $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'cp_notice')) { wp_send_json_error('invalid nonce', 403); } $notice = isset($_POST['notice']) ? sanitize_textarea_field(wp_unslash($_POST['notice'])) : ''; update_option('cp_notice', $notice, false); wp_send_json_success(); }如果接口修改的是文章或其他业务对象,应把对象权限放在实际写入之前:$post_id = absint($_POST['post_id'] ?? 0); if (!$post_id || !current_user_can('edit_post', $post_id)) { wp_send_json_error('forbidden', 403); } $body = isset($_POST['body']) ? wp_kses_post(wp_unslash($_POST['body'])) : ''; wp_update_post([ 'ID' => $post_id, 'post_content' => $body, ]);日志留痕与排查修复后建议保留接口访问日志和失败授权日志,便于确认是否存在历史滥用。Nginx 或 Apache 日志应至少记录时间、源地址、请求方法、URI、状态码和用户代理;应用层可记录操作用户、对象 ID 和结果,但不要记录 nonce、Cookie 或完整敏感参数。error_log('cp_update_notice denied: user=' . get_current_user_id() . ' ip=' . ($_SERVER['REMOTE_ADDR'] ?? 'unknown')); if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { error_log('cp_update_notice result=success user=' . get_current_user_id()); }排查历史影响时,可以核对 wp_options、wp_postmeta 以及插件自建表的修改时间,并结合 Web 访问日志搜索 admin-ajax.php 和相关 action。若发现未知来源的配置变化,应同时检查管理员账户、定时任务、主题文件和可疑插件,不能只回滚单个配置项。风险与修复建议删除不必要的 wp_ajax_nopriv_ 注册,默认拒绝未登录状态变更请求;在每个写操作前执行 capability 和对象归属检查;对输入执行类型约束、长度限制和上下文相关的输出编码;不要把 nonce 当作权限校验,不要在客户端决定是否允许修改;升级插件及其依赖,并对自研插件增加未登录、低权限和越权单元测试;修复后使用未登录用户、普通编辑者和管理员三种身份分别回归验证,预期结果应为 401、403 和 200。总结WordPress AJAX 权限缺失的确认重点是“未授权身份是否能够造成持久化状态变化”,而不是单纯发现一个可访问接口。漏洞库记录中应同时保留受影响 action、写入位置、复现条件、权限边界、日志证据和修复版本,便于后续复测和审计追踪。 代码审计、调用链与关键函数定位示意 网络请求、日志与边界流量分析示意
35分钟前35分钟 这类问题重点不只是“有没有 nonce”,而是接口是否同时做了身份、权限和参数校验。WordPress 的 nonce 只能降低 CSRF 风险,不能证明请求者具备修改权限。 先检查插件是否同时注册了未登录入口: add_action('wp_ajax_update_xxx', 'my_update_handler'); add_action('wp_ajax_nopriv_update_xxx', 'my_update_handler'); 如果修改操作不应允许匿名访问,应删除 wp_ajax_nopriv_*,并在处理函数中再次检查能力,避免仅依赖前端隐藏按钮: function my_update_handler() { if ( ! is_user_logged_in() ) { wp_send_json_error(array('message' => '需要登录'), 401); } if ( ! current_user_can('manage_options') ) { wp_send_json_error(array('message' => '无权限'), 403); } check_ajax_referer('my_update_action', 'nonce'); $item_id = isset($_POST['item_id']) ? absint($_POST['item_id']) : 0; $value = isset($_POST['value']) ? sanitize_text_field(wp_unslash($_POST['value'])) : ''; if ( ! $item_id || $value === '' ) { wp_send_json_error(array('message' => '参数无效'), 400); } // 如对象有归属关系,还应校验当前用户是否能修改该对象 $updated = update_option('my_option_' . $item_id, $value, false); if ( ! $updated ) { wp_send_json_error(array('message' => '更新失败'), 500); } wp_send_json_success(); } 如果这是编辑文章、订单或自定义帖子,manage_options 不一定合适,应该使用对象级能力,例如: if ( ! current_user_can('edit_post', $post_id) ) { wp_send_json_error(array('message' => '无权修改该内容'), 403); } 排查时可以直接抓包确认:匿名请求是否返回成功、删除 Cookie 后是否仍能修改、替换低权限账号 Cookie 后是否越权,以及修改 item_id 是否能操作其他用户的数据。修复后建议对每个 AJAX action 做一组未登录、低权限、高权限和跨对象 ID 的回归测试;不要只测试 nonce 是否有效。
创建帐户或登录后发表意见