跳转到帖子

WordPress 插件未授权文件上传漏洞的验证、审计与修复

精选回复

发布于

本文记录一种在授权渗透测试中较常见的 WordPress 插件漏洞:插件提供上传接口,但仅依赖前端限制或未校验当前用户权限,导致未登录用户或低权限用户可以上传任意文件。该问题的实际风险取决于上传目录是否可执行 PHP、Web 服务器是否允许脚本解析,以及文件是否能够被外部访问。

验证时应使用隔离测试环境和无害标记文件,不要上传 WebShell、反弹脚本或其他可执行载荷。漏洞确认的目标是证明“权限校验缺失”和“文件类型校验可绕过”,而不是获得不必要的持久化权限。

一、典型场景

目标站点安装了一个自定义媒体、头像或表单附件插件,接口类似 /wp-admin/admin-ajax.php?action=plugin_upload。插件代码只检查了 $_FILES 是否存在,或者仅依赖 JavaScript 将文件选择框限制为图片类型,但服务端没有调用 WordPress 的权限和文件类型校验函数。

常见危险代码如下:

<?php
add_action('wp_ajax_nopriv_plugin_upload', 'plugin_upload');
add_action('wp_ajax_plugin_upload', 'plugin_upload');

function plugin_upload() {
    if (empty($_FILES['file'])) {
        wp_send_json_error('missing file');
    }

    $dir = WP_CONTENT_DIR . '/uploads/plugin/';
    move_uploaded_file($_FILES['file']['tmp_name'], $dir . $_FILES['file']['name']);
    wp_send_json_success();
}

这里至少存在四个问题:允许未登录用户访问处理函数;没有检查用户是否具备上传权限;直接使用客户端提供的文件名;没有使用 WordPress 的安全上传接口;上传目录如果位于可执行路径,可能进一步演变为远程代码执行。

二、问题定位与代码审计

审计插件时,优先搜索上传入口、AJAX action 和文件移动函数:

grep -RInE "wp_ajax_nopriv|wp_ajax_|move_uploaded_file|media_handle_upload|wp_handle_upload|\$_FILES" wp-content/plugins/target-plugin/

重点检查以下位置:

  • 是否注册了 wp_ajax_nopriv_*,该钩子表示未登录用户也可调用接口。
  • 处理函数中是否调用 current_user_can()、check_admin_referer() 或等效的 nonce 校验。
  • 是否直接拼接 $_FILES['name']、$_POST['filename'] 到文件路径。
  • 是否只根据扩展名或请求头判断文件类型,而没有使用服务端 MIME 检测。
  • 上传目录是否位于 wp-content/uploads,且 Web 服务器对其中的 PHP 文件启用了脚本解析。

注意:nonce 不是权限控制。WordPress nonce 主要用于防止跨站请求伪造,不能单独证明调用者具备上传权限。即使请求包含有效 nonce,也必须执行能力检查。

三、授权环境中的安全验证

验证前先确认测试范围、目标域名和允许的请求类型。建议准备一个不包含 PHP 代码的文本文件,例如:

printf 'authorized-upload-test-2025\n' > marker.txt
sha256sum marker.txt

如果接口参数已经通过代码审计确认,且测试账号和测试范围均得到授权,可使用无害文件进行请求验证:

curl -i -k -X POST \
  -F 'action=plugin_upload' \
  -F '[email protected];type=text/plain' \
  ' HTTP 状态码。应同时记录:

  • 响应是否返回了可访问的文件路径或文件 URL。
  • 服务器是否生成了新的上传文件。
  • 请求是否在未携带登录 Cookie 的情况下成功。
  • 上传文件是否能够通过浏览器或 curl 读取。
  • 服务端日志中是否记录了该请求和最终文件路径。

如果接口要求 nonce,应使用测试账号在合法流程中获取 nonce,再验证低权限账号是否能够调用。不要通过暴力猜测或绕过生产环境认证来获取令牌。

四、确认是否存在脚本执行风险

在没有明确书面授权的情况下,不应上传 PHP 文件进行执行测试。可先通过配置和静态分析判断风险:

# Nginx 配置检查
nginx -T 2>/dev/null | grep -nE 'location|fastcgi_pass|wp-content/uploads'

# Apache 配置及上传目录规则检查
find /var/www -path '*/wp-content/uploads/*' -type f \( -name '.htaccess' -o -name '*.conf' \) -print

grep -RInE 'AddHandler|SetHandler|FilesMatch|php-fpm' /etc/apache2 /etc/httpd 2>/dev/null

如果上传目录允许 PHP-FPM 处理,且攻击者能控制文件内容,就不应继续进行执行验证,应立即停止测试并按高风险漏洞处理。对于必须确认的场景,应在离线克隆环境中使用固定的无害响应程序,并由系统所有者执行复核。

五、日志与留痕排查

漏洞验证期间应保存时间、源 IP、请求方法、接口路径、响应状态和生成文件名,便于复盘。Linux 环境可以使用以下方式快速定位:

# Nginx 访问日志
awk '$7 ~ /admin-ajax.php|plugin_upload/ {print}' /var/log/nginx/access.log

# Apache 访问日志
 grep -E 'admin-ajax\.php|plugin_upload' /var/log/apache2/access.log

# 查找近期新增的上传文件
find /var/www/html/wp-content/uploads -type f -mmin -60 -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n' | sort

重点关注未登录请求、异常 User-Agent、短时间内大量不同扩展名文件、文件名包含路径分隔符或双扩展名,以及上传后紧接着访问同一路径的行为。若使用 CDN、WAF 或反向代理,还应对照边缘节点日志,避免只查看源站日志造成误判。

六、安全修复示例

修复时应同时处理身份、权限、文件类型、文件名和存储位置,而不是只增加扩展名黑名单。一个较安全的 WordPress 处理示例如下:

<?php
function safe_plugin_upload() {
    if (!is_user_logged_in() || !current_user_can('upload_files')) {
        wp_send_json_error('forbidden', 403);
    }

    check_ajax_referer('plugin_upload', 'nonce');

    if (empty($_FILES['file']) || !empty($_FILES['file']['error'])) {
        wp_send_json_error('invalid upload', 400);
    }

    require_once ABSPATH . 'wp-admin/includes/file.php';

    $allowed = array(
        'jpg|jpeg|jpe' => 'image/jpeg',
        'png'          => 'image/png',
        'pdf'          => 'application/pdf',
    );

    $overrides = array(
        'test_form' => false,
        'mimes'     => $allowed,
    );

    $result = wp_handle_upload($_FILES['file'], $overrides);
    if (isset($result['error'])) {
        wp_send_json_error($result['error'], 400);
    }

    wp_send_json_success(array('url' => esc_url_raw($result['url'])));
}

代码层面还应注意:

  • 根据业务需要选择能力,例如普通媒体上传通常使用 upload_files,不要直接信任角色名称。
  • 使用随机化后的服务端文件名,避免覆盖已有文件和路径穿越。
  • 限制单文件大小、请求体大小和上传频率。
  • 对图片进行实际解码或重编码,不能只检查文件扩展名。
  • 上传目录禁止执行脚本。Nginx 应避免将该目录交给 PHP-FPM;Apache 可在上传目录配置 php_admin_flag engine off 或禁用相应处理器。
  • 插件接口不需要匿名访问时,应删除 wp_ajax_nopriv_* 注册。

七、复测与风险判定

修复后至少使用三类请求复测:未登录请求、普通低权限账号请求和具备明确上传权限的测试账号请求。前两类应返回 401 或 403,合法账号只能上传允许的文件类型,且服务端生成的文件不能被当作脚本执行。

风险判定可以按实际影响区分:

  • 仅能上传不可执行的图片或文档,且目录不可列出:通常为中低风险,但仍可能造成存储滥用或钓鱼内容托管。
  • 未登录用户可上传任意可访问文件:风险上升,可能导致存储型 XSS、恶意内容分发或敏感信息覆盖。
  • 上传目录可解析 PHP,且接口无需认证:属于高危,可能直接导致服务器被接管。

总结

WordPress 上传漏洞的确认不能停留在“接口返回成功”。漏洞库记录应同时说明入口、认证状态、能力校验、文件类型处理、存储路径、脚本解析配置和日志证据。授权验证优先使用无害标记文件,结合源码、Web 服务器配置和访问日志完成闭环;修复则必须同时补齐权限控制、nonce、服务端类型校验、随机文件名和上传目录隔离,最后用未登录、低权限和合法权限三种身份复测。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意
网络流量与边界分析示意图
网络请求、日志与边界流量分析示意

这类问题建议把“能上传”与“能执行”分开验证,避免直接在生产环境放置 PHP 文件。优先在测试副本中使用一个无害的 .txt 文件,确认未登录请求是否能到达上传处理逻辑,并记录响应中的文件路径、文件名和状态码。

审计插件时可以先定位上传入口:

grep -RInE 'wp_handle_upload|media_handle_upload|move_uploaded_file|wp_ajax_nopriv|upload_bits|file_put_contents' wp-content/plugins/target-plugin/

重点检查以下几点:

  • 入口是否同时注册了 wp_ajax_nopriv_*、REST 公共路由或公开表单处理函数;
  • 是否只校验了 nonce。nonce 不是权限控制,必须再校验用户身份和能力;
  • 是否直接信任 $_FILES['type']、原始扩展名或客户端文件名;
  • 上传目录是否位于 Web 根目录下,服务器是否会解析其中的 PHP;
  • 文件名是否可控,是否存在路径穿越、覆盖已有文件或符号链接处理问题。

一个最低限度的上传处理应类似下面这样,具体能力名按业务场景调整:

if ( ! is_user_logged_in() || ! current_user_can( 'upload_files' ) ) {
    wp_send_json_error( 'forbidden', 403 );
}

check_ajax_referer( 'plugin_upload', 'nonce' );

if ( empty( $_FILES['file'] ) || UPLOAD_ERR_OK !== $_FILES['file']['error'] ) {
    wp_send_json_error( 'invalid_upload', 400 );
}

$file = $_FILES['file'];

$allowed = array(
    'jpg|jpeg' => 'image/jpeg',
    'png'       => 'image/png',
    'pdf'       => 'application/pdf',
);

$check = wp_check_filetype_and_ext(
    $file['tmp_name'],
    $file['name'],
    $allowed
);

if ( empty( $check['ext'] ) || empty( $check['type'] ) ) {
    wp_send_json_error( 'file_type_not_allowed', 400 );
}

require_once ABSPATH . 'wp-admin/includes/file.php';

$result = wp_handle_upload(
    $file,
    array(
        'test_form' => false,
        'mimes'     => $allowed,
    )
);

if ( isset( $result['error'] ) ) {
    wp_send_json_error( $result['error'], 400 );
}

如果插件只是保存附件,最好交给 WordPress 的上传 API,不要自行调用 move_uploaded_file()。图片类文件还应使用 wp_getimagesize() 或图像库重新解码,不能仅凭扩展名判断格式。

部署侧也应做第二道防线:对插件自建上传目录禁止脚本执行,Apache 可放置:

<FilesMatch "\.(php|phtml|phar|pht)$">
    Require all denied
</FilesMatch>

Nginx 则不要把该目录交给 PHP-FPM,例如:

location ^~ /wp-content/uploads/plugin-private/ {
    location ~ \.(php|phtml|phar)$ {
        deny all;
    }
}

修复后不要只验证返回 403,还要检查三件事:未登录和低权限用户确实无法进入处理函数;伪造 MIME、双扩展名和超长文件名会被拒绝;历史上传目录中已经存在的脚本文件无法被 Web 服务器执行。最后结合 Web 访问日志和 PHP 日志检索该接口的异常请求,必要时清理旧版本留下的可疑文件。

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。

SiteMap HACKHAT © 2026 All rights reserved.